diff --git a/src/sql/platform/telemetry/common/telemetryKeys.ts b/src/sql/platform/telemetry/common/telemetryKeys.ts index a2b55d77ab..aef90496d0 100644 --- a/src/sql/platform/telemetry/common/telemetryKeys.ts +++ b/src/sql/platform/telemetry/common/telemetryKeys.ts @@ -96,7 +96,9 @@ export const enum NbTelemetryAction { RunAll = 'RunNotebook', AddCell = 'AddCell', KernelChanged = 'KernelChanged', - NewNotebookFromConnections = 'NewNotebookWithConnectionProfile' + NewNotebookFromConnections = 'NewNotebookWithConnectionProfile', + UndoCell = 'UndoCell', + RedoCell = 'RedoCell' } export const enum TelemetryPropertyName { diff --git a/src/sql/workbench/contrib/notebook/browser/notebookActions.ts b/src/sql/workbench/contrib/notebook/browser/notebookActions.ts index 2af1d1ad2b..2c69abb7fa 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookActions.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookActions.ts @@ -61,7 +61,6 @@ export class AddCellAction extends Action { constructor( id: string, label: string, cssClass: string, @INotebookService private _notebookService: INotebookService, - @IAdsTelemetryService private _telemetryService: IAdsTelemetryService, ) { super(id, label, cssClass); } @@ -76,16 +75,15 @@ export class AddCellAction extends Action { } if (context?.model) { context.model.addCell(this.cellType, index); + context.model.sendNotebookTelemetryActionEvent(TelemetryKeys.NbTelemetryAction.AddCell, { cell_type: this.cellType }); } } else { //Add Cell after current selected cell. const editor = this._notebookService.findNotebookEditor(context); const index = editor.cells?.findIndex(cell => cell.active) ?? 0; editor.addCell(this.cellType, index); + editor.model.sendNotebookTelemetryActionEvent(TelemetryKeys.NbTelemetryAction.AddCell, { cell_type: this.cellType }); } - this._telemetryService.createActionEvent(TelemetryKeys.TelemetryView.Notebook, TelemetryKeys.NbTelemetryAction.AddCell) - .withAdditionalProperties({ cell_type: this.cellType }) - .send(); } } @@ -369,19 +367,13 @@ export class RunAllCellsAction extends Action { id: string, label: string, cssClass: string, @INotificationService private notificationService: INotificationService, @INotebookService private _notebookService: INotebookService, - @IAdsTelemetryService private _telemetryService: IAdsTelemetryService, ) { super(id, label, cssClass); } public override async run(context: URI): Promise { try { const editor = this._notebookService.findNotebookEditor(context); - - const azdata_notebook_guid: string = editor.model.getMetaValue('azdata_notebook_guid'); - this._telemetryService.createActionEvent(TelemetryKeys.TelemetryView.Notebook, TelemetryKeys.NbTelemetryAction.RunAll) - .withAdditionalProperties({ azdata_notebook_guid }) - .send(); - + editor.model.sendNotebookTelemetryActionEvent(TelemetryKeys.NbTelemetryAction.RunAll); await editor.runAllCells(); } catch (e) { this.notificationService.error(getErrorMessage(e)); diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookActions.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookActions.test.ts index cc2f35a0ed..d495186415 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookActions.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookActions.test.ts @@ -29,6 +29,8 @@ import { MockQuickInputService } from 'sql/workbench/contrib/notebook/test/commo import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { Separator } from 'vs/base/common/actions'; import { INotebookView, INotebookViews } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews'; +import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; +import { ITelemetryEventProperties } from 'sql/platform/telemetry/common/telemetry'; class TestClientSession extends ClientSessionStub { private _errorState: boolean = false; @@ -106,6 +108,9 @@ class TestNotebookModel extends NotebookModelStub { public override getStandardKernelFromName(name: string): IStandardKernelWithProvider { return this._standardKernelsMap.get(name); } + + public override sendNotebookTelemetryActionEvent(action: TelemetryKeys.TelemetryAction | TelemetryKeys.NbTelemetryAction, additionalProperties?: ITelemetryEventProperties): void { + } } suite('Notebook Actions', function (): void { @@ -113,9 +118,11 @@ suite('Notebook Actions', function (): void { let mockNotebookEditor: TypeMoq.Mock; let mockNotebookService: TypeMoq.Mock; const testUri = URI.parse('untitled'); + let testNotebookModel = new TestNotebookModel(); suiteSetup(function (): void { mockNotebookEditor = TypeMoq.Mock.ofType(NotebookEditorStub); + mockNotebookEditor.setup(x => x.model).returns(() => testNotebookModel); mockNotebookService = TypeMoq.Mock.ofType(NotebookServiceStub); mockNotebookService.setup(x => x.findNotebookEditor(TypeMoq.It.isAny())).returns(uri => mockNotebookEditor.object); }); @@ -129,7 +136,7 @@ suite('Notebook Actions', function (): void { let actualCellType: CellType; - let action = new AddCellAction('TestId', 'TestLabel', 'TestClass', mockNotebookService.object, new NullAdsTelemetryService()); + let action = new AddCellAction('TestId', 'TestLabel', 'TestClass', mockNotebookService.object); action.cellType = testCellType; // Normal use case @@ -196,7 +203,7 @@ suite('Notebook Actions', function (): void { let mockNotification = TypeMoq.Mock.ofType(TestNotificationService); mockNotification.setup(n => n.notify(TypeMoq.It.isAny())); - let action = new RunAllCellsAction('TestId', 'TestLabel', 'TestClass', mockNotification.object, mockNotebookService.object, new NullAdsTelemetryService()); + let action = new RunAllCellsAction('TestId', 'TestLabel', 'TestClass', mockNotification.object, mockNotebookService.object); // Normal use case mockNotebookEditor.setup(c => c.runAllCells()).returns(() => Promise.resolve(true)); diff --git a/src/sql/workbench/contrib/notebook/test/stubs.ts b/src/sql/workbench/contrib/notebook/test/stubs.ts index 340582dc66..d246e9d431 100644 --- a/src/sql/workbench/contrib/notebook/test/stubs.ts +++ b/src/sql/workbench/contrib/notebook/test/stubs.ts @@ -21,6 +21,8 @@ import { IEditorPane } from 'vs/workbench/common/editor'; import { INotebookShowOptions } from 'sql/workbench/api/common/sqlExtHost.protocol'; import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension'; import { INotebookView, INotebookViewCell, INotebookViewMetadata, INotebookViews } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews'; +import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; +import { ITelemetryEventProperties } from 'sql/platform/telemetry/common/telemetry'; export class NotebookModelStub implements INotebookModel { constructor(private _languageInfo?: nb.ILanguageInfo, private _cells?: ICellModel[], private _testContents?: nb.INotebookContents) { @@ -158,6 +160,8 @@ export class NotebookModelStub implements INotebookModel { requestConnection(): Promise { throw new Error('Method not implemented.'); } + sendNotebookTelemetryActionEvent(action: TelemetryKeys.TelemetryAction | TelemetryKeys.NbTelemetryAction, additionalProperties?: ITelemetryEventProperties): void { + } } export class NotebookFindModelStub implements INotebookFindModel { diff --git a/src/sql/workbench/services/notebook/browser/models/cell.ts b/src/sql/workbench/services/notebook/browser/models/cell.ts index 346bb63067..1e2321d939 100644 --- a/src/sql/workbench/services/notebook/browser/models/cell.ts +++ b/src/sql/workbench/services/notebook/browser/models/cell.ts @@ -29,7 +29,6 @@ import { tryMatchCellMagic, extractCellMagicCommandPlusArgs } from 'sql/workbenc import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Disposable } from 'vs/base/common/lifecycle'; import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; -import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import { IInsightOptions } from 'sql/workbench/common/editor/query/chartState'; import { IPosition } from 'vs/editor/common/core/position'; @@ -94,7 +93,6 @@ export class CellModel extends Disposable implements ICellModel { @optional(INotebookService) private _notebookService?: INotebookService, @optional(ICommandService) private _commandService?: ICommandService, @optional(IConfigurationService) private _configurationService?: IConfigurationService, - @optional(IAdsTelemetryService) private _telemetryService?: IAdsTelemetryService, ) { super(); this.id = `${modelId++}`; @@ -568,10 +566,7 @@ export class CellModel extends Disposable implements ICellModel { this._outputCounter = 0; // Hide IntelliSense suggestions list when running cell to match SSMS behavior this._commandService.executeCommand('hideSuggestWidget'); - const azdata_notebook_guid: string = this.notebookModel.getMetaValue('azdata_notebook_guid'); - this._telemetryService?.createActionEvent(TelemetryKeys.TelemetryView.Notebook, TelemetryKeys.NbTelemetryAction.RunCell) - .withAdditionalProperties({ cell_language: kernel.name, azdata_cell_guid: this._cellGuid, azdata_notebook_guid }) - .send(); + this.notebookModel.sendNotebookTelemetryActionEvent(TelemetryKeys.NbTelemetryAction.RunCell, { cell_language: kernel.name, azdata_cell_guid: this._cellGuid }); // If cell is currently running and user clicks the stop/cancel button, call kernel.interrupt() // This matches the same behavior as JupyterLab if (this.future && this.future.inProgress) { diff --git a/src/sql/workbench/services/notebook/browser/models/cellEdit.ts b/src/sql/workbench/services/notebook/browser/models/cellEdit.ts index 1c3925a83a..437258c4a8 100644 --- a/src/sql/workbench/services/notebook/browser/models/cellEdit.ts +++ b/src/sql/workbench/services/notebook/browser/models/cellEdit.ts @@ -6,12 +6,14 @@ import { IResourceUndoRedoElement, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; import { ICellModel, MoveDirection } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel'; +import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; import { localize } from 'vs/nls'; export class MoveCellEdit implements IResourceUndoRedoElement { type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; label: string = localize('moveCellEdit', "Move Cell"); resource = this.model.notebookUri; + private readonly cellOperation = { cell_operation: 'move_cell' }; constructor(private model: NotebookModel, private cell: ICellModel, private moveDirection: MoveDirection) { } @@ -19,10 +21,12 @@ export class MoveCellEdit implements IResourceUndoRedoElement { undo(): void { const direction = this.moveDirection === MoveDirection.Down ? MoveDirection.Up : MoveDirection.Down; this.model.moveCell(this.cell, direction, false); + this.model.sendNotebookTelemetryActionEvent(TelemetryKeys.NbTelemetryAction.UndoCell, this.cellOperation); } redo(): void { this.model.moveCell(this.cell, this.moveDirection, false); + this.model.sendNotebookTelemetryActionEvent(TelemetryKeys.NbTelemetryAction.RedoCell, this.cellOperation); } } @@ -30,12 +34,14 @@ export class SplitCellEdit implements IResourceUndoRedoElement { type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; label: string = localize('splitCellEdit', "Split Cell"); resource = this.model.notebookUri; + private readonly cellOperation = { cell_operation: 'split_cell' }; constructor(private model: NotebookModel, private firstCell: ICellModel, private secondCell: ICellModel, private newLinesRemoved: string[]) { } undo(): void { this.model.mergeCells(this.firstCell, this.secondCell, this.newLinesRemoved); + this.model.sendNotebookTelemetryActionEvent(TelemetryKeys.NbTelemetryAction.UndoCell, this.cellOperation); } redo(): void { @@ -47,16 +53,19 @@ export class DeleteCellEdit implements IResourceUndoRedoElement { type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; label: string = localize('deleteCellEdit', "Delete Cell"); resource = this.model.notebookUri; + private readonly cellOperation = { cell_operation: 'delete_cell' }; constructor(private model: NotebookModel, private cell: ICellModel, private index: number) { } undo(): void { this.model.insertCell(this.cell, this.index, false); + this.model.sendNotebookTelemetryActionEvent(TelemetryKeys.NbTelemetryAction.UndoCell, this.cellOperation); } redo(): void { this.model.deleteCell(this.cell, false); + this.model.sendNotebookTelemetryActionEvent(TelemetryKeys.NbTelemetryAction.RedoCell, this.cellOperation); } } @@ -64,15 +73,18 @@ export class AddCellEdit implements IResourceUndoRedoElement { type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; label: string = localize('addCellEdit', "Add Cell"); resource = this.model.notebookUri; + private readonly cellOperation = { cell_operation: 'add_cell' }; constructor(private model: NotebookModel, private cell: ICellModel, private index: number) { } undo(): void { this.model.deleteCell(this.cell, false); + this.model.sendNotebookTelemetryActionEvent(TelemetryKeys.NbTelemetryAction.UndoCell, this.cellOperation); } redo(): void { this.model.insertCell(this.cell, this.index, false); + this.model.sendNotebookTelemetryActionEvent(TelemetryKeys.NbTelemetryAction.RedoCell, this.cellOperation); } } diff --git a/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts b/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts index 48643f8af0..450d2406d2 100644 --- a/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts +++ b/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts @@ -24,6 +24,9 @@ import type { FutureInternal } from 'sql/workbench/services/notebook/browser/int import { ICellValue, ResultSetSummary } from 'sql/workbench/services/query/common/query'; import { QueryResultId } from 'sql/workbench/services/notebook/browser/models/cell'; import { IPosition } from 'vs/editor/common/core/position'; +import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; +import { ITelemetryEventProperties } from 'sql/platform/telemetry/common/telemetry'; + export enum ViewMode { Notebook, @@ -439,6 +442,12 @@ export interface INotebookModel { requestConnection(): Promise; + /** + * Create and send a Notebook Telemetry Event + * @param action Telemetry action + * @param additionalProperties Additional properties to send. + */ + sendNotebookTelemetryActionEvent(action: TelemetryKeys.TelemetryAction | TelemetryKeys.NbTelemetryAction, additionalProperties?: ITelemetryEventProperties): void; } export interface NotebookContentChange { diff --git a/src/sql/workbench/services/notebook/browser/models/notebookModel.ts b/src/sql/workbench/services/notebook/browser/models/notebookModel.ts index 5df7f59a67..ed175fba9c 100644 --- a/src/sql/workbench/services/notebook/browser/models/notebookModel.ts +++ b/src/sql/workbench/services/notebook/browser/models/notebookModel.ts @@ -25,7 +25,7 @@ import { uriPrefixes } from 'sql/platform/connection/common/utils'; import { ILogService } from 'vs/platform/log/common/log'; import { getErrorMessage } from 'vs/base/common/errors'; import { notebookConstants } from 'sql/workbench/services/notebook/browser/interfaces'; -import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; +import { IAdsTelemetryService, ITelemetryEventProperties } from 'sql/platform/telemetry/common/telemetry'; import { Deferred } from 'sql/base/common/promise'; import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; @@ -37,6 +37,8 @@ import { QueryTextEditor } from 'sql/workbench/browser/modelComponents/queryText import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { AddCellEdit, DeleteCellEdit, MoveCellEdit, SplitCellEdit } from 'sql/workbench/services/notebook/browser/models/cellEdit'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { deepClone } from 'vs/base/common/objects'; + /* * Used to control whether a message in a dialog/wizard is displayed as an error, * warning, or informational message. Default is error. @@ -476,6 +478,14 @@ export class NotebookModel extends Disposable implements INotebookModel { } } + public sendNotebookTelemetryActionEvent(action: TelemetryKeys.TelemetryAction | TelemetryKeys.NbTelemetryAction, additionalProperties: ITelemetryEventProperties = {}): void { + let properties: ITelemetryEventProperties = deepClone(additionalProperties); + properties['azdata_notebook_guid'] = this.getMetaValue('azdata_notebook_guid'); + this.adstelemetryService.createActionEvent(TelemetryKeys.TelemetryView.Notebook, action) + .withAdditionalProperties(properties) + .send(); + } + private loadContentMetadata(metadata: INotebookMetadataInternal): void { this._savedKernelInfo = metadata.kernelspec; this._defaultLanguageInfo = metadata.language_info; @@ -491,9 +501,7 @@ export class NotebookModel extends Disposable implements INotebookModel { if (metadata.azdata_notebook_guid && metadata.azdata_notebook_guid.length === 36) { //Verify if it is actual GUID and then send it to the telemetry if (isUUID(metadata.azdata_notebook_guid)) { - this.adstelemetryService.createActionEvent(TelemetryKeys.TelemetryView.Notebook, TelemetryKeys.TelemetryAction.Open) - .withAdditionalProperties({ azdata_notebook_guid: metadata.azdata_notebook_guid }) - .send(); + this.sendNotebookTelemetryActionEvent(TelemetryKeys.TelemetryAction.Open); } } Object.keys(metadata).forEach(key => { @@ -1162,12 +1170,10 @@ export class NotebookModel extends Disposable implements INotebookModel { } else if (kernel.info) { this.updateLanguageInfo(kernel.info.language_info); } - this.adstelemetryService.createActionEvent(TelemetryKeys.TelemetryView.Notebook, TelemetryKeys.NbTelemetryAction.KernelChanged) - .withAdditionalProperties({ - name: kernel.name, - alias: kernelAlias || '' - }) - .send(); + this.sendNotebookTelemetryActionEvent(TelemetryKeys.NbTelemetryAction.KernelChanged, { + name: kernel.name, + alias: kernelAlias || '' + }); this._kernelChangedEmitter.fire({ newValue: kernel, oldValue: undefined,