Agent feature usage metrics (#3346)

* agent feature usage metrics

* generalized feature telemetry via dialogs

* renamed eventName property to dialogName

* made dialogName an optional field
This commit is contained in:
Aditya Bist
2018-12-05 11:01:46 -08:00
committed by GitHub
parent 8f817ce689
commit c21611661b
20 changed files with 133 additions and 52 deletions

View File

@@ -25,7 +25,6 @@ export const NewQuery = 'NewQuery';
export const FirewallRuleRequested = 'FirewallRuleCreated';
export const DashboardNavigated = 'DashboardNavigated';
// Telemetry Properties
// Modal Dialogs:
@@ -42,3 +41,21 @@ export const Accounts = 'Accounts';
export const FireWallRule = 'FirewallRule';
export const AutoOAuth = 'AutoOAuth';
export const AddNewDashboardTab = 'AddNewDashboardTab';
// SQL Agent Events:
// Views
export const JobsView = 'JobsViewOpened';
export const JobHistoryView = 'JobHistoryViewOpened';
export const JobStepsView = 'JobStepsViewOpened';
// Actions
export const RunAgentJob = 'RunAgentJob';
export const StopAgentJob = 'StopAgentJob';
export const DeleteAgentJob = 'DeleteAgentJob';
export const DeleteAgentJobStep = 'DeleteAgentJobStep';
export const DeleteAgentAlert = 'DeleteAgentAlert';
export const DeleteAgentOperator = 'DeleteAgentOperator';
export const DeleteAgentProxy = 'DeleteAgentProxy';

View File

@@ -4,31 +4,10 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as crypto from 'crypto';
import * as os from 'os';
import { ITelemetryService, ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { warn } from 'sql/base/common/log';
import { generateUuid } from 'vs/base/common/uuid';
// Generate a unique, deterministic ID for the current user of the extension
export function generateUserId(): Promise<string> {
return new Promise<string>(resolve => {
try {
getmac.getMac((error, macAddress) => {
if (!error) {
resolve(crypto.createHash('sha256').update(macAddress + os.homedir(), 'utf8').digest('hex'));
} else {
resolve(generateUuid()); // fallback
}
});
} catch (err) {
resolve(generateUuid()); // fallback
}
});
}
export interface IConnectionTelemetryData extends ITelemetryData {
provider?: string;
}

View File

@@ -17,6 +17,9 @@ import { AlertsViewComponent } from 'sql/parts/jobManagement/views/alertsView.co
import { OperatorsViewComponent } from 'sql/parts/jobManagement/views/operatorsView.component';
import { ProxiesViewComponent } from 'sql/parts/jobManagement/views/proxiesView.component';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
import { telemetryURIDescriptor } from 'vs/platform/telemetry/common/telemetryUtils';
export enum JobActions {
Run = 'run',
@@ -80,7 +83,8 @@ export class RunJobAction extends Action {
constructor(
@INotificationService private notificationService: INotificationService,
@IJobManagementService private jobManagementService: IJobManagementService,
@IInstantiationService private instantationService: IInstantiationService
@IInstantiationService private instantationService: IInstantiationService,
@ITelemetryService private telemetryService: ITelemetryService
) {
super(RunJobAction.ID, RunJobAction.LABEL, 'runJobIcon');
}
@@ -89,6 +93,7 @@ export class RunJobAction extends Action {
let jobName = context.agentJobInfo.name;
let ownerUri = context.ownerUri;
let refreshAction = this.instantationService.createInstance(JobsRefreshAction);
this.telemetryService.publicLog(TelemetryKeys.RunAgentJob);
return new TPromise<boolean>((resolve, reject) => {
this.jobManagementService.jobAction(ownerUri, jobName, JobActions.Run).then(result => {
if (result.success) {
@@ -118,7 +123,8 @@ export class StopJobAction extends Action {
constructor(
@INotificationService private notificationService: INotificationService,
@IJobManagementService private jobManagementService: IJobManagementService,
@IInstantiationService private instantationService: IInstantiationService
@IInstantiationService private instantationService: IInstantiationService,
@ITelemetryService private telemetryService: ITelemetryService
) {
super(StopJobAction.ID, StopJobAction.LABEL, 'stopJobIcon');
}
@@ -127,6 +133,7 @@ export class StopJobAction extends Action {
let jobName = context.agentJobInfo.name;
let ownerUri = context.ownerUri;
let refreshAction = this.instantationService.createInstance(JobsRefreshAction);
this.telemetryService.publicLog(TelemetryKeys.StopAgentJob);
return new TPromise<boolean>((resolve, reject) => {
this.jobManagementService.jobAction(ownerUri, jobName, JobActions.Stop).then(result => {
if (result.success) {
@@ -174,7 +181,8 @@ export class DeleteJobAction extends Action {
constructor(
@INotificationService private _notificationService: INotificationService,
@IJobManagementService private _jobService: IJobManagementService
@IJobManagementService private _jobService: IJobManagementService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
super(DeleteJobAction.ID, DeleteJobAction.LABEL);
}
@@ -188,6 +196,7 @@ export class DeleteJobAction extends Action {
[{
label: DeleteJobAction.LABEL,
run: () => {
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentJob);
self._jobService.deleteJob(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
if (!result || !result.success) {
let errorMessage = nls.localize("jobaction.failedToDeleteJob", "Could not delete job '{0}'.\nError: {1}",
@@ -234,7 +243,8 @@ export class DeleteStepAction extends Action {
constructor(
@INotificationService private _notificationService: INotificationService,
@IJobManagementService private _jobService: IJobManagementService,
@IInstantiationService private instantationService: IInstantiationService
@IInstantiationService private instantationService: IInstantiationService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
super(DeleteStepAction.ID, DeleteStepAction.LABEL);
}
@@ -249,6 +259,7 @@ export class DeleteStepAction extends Action {
[{
label: DeleteStepAction.LABEL,
run: () => {
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentJobStep);
self._jobService.deleteJobStep(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
if (!result || !result.success) {
let errorMessage = nls.localize("jobaction.failedToDeleteStep", "Could not delete step '{0}'.\nError: {1}",
@@ -318,7 +329,8 @@ export class DeleteAlertAction extends Action {
constructor(
@INotificationService private _notificationService: INotificationService,
@IJobManagementService private _jobService: IJobManagementService
@IJobManagementService private _jobService: IJobManagementService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
super(DeleteAlertAction.ID, DeleteAlertAction.LABEL);
}
@@ -332,6 +344,7 @@ export class DeleteAlertAction extends Action {
[{
label: DeleteAlertAction.LABEL,
run: () => {
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentAlert);
self._jobService.deleteAlert(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
if (!result || !result.success) {
let errorMessage = nls.localize("jobaction.failedToDeleteAlert", "Could not delete alert '{0}'.\nError: {1}",
@@ -397,7 +410,8 @@ export class DeleteOperatorAction extends Action {
constructor(
@INotificationService private _notificationService: INotificationService,
@IJobManagementService private _jobService: IJobManagementService
@IJobManagementService private _jobService: IJobManagementService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
super(DeleteOperatorAction.ID, DeleteOperatorAction.LABEL);
}
@@ -411,6 +425,7 @@ export class DeleteOperatorAction extends Action {
[{
label: DeleteOperatorAction.LABEL,
run: () => {
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentOperator);
self._jobService.deleteOperator(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
if (!result || !result.success) {
let errorMessage = nls.localize("jobaction.failedToDeleteOperator", "Could not delete operator '{0}'.\nError: {1}",
@@ -477,7 +492,8 @@ export class DeleteProxyAction extends Action {
constructor(
@INotificationService private _notificationService: INotificationService,
@IJobManagementService private _jobService: IJobManagementService
@IJobManagementService private _jobService: IJobManagementService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
super(DeleteProxyAction.ID, DeleteProxyAction.LABEL);
}
@@ -491,6 +507,7 @@ export class DeleteProxyAction extends Action {
[{
label: DeleteProxyAction.LABEL,
run: () => {
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentProxy);
self._jobService.deleteProxy(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
if (!result || !result.success) {
let errorMessage = nls.localize("jobaction.failedToDeleteProxy", "Could not delete proxy '{0}'.\nError: {1}",

View File

@@ -29,6 +29,8 @@ import { JobManagementView } from 'sql/parts/jobManagement/views/jobManagementVi
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { IDashboardService } from 'sql/services/dashboard/common/dashboardService';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
export const DASHBOARD_SELECTOR: string = 'jobhistory-component';
@@ -76,7 +78,8 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
@Inject(IContextMenuService) private contextMenuService: IContextMenuService,
@Inject(IJobManagementService) private _jobManagementService: IJobManagementService,
@Inject(IKeybindingService) keybindingService: IKeybindingService,
@Inject(IDashboardService) dashboardService: IDashboardService
@Inject(IDashboardService) dashboardService: IDashboardService,
@Inject(ITelemetryService) private _telemetryService: ITelemetryService
) {
super(commonService, dashboardService, contextMenuService, keybindingService, instantiationService);
this._treeController = new JobHistoryController();
@@ -142,6 +145,7 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
this._register(attachListStyler(this._tree, this.themeService));
this._tree.layout(dom.getContentHeight(this._tableContainer.nativeElement));
this.initActionBar();
this._telemetryService.publicLog(TelemetryKeys.JobHistoryView);
}
private loadHistory() {

View File

@@ -21,6 +21,8 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
export const JOBSTEPSVIEW_SELECTOR: string = 'jobstepsview-component';
@@ -48,7 +50,8 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit,
@Inject(IInstantiationService) instantiationService: IInstantiationService,
@Inject(IContextMenuService) contextMenuService: IContextMenuService,
@Inject(IKeybindingService) keybindingService: IKeybindingService,
@Inject(IDashboardService) dashboardService: IDashboardService
@Inject(IDashboardService) dashboardService: IDashboardService,
@Inject(ITelemetryService) private _telemetryService: ITelemetryService
) {
super(commonService, dashboardService, contextMenuService, keybindingService, instantiationService);
}
@@ -72,6 +75,7 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit,
}, {verticalScrollMode: ScrollbarVisibility.Visible, horizontalScrollMode: ScrollbarVisibility.Visible });
this.layout();
this._register(attachListStyler(this._tree, this.themeService));
this._telemetryService.publicLog(TelemetryKeys.JobStepsView);
}
public onFirstVisible() {

View File

@@ -37,6 +37,8 @@ import { IDashboardService } from 'sql/services/dashboard/common/dashboardServic
import { escape } from 'sql/base/common/strings';
import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { tableBackground, cellBackground, cellBorderColor } from 'sql/common/theme/colors';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
export const JOBSVIEW_SELECTOR: string = 'jobsview-component';
export const ROW_HEIGHT: number = 45;
@@ -106,7 +108,8 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
@Inject(IInstantiationService) instantiationService: IInstantiationService,
@Inject(IContextMenuService) contextMenuService: IContextMenuService,
@Inject(IKeybindingService) keybindingService: IKeybindingService,
@Inject(IDashboardService) _dashboardService: IDashboardService
@Inject(IDashboardService) _dashboardService: IDashboardService,
@Inject(ITelemetryService) private _telemetryService: ITelemetryService
) {
super(commonService, _dashboardService, contextMenuService, keybindingService, instantiationService);
this._didTabChange = false;
@@ -127,6 +130,7 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
this._visibilityElement = this._gridEl;
this._parentComponent = this._agentViewComponent;
this._register(this._themeService.onDidColorThemeChange(e => this.updateTheme(e)));
this._telemetryService.publicLog(TelemetryKeys.JobsView);
}
ngOnDestroy() {

View File

@@ -22,8 +22,9 @@ export class CustomDialogService {
constructor( @IInstantiationService private _instantiationService: IInstantiationService) { }
public showDialog(dialog: Dialog, options?: IModalOptions): void {
let dialogModal = this._instantiationService.createInstance(DialogModal, dialog, 'CustomDialog', options || defaultOptions);
public showDialog(dialog: Dialog, dialogName?: string, options?: IModalOptions): void {
let name = dialogName ? dialogName : 'CustomDialog';
let dialogModal = this._instantiationService.createInstance(DialogModal, dialog, name, options || defaultOptions);
this._dialogModals.set(dialog, dialogModal);
dialogModal.render();
dialogModal.open();

View File

@@ -839,7 +839,7 @@ declare module 'sqlops' {
* Create a dialog with the given title
* @param title The title of the dialog, displayed at the top
*/
export function createDialog(title: string): Dialog;
export function createDialog(title: string, dialogName?: string): Dialog;
/**
* Create a dialog tab which can be included as part of the content of a dialog
@@ -951,6 +951,12 @@ declare module 'sqlops' {
*/
message: DialogMessage;
/**
* Set the dialog name when opening
* the dialog for telemetry
*/
dialogName?: string;
/**
* Register a callback that will be called when the user tries to click done. Only
* one callback can be registered at once, so each registration call will clear

View File

@@ -16,6 +16,8 @@ import * as sqlops from 'sqlops';
import { SqlMainContext, ExtHostModelViewDialogShape, MainThreadModelViewDialogShape, ExtHostModelViewShape, ExtHostBackgroundTaskManagementShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
import { IItemConfig, ModelComponentTypes, IComponentShape } from 'sql/workbench/api/common/sqlExtHostTypes';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { Inject } from '@angular/core';
const DONE_LABEL = nls.localize('dialogDoneLabel', 'Done');
const CANCEL_LABEL = nls.localize('dialogCancelLabel', 'Cancel');
@@ -125,6 +127,7 @@ class DialogImpl extends ModelViewPanelImpl implements sqlops.window.modelviewdi
private _message: sqlops.window.modelviewdialog.DialogMessage;
private _closeValidator: () => boolean | Thenable<boolean>;
private _operationHandler: BackgroundOperationHandler;
private _dialogName: string;
constructor(extHostModelViewDialog: ExtHostModelViewDialog,
extHostModelView: ExtHostModelViewShape,
@@ -157,6 +160,14 @@ class DialogImpl extends ModelViewPanelImpl implements sqlops.window.modelviewdi
this._extHostModelViewDialog.updateDialogContent(this);
}
public get dialogName(): string {
return this._dialogName;
}
public set dialogName(value: string) {
this._dialogName = value;
}
public registerCloseValidator(validator: () => boolean | Thenable<boolean>): void {
this._closeValidator = validator;
}
@@ -503,7 +514,8 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape {
public openDialog(dialog: sqlops.window.modelviewdialog.Dialog): void {
let handle = this.getHandle(dialog);
this.updateDialogContent(dialog);
this._proxy.$openDialog(handle);
dialog.dialogName ? this._proxy.$openDialog(handle, dialog.dialogName) :
this._proxy.$openDialog(handle);
}
public closeDialog(dialog: sqlops.window.modelviewdialog.Dialog): void {
@@ -560,8 +572,11 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape {
this._onClickCallbacks.set(handle, callback);
}
public createDialog(title: string, extensionLocation?: URI): sqlops.window.modelviewdialog.Dialog {
public createDialog(title: string, dialogName?: string, extensionLocation?: URI): sqlops.window.modelviewdialog.Dialog {
let dialog = new DialogImpl(this, this._extHostModelView, this._extHostTaskManagement, extensionLocation);
if (dialogName) {
dialog.dialogName = dialogName;
}
dialog.title = title;
dialog.handle = this.getHandle(dialog);
return dialog;

View File

@@ -19,6 +19,7 @@ import { ModelViewInput, ModelViewInputModel, ModeViewSaveHandler } from 'sql/pa
import * as vscode from 'vscode';
import * as sqlops from 'sqlops';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@extHostNamedCustomer(SqlMainContext.MainThreadModelViewDialog)
export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape {
@@ -35,7 +36,8 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape
constructor(
context: IExtHostContext,
@IInstantiationService private _instatiationService: IInstantiationService,
@IEditorService private _editorService: IEditorService
@IEditorService private _editorService: IEditorService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
this._proxy = context.getProxy(SqlExtHostContext.ExtHostModelViewDialog);
this._dialogService = new CustomDialogService(_instatiationService);
@@ -68,9 +70,9 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape
return this._proxy.$handleSave(handle);
}
public $openDialog(handle: number): Thenable<void> {
public $openDialog(handle: number, dialogName?: string): Thenable<void> {
let dialog = this.getDialog(handle);
this._dialogService.showDialog(dialog);
dialogName ? this._dialogService.showDialog(dialog, dialogName) : this._dialogService.showDialog(dialog);
return Promise.resolve();
}

View File

@@ -348,8 +348,8 @@ export function createApiFactory(
};
const modelViewDialog: typeof sqlops.window.modelviewdialog = {
createDialog(title: string): sqlops.window.modelviewdialog.Dialog {
return extHostModelViewDialog.createDialog(title, extension.extensionLocation);
createDialog(title: string, dialogName?: string): sqlops.window.modelviewdialog.Dialog {
return extHostModelViewDialog.createDialog(title, dialogName, extension.extensionLocation);
},
createTab(title: string): sqlops.window.modelviewdialog.DialogTab {
return extHostModelViewDialog.createTab(title, extension.extensionLocation);

View File

@@ -716,7 +716,7 @@ export interface ExtHostModelViewDialogShape {
export interface MainThreadModelViewDialogShape extends IDisposable {
$openEditor(handle: number, modelViewId: string, title: string, options?: sqlops.ModelViewEditorOptions, position?: vscode.ViewColumn): Thenable<void>;
$openDialog(handle: number): Thenable<void>;
$openDialog(handle: number, dialogName?: string): Thenable<void>;
$closeDialog(handle: number): Thenable<void>;
$setDialogDetails(handle: number, details: IModelViewDialogDetails): Thenable<void>;
$setTabDetails(handle: number, details: IModelViewTabDetails): Thenable<void>;