microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2d3a052eafbcd8bddff2825d5f0049a168da4b16

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/common/telemetry.ts

394lines · 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 console.log("!!! Telemetry Event !!!");
106 console.log(event);
107 console.log("!!! End - Telemetry Event !!!");
108 }
109
110 if (appInsights.client) { // no-op if telemetry is not initialized
111 appInsights.client.trackEvent(event.name, event.properties);
112 console.log("!!! Telemetry Event !!!");
113 console.log(event);
114 console.log("!!! End - Telemetry Event !!!");
115 }
116
117 } catch (err) {
118 console.error(err);
119 }
120 }
121 }
122
123 export function sendPendingData(): Q.Promise<string> {
124 let defer: Q.Deferred<string> = Q.defer<string>();
125 appInsights.client.sendPendingData((result: string) => defer.resolve(result));
126 return defer.promise;
127 }
128
129 export function isInternal(): boolean {
130 return TelemetryUtils.userType === TelemetryUtils.USERTYPE_INTERNAL;
131 }
132
133 export function getSessionId(): string {
134 return TelemetryUtils.sessionId;
135 }
136
137 export function setSessionId(sessionId: string): void {
138 TelemetryUtils.sessionId = sessionId;
139 }
140
141 interface ITelemetrySettings {
142 [settingKey: string]: any;
143 userId?: string;
144 machineId?: string;
145 optIn?: boolean;
146 userType?: string;
147 }
148
149 class TelemetryUtils {
150 public static USERTYPE_INTERNAL: string = "Internal";
151 public static USERTYPE_EXTERNAL: string = "External";
152 public static userType: string;
153 public static sessionId: string;
154 public static optInCollectedForCurrentSession: boolean;
155
156 private static userId: string;
157 private static machineId: string;
158 private static telemetrySettings: ITelemetrySettings = null;
159 private static TELEMETRY_SETTINGS_FILENAME: string = "VSCodeTelemetrySettings.json";
160 private static APPINSIGHTS_INSTRUMENTATIONKEY: string = "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217"; // Matches vscode telemetry key
161 private static REGISTRY_SQMCLIENT_NODE: string = "\\SOFTWARE\\Microsoft\\SQMClient";
162 private static REGISTRY_USERID_VALUE: string = "UserId";
163 private static INTERNAL_DOMAIN_SUFFIX: string = "microsoft.com";
164 private static INTERNAL_USER_ENV_VAR: string = "TACOINTERNAL";
165
166 private static get settingsHome(): string {
167 switch (os.platform()) {
168 case "win32":
169 return path.join(process.env.APPDATA, "vscode-react-native");
170 case "darwin":
171 case "linux":
172 return path.join(process.env.HOME, ".vscode-react-native");
173 default:
174 throw new Error("UnexpectedPlatform");
175 }
176 }
177
178 private static get telemetrySettingsFile(): string {
179 return path.join(TelemetryUtils.settingsHome, TelemetryUtils.TELEMETRY_SETTINGS_FILENAME);
180 }
181
182 public static init(appVersion: string, isOptedInValue: boolean): Q.Promise<any> {
183 TelemetryUtils.loadSettings();
184
185 let client = appInsights.setup(TelemetryUtils.APPINSIGHTS_INSTRUMENTATIONKEY)
186 .setOfflineMode(true)
187 .setAutoCollectConsole(false)
188 .setAutoCollectRequests(false)
189 .setAutoCollectPerformance(false)
190 .setAutoCollectExceptions(false)
191 .start().client;
192 appInsights.client.config.maxBatchIntervalMs = 100;
193 sender.WAIT_BETWEEN_RESEND = 0;
194 telemetryLogger.disableWarnings = true;
195
196 if (client && client.context && client.context.keys && client.context.tags) {
197 // Remove potential PII
198 let machineNameKey = client.context.keys.deviceMachineName;
199 client.context.tags[machineNameKey] = "";
200 }
201
202 if (appVersion) {
203 let context: Context = appInsights.client.context;
204 context.tags[context.keys.applicationVersion] = appVersion;
205 }
206
207 // Change endpoint to match Aimov key
208 client.config.endpointUrl = "https://vortex.data.microsoft.com/collect/v1";
209
210 return Q.all([TelemetryUtils.getUserId(), TelemetryUtils.getMachineId()])
211 .spread<any>(function (userId: string, machineId: string): void {
212 TelemetryUtils.userId = userId;
213 TelemetryUtils.machineId = machineId;
214 TelemetryUtils.sessionId = TelemetryUtils.generateGuid();
215 TelemetryUtils.userType = TelemetryUtils.getUserType();
216
217 Telemetry.isOptedIn = TelemetryUtils.getTelemetryOptInSetting();
218 TelemetryUtils.saveSettings();
219 });
220 }
221
222 public static addCommonProperties(event: any): void {
223 if (Telemetry.isOptedIn) {
224 // for the opt out event, don"t include tracking properties
225 event.properties["RN.userId"] = TelemetryUtils.userId;
226 event.properties["RN.machineId"] = TelemetryUtils.machineId;
227 }
228
229 event.properties["RN.sessionId"] = TelemetryUtils.sessionId;
230 event.properties["RN.userType"] = TelemetryUtils.userType;
231 event.properties["RN.hostOS"] = os.platform();
232 event.properties["RN.hostOSRelease"] = os.release();
233 }
234
235 public static generateGuid(): string {
236 let hexValues: string[] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
237 // c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
238 let oct: string = "";
239 let tmp: number;
240 /* tslint:disable:no-bitwise */
241 for (let a: number = 0; a < 4; a++) {
242 tmp = (4294967296 * Math.random()) | 0;
243 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];
244 }
245
246 // "Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively"
247 let clockSequenceHi: string = hexValues[8 + (Math.random() * 4) | 0];
248 return oct.substr(0, 8) + "-" + oct.substr(9, 4) + "-4" + oct.substr(13, 3) + "-" + clockSequenceHi + oct.substr(16, 3) + "-" + oct.substr(19, 12);
249 /* tslint:enable:no-bitwise */
250 }
251
252 public static getTelemetryOptInSetting(): boolean {
253 if (TelemetryUtils.telemetrySettings.optIn === undefined) {
254 // Opt-in by default
255 TelemetryUtils.telemetrySettings.optIn = true;
256 }
257
258 return TelemetryUtils.telemetrySettings.optIn;
259 }
260
261 public static setTelemetryOptInSetting(optIn: boolean): void {
262 TelemetryUtils.telemetrySettings.optIn = optIn;
263
264 if (!optIn) {
265 Telemetry.send(new TelemetryEvent(Telemetry.appName + "/telemetryOptOut"), true);
266 }
267
268 TelemetryUtils.optInCollectedForCurrentSession = true;
269 TelemetryUtils.saveSettings();
270 }
271
272 private static getUserType(): string {
273 let userType: string = TelemetryUtils.telemetrySettings.userType;
274
275 if (userType === undefined) {
276 if (process.env[TelemetryUtils.INTERNAL_USER_ENV_VAR]) {
277 userType = TelemetryUtils.USERTYPE_INTERNAL;
278 } else if (os.platform() === "win32") {
279 let domain: string = process.env.USERDNSDOMAIN;
280 domain = domain ? domain.toLowerCase().substring(domain.length - TelemetryUtils.INTERNAL_DOMAIN_SUFFIX.length) : null;
281 userType = domain === TelemetryUtils.INTERNAL_DOMAIN_SUFFIX ? TelemetryUtils.USERTYPE_INTERNAL : TelemetryUtils.USERTYPE_EXTERNAL;
282 } else {
283 userType = TelemetryUtils.USERTYPE_EXTERNAL;
284 }
285
286 TelemetryUtils.telemetrySettings.userType = userType;
287 }
288
289 return userType;
290 }
291
292 private static getRegistryValue(key: string, value: string, hive: string): Q.Promise<string> {
293 let deferred: Q.Deferred<string> = Q.defer<string>();
294 let regKey = new winreg({
295 hive: hive,
296 key: key
297 });
298 regKey.get(value, function (err: any, itemValue: winreg.RegistryItem) {
299 if (err) {
300 // Fail gracefully by returning null if there was an error.
301 deferred.resolve(null);
302 } else {
303 deferred.resolve(itemValue.value);
304 }
305 });
306
307 return deferred.promise;
308 }
309
310 /*
311 * Load settings data from settingsHome/TelemetrySettings.json
312 */
313 private static loadSettings(): ITelemetrySettings {
314 try {
315 TelemetryUtils.telemetrySettings = JSON.parse(<any>fs.readFileSync(TelemetryUtils.telemetrySettingsFile));
316 } catch (e) {
317 // if file does not exist or fails to parse then assume no settings are saved and start over
318 TelemetryUtils.telemetrySettings = {};
319 }
320
321 return TelemetryUtils.telemetrySettings;
322 }
323
324 /*
325 * Save settings data in settingsHome/TelemetrySettings.json
326 */
327 private static saveSettings(): void {
328 if (!fs.existsSync(TelemetryUtils.settingsHome)) {
329 fs.mkdirSync(TelemetryUtils.settingsHome);
330 }
331
332 fs.writeFileSync(TelemetryUtils.telemetrySettingsFile, JSON.stringify(TelemetryUtils.telemetrySettings));
333 }
334
335 private static getUniqueId(regValue: string, regHive: string, fallback: () => string): Q.Promise<any> {
336 let uniqueId: string;
337 if (os.platform() === "win32") {
338 return TelemetryUtils.getRegistryValue(TelemetryUtils.REGISTRY_SQMCLIENT_NODE, regValue, regHive)
339 .then(function(id: string): Q.Promise<string> {
340 if (id) {
341 uniqueId = id.replace(/[{}]/g, "");
342 return Q.resolve(uniqueId);
343 } else {
344 return Q.resolve(fallback());
345 }
346 });
347 } else {
348 return Q.resolve(fallback());
349 }
350 }
351
352 private static generateMachineId(): Q.Promise<string> {
353 return TelemetryUtils.getMacAddress().then((macAddress: string) => {
354 return crypto.createHash("sha256").update(macAddress, "utf8").digest("hex");
355 });
356 }
357
358 private static getMachineId(): Q.Promise<string> {
359 let machineId: string = TelemetryUtils.telemetrySettings.machineId;
360 if (!machineId) {
361 return TelemetryUtils.generateMachineId()
362 .then(function(id: string): Q.Promise<string> {
363 TelemetryUtils.telemetrySettings.machineId = id;
364 return Q.resolve(id);
365 });
366 } else {
367 TelemetryUtils.telemetrySettings.machineId = machineId;
368 return Q.resolve(machineId);
369 }
370 }
371
372 private static getMacAddress(): Q.Promise<string> {
373 // Return a mac address, or failing that, a unique ID
374 // using getmac to attempt to match telemetry identifiers of vs code
375 return Q.nfcall(getmac.getMac).catch(() => {
376 return TelemetryUtils.generateGuid();
377 });
378 }
379
380 private static getUserId(): Q.Promise<string> {
381 let userId: string = TelemetryUtils.telemetrySettings.userId;
382 if (!userId) {
383 return TelemetryUtils.getUniqueId(TelemetryUtils.REGISTRY_USERID_VALUE, winreg.HKCU, TelemetryUtils.generateGuid)
384 .then(function(id: string): Q.Promise<string> {
385 TelemetryUtils.telemetrySettings.userId = id;
386 return Q.resolve(id);
387 });
388 } else {
389 TelemetryUtils.telemetrySettings.userId = userId;
390 return Q.resolve(userId);
391 }
392 }
393 };
394 };
395