From 4e67a84531aaa350a0abf188f9e11224d6045a2a Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Mon, 27 Mar 2017 02:18:44 -0400 Subject: [PATCH] Adds basic telemetry --- src/commands/commands.ts | 3 ++ src/constants.ts | 5 ++- src/extension.ts | 23 ++++++++--- src/logger.ts | 3 ++ src/system/object.ts | 44 ++++++++++++++++++++ src/telemetry.ts | 105 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 176 insertions(+), 7 deletions(-) create mode 100644 src/telemetry.ts diff --git a/src/commands/commands.ts b/src/commands/commands.ts index a34b5b0..6417a3c 100644 --- a/src/commands/commands.ts +++ b/src/commands/commands.ts @@ -1,6 +1,7 @@ 'use strict'; import { commands, Disposable, TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode'; import { BuiltInCommands } from '../constants'; +import { Telemetry } from '../telemetry'; export type Commands = 'gitlens.closeUnchangedFiles' | 'gitlens.copyMessageToClipboard' | 'gitlens.copyShaToClipboard' | 'gitlens.diffDirectory' | 'gitlens.diffWithBranch' | 'gitlens.diffWithNext' | 'gitlens.diffWithPrevious' | 'gitlens.diffLineWithPrevious' | 'gitlens.diffWithWorking' | 'gitlens.diffLineWithWorking' | @@ -54,6 +55,7 @@ export abstract class Command extends Disposable { } protected _execute(...args: any[]): any { + Telemetry.trackEvent(this.command); return this.execute(...args); } @@ -74,6 +76,7 @@ export abstract class EditorCommand extends Disposable { } private _execute(editor: TextEditor, edit: TextEditorEdit, ...args: any[]): any { + Telemetry.trackEvent(this.command); return this.execute(editor, edit, ...args); } diff --git a/src/constants.ts b/src/constants.ts index 80788d6..a94f5af 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -31,4 +31,7 @@ export const WorkspaceState = { GitLensVersion: 'gitlensVersion' as WorkspaceState, RepoPath: 'repoPath' as WorkspaceState, SuppressGitVersionWarning: 'suppressGitVersionWarning' as WorkspaceState -}; \ No newline at end of file +}; + +export const ExtensionId = 'eamodio.gitlens'; +export const ApplicationInsightsKey = 'a9c302f8-6483-4d01-b92c-c159c799c679'; \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index e4ee11e..6f16878 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,4 +1,5 @@ 'use strict'; +import { Objects } from './system'; import { commands, ExtensionContext, extensions, languages, Uri, window, workspace } from 'vscode'; import { BlameabilityTracker } from './blameabilityTracker'; import { BlameActiveLineController } from './blameActiveLineController'; @@ -14,18 +15,20 @@ import { ShowBlameHistoryCommand, ShowFileHistoryCommand } from './commands'; import { ShowLastQuickPickCommand, ShowQuickBranchHistoryCommand, ShowQuickCurrentBranchHistoryCommand, ShowQuickCommitDetailsCommand, ShowQuickCommitFileDetailsCommand, ShowQuickFileHistoryCommand, ShowQuickRepoStatusCommand} from './commands'; import { ToggleCodeLensCommand } from './commands'; import { Keyboard } from './commands'; -import { IAdvancedConfig, IBlameConfig } from './configuration'; -import { BuiltInCommands, WorkspaceState } from './constants'; +import { IConfig } from './configuration'; +import { ApplicationInsightsKey, BuiltInCommands, ExtensionId, WorkspaceState } from './constants'; import { GitContentProvider } from './gitContentProvider'; import { Git, GitService } from './gitService'; import { GitRevisionCodeLensProvider } from './gitRevisionCodeLensProvider'; import { Logger } from './logger'; +import { Telemetry } from './telemetry'; // this method is called when your extension is activated export async function activate(context: ExtensionContext) { Logger.configure(context); + Telemetry.configure(ApplicationInsightsKey); - const gitlens = extensions.getExtension('eamodio.gitlens'); + const gitlens = extensions.getExtension(ExtensionId); const gitlensVersion = gitlens.packageJSON.version; // Workspace not using a folder. No access to git repo. @@ -38,10 +41,10 @@ export async function activate(context: ExtensionContext) { const rootPath = workspace.rootPath.replace(/\\/g, '/'); Logger.log(`GitLens(v${gitlensVersion}) active: ${rootPath}`); - const config = workspace.getConfiguration('gitlens'); - const gitPath = config.get('advanced').git; + const config = workspace.getConfiguration('').get('gitlens'); + const gitPath = config.advanced.git; - configureCssCharacters(config.get('blame')); + configureCssCharacters(config.blame); let repoPath: string; try { @@ -59,6 +62,12 @@ export async function activate(context: ExtensionContext) { const gitVersion = Git.gitInfo().version; Logger.log(`Git version: ${gitVersion}`); + const telemetryContext: { [id: string]: any } = Object.create(null); + telemetryContext.name = ExtensionId; + telemetryContext.version = gitlensVersion; + telemetryContext.git_version = gitVersion; + Telemetry.setContext(telemetryContext); + notifyOnUnsupportedGitVersion(context, gitVersion); notifyOnNewGitLensVersion(context, gitlensVersion); @@ -110,6 +119,8 @@ export async function activate(context: ExtensionContext) { context.subscriptions.push(new ShowQuickFileHistoryCommand(git)); context.subscriptions.push(new ShowQuickRepoStatusCommand(git, repoPath)); context.subscriptions.push(new ToggleCodeLensCommand(git)); + + Telemetry.trackEvent('initialized', Objects.flatten(config, 'config', true)); } // this method is called when your extension is deactivated diff --git a/src/logger.ts b/src/logger.ts index 0b741c3..4b3fd96 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,6 +1,7 @@ 'use strict'; import { ExtensionContext, OutputChannel, window, workspace } from 'vscode'; import { IAdvancedConfig } from './configuration'; +import { Telemetry } from './telemetry'; const ConfigurationName = 'gitlens'; const OutputChannelName = 'GitLens'; @@ -58,6 +59,8 @@ export class Logger { if (level !== OutputLevel.Silent) { output.appendLine([ex, ...params].join(' ')); } + + Telemetry.trackException(ex); } static warn(message?: any, ...params: any[]): void { diff --git a/src/system/object.ts b/src/system/object.ts index 3c53026..d0bc626 100644 --- a/src/system/object.ts +++ b/src/system/object.ts @@ -12,4 +12,48 @@ export namespace Objects { yield [key, o[key]]; } } + + export function flatten(o: any, prefix: string = '', stringify: boolean = false): { [key: string]: any } { + let flattened = Object.create(null); + _flatten(flattened, prefix, o, stringify); + return flattened; + } + + function _flatten(flattened: { [key: string]: any }, key: string, value: any, stringify: boolean = false) { + if (Object(value) !== value) { + if (stringify) { + if (value == null) { + flattened[key] = null; + } + else if (typeof value === 'string') { + flattened[key] = value; + } + else { + flattened[key] = JSON.stringify(value); + } + } + else { + flattened[key] = value; + } + } + else if (Array.isArray(value)) { + let len = value.length; + for (let i = 0; i < len; i++) { + _flatten(flattened, `${key}[${i}]`, value[i], stringify); + } + if (len === 0) { + flattened[key] = null; + } + } + else { + let isEmpty = true; + for (let p in value) { + isEmpty = false; + _flatten(flattened, key ? `${key}.${p}` : p, value[p], stringify); + } + if (isEmpty && key) { + flattened[key] = null; + } + } + } } \ No newline at end of file diff --git a/src/telemetry.ts b/src/telemetry.ts new file mode 100644 index 0000000..c8eabad --- /dev/null +++ b/src/telemetry.ts @@ -0,0 +1,105 @@ +'use strict'; +import { Disposable, workspace } from 'vscode'; +import * as vscode from 'vscode'; +import * as appInsights from 'applicationinsights'; +import * as os from 'os'; + +let _reporter: TelemetryReporter; + +export class Telemetry extends Disposable { + + static configure(key: string) { + _reporter = new TelemetryReporter(key); + } + + static setContext(context?: { [key: string]: string }) { + _reporter && _reporter.setContext(context); + } + + static trackEvent(name: string, properties?: { [key: string]: string }, measurements?: { [key: string]: number; }) { + _reporter && _reporter.trackEvent(name, properties, measurements); + } + + static trackException(ex: Error) { + _reporter && _reporter.trackException(ex); + } +} + +export class TelemetryReporter extends Disposable { + + private _client: typeof appInsights.client; + private _context: { [key: string]: string }; + private _disposable: Disposable; + private _enabled: boolean; + + constructor(key: string) { + super(() => this.dispose()); + + appInsights.setup(key) + .setAutoCollectConsole(false) + .setAutoCollectExceptions(false) + .setAutoCollectPerformance(false) + .setAutoCollectRequests(false); + + (appInsights as any).setAutoCollectDependencies(false) + .setOfflineMode(true); + + this._client = appInsights.start().client; + + this.setContext(); + this._stripPII(appInsights.client); + + this._onConfigurationChanged(); + + const subscriptions: Disposable[] = []; + + subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this)); + + this._disposable = Disposable.from(...subscriptions); + } + + dispose() { + this._disposable && this._disposable.dispose(); + } + + setContext(context?: { [key: string]: string }) { + if (!this._context) { + this._context = Object.create(null); + + // Add vscode properties + this._context.code_language = vscode.env.language; + this._context.code_version = vscode.version; + + // Add os properties + this._context.os = os.platform(); + this._context.os_version = os.release(); + } + + if (context) { + Object.assign(this._context, context); + } + + Object.assign(this._client.commonProperties, this._context); + } + + trackEvent(name: string, properties?: { [key: string]: string }, measurements?: { [key: string]: number; }) { + if (!this._enabled) return; + this._client.trackEvent(name, properties, measurements); + } + + trackException(ex: Error) { + if (!this._enabled) return; + this._client.trackException(ex); + } + + private _onConfigurationChanged() { + this._enabled = workspace.getConfiguration('telemetry').get('enableTelemetry', true); + } + + private _stripPII(client: typeof appInsights.client) { + if (client && client.context && client.context.keys && client.context.tags) { + const machineNameKey = client.context.keys.deviceMachineName; + client.context.tags[machineNameKey] = ''; + } + } +} \ No newline at end of file