microsoft/vscode-react-native
Publicmirrored fromhttps://github.com/microsoft/vscode-react-nativeAvailable
src/common/telemetryHelper.ts
238lines · modecode
| 1 | // Copyright (c) Microsoft Corporation. All rights reserved. |
| 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. |
| 3 | |
| 4 | import * as Q from "q"; |
| 5 | import {Telemetry} from "./telemetry"; |
| 6 | |
| 7 | export interface ITelemetryPropertyInfo { |
| 8 | value: any; |
| 9 | isPii: boolean; |
| 10 | } |
| 11 | |
| 12 | export interface ICommandTelemetryProperties { |
| 13 | [propertyName: string]: ITelemetryPropertyInfo; |
| 14 | } |
| 15 | |
| 16 | export interface IExternalTelemetryProvider { |
| 17 | sendTelemetry: (event: string, props: Telemetry.ITelemetryProperties, error?: Error) => void; |
| 18 | } |
| 19 | |
| 20 | interface IDictionary<T> { |
| 21 | [key: string]: T; |
| 22 | } |
| 23 | |
| 24 | interface IHasErrorCode { |
| 25 | errorCode: number; |
| 26 | } |
| 27 | |
| 28 | export abstract class TelemetryGeneratorBase { |
| 29 | protected telemetryProperties: ICommandTelemetryProperties = {}; |
| 30 | private componentName: string; |
| 31 | private currentStepStartTime: number[]; |
| 32 | private currentStep: string = "initialStep"; |
| 33 | private errorIndex: number = -1; // In case we have more than one error (We start at -1 because we increment it before using it) |
| 34 | |
| 35 | constructor(componentName: string) { |
| 36 | this.componentName = componentName; |
| 37 | this.currentStepStartTime = process.hrtime(); |
| 38 | } |
| 39 | |
| 40 | protected abstract sendTelemetryEvent(telemetryEvent: Telemetry.TelemetryEvent): void; |
| 41 | |
| 42 | public add(baseName: string, value: any, isPii: boolean): TelemetryGeneratorBase { |
| 43 | return this.addWithPiiEvaluator(baseName, value, () => isPii); |
| 44 | } |
| 45 | |
| 46 | public addWithPiiEvaluator(baseName: string, value: any, piiEvaluator: { (value: string, name: string): boolean }): TelemetryGeneratorBase { |
| 47 | // We have 3 cases: |
| 48 | // * Object is an array, we add each element as baseNameNNN |
| 49 | // * Object is a hash, we add each element as baseName.KEY |
| 50 | // * Object is a value, we add the element as baseName |
| 51 | try { |
| 52 | if (Array.isArray(value)) { |
| 53 | this.addArray(baseName, <any[]> value, piiEvaluator); |
| 54 | } else if (!!value && (typeof value === "object" || typeof value === "function")) { |
| 55 | this.addHash(baseName, <IDictionary<any>> value, piiEvaluator); |
| 56 | } else { |
| 57 | this.addString(baseName, String(value), piiEvaluator); |
| 58 | } |
| 59 | } catch (error) { |
| 60 | // We don"t want to crash the functionality if the telemetry fails. |
| 61 | // This error message will be a javascript error message, so it"s not pii |
| 62 | this.addString("telemetryGenerationError." + baseName, String(error), () => false); |
| 63 | } |
| 64 | |
| 65 | return this; |
| 66 | } |
| 67 | |
| 68 | public addError(error: Error): TelemetryGeneratorBase { |
| 69 | this.add("error.message" + ++this.errorIndex, error.message, /*isPii*/ true); |
| 70 | let errorWithErrorCode: IHasErrorCode = <IHasErrorCode> <Object> error; |
| 71 | if (errorWithErrorCode.errorCode) { |
| 72 | this.add("error.code" + this.errorIndex, errorWithErrorCode.errorCode, /*isPii*/ false); |
| 73 | } |
| 74 | |
| 75 | return this; |
| 76 | } |
| 77 | |
| 78 | public time<T>(name: string, codeToMeasure: { (): Thenable<T> }): Q.Promise<T> { |
| 79 | let startTime: number[] = process.hrtime(); |
| 80 | return Q(codeToMeasure()) |
| 81 | .finally(() => this.finishTime(name, startTime)) |
| 82 | .fail((reason: any): Q.Promise<T> => { |
| 83 | this.addError(reason); |
| 84 | return Q.reject<T>(reason); |
| 85 | }); |
| 86 | } |
| 87 | |
| 88 | public step(name: string): TelemetryGeneratorBase { |
| 89 | // First we finish measuring this step time, and we send a telemetry event for this step |
| 90 | this.finishTime(this.currentStep, this.currentStepStartTime); |
| 91 | this.sendCurrentStep(); |
| 92 | |
| 93 | // Then we prepare to start gathering information about the next step |
| 94 | this.currentStep = name; |
| 95 | this.telemetryProperties = {}; |
| 96 | this.currentStepStartTime = process.hrtime(); |
| 97 | return this; |
| 98 | } |
| 99 | |
| 100 | public send(): void { |
| 101 | if (this.currentStep) { |
| 102 | this.add("lastStepExecuted", this.currentStep, /*isPii*/ false); |
| 103 | } |
| 104 | |
| 105 | this.step(null); // Send the last step |
| 106 | } |
| 107 | |
| 108 | private sendCurrentStep(): void { |
| 109 | this.add("step", this.currentStep, /*isPii*/ false); |
| 110 | let telemetryEvent: Telemetry.TelemetryEvent = new Telemetry.TelemetryEvent(Telemetry.appName + "/" + this.componentName); |
| 111 | TelemetryHelper.addTelemetryEventProperties(telemetryEvent, this.telemetryProperties); |
| 112 | this.sendTelemetryEvent(telemetryEvent); |
| 113 | } |
| 114 | |
| 115 | private addArray(baseName: string, array: any[], piiEvaluator: { (value: string, name: string): boolean }): void { |
| 116 | // Object is an array, we add each element as baseNameNNN |
| 117 | let elementIndex: number = 1; // We send telemetry properties in a one-based index |
| 118 | array.forEach((element: any) => this.addWithPiiEvaluator(baseName + elementIndex++, element, piiEvaluator)); |
| 119 | } |
| 120 | |
| 121 | private addHash(baseName: string, hash: IDictionary<any>, piiEvaluator: { (value: string, name: string): boolean }): void { |
| 122 | // Object is a hash, we add each element as baseName.KEY |
| 123 | Object.keys(hash).forEach((key: string) => this.addWithPiiEvaluator(baseName + "." + key, hash[key], piiEvaluator)); |
| 124 | } |
| 125 | |
| 126 | private addString(name: string, value: string, piiEvaluator: { (value: string, name: string): boolean }): void { |
| 127 | this.telemetryProperties[name] = TelemetryHelper.telemetryProperty(value, piiEvaluator(value, name)); |
| 128 | } |
| 129 | |
| 130 | private combine(...components: string[]): string { |
| 131 | let nonNullComponents: string[] = components.filter((component: string) => component !== null); |
| 132 | return nonNullComponents.join("."); |
| 133 | } |
| 134 | |
| 135 | private finishTime(name: string, startTime: number[]): void { |
| 136 | let endTime: number[] = process.hrtime(startTime); |
| 137 | this.add(this.combine(name, "time"), String(endTime[0] * 1000 + endTime[1] / 1000000), /*isPii*/ false); |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | export class TelemetryGenerator extends TelemetryGeneratorBase { |
| 142 | protected sendTelemetryEvent(telemetryEvent: Telemetry.TelemetryEvent): void { |
| 143 | Telemetry.send(telemetryEvent); |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | export class TelemetryHelper { |
| 148 | public static sendSimpleEvent(eventName: string, properties?: Telemetry.ITelemetryProperties): void { |
| 149 | const event = TelemetryHelper.createTelemetryEvent(eventName, properties); |
| 150 | Telemetry.send(event); |
| 151 | } |
| 152 | public static createTelemetryEvent(eventName: string, properties?: Telemetry.ITelemetryProperties): Telemetry.TelemetryEvent { |
| 153 | return new Telemetry.TelemetryEvent(Telemetry.appName + "/" + eventName, properties); |
| 154 | } |
| 155 | |
| 156 | public static telemetryProperty(propertyValue: any, pii?: boolean): ITelemetryPropertyInfo { |
| 157 | return { value: String(propertyValue), isPii: pii || false }; |
| 158 | } |
| 159 | |
| 160 | public static addTelemetryEventProperties(event: Telemetry.TelemetryEvent, properties: ICommandTelemetryProperties): void { |
| 161 | if (!properties) { |
| 162 | return; |
| 163 | } |
| 164 | |
| 165 | Object.keys(properties).forEach(function (propertyName: string): void { |
| 166 | TelemetryHelper.addTelemetryEventProperty(event, propertyName, properties[propertyName].value, properties[propertyName].isPii); |
| 167 | }); |
| 168 | } |
| 169 | |
| 170 | public static sendCommandSuccessTelemetry(commandName: string, commandProperties: ICommandTelemetryProperties, args: string[] = null): void { |
| 171 | let successEvent: Telemetry.TelemetryEvent = TelemetryHelper.createBasicCommandTelemetry(commandName, args); |
| 172 | |
| 173 | TelemetryHelper.addTelemetryEventProperties(successEvent, commandProperties); |
| 174 | |
| 175 | Telemetry.send(successEvent); |
| 176 | } |
| 177 | |
| 178 | public static addTelemetryEventProperty(event: Telemetry.TelemetryEvent, propertyName: string, propertyValue: any, isPii: boolean): void { |
| 179 | if (Array.isArray(propertyValue)) { |
| 180 | TelemetryHelper.addMultiValuedTelemetryEventProperty(event, propertyName, propertyValue, isPii); |
| 181 | } else { |
| 182 | TelemetryHelper.setTelemetryEventProperty(event, propertyName, propertyValue, isPii); |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | public static addPropertiesFromOptions(telemetryProperties: ICommandTelemetryProperties, knownOptions: any, commandOptions: {[flag: string]: any}, nonPiiOptions: string[] = []): ICommandTelemetryProperties { |
| 187 | // We parse only the known options, to avoid potential private information that may appear on the command line |
| 188 | let unknownOptionIndex: number = 1; |
| 189 | Object.keys(commandOptions).forEach((key: string) => { |
| 190 | let value: any = commandOptions[key]; |
| 191 | if (Object.keys(knownOptions).indexOf(key) >= 0) { |
| 192 | // This is a known option. We"ll check the list to decide if it"s pii or not |
| 193 | if (typeof (value) !== "undefined") { |
| 194 | // We encrypt all options values unless they are specifically marked as nonPii |
| 195 | telemetryProperties["options." + key] = this.telemetryProperty(value, nonPiiOptions.indexOf(key) < 0); |
| 196 | } |
| 197 | } else { |
| 198 | // This is a not known option. We"ll assume that both the option and the value are pii |
| 199 | telemetryProperties["unknownOption" + unknownOptionIndex + ".name"] = this.telemetryProperty(key, /*isPii*/ true); |
| 200 | telemetryProperties["unknownOption" + unknownOptionIndex++ + ".value"] = this.telemetryProperty(value, /*isPii*/ true); |
| 201 | } |
| 202 | }); |
| 203 | return telemetryProperties; |
| 204 | } |
| 205 | |
| 206 | public static generate<T>(name: string, codeGeneratingTelemetry: { (telemetry: TelemetryGenerator): Thenable<T> }): Q.Promise<T> { |
| 207 | let generator: TelemetryGenerator = new TelemetryGenerator(name); |
| 208 | return generator.time(null, () => codeGeneratingTelemetry(generator)).finally(() => generator.send()); |
| 209 | } |
| 210 | |
| 211 | private static createBasicCommandTelemetry(commandName: string, args: string[] = null): Telemetry.TelemetryEvent { |
| 212 | let commandEvent: Telemetry.TelemetryEvent = new Telemetry.TelemetryEvent(Telemetry.appName + "/" + (commandName || "command")); |
| 213 | |
| 214 | if (!commandName && args && args.length > 0) { |
| 215 | commandEvent.setPiiProperty("command", args[0]); |
| 216 | } |
| 217 | |
| 218 | if (args) { |
| 219 | TelemetryHelper.addTelemetryEventProperty(commandEvent, "argument", args, true); |
| 220 | } |
| 221 | |
| 222 | return commandEvent; |
| 223 | } |
| 224 | |
| 225 | private static setTelemetryEventProperty(event: Telemetry.TelemetryEvent, propertyName: string, propertyValue: string, isPii: boolean): void { |
| 226 | if (isPii) { |
| 227 | event.setPiiProperty(propertyName, String(propertyValue)); |
| 228 | } else { |
| 229 | event.properties[propertyName] = String(propertyValue); |
| 230 | } |
| 231 | } |
| 232 | |
| 233 | private static addMultiValuedTelemetryEventProperty(event: Telemetry.TelemetryEvent, propertyName: string, propertyValue: string, isPii: boolean): void { |
| 234 | for (let i: number = 0; i < propertyValue.length; i++) { |
| 235 | TelemetryHelper.setTelemetryEventProperty(event, propertyName + i, propertyValue[i], isPii); |
| 236 | } |
| 237 | } |
| 238 | }; |