microsoft/vscode-react-native

Public

mirrored fromhttps://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
e034b2f8ed9e645a9f8ffe314b40f4e0b988e473

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/common/telemetry.ts

388lines · 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/// <reference path="../typings/applicationinsights/applicationinsights.d.ts" />
5/// <reference path="../typings/winreg/winreg.d.ts" />
6
7import * as appInsights from "applicationinsights";
8import * as crypto from "crypto";
9import * as fs from "fs";
10import * as getmac from "getmac";
11import * as os from "os";
12import * as path from "path";
13import * as Q from "q";
14import * as winreg from "winreg";
15
16// for poking around at internal applicationinsights options
17/* tslint:disable:no-var-requires */
18let sender = require ("applicationinsights/Library/Sender");
19let telemetryLogger = require ("applicationinsights/Library/Logging");
20/* tslint:enable:no-var-requires */
21
22/**
23 * Telemetry module specialized for vscode integration.
24 */
25export module Telemetry {
26 export let appName: string;
27 export let isOptedIn: boolean = false;
28
29 export interface ITelemetryProperties {
30 [propertyName: string]: any;
31 };
32
33 /**
34 * TelemetryEvent represents a basic telemetry data point
35 */
36 export class TelemetryEvent {
37 public name: string;
38 public properties: ITelemetryProperties;
39 private static PII_HASH_KEY: string = "959069c9-9e93-4fa1-bf16-3f8120d7db0c";
40 private eventId: string;
41
42 constructor(name: string, properties?: ITelemetryProperties) {
43 this.name = name;
44 this.properties = properties || {};
45
46 this.eventId = TelemetryUtils.generateGuid();
47 }
48
49 public setPiiProperty(name: string, value: string): void {
50 let hmac: any = crypto.createHmac("sha256", new Buffer(TelemetryEvent.PII_HASH_KEY, "utf8"));
51 let hashedValue: any = hmac.update(value).digest("hex");
52
53 this.properties[name] = hashedValue;
54
55 if (Telemetry.isInternal()) {
56 this.properties[name + ".nothashed"] = value;
57 }
58 }
59 };
60
61 /**
62 * TelemetryActivity automatically includes timing data, used for scenarios where we want to track performance.
63 * Calls to start() and end() are optional, if not called explicitly then the constructor will be the start and send will be the end.
64 * This event will include a property called reserved.activity.duration which represents time in milliseconds.
65 */
66 export class TelemetryActivity extends TelemetryEvent {
67 private startTime: number[];
68 private endTime: number[];
69
70 constructor(name: string, properties?: ITelemetryProperties) {
71 super(name, properties);
72 this.start();
73 }
74
75 public start(): void {
76 this.startTime = process.hrtime();
77 }
78
79 public end(): void {
80 if (!this.endTime) {
81 this.endTime = process.hrtime(this.startTime);
82
83 // convert [seconds, nanoseconds] to milliseconds and include as property
84 this.properties["reserved.activity.duration"] = this.endTime[0] * 1000 + this.endTime[1] / 1000000;
85 }
86 }
87 };
88
89 export function init(appNameValue: string, appVersion?: string, isOptedInValue?: boolean): Q.Promise<any> {
90 try {
91 Telemetry.appName = appNameValue;
92 return TelemetryUtils.init(appVersion, isOptedInValue);
93 } catch (err) {
94 console.error(err);
95 }
96 }
97
98 export function send(event: TelemetryEvent, ignoreOptIn: boolean = false): void {
99 if (Telemetry.isOptedIn || ignoreOptIn) {
100 TelemetryUtils.addCommonProperties(event);
101
102 try {
103 if (event instanceof TelemetryActivity) {
104 (<TelemetryActivity> event).end();
105 }
106
107 if (appInsights.client) { // no-op if telemetry is not initialized
108 appInsights.client.trackEvent(event.name, event.properties);
109 }
110
111 } catch (err) {
112 console.error(err);
113 }
114 }
115 }
116
117 export function sendPendingData(): Q.Promise<string> {
118 let defer: Q.Deferred<string> = Q.defer<string>();
119 appInsights.client.sendPendingData((result: string) => defer.resolve(result));
120 return defer.promise;
121 }
122
123 export function isInternal(): boolean {
124 return TelemetryUtils.userType === TelemetryUtils.USERTYPE_INTERNAL;
125 }
126
127 export function getSessionId(): string {
128 return TelemetryUtils.sessionId;
129 }
130
131 export function setSessionId(sessionId: string): void {
132 TelemetryUtils.sessionId = sessionId;
133 }
134
135 interface ITelemetrySettings {
136 [settingKey: string]: any;
137 userId?: string;
138 machineId?: string;
139 optIn?: boolean;
140 userType?: string;
141 }
142
143 class TelemetryUtils {
144 public static USERTYPE_INTERNAL: string = "Internal";
145 public static USERTYPE_EXTERNAL: string = "External";
146 public static userType: string;
147 public static sessionId: string;
148 public static optInCollectedForCurrentSession: boolean;
149
150 private static userId: string;
151 private static machineId: string;
152 private static telemetrySettings: ITelemetrySettings = null;
153 private static TELEMETRY_SETTINGS_FILENAME: string = "VSCodeTelemetrySettings.json";
154 private static APPINSIGHTS_INSTRUMENTATIONKEY: string = "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217"; // Matches vscode telemetry key
155 private static REGISTRY_SQMCLIENT_NODE: string = "\\SOFTWARE\\Microsoft\\SQMClient";
156 private static REGISTRY_USERID_VALUE: string = "UserId";
157 private static INTERNAL_DOMAIN_SUFFIX: string = "microsoft.com";
158 private static INTERNAL_USER_ENV_VAR: string = "TACOINTERNAL";
159
160 private static get settingsHome(): string {
161 switch (os.platform()) {
162 case "win32":
163 return path.join(process.env.APPDATA, "vscode-react-native");
164 case "darwin":
165 case "linux":
166 return path.join(process.env.HOME, ".vscode-react-native");
167 default:
168 throw new Error("UnexpectedPlatform");
169 }
170 }
171
172 private static get telemetrySettingsFile(): string {
173 return path.join(TelemetryUtils.settingsHome, TelemetryUtils.TELEMETRY_SETTINGS_FILENAME);
174 }
175
176 public static init(appVersion: string, isOptedInValue: boolean): Q.Promise<any> {
177 TelemetryUtils.loadSettings();
178
179 let client = appInsights.setup(TelemetryUtils.APPINSIGHTS_INSTRUMENTATIONKEY)
180 .setOfflineMode(true)
181 .setAutoCollectConsole(false)
182 .setAutoCollectRequests(false)
183 .setAutoCollectPerformance(false)
184 .setAutoCollectExceptions(false)
185 .start().client;
186 appInsights.client.config.maxBatchIntervalMs = 100;
187 sender.WAIT_BETWEEN_RESEND = 0;
188 telemetryLogger.disableWarnings = true;
189
190 if (client && client.context && client.context.keys && client.context.tags) {
191 // Remove potential PII
192 let machineNameKey = client.context.keys.deviceMachineName;
193 client.context.tags[machineNameKey] = "";
194 }
195
196 if (appVersion) {
197 let context: Context = appInsights.client.context;
198 context.tags[context.keys.applicationVersion] = appVersion;
199 }
200
201 // Change endpoint to match Aimov key
202 client.config.endpointUrl = "https://vortex.data.microsoft.com/collect/v1";
203
204 return Q.all([TelemetryUtils.getUserId(), TelemetryUtils.getMachineId()])
205 .spread<any>(function (userId: string, machineId: string): void {
206 TelemetryUtils.userId = userId;
207 TelemetryUtils.machineId = machineId;
208 TelemetryUtils.sessionId = TelemetryUtils.generateGuid();
209 TelemetryUtils.userType = TelemetryUtils.getUserType();
210
211 Telemetry.isOptedIn = TelemetryUtils.getTelemetryOptInSetting();
212 TelemetryUtils.saveSettings();
213 });
214 }
215
216 public static addCommonProperties(event: any): void {
217 if (Telemetry.isOptedIn) {
218 // for the opt out event, don"t include tracking properties
219 event.properties["RN.userId"] = TelemetryUtils.userId;
220 event.properties["RN.machineId"] = TelemetryUtils.machineId;
221 }
222
223 event.properties["RN.sessionId"] = TelemetryUtils.sessionId;
224 event.properties["RN.userType"] = TelemetryUtils.userType;
225 event.properties["RN.hostOS"] = os.platform();
226 event.properties["RN.hostOSRelease"] = os.release();
227 }
228
229 public static generateGuid(): string {
230 let hexValues: string[] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
231 // c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
232 let oct: string = "";
233 let tmp: number;
234 /* tslint:disable:no-bitwise */
235 for (let a: number = 0; a < 4; a++) {
236 tmp = (4294967296 * Math.random()) | 0;
237 oct += hexValues[tmp & 0xF] + hexValues[tmp >> 4 & 0xF] + hexValues[tmp >> 8 & 0xF] + hexValues[tmp >> 12 & 0xF] + hexValues[tmp >> 16 & 0xF] + hexValues[tmp >> 20 & 0xF] + hexValues[tmp >> 24 & 0xF] + hexValues[tmp >> 28 & 0xF];
238 }
239
240 // "Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively"
241 let clockSequenceHi: string = hexValues[8 + (Math.random() * 4) | 0];
242 return oct.substr(0, 8) + "-" + oct.substr(9, 4) + "-4" + oct.substr(13, 3) + "-" + clockSequenceHi + oct.substr(16, 3) + "-" + oct.substr(19, 12);
243 /* tslint:enable:no-bitwise */
244 }
245
246 public static getTelemetryOptInSetting(): boolean {
247 if (TelemetryUtils.telemetrySettings.optIn === undefined) {
248 // Opt-in by default
249 TelemetryUtils.telemetrySettings.optIn = true;
250 }
251
252 return TelemetryUtils.telemetrySettings.optIn;
253 }
254
255 public static setTelemetryOptInSetting(optIn: boolean): void {
256 TelemetryUtils.telemetrySettings.optIn = optIn;
257
258 if (!optIn) {
259 Telemetry.send(new TelemetryEvent(Telemetry.appName + "/telemetryOptOut"), true);
260 }
261
262 TelemetryUtils.optInCollectedForCurrentSession = true;
263 TelemetryUtils.saveSettings();
264 }
265
266 private static getUserType(): string {
267 let userType: string = TelemetryUtils.telemetrySettings.userType;
268
269 if (userType === undefined) {
270 if (process.env[TelemetryUtils.INTERNAL_USER_ENV_VAR]) {
271 userType = TelemetryUtils.USERTYPE_INTERNAL;
272 } else if (os.platform() === "win32") {
273 let domain: string = process.env.USERDNSDOMAIN;
274 domain = domain ? domain.toLowerCase().substring(domain.length - TelemetryUtils.INTERNAL_DOMAIN_SUFFIX.length) : null;
275 userType = domain === TelemetryUtils.INTERNAL_DOMAIN_SUFFIX ? TelemetryUtils.USERTYPE_INTERNAL : TelemetryUtils.USERTYPE_EXTERNAL;
276 } else {
277 userType = TelemetryUtils.USERTYPE_EXTERNAL;
278 }
279
280 TelemetryUtils.telemetrySettings.userType = userType;
281 }
282
283 return userType;
284 }
285
286 private static getRegistryValue(key: string, value: string, hive: string): Q.Promise<string> {
287 let deferred: Q.Deferred<string> = Q.defer<string>();
288 let regKey = new winreg({
289 hive: hive,
290 key: key
291 });
292 regKey.get(value, function (err: any, itemValue: winreg.RegistryItem) {
293 if (err) {
294 // Fail gracefully by returning null if there was an error.
295 deferred.resolve(null);
296 } else {
297 deferred.resolve(itemValue.value);
298 }
299 });
300
301 return deferred.promise;
302 }
303
304 /*
305 * Load settings data from settingsHome/TelemetrySettings.json
306 */
307 private static loadSettings(): ITelemetrySettings {
308 try {
309 TelemetryUtils.telemetrySettings = JSON.parse(<any>fs.readFileSync(TelemetryUtils.telemetrySettingsFile));
310 } catch (e) {
311 // if file does not exist or fails to parse then assume no settings are saved and start over
312 TelemetryUtils.telemetrySettings = {};
313 }
314
315 return TelemetryUtils.telemetrySettings;
316 }
317
318 /*
319 * Save settings data in settingsHome/TelemetrySettings.json
320 */
321 private static saveSettings(): void {
322 if (!fs.existsSync(TelemetryUtils.settingsHome)) {
323 fs.mkdirSync(TelemetryUtils.settingsHome);
324 }
325
326 fs.writeFileSync(TelemetryUtils.telemetrySettingsFile, JSON.stringify(TelemetryUtils.telemetrySettings));
327 }
328
329 private static getUniqueId(regValue: string, regHive: string, fallback: () => string): Q.Promise<any> {
330 let uniqueId: string;
331 if (os.platform() === "win32") {
332 return TelemetryUtils.getRegistryValue(TelemetryUtils.REGISTRY_SQMCLIENT_NODE, regValue, regHive)
333 .then(function(id: string): Q.Promise<string> {
334 if (id) {
335 uniqueId = id.replace(/[{}]/g, "");
336 return Q.resolve(uniqueId);
337 } else {
338 return Q.resolve(fallback());
339 }
340 });
341 } else {
342 return Q.resolve(fallback());
343 }
344 }
345
346 private static generateMachineId(): Q.Promise<string> {
347 return TelemetryUtils.getMacAddress().then((macAddress: string) => {
348 return crypto.createHash("sha256").update(macAddress, "utf8").digest("hex");
349 });
350 }
351
352 private static getMachineId(): Q.Promise<string> {
353 let machineId: string = TelemetryUtils.telemetrySettings.machineId;
354 if (!machineId) {
355 return TelemetryUtils.generateMachineId()
356 .then(function(id: string): Q.Promise<string> {
357 TelemetryUtils.telemetrySettings.machineId = id;
358 return Q.resolve(id);
359 });
360 } else {
361 TelemetryUtils.telemetrySettings.machineId = machineId;
362 return Q.resolve(machineId);
363 }
364 }
365
366 private static getMacAddress(): Q.Promise<string> {
367 // Return a mac address, or failing that, a unique ID
368 // using getmac to attempt to match telemetry identifiers of vs code
369 return Q.nfcall(getmac.getMac).catch(() => {
370 return TelemetryUtils.generateGuid();
371 });
372 }
373
374 private static getUserId(): Q.Promise<string> {
375 let userId: string = TelemetryUtils.telemetrySettings.userId;
376 if (!userId) {
377 return TelemetryUtils.getUniqueId(TelemetryUtils.REGISTRY_USERID_VALUE, winreg.HKCU, TelemetryUtils.generateGuid)
378 .then(function(id: string): Q.Promise<string> {
379 TelemetryUtils.telemetrySettings.userId = id;
380 return Q.resolve(id);
381 });
382 } else {
383 TelemetryUtils.telemetrySettings.userId = userId;
384 return Q.resolve(userId);
385 }
386 }
387 };
388 };
389