mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-15 02:48:30 -05:00
Add query history telemetry (#20722)
* Add query history telemetry * Update property name * Only send on success * Update name
This commit is contained in:
@@ -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<QueryHistor
|
||||
constructor(private _context: vscode.ExtensionContext, storageUri: vscode.Uri) {
|
||||
this._historyStorageFile = path.join(storageUri.fsPath, HISTORY_STORAGE_FILE_NAME);
|
||||
// Kick off initialization but then continue on since that may take a while and we don't want to block extension activation
|
||||
this._initPromise = this.initialize();
|
||||
const initializeAction = new TimedAction(TelemetryViews.QueryHistoryProvider, TelemetryActions.Initialize);
|
||||
this._initPromise = this.initialize().then(() => 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<QueryHistor
|
||||
// We need to compare URIs, but the event Uri comes in as string so while it should be in the same format as
|
||||
// the textDocument uri.toString() we parse it into a vscode.Uri first to be absolutely sure.
|
||||
if (textEditor?.document.uri.toString() !== vscode.Uri.parse(document.uri).toString()) {
|
||||
TelemetryReporter.sendErrorEvent(TelemetryViews.QueryHistoryProvider, 'UriMismatch');
|
||||
// If we couldn't find the document then we can't get the text so just log the error and move on
|
||||
console.error(`Active text editor ${textEditor?.document.uri} does not match URI ${document.uri} for query event`);
|
||||
return;
|
||||
@@ -114,6 +117,7 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistor
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error getting persistance storage IV: ${err}`);
|
||||
TelemetryReporter.sendErrorEvent(TelemetryViews.QueryHistoryProvider, 'InitializingIV');
|
||||
// An IV is required to read/write the encrypted file so if we can't get it then just fail early
|
||||
return;
|
||||
}
|
||||
@@ -129,21 +133,31 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistor
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error getting persistance storage key: ${err}`);
|
||||
TelemetryReporter.sendErrorEvent(TelemetryViews.QueryHistoryProvider, 'InitializingKey');
|
||||
// A key is required to read/write the encrypted file so if we can't get it then just fail early
|
||||
return;
|
||||
}
|
||||
|
||||
this.writeHistoryFileWorker = (): void => {
|
||||
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<QueryHistor
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const readStorageFileAction = new TimedAction(TelemetryViews.QueryHistoryProvider, TelemetryActions.ReadStorageFile);
|
||||
// Read and decrypt any previous history items
|
||||
const encryptedItems = await fs.promises.readFile(this._historyStorageFile);
|
||||
readStorageFileAction.send();
|
||||
const decipher = crypto.createDecipheriv(STORAGE_ENCRYPTION_ALGORITHM, key, iv);
|
||||
const result = Buffer.concat([decipher.update(encryptedItems), decipher.final()]).toString();
|
||||
this._queryHistoryItems = JSON.parse(result);
|
||||
@@ -165,6 +182,7 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistor
|
||||
} catch (err) {
|
||||
// Ignore ENOENT errors, those are expected if the storage file doesn't exist (on first run or if results aren't being persisted)
|
||||
if (err.code !== 'ENOENT') {
|
||||
TelemetryReporter.sendErrorEvent(TelemetryViews.QueryHistoryProvider, 'ReadStorageFile');
|
||||
console.error(`Error deserializing stored history items: ${err}`);
|
||||
void vscode.window.showWarningMessage(loc.errorLoading(err));
|
||||
// Rename the file to avoid attempting to load a potentially corrupted or unreadable file every time we start up, we'll make
|
||||
@@ -173,6 +191,7 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistor
|
||||
const bakPath = path.join(path.dirname(this._historyStorageFile), `${HISTORY_STORAGE_FILE_NAME}.bak`);
|
||||
await fs.promises.rename(this._historyStorageFile, bakPath);
|
||||
} catch (err) {
|
||||
TelemetryReporter.sendErrorEvent(TelemetryViews.QueryHistoryProvider, 'MovingBadStorageFile');
|
||||
console.error(`Error moving corrupted history file: ${err}`);
|
||||
}
|
||||
}
|
||||
@@ -231,9 +250,21 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistor
|
||||
|
||||
private async updateConfigurationValues(): Promise<void> {
|
||||
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<QueryHistor
|
||||
} catch (err) {
|
||||
// Ignore ENOENT errors, those are expected if the storage file doesn't exist (on first run or if results aren't being persisted)
|
||||
if (err.code !== 'ENOENT') {
|
||||
TelemetryReporter.sendErrorEvent(TelemetryViews.QueryHistoryProvider, 'CleaningUpStorageFile');
|
||||
// Best effort, we don't want other things to fail if we can't delete the file for some reason
|
||||
console.error(`Error cleaning up query history storage: ${this._historyStorageFile}. ${err}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user