// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. import * as crypto from "crypto"; import * as fs from "fs"; import * as path from "path"; import { HostPlatform } from "./hostPlatform"; /* eslint-disable */ /* eslint-enable prettier/prettier*/ /** * Telemetry module specialized for vscode integration. */ export namespace Telemetry { export let appName: string; export let isOptedIn: boolean = false; export let reporter: ITelemetryReporter; export let reporterDictionary: { [key: string]: ITelemetryReporter } = {}; export interface ITelemetryProperties { [propertyName: string]: any; } export interface TelemetryRequest { extensionId: string; extensionVersion: string; appInsightsKey: string; eventName: string; properties: Telemetry.ITelemetryEventProperties; measures: Telemetry.ITelemetryEventMeasures; } class TelemetryUtils { private static telemetrySettings: ITelemetrySettings; private static TELEMETRY_SETTINGS_FILENAME: string = "VSCodeTelemetrySettings.json"; private static get telemetrySettingsFile(): string { let settingsHome = HostPlatform.getSettingsHome(); return path.join(settingsHome, TelemetryUtils.TELEMETRY_SETTINGS_FILENAME); } public static init(appVersion: string, reporterToUse: ITelemetryReporter): void { TelemetryUtils.loadSettings(); Telemetry.reporter = reporterToUse; const telemetryVsSettings = TelemetryUtils.getTelemetryOptInVscodeSetting(); Telemetry.isOptedIn = telemetryVsSettings === "" ? TelemetryUtils.getTelemetryOptInSetting() : telemetryVsSettings; TelemetryUtils.saveSettings(); } public static getTelemetryOptInSetting(): boolean { if (TelemetryUtils.telemetrySettings.optIn === undefined) { // Opt-in by default TelemetryUtils.telemetrySettings.optIn = true; } return TelemetryUtils.telemetrySettings.optIn; } public static getTelemetryOptInVscodeSetting() { const SettingsHelper = require("../extension/settingsHelper").SettingsHelper; return SettingsHelper.getWorkspaceTelemetry(); } /** * Load settings data from settingsHome/TelemetrySettings.json */ private static loadSettings(): ITelemetrySettings { try { TelemetryUtils.telemetrySettings = JSON.parse( fs.readFileSync(TelemetryUtils.telemetrySettingsFile), ); } catch (e) { // if file does not exist or fails to parse then assume no settings are saved and start over TelemetryUtils.telemetrySettings = {}; } return TelemetryUtils.telemetrySettings; } /** * Save settings data in settingsHome/TelemetrySettings.json */ private static saveSettings(): void { let settingsHome = HostPlatform.getSettingsHome(); if (!fs.existsSync(settingsHome)) { fs.mkdirSync(settingsHome); } fs.writeFileSync( TelemetryUtils.telemetrySettingsFile, JSON.stringify(TelemetryUtils.telemetrySettings), ); } } /** * TelemetryEvent represents a basic telemetry data point */ export class TelemetryEvent { public name: string; public properties: ITelemetryProperties; private static PII_HASH_KEY: string = "959069c9-9e93-4fa1-bf16-3f8120d7db0c"; constructor(name: string, properties?: ITelemetryProperties) { this.name = name; this.properties = properties || {}; } public setPiiProperty(name: string, value: string): void { let hmac: crypto.Hmac = crypto.createHmac( "sha256", Buffer.from(TelemetryEvent.PII_HASH_KEY, "utf8"), ); let hashedValue: string = hmac.update(value).digest("hex"); this.properties[name] = hashedValue; } } /** * TelemetryActivity automatically includes timing data, used for scenarios where we want to track performance. * Calls to start() and end() are optional, if not called explicitly then the constructor will be the start and send will be the end. * This event will include a property called reserved.activity.duration which represents time in milliseconds. */ export class TelemetryActivity extends TelemetryEvent { private startTime!: [number, number]; private endTime!: [number, number]; constructor(name: string, properties?: ITelemetryProperties) { super(name, properties); this.start(); } public start(): void { this.startTime = process.hrtime(); } public end(): void { if (!this.endTime) { this.endTime = process.hrtime(this.startTime); // convert [seconds, nanoseconds] to milliseconds and include as property this.properties["reserved.activity.duration"] = this.endTime[0] * 1000 + this.endTime[1] / 1000000; } } } export function init( appNameValue: string, appVersion: string, reporterToUse: ITelemetryReporter, ): void { try { Telemetry.appName = appNameValue; TelemetryUtils.init(appVersion, reporterToUse); } catch (err) { console.error(err); } } export function send(event: TelemetryEvent, ignoreOptIn: boolean = false): void { if (Telemetry.isOptedIn || ignoreOptIn) { try { if (event instanceof TelemetryActivity) { (event).end(); } if (Telemetry.reporter) { let properties: ITelemetryEventProperties = {}; let measures: ITelemetryEventMeasures = {}; Object.keys(event.properties || {}).forEach(function (key: string) { switch (typeof event.properties[key]) { case "string": properties[key] = event.properties[key]; break; case "number": measures[key] = event.properties[key]; break; default: properties[key] = JSON.stringify(event.properties[key]); break; } }); Telemetry.reporter.sendTelemetryEvent(event.name, properties, measures); } } catch (err) { console.error(err); } } } interface ITelemetrySettings { [settingKey: string]: any; userId?: string; machineId?: string; optIn?: boolean; userType?: string; } export const APPINSIGHTS_INSTRUMENTATIONKEY: string = "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217"; // Matches vscode telemetry key export interface ITelemetryEventProperties { [key: string]: string; } export interface ITelemetryEventMeasures { [key: string]: number; } export interface ITelemetryReporter { sendTelemetryEvent( eventName: string, properties?: ITelemetryEventProperties, measures?: ITelemetryEventMeasures, ): void; } export function sendExtensionTelemetry( extensionId: string, extensionVersion: string, appInsightsKey: string, eventName: string, properties?: ITelemetryEventProperties, measures?: ITelemetryEventMeasures, ): void { let extensionTelemetryReporter: ITelemetryReporter = Telemetry.reporterDictionary[extensionId]; if (!extensionTelemetryReporter) { let TelemetryReporter = require("vscode-extension-telemetry").default; Telemetry.reporterDictionary[extensionId] = new TelemetryReporter( extensionId, extensionVersion, appInsightsKey, ); extensionTelemetryReporter = Telemetry.reporterDictionary[extensionId]; } extensionTelemetryReporter.sendTelemetryEvent(eventName, properties, measures); } }