mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-06 09:35:41 -05:00
Telemetry for Data Workspaces extension (#13846)
* Add CodeQL Analysis workflow (#10195) * Add CodeQL Analysis workflow * Fix path * test commit pls ignore * telemetry points * yarn lock changes * making test xplat friendly * PR feedback * Adding additional telemetry points Co-authored-by: Justin Hutchings <jhutchings1@users.noreply.github.com>
This commit is contained in:
@@ -6,13 +6,13 @@
|
||||
import { IProjectProvider } from 'dataworkspace';
|
||||
import * as vscode from 'vscode';
|
||||
import { IProjectProviderRegistry } from './interfaces';
|
||||
import { TelemetryReporter, TelemetryViews } from './telemetry';
|
||||
|
||||
export const ProjectProviderRegistry: IProjectProviderRegistry = new class implements IProjectProviderRegistry {
|
||||
private _providers = new Array<IProjectProvider>();
|
||||
private _providerFileExtensionMapping: { [key: string]: IProjectProvider } = {};
|
||||
private _providerProjectTypeMapping: { [key: string]: IProjectProvider } = {};
|
||||
|
||||
|
||||
registerProvider(provider: IProjectProvider): vscode.Disposable {
|
||||
this.validateProvider(provider);
|
||||
this._providers.push(provider);
|
||||
@@ -20,6 +20,14 @@ export const ProjectProviderRegistry: IProjectProviderRegistry = new class imple
|
||||
this._providerFileExtensionMapping[projectType.projectFileExtension.toUpperCase()] = provider;
|
||||
this._providerProjectTypeMapping[projectType.id.toUpperCase()] = provider;
|
||||
});
|
||||
|
||||
TelemetryReporter.createActionEvent(TelemetryViews.ProviderRegistration, 'ProviderRegistered')
|
||||
.withAdditionalProperties({
|
||||
providerId: provider.providerExtensionId,
|
||||
extensions: provider.supportedProjectTypes.map(p => p.projectFileExtension).sort().join(', ')
|
||||
})
|
||||
.send();
|
||||
|
||||
return new vscode.Disposable(() => {
|
||||
const idx = this._providers.indexOf(provider);
|
||||
if (idx >= 0) {
|
||||
|
||||
@@ -3,15 +3,43 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as utils from './utils';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import AdsTelemetryReporter from 'ads-extension-telemetry';
|
||||
|
||||
import * as Utils from './utils';
|
||||
const packageJson = require('../../package.json');
|
||||
|
||||
const packageJson = require('../package.json');
|
||||
|
||||
let packageInfo = Utils.getPackageInfo(packageJson)!;
|
||||
let packageInfo = utils.getPackageInfo(packageJson)!;
|
||||
|
||||
export const TelemetryReporter = new AdsTelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey);
|
||||
|
||||
export enum TelemetryViews {
|
||||
WorkspaceTreePane = 'WorkspaceTreePane',
|
||||
OpenExistingDialog = 'OpenExistingDialog',
|
||||
NewProjectDialog = 'NewProjectDialog',
|
||||
ProviderRegistration = 'ProviderRegistration'
|
||||
}
|
||||
|
||||
export function calculateRelativity(projectPath: string, workspacePath?: string): string {
|
||||
workspacePath = workspacePath ?? vscode.workspace.workspaceFile?.fsPath;
|
||||
|
||||
if (!workspacePath) {
|
||||
return 'noWorkspace';
|
||||
}
|
||||
|
||||
const relativePath = path.relative(path.dirname(projectPath), path.dirname(workspacePath));
|
||||
|
||||
if (relativePath.length === 0) { // no path difference
|
||||
return 'sameFolder';
|
||||
}
|
||||
|
||||
const pathParts = relativePath.split(path.sep);
|
||||
|
||||
if (pathParts.every(x => x === '..')) {
|
||||
return 'directAncestor';
|
||||
}
|
||||
|
||||
return 'other'; // sibling, cousin, descendant, etc.
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { IWorkspaceService } from './interfaces';
|
||||
import { UnknownProjectsError } from './constants';
|
||||
import { WorkspaceTreeItem } from 'dataworkspace';
|
||||
import { TelemetryReporter } from './telemetry';
|
||||
|
||||
/**
|
||||
* Tree data provider for the workspace main view
|
||||
@@ -40,8 +42,14 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider<Worksp
|
||||
await vscode.commands.executeCommand('setContext', 'isProjectsViewEmpty', projects.length === 0);
|
||||
const unknownProjects: string[] = [];
|
||||
const treeItems: WorkspaceTreeItem[] = [];
|
||||
|
||||
const typeMetric: Record<string, number> = {};
|
||||
|
||||
for (const project of projects) {
|
||||
const projectProvider = await this._workspaceService.getProjectProvider(project);
|
||||
|
||||
this.incrementProjectTypeMetric(typeMetric, project);
|
||||
|
||||
if (projectProvider === undefined) {
|
||||
unknownProjects.push(project.path);
|
||||
continue;
|
||||
@@ -60,10 +68,30 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider<Worksp
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
TelemetryReporter.sendMetricsEvent(typeMetric, 'OpenWorkspaceProjectTypes');
|
||||
TelemetryReporter.sendMetricsEvent(
|
||||
{
|
||||
'handled': projects.length - unknownProjects.length,
|
||||
'unhandled': unknownProjects.length
|
||||
},
|
||||
'OpenWorkspaceProjectsHandled');
|
||||
|
||||
if (unknownProjects.length > 0) {
|
||||
vscode.window.showErrorMessage(UnknownProjectsError(unknownProjects));
|
||||
}
|
||||
|
||||
return treeItems;
|
||||
}
|
||||
}
|
||||
|
||||
private incrementProjectTypeMetric(typeMetric: Record<string, number>, projectUri: vscode.Uri) {
|
||||
const ext = path.extname(projectUri.fsPath);
|
||||
|
||||
if (!typeMetric.hasOwnProperty(ext)) {
|
||||
typeMetric[ext] = 0;
|
||||
}
|
||||
|
||||
typeMetric[ext]++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,11 @@ declare module 'dataworkspace' {
|
||||
* Gets the supported project types
|
||||
*/
|
||||
readonly supportedProjectTypes: IProjectType[];
|
||||
|
||||
/**
|
||||
* Gets the extension ID for the project provider
|
||||
*/
|
||||
readonly providerExtensionId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,6 +13,7 @@ import { IProjectType } from 'dataworkspace';
|
||||
import { directoryExist } from '../common/utils';
|
||||
import { IconPathHelper } from '../common/iconHelper';
|
||||
import { defaultProjectSaveLocation } from '../common/projectLocationHelper';
|
||||
import { TelemetryReporter, TelemetryViews } from '../common/telemetry';
|
||||
|
||||
class NewProjectDialogModel {
|
||||
projectTypeId: string = '';
|
||||
@@ -25,6 +26,11 @@ export class NewProjectDialog extends DialogBase {
|
||||
|
||||
constructor(private workspaceService: IWorkspaceService) {
|
||||
super(constants.NewProjectDialogTitle, 'NewProject');
|
||||
|
||||
// dialog launched from Welcome message button (only visible when no current workspace) vs. "add project" button
|
||||
TelemetryReporter.createActionEvent(TelemetryViews.NewProjectDialog, 'NewProjectDialogLaunched')
|
||||
.withAdditionalProperties({ isWorkspaceOpen: (vscode.workspace.workspaceFile !== undefined).toString() })
|
||||
.send();
|
||||
}
|
||||
|
||||
async validate(): Promise<boolean> {
|
||||
@@ -59,11 +65,21 @@ export class NewProjectDialog extends DialogBase {
|
||||
async onComplete(): Promise<void> {
|
||||
try {
|
||||
const validateWorkspace = await this.workspaceService.validateWorkspace();
|
||||
|
||||
TelemetryReporter.createActionEvent(TelemetryViews.NewProjectDialog, 'NewProjectDialogCompleted')
|
||||
.withAdditionalProperties({ projectFileExtension: this.model.projectFileExtension, projectTemplateId: this.model.projectTypeId, workspaceValidationPassed: validateWorkspace.toString() })
|
||||
.send();
|
||||
|
||||
if (validateWorkspace) {
|
||||
await this.workspaceService.createProject(this.model.name, vscode.Uri.file(this.model.location), this.model.projectTypeId, vscode.Uri.file(this.workspaceInputBox!.value!));
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
|
||||
TelemetryReporter.createActionEvent(TelemetryViews.NewProjectDialog, 'NewProjectDialogErrorThrown')
|
||||
.withAdditionalProperties({ projectFileExtension: this.model.projectFileExtension, projectTemplateId: this.model.projectTypeId, error: err?.message ? err.message : err })
|
||||
.send();
|
||||
|
||||
vscode.window.showErrorMessage(err?.message ? err.message : err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as constants from '../common/constants';
|
||||
import { IWorkspaceService } from '../common/interfaces';
|
||||
import { fileExist } from '../common/utils';
|
||||
import { IconPathHelper } from '../common/iconHelper';
|
||||
import { calculateRelativity, TelemetryReporter, TelemetryViews } from '../common/telemetry';
|
||||
|
||||
export class OpenExistingDialog extends DialogBase {
|
||||
public _targetTypeRadioCardGroup: azdata.RadioCardGroupComponent | undefined;
|
||||
@@ -29,6 +30,11 @@ export class OpenExistingDialog extends DialogBase {
|
||||
|
||||
constructor(private workspaceService: IWorkspaceService, private extensionContext: vscode.ExtensionContext) {
|
||||
super(constants.OpenExistingDialogTitle, 'OpenProject');
|
||||
|
||||
// dialog launched from Welcome message button (only visible when no current workspace) vs. "add project" button
|
||||
TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, 'OpenWorkspaceProjectDialogLaunched')
|
||||
.withAdditionalProperties({ isWorkspaceOpen: (vscode.workspace.workspaceFile !== undefined).toString() })
|
||||
.send();
|
||||
}
|
||||
|
||||
async validate(): Promise<boolean> {
|
||||
@@ -62,12 +68,34 @@ export class OpenExistingDialog extends DialogBase {
|
||||
async onComplete(): Promise<void> {
|
||||
try {
|
||||
if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) {
|
||||
// capture that workspace was selected, also if there's already an open workspace that's being replaced
|
||||
TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, 'OpeningWorkspace')
|
||||
.withAdditionalProperties({ hasWorkspaceOpen: (vscode.workspace.workspaceFile !== undefined).toString() })
|
||||
.send();
|
||||
|
||||
await this.workspaceService.enterWorkspace(vscode.Uri.file(this._filePathTextBox!.value!));
|
||||
} else {
|
||||
// save datapoint now because it'll get set to new value during validateWorkspace()
|
||||
const telemetryProps: any = { hasWorkspaceOpen: (vscode.workspace.workspaceFile !== undefined).toString() };
|
||||
|
||||
const validateWorkspace = await this.workspaceService.validateWorkspace();
|
||||
let addProjectsPromise: Promise<void>;
|
||||
|
||||
if (validateWorkspace) {
|
||||
await this.workspaceService.addProjectsToWorkspace([vscode.Uri.file(this._filePathTextBox!.value!)], vscode.Uri.file(this.workspaceInputBox!.value!));
|
||||
telemetryProps.workspaceProjectRelativity = calculateRelativity(this._filePathTextBox!.value!, this.workspaceInputBox!.value!);
|
||||
telemetryProps.cancelled = 'false';
|
||||
addProjectsPromise = this.workspaceService.addProjectsToWorkspace([vscode.Uri.file(this._filePathTextBox!.value!)], vscode.Uri.file(this.workspaceInputBox!.value!));
|
||||
} else {
|
||||
telemetryProps.workspaceProjectRelativity = 'none';
|
||||
telemetryProps.cancelled = 'true';
|
||||
addProjectsPromise = this.workspaceService.addProjectsToWorkspace([vscode.Uri.file(this._filePathTextBox!.value!)], vscode.Uri.file(this.workspaceInputBox!.value!));
|
||||
}
|
||||
|
||||
TelemetryReporter.createActionEvent(TelemetryViews.OpenExistingDialog, 'OpeningProject')
|
||||
.withAdditionalProperties(telemetryProps)
|
||||
.send();
|
||||
|
||||
await addProjectsPromise;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
|
||||
@@ -23,21 +23,26 @@ export function activate(context: vscode.ExtensionContext): Promise<IExtension>
|
||||
setProjectProviderContextValue(workspaceService);
|
||||
}));
|
||||
setProjectProviderContextValue(workspaceService);
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('projects.new', async () => {
|
||||
const dialog = new NewProjectDialog(workspaceService);
|
||||
await dialog.open();
|
||||
}));
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('projects.openExisting', async () => {
|
||||
const dialog = new OpenExistingDialog(workspaceService, context);
|
||||
await dialog.open();
|
||||
|
||||
}));
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('dataworkspace.refresh', () => {
|
||||
workspaceTreeDataProvider.refresh();
|
||||
}));
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('dataworkspace.close', () => {
|
||||
vscode.commands.executeCommand('workbench.action.closeFolder');
|
||||
}));
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('projects.removeProject', async (treeItem: WorkspaceTreeItem) => {
|
||||
await workspaceService.removeProject(vscode.Uri.file(treeItem.element.project.projectFilePath));
|
||||
}));
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as constants from '../common/constants';
|
||||
import { IWorkspaceService } from '../common/interfaces';
|
||||
import { ProjectProviderRegistry } from '../common/projectProviderRegistry';
|
||||
import Logger from '../common/logger';
|
||||
import { TelemetryReporter, TelemetryViews, calculateRelativity } from '../common/telemetry';
|
||||
|
||||
const WorkspaceConfigurationName = 'dataworkspace';
|
||||
const ProjectsConfigurationName = 'projects';
|
||||
@@ -115,6 +116,12 @@ export class WorkspaceService implements IWorkspaceService {
|
||||
currentProjects.push(projectFile);
|
||||
newProjectFileAdded = true;
|
||||
|
||||
TelemetryReporter.createActionEvent(TelemetryViews.WorkspaceTreePane, 'ProjectAddedToWorkspace')
|
||||
.withAdditionalProperties({
|
||||
workspaceProjectRelativity: calculateRelativity(projectFile.fsPath),
|
||||
projectType: path.extname(projectFile.fsPath)
|
||||
}).send();
|
||||
|
||||
// if the relativePath and the original path is the same, that means the project file is not under
|
||||
// any workspace folders, we should add the parent folder of the project file to the workspace
|
||||
const relativePath = vscode.workspace.asRelativePath(projectFile, false);
|
||||
@@ -166,6 +173,12 @@ export class WorkspaceService implements IWorkspaceService {
|
||||
const projectIdx = currentProjects.findIndex((p: vscode.Uri) => p.fsPath === projectFile.fsPath);
|
||||
if (projectIdx !== -1) {
|
||||
currentProjects.splice(projectIdx, 1);
|
||||
|
||||
TelemetryReporter.createActionEvent(TelemetryViews.WorkspaceTreePane, 'ProjectRemovedFromWorkspace')
|
||||
.withAdditionalProperties({
|
||||
projectType: path.extname(projectFile.fsPath)
|
||||
}).send();
|
||||
|
||||
await this.setWorkspaceConfigurationValue(ProjectsConfigurationName, currentProjects.map(project => this.toRelativePath(project)));
|
||||
this._onDidWorkspaceProjectsChange.fire();
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export function createProjectProvider(projectTypes: IProjectType[]): IProjectPro
|
||||
const treeDataProvider = new MockTreeDataProvider();
|
||||
const projectProvider: IProjectProvider = {
|
||||
supportedProjectTypes: projectTypes,
|
||||
providerExtensionId: 'testProvider',
|
||||
RemoveProject: (projectFile: vscode.Uri): Promise<void> => {
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
21
extensions/data-workspace/src/test/utils.test.ts
Normal file
21
extensions/data-workspace/src/test/utils.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'mocha';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import should = require('should');
|
||||
import { calculateRelativity } from '../common/telemetry';
|
||||
|
||||
suite('Utilities Tests', function (): void {
|
||||
test('test CalculateRelativity', async () => {
|
||||
const root = os.platform() === 'win32' ? 'Z:\\' : '/';
|
||||
const workspacePath = path.join(root, 'Source', 'Workspace', 'qwerty.code-workspace');
|
||||
|
||||
should.equal(calculateRelativity(path.join(root, 'Source', 'Workspace', 'qwerty.sqlproj'), workspacePath), 'sameFolder');
|
||||
should.equal(calculateRelativity(path.join(root, 'Source', 'Workspace', 'qwerty', 'asdfg', 'qwerty.sqlproj'), workspacePath), 'directAncestor');
|
||||
should.equal(calculateRelativity(path.join(root, 'Users', 'BillG', 'qwerty.sqlproj'), workspacePath), 'other');
|
||||
});
|
||||
});
|
||||
@@ -78,6 +78,7 @@ suite('workspaceTreeDataProvider Tests', function (): void {
|
||||
displayName: 'sql project',
|
||||
description: ''
|
||||
}],
|
||||
providerExtensionId: 'testProvider',
|
||||
RemoveProject: (projectFile: vscode.Uri): Promise<void> => {
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user