diff --git a/extensions/query-history/package.json b/extensions/query-history/package.json index 9d9b5f8bd1..aa642488c2 100644 --- a/extensions/query-history/package.json +++ b/extensions/query-history/package.json @@ -209,6 +209,7 @@ } }, "dependencies": { + "@microsoft/ads-extension-telemetry": "1.2.0", "vscode-nls": "^4.1.2" }, "devDependencies": { diff --git a/extensions/query-history/src/main.ts b/extensions/query-history/src/main.ts index fe5055271f..41b9f52deb 100644 --- a/extensions/query-history/src/main.ts +++ b/extensions/query-history/src/main.ts @@ -9,6 +9,7 @@ import { DOUBLE_CLICK_ACTION_CONFIG_SECTION, ITEM_SELECTED_COMMAND_ID, QUERY_HIS import { QueryHistoryItem } from './queryHistoryItem'; import { QueryHistoryProvider, setLoadingContext } from './queryHistoryProvider'; import { promises as fs } from 'fs'; +import { TelemetryActions, TelemetryReporter, TelemetryViews } from './telemetry'; let lastSelectedItem: { item: QueryHistoryItem | undefined, time: number | undefined } = { item: undefined, @@ -26,6 +27,7 @@ export async function activate(context: vscode.ExtensionContext): Promise await fs.mkdir(storageUri.fsPath); } catch (err) { if (err.code !== 'EEXIST') { + TelemetryReporter.sendErrorEvent(TelemetryViews.QueryHistory, 'CreatingStorageFolder'); console.error(`Error creating query history global storage folder ${context.globalStorageUri.fsPath}. ${err}`); } } @@ -44,6 +46,7 @@ export async function activate(context: vscode.ExtensionContext): Promise const clickTime = new Date().getTime(); if (lastSelectedItem.item === selectedItem && lastSelectedItem.time && (clickTime - lastSelectedItem.time) < DOUBLE_CLICK_TIMEOUT_MS) { const doubleClickAction = vscode.workspace.getConfiguration(QUERY_HISTORY_CONFIG_SECTION).get(DOUBLE_CLICK_ACTION_CONFIG_SECTION); + TelemetryReporter.sendActionEvent(TelemetryViews.QueryHistory, TelemetryActions.DoubleClick, doubleClickAction); switch (doubleClickAction) { case 'run': await runQuery(selectedItem); @@ -91,21 +94,37 @@ export async function activate(context: vscode.ExtensionContext): Promise } async function openQuery(item: QueryHistoryItem): Promise { - await azdata.queryeditor.openQueryDocument( - { - content: item.queryText - }, item.connectionProfile?.providerId); + try { + await azdata.queryeditor.openQueryDocument( + { + content: item.queryText + }, item.connectionProfile?.providerId); + } catch (err) { + TelemetryReporter.sendErrorEvent(TelemetryViews.QueryHistory, 'OpenQuery'); + } + } async function runQuery(item: QueryHistoryItem): Promise { - const doc = await azdata.queryeditor.openQueryDocument( - { - content: item.queryText - }, item.connectionProfile?.providerId); - if (item.connectionProfile) { - await doc.connect(item.connectionProfile); - } else { - await azdata.queryeditor.connect(doc.uri, ''); + let step = 'OpenDoc'; + try { + const doc = await azdata.queryeditor.openQueryDocument( + { + content: item.queryText + }, item.connectionProfile?.providerId); + if (item.connectionProfile) { + step = 'ConnectWithProfile'; + await doc.connect(item.connectionProfile); + } else { + step = 'ConnectWithoutProfile'; + await azdata.queryeditor.connect(doc.uri, ''); + } + step = 'Run'; + azdata.queryeditor.runQuery(doc.uri); + } catch (err) { + TelemetryReporter.createErrorEvent(TelemetryViews.QueryHistory, 'RunQuery') + .withAdditionalProperties({ step }) + .send(); } - azdata.queryeditor.runQuery(doc.uri); + } diff --git a/extensions/query-history/src/queryHistoryProvider.ts b/extensions/query-history/src/queryHistoryProvider.ts index f19054792d..cb510dbc9d 100644 --- a/extensions/query-history/src/queryHistoryProvider.ts +++ b/extensions/query-history/src/queryHistoryProvider.ts @@ -12,6 +12,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as crypto from 'crypto'; import * as loc from './localizedConstants'; +import { sendSettingChangedEvent, TelemetryActions, TelemetryReporter, TelemetryViews, TimedAction } from './telemetry'; const STORAGE_IV_KEY = 'queryHistory.storage-iv'; const STORAGE_KEY_KEY = 'queryHistory.storage-key'; @@ -50,7 +51,8 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider initializeAction.send()); this._disposables.push(vscode.workspace.onDidChangeConfiguration(async e => { if (e.affectsConfiguration(QUERY_HISTORY_CONFIG_SECTION) || e.affectsConfiguration(MAX_ENTRIES_CONFIG_SECTION)) { await this.updateConfigurationValues(); @@ -82,6 +84,7 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider { if (this._persistHistory) { + try { // We store the history entries in an encrypted file because they may contain sensitive information // such as passwords (even in the query text itself) const cipher = crypto.createCipheriv(STORAGE_ENCRYPTION_ALGORITHM, key!, iv!); const stringifiedItems = JSON.stringify(this._queryHistoryItems); const encryptedText = Buffer.concat([cipher.update(Buffer.from(stringifiedItems)), cipher.final()]); + const writeStorageFileAction = new TimedAction(TelemetryViews.QueryHistoryProvider, TelemetryActions.WriteStorageFile, + {}, + { + ItemCount: this._queryHistoryItems.length, + ItemLengthChars: stringifiedItems.length + }); // Use sync here so that we can write this out when the object is disposed fs.writeFileSync(this._historyStorageFile, encryptedText); + writeStorageFileAction.send(); } catch (err) { + TelemetryReporter.sendErrorEvent(TelemetryViews.QueryHistoryProvider, 'WriteStorageFile'); console.error(`Error writing query history to disk: ${err}`); } @@ -155,9 +169,12 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider { const configSection = vscode.workspace.getConfiguration(QUERY_HISTORY_CONFIG_SECTION); - this._captureEnabled = configSection.get(CAPTURE_ENABLED_CONFIG_SECTION, DEFAULT_CAPTURE_ENABLED); - this._persistHistory = configSection.get(PERSIST_HISTORY_CONFIG_SECTION, DEFAULT_PERSIST_HISTORY); - this._maxEntries = configSection.get(MAX_ENTRIES_CONFIG_SECTION, DEFAULT_MAX_ENTRIES); + const newCaptureEnabled = configSection.get(CAPTURE_ENABLED_CONFIG_SECTION, DEFAULT_CAPTURE_ENABLED); + if (this._captureEnabled !== newCaptureEnabled) { + sendSettingChangedEvent('CaptureEnabled', String(this._captureEnabled), String(newCaptureEnabled)); + this._captureEnabled = newCaptureEnabled; + } + const newPersistHistory = configSection.get(PERSIST_HISTORY_CONFIG_SECTION, DEFAULT_PERSIST_HISTORY); + if (this._persistHistory !== newPersistHistory) { + sendSettingChangedEvent('PersistHistory', String(this._persistHistory), String(newPersistHistory)); + this._persistHistory = newPersistHistory; + } + const newMaxEntries = configSection.get(MAX_ENTRIES_CONFIG_SECTION, DEFAULT_MAX_ENTRIES); + if (this._maxEntries !== newMaxEntries) { + sendSettingChangedEvent('MaxEntries', String(this._maxEntries), String(newMaxEntries)); + this._maxEntries = newMaxEntries; + } this.trimExtraEntries(); if (!this._persistHistory) { // We're not persisting history so we can immediately set loading to false to immediately @@ -246,6 +277,7 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider