diff --git a/src/vs/base/node/aiAdapter.ts b/src/vs/base/node/aiAdapter.ts new file mode 100644 index 0000000000000000000000000000000000000000..de74ca56e426dc0f368d85d59f3afa18ca5a0712 --- /dev/null +++ b/src/vs/base/node/aiAdapter.ts @@ -0,0 +1,154 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import types = require('vs/base/common/types'); +import {safeStringify} from 'vs/base/common/objects'; + +import appInsights = require('applicationinsights'); + +export interface IAIAdapter { + log(eventName: string, data?: any): void; + logException(exception: any): void; + dispose(): void; +} + +export class AIAdapter implements IAIAdapter { + + private appInsights: typeof appInsights.client; + + constructor( + private aiKey: string, + private eventPrefix: string, + /* for test only */ + client?: any + ) { + // for test + if (client) { + this.appInsights = client; + return; + } + + if (aiKey) { + // if another client is already initialized + if (appInsights.client) { + this.appInsights = appInsights.getClient(aiKey); + // no other way to enable offline mode + this.appInsights.channel.setOfflineMode(true); + + } else { + this.appInsights = appInsights.setup(aiKey) + .setAutoCollectRequests(false) + .setAutoCollectPerformance(false) + .setAutoCollectExceptions(false) + .setOfflineMode(true) + .start() + .client; + } + + + if(aiKey.indexOf('AIF-') === 0) { + this.appInsights.config.endpointUrl = 'https://vortex.data.microsoft.com/collect/v1'; + } + + this.setupAIClient(this.appInsights); + } + } + + private setupAIClient(client: typeof appInsights.client): void { + //prevent App Insights from reporting machine name + if (client && client.context && + client.context.keys && client.context.tags) { + var machineNameKey = client.context.keys.deviceMachineName; + client.context.tags[machineNameKey] = ''; + } + } + + private getData(data?: any): any { + var properties: {[key: string]: string;} = {}; + var measurements: {[key: string]: number;} = {}; + + var event_data = this.flaten(data); + for(var prop in event_data) { + // enforce property names less than 150 char, take the last 150 char + var propName = prop && prop.length > 150 ? prop.substr( prop.length - 149) : prop; + var property = event_data[prop]; + if (types.isNumber(property)) { + measurements[propName] = property; + + } else if (types.isBoolean(property)) { + measurements[propName] = property ? 1:0; + } else if (types.isString(property)) { + //enforce proeprty value to be less than 1024 char, take the first 1024 char + var propValue = property && property.length > 1024 ? property.substring(0, 1023): property; + properties[propName] = propValue; + } else if (!types.isUndefined(property) && property !== null) { + properties[propName] = property; + } + } + + return { + properties: properties, + measurements: measurements + }; + } + + private flaten(obj:any, order:number = 0, prefix? : string): any { + var result:{[key:string]: any} = {}; + var properties = obj ? Object.getOwnPropertyNames(obj) : []; + for (var i =0; i < properties.length; i++) { + var item = properties[i]; + var index = prefix ? prefix + item : item; + + if (types.isArray(obj[item])) { + try { + result[index] = safeStringify(obj[item]); + } catch (e) { + // workaround for catching the edge case for #18383 + // safe stringfy should never throw circular object exception + result[index] = '[Circular-Array]'; + } + } else if (obj[item] instanceof Date) { + result[index] = ( obj[item]).toISOString(); + } else if (types.isObject(obj[item])) { + if (order < 2) { + var item_result = this.flaten(obj[item], order + 1, index + '.'); + for (var prop in item_result) { + result[prop] = item_result[prop]; + } + } else { + try { + result[index] = safeStringify(obj[item]); + } catch (e) { + // workaround for catching the edge case for #18383 + // safe stringfy should never throw circular object exception + result[index] = '[Circular]'; + } + } + } else { + result[index] = obj[item]; + } + } + return result; + } + + public log(eventName: string, data?: any): void { + var result = this.getData(data); + + if (this.appInsights) { + this.appInsights.trackEvent(this.eventPrefix+'/'+eventName, result.properties, result.measurements); + } + } + + public logException(exception: any): void { + if (this.appInsights) { + this.appInsights.trackException(exception); + } + } + + public dispose(): void { + this.appInsights = null; + } +} \ No newline at end of file diff --git a/src/vs/base/test/node/aiAdapter/aiAdapter.test.ts b/src/vs/base/test/node/aiAdapter/aiAdapter.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..7ce9093479b87868c19ee24dc81ae2e77543d481 --- /dev/null +++ b/src/vs/base/test/node/aiAdapter/aiAdapter.test.ts @@ -0,0 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as assert from 'assert'; +import { AIAdapter } from 'vs/base/node/aiAdapter'; + +interface IAppInsightsEvent { + eventName: string; + properties?: {string?: string;}; + measurements?: {string?: number;} +} + +class AppInsightsMock { + + public events: IAppInsightsEvent[]=[]; + public IsTrackingPageView: boolean = false; + public exceptions: any[] =[]; + + public trackEvent(eventName: string, properties?: {string?: string;}, measurements?: {string?: number;}): void { + this.events.push({ + eventName: eventName, + properties: properties, + measurements: measurements + }); + } + public trackPageView(): void { + this.IsTrackingPageView = true; + } + + public trackException(exception: any): void { + this.exceptions.push(exception); + } +} + +suite('AIAdapter', () => { + var appInsightsMock: AppInsightsMock; + var adapter: AIAdapter; + var prefix = 'prefix'; + + setup(() => { + appInsightsMock = new AppInsightsMock(); + adapter = new AIAdapter(null, prefix, appInsightsMock); + }); + + teardown(() => { + adapter.dispose(); + }); + + test('Simple event', () => { + adapter.log('testEvent'); + + assert.equal(appInsightsMock.events.length, 1); + assert.equal(appInsightsMock.events[0].eventName, `${prefix}/testEvent`); + }); + + test('Track UnhandledError as exception and events', () => { + var sampleError = new Error('test'); + + adapter.logException(sampleError); + assert.equal(appInsightsMock.exceptions.length, 1); + }); + + test('property limits', () => { + var reallyLongPropertyName = 'abcdefghijklmnopqrstuvwxyz'; + for (var i =0; i <6; i++) { + reallyLongPropertyName +='abcdefghijklmnopqrstuvwxyz'; + } + assert(reallyLongPropertyName.length > 150); + + var reallyLongPropertyValue = 'abcdefghijklmnopqrstuvwxyz012345678901234567890123'; + for (var i =0; i <21; i++) { + reallyLongPropertyValue +='abcdefghijklmnopqrstuvwxyz012345678901234567890123'; + } + assert(reallyLongPropertyValue.length > 1024); + + var data = {}; + data[reallyLongPropertyName] = '1234'; + data['reallyLongPropertyValue'] = reallyLongPropertyValue; + adapter.log('testEvent', data); + + assert.equal(appInsightsMock.events.length, 1); + + for (var prop in appInsightsMock.events[0].properties){ + assert(prop.length < 150); + assert(appInsightsMock.events[0].properties[prop].length <1024); + } + }); + + test('Different data types', () => { + var date = new Date(); + adapter.log('testEvent', { favoriteDate: date, likeRed: false, likeBlue: true, favoriteNumber:1, favoriteColor: 'blue', favoriteCars: ['bmw', 'audi', 'ford']}); + + assert.equal(appInsightsMock.events.length, 1); + assert.equal(appInsightsMock.events[0].eventName, `${prefix}/testEvent`); + assert.equal(appInsightsMock.events[0].properties['favoriteColor'], 'blue'); + assert.equal(appInsightsMock.events[0].measurements['likeRed'], 0); + assert.equal(appInsightsMock.events[0].measurements['likeBlue'], 1); + assert.equal(appInsightsMock.events[0].properties['favoriteDate'], date.toISOString()); + assert.equal(appInsightsMock.events[0].properties['favoriteCars'], JSON.stringify(['bmw', 'audi', 'ford'])); + assert.equal(appInsightsMock.events[0].measurements['favoriteNumber'], 1); + }); + + test('Nested data', () => { + adapter.log('testEvent', { + window : { + title: 'some title', + measurements: { + width: 100, + height: 200 + } + }, + nestedObj: { + nestedObj2: { + nestedObj3: { + testProperty: 'test', + } + }, + testMeasurement:1 + } + }); + + assert.equal(appInsightsMock.events.length, 1); + assert.equal(appInsightsMock.events[0].eventName, `${prefix}/testEvent`); + + assert.equal(appInsightsMock.events[0].properties['window.title'], 'some title'); + assert.equal(appInsightsMock.events[0].measurements['window.measurements.width'], 100); + assert.equal(appInsightsMock.events[0].measurements['window.measurements.height'], 200); + + assert.equal(appInsightsMock.events[0].properties['nestedObj.nestedObj2.nestedObj3'], JSON.stringify({"testProperty":"test"})); + assert.equal(appInsightsMock.events[0].measurements['nestedObj.testMeasurement'],1); + }); +}); \ No newline at end of file diff --git a/src/vs/workbench/parts/telemetry/node/nodeAppInsightsTelemetryAppender.ts b/src/vs/workbench/parts/telemetry/node/nodeAppInsightsTelemetryAppender.ts index 8726430c0bb4d938de147f72facb30c261271a1c..36bad3f6e1f413c223ac72cfe249d4d31a484173 100644 --- a/src/vs/workbench/parts/telemetry/node/nodeAppInsightsTelemetryAppender.ts +++ b/src/vs/workbench/parts/telemetry/node/nodeAppInsightsTelemetryAppender.ts @@ -7,15 +7,13 @@ /* tslint:disable:semicolon */ import errors = require('vs/base/common/errors'); -import types = require('vs/base/common/types'); -import {safeStringify} from 'vs/base/common/objects'; import {IStorageService} from 'vs/platform/storage/common/storage'; import {ITelemetryAppender} from 'vs/platform/telemetry/common/telemetry'; import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace'; +import {AIAdapter, IAIAdapter} from 'vs/base/node/aiAdapter'; import winreg = require('winreg'); import os = require('os'); -import appInsights = require('applicationinsights'); class StorageKeys { public static sqmUserId: string = 'telemetry.sqm.userId'; @@ -26,15 +24,15 @@ class StorageKeys { export class NodeAppInsightsTelemetryAppender implements ITelemetryAppender { - public static EVENT_NAME_PREFIX: string = 'monacoworkbench/'; + public static EVENT_NAME_PREFIX: string = 'monacoworkbench'; private static SQM_KEY: string = '\\Software\\Microsoft\\SQMClient'; private storageService:IStorageService; private contextService: IWorkspaceContextService; - private appInsights: typeof appInsights.client; - private appInsightsVortex: typeof appInsights.client; + private appInsights: IAIAdapter; + private appInsightsVortex: IAIAdapter; protected commonProperties: {[key:string] : string}; protected commonMetrics: {[key: string]: number}; @@ -66,35 +64,16 @@ export class NodeAppInsightsTelemetryAppender implements ITelemetryAppender { } if (key) { - this.appInsights = appInsights.setup(key) - .setAutoCollectRequests(false) - .setAutoCollectPerformance(false) - .setAutoCollectExceptions(false) - .setOfflineMode(true) - .start() - .client; - - this.setupAIClient(this.appInsights); + this.appInsights = new AIAdapter(key, NodeAppInsightsTelemetryAppender.EVENT_NAME_PREFIX); } if(asimovKey) { - this.appInsightsVortex = appInsights.getClient(asimovKey); - this.appInsightsVortex.config.endpointUrl = 'https://vortex.data.microsoft.com/collect/v1'; - this.setupAIClient(this.appInsightsVortex); + this.appInsightsVortex = new AIAdapter(asimovKey, NodeAppInsightsTelemetryAppender.EVENT_NAME_PREFIX); } this.loadAddtionaProperties(); } - private setupAIClient(client: typeof appInsights.client): void { - //prevent App Insights from reporting machine name - if (client && client.context && - client.context.keys && client.context.tags) { - var machineNameKey = client.context.keys.deviceMachineName; - client.context.tags[machineNameKey] = ''; - } - } - private loadAddtionaProperties(): void { // add shell & render version if (process.versions) { @@ -177,109 +156,48 @@ export class NodeAppInsightsTelemetryAppender implements ITelemetryAppender { } } - private getData(data?: any): any { - var properties: {[key: string]: string;} = {}; - var measurements: {[key: string]: number;} = {}; - - var event_data = this.flaten(data); - for(var prop in event_data) { - // enforce property names less than 150 char, take the last 150 char - var propName = prop && prop.length > 150 ? prop.substr( prop.length - 149) : prop; - var property = event_data[prop]; - if (types.isNumber(property)) { - measurements[propName] = property; - - } else if (types.isBoolean(property)) { - measurements[propName] = property ? 1:0; - } else if (types.isString(property)) { - //enforce proeprty value to be less than 1024 char, take the first 1024 char - var propValue = property && property.length > 1024 ? property.substring(0, 1023): property; - properties[propName] = propValue; - } else if (!types.isUndefined(property) && property !== null) { - properties[propName] = property; - } - } - - properties = this.addCommonProperties(properties); - measurements = this.addCommonMetrics(measurements); - - return { - properties: properties, - measurements: measurements - }; - } - - private flaten(obj:any, order:number = 0, prefix? : string): any { - var result:{[key:string]: any} = {}; - var properties = obj ? Object.getOwnPropertyNames(obj) : []; - for (var i =0; i < properties.length; i++) { - var item = properties[i]; - var index = prefix ? prefix + item : item; - - if (types.isArray(obj[item])) { - try { - result[index] = safeStringify(obj[item]); - } catch (e) { - // workaround for catching the edge case for #18383 - // safe stringfy should never throw circular object exception - result[index] = '[Circular-Array]'; - } - } else if (obj[item] instanceof Date) { - result[index] = ( obj[item]).toISOString(); - } else if (types.isObject(obj[item])) { - if (order < 2) { - var item_result = this.flaten(obj[item], order + 1, index + '.'); - for (var prop in item_result) { - result[prop] = item_result[prop]; - } - } else { - try { - result[index] = safeStringify(obj[item]); - } catch (e) { - // workaround for catching the edge case for #18383 - // safe stringfy should never throw circular object exception - result[index] = '[Circular]'; - } - } - } else { - result[index] = obj[item]; - } - } - return result; - } - public log(eventName: string, data?: any): void { - var result = this.getData(data); + + data = data || Object.create(null); + data = this.addCommonMetrics(data); + data = this.addCommonProperties(data); if (this.appInsights) { if (eventName === 'UnhandledError' && data) { - this.appInsights.trackException(data); + this.appInsights.logException(data); } - - this.appInsights.trackEvent(NodeAppInsightsTelemetryAppender.EVENT_NAME_PREFIX+eventName, result.properties, result.measurements); + this.appInsights.log(eventName, data); } if (this.appInsightsVortex) { if (eventName === 'UnhandledError' && data) { - this.appInsightsVortex.trackException(data); + this.appInsightsVortex.logException(data); } - this.appInsightsVortex.trackEvent(NodeAppInsightsTelemetryAppender.EVENT_NAME_PREFIX+eventName, result.properties, result.measurements); + this.appInsightsVortex.log(eventName, data); } } public dispose(): void { + if (this.appInsights) { + this.appInsights.dispose(); + } + + if (this.appInsightsVortex) { + this.appInsightsVortex.dispose(); + } + this.appInsights = null; this.appInsightsVortex = null; } - protected addCommonProperties(properties: { [key: string]: string }): any { + protected addCommonProperties(properties: any): any { for (var prop in this.commonProperties) { properties['common.' + prop] = this.commonProperties[prop]; } return properties; } - protected addCommonMetrics = function (metrics: { [key: string]: number }): any { + protected addCommonMetrics(metrics: any): any { for (var prop in this.commonMetrics) { metrics['common.' + prop] = this.commonMetrics[prop]; } diff --git a/src/vs/workbench/parts/telemetry/test/node/appInsightsTelemetryAppender.test.ts b/src/vs/workbench/parts/telemetry/test/node/appInsightsTelemetryAppender.test.ts index 099bd31823d6c625e49687b516f511c370d65def..d02c02a50fef9bd71c5c16c3ff8192c13c7c0240 100644 --- a/src/vs/workbench/parts/telemetry/test/node/appInsightsTelemetryAppender.test.ts +++ b/src/vs/workbench/parts/telemetry/test/node/appInsightsTelemetryAppender.test.ts @@ -5,34 +5,36 @@ 'use strict'; import * as assert from 'assert'; +import {IAIAdapter} from 'vs/base/node/aiAdapter'; import { NodeAppInsightsTelemetryAppender } from 'vs/workbench/parts/telemetry/node/nodeAppInsightsTelemetryAppender'; interface IAppInsightsEvent { eventName: string; - properties?: {string?: string;}; - measurements?: {string?: number;} + data: any; } -class AppInsightsMock { +class AIAdapterMock implements IAIAdapter { public events: IAppInsightsEvent[]=[]; public IsTrackingPageView: boolean = false; public exceptions: any[] =[]; - public trackEvent(eventName: string, properties?: {string?: string;}, measurements?: {string?: number;}): void { + constructor(private prefix: string, private eventPrefix: string, client?: any) { + } + + public log(eventName: string, data?: any): void { this.events.push({ - eventName: eventName, - properties: properties, - measurements: measurements + eventName: this.prefix+'/'+eventName, + data: data }); } - public trackPageView(): void { - this.IsTrackingPageView = true; - } - public trackException(exception: any): void { + public logException(exception: any): void { this.exceptions.push(exception); } + + public dispose(): void { + } } class ContextServiceMock { @@ -52,11 +54,11 @@ class ContextServiceMock { } suite('Telemetry - AppInsightsTelemetryAppender', () => { - var appInsightsMock: AppInsightsMock; + var appInsightsMock: AIAdapterMock; var appender: NodeAppInsightsTelemetryAppender; setup(() => { - appInsightsMock = new AppInsightsMock(); + appInsightsMock = new AIAdapterMock(NodeAppInsightsTelemetryAppender.EVENT_NAME_PREFIX, NodeAppInsightsTelemetryAppender.EVENT_NAME_PREFIX); appender = new NodeAppInsightsTelemetryAppender(null, new ContextServiceMock('123'), appInsightsMock); }); @@ -68,7 +70,7 @@ suite('Telemetry - AppInsightsTelemetryAppender', () => { appender.log('testEvent'); assert.equal(appInsightsMock.events.length, 1); - assert.equal(appInsightsMock.events[0].eventName, NodeAppInsightsTelemetryAppender.EVENT_NAME_PREFIX+'testEvent'); + assert.equal(appInsightsMock.events[0].eventName, NodeAppInsightsTelemetryAppender.EVENT_NAME_PREFIX+'/testEvent'); }); test('Track UnhandledError as exception and events', () => { @@ -77,79 +79,25 @@ suite('Telemetry - AppInsightsTelemetryAppender', () => { appender.log('UnhandledError', sampleError); assert.equal(appInsightsMock.events.length, 1); - assert.equal(appInsightsMock.events[0].eventName, NodeAppInsightsTelemetryAppender.EVENT_NAME_PREFIX+'UnhandledError'); + assert.equal(appInsightsMock.events[0].eventName, NodeAppInsightsTelemetryAppender.EVENT_NAME_PREFIX+'/UnhandledError'); assert.equal(appInsightsMock.exceptions.length, 1); }); - test('property limits', () => { - var reallyLongPropertyName = 'abcdefghijklmnopqrstuvwxyz'; - for (var i =0; i <6; i++) { - reallyLongPropertyName +='abcdefghijklmnopqrstuvwxyz'; - } - assert(reallyLongPropertyName.length > 150); - - var reallyLongPropertyValue = 'abcdefghijklmnopqrstuvwxyz012345678901234567890123'; - for (var i =0; i <21; i++) { - reallyLongPropertyValue +='abcdefghijklmnopqrstuvwxyz012345678901234567890123'; - } - assert(reallyLongPropertyValue.length > 1024); - - var data = {}; - data[reallyLongPropertyName] = '1234'; - data['reallyLongPropertyValue'] = reallyLongPropertyValue; - appender.log('testEvent', data); - - assert.equal(appInsightsMock.events.length, 1); - - for (var prop in appInsightsMock.events[0].properties){ - assert(prop.length < 150); - assert(appInsightsMock.events[0].properties[prop].length <1024); - } - }); - - test('Different data types', () => { - var date = new Date(); - appender.log('testEvent', { favoriteDate: date, likeRed: false, likeBlue: true, favoriteNumber:1, favoriteColor: 'blue', favoriteCars: ['bmw', 'audi', 'ford']}); - - assert.equal(appInsightsMock.events.length, 1); - assert.equal(appInsightsMock.events[0].eventName, NodeAppInsightsTelemetryAppender.EVENT_NAME_PREFIX+'testEvent'); - assert.equal(appInsightsMock.events[0].properties['favoriteColor'], 'blue'); - assert.equal(appInsightsMock.events[0].measurements['likeRed'], 0); - assert.equal(appInsightsMock.events[0].measurements['likeBlue'], 1); - assert.equal(appInsightsMock.events[0].properties['favoriteDate'], date.toISOString()); - assert.equal(appInsightsMock.events[0].properties['favoriteCars'], JSON.stringify(['bmw', 'audi', 'ford'])); - assert.equal(appInsightsMock.events[0].measurements['favoriteNumber'], 1); - }); - - test('Nested data', () => { + test('Event with data', () => { appender.log('testEvent', { - window : { - title: 'some title', - measurements: { - width: 100, - height: 200 - } - }, - nestedObj: { - nestedObj2: { - nestedObj3: { - testProperty: 'test', - } - }, - testMeasurement:1 - } + title: 'some title', + width: 100, + height: 200 }); assert.equal(appInsightsMock.events.length, 1); - assert.equal(appInsightsMock.events[0].eventName, NodeAppInsightsTelemetryAppender.EVENT_NAME_PREFIX+'testEvent'); + assert.equal(appInsightsMock.events[0].eventName, NodeAppInsightsTelemetryAppender.EVENT_NAME_PREFIX+'/testEvent'); - assert.equal(appInsightsMock.events[0].properties['window.title'], 'some title'); - assert.equal(appInsightsMock.events[0].measurements['window.measurements.width'], 100); - assert.equal(appInsightsMock.events[0].measurements['window.measurements.height'], 200); + assert.equal(appInsightsMock.events[0].data['title'], 'some title'); + assert.equal(appInsightsMock.events[0].data['width'], 100); + assert.equal(appInsightsMock.events[0].data['height'], 200); - assert.equal(appInsightsMock.events[0].properties['nestedObj.nestedObj2.nestedObj3'], JSON.stringify({"testProperty":"test"})); - assert.equal(appInsightsMock.events[0].measurements['nestedObj.testMeasurement'],1); }); test('Test asimov', () => { @@ -158,9 +106,9 @@ suite('Telemetry - AppInsightsTelemetryAppender', () => { appender.log('testEvent'); assert.equal(appInsightsMock.events.length, 2); - assert.equal(appInsightsMock.events[0].eventName, NodeAppInsightsTelemetryAppender.EVENT_NAME_PREFIX+'testEvent'); + assert.equal(appInsightsMock.events[0].eventName, NodeAppInsightsTelemetryAppender.EVENT_NAME_PREFIX+'/testEvent'); // test vortex - assert.equal(appInsightsMock.events[1].eventName, NodeAppInsightsTelemetryAppender.EVENT_NAME_PREFIX+'testEvent'); + assert.equal(appInsightsMock.events[1].eventName, NodeAppInsightsTelemetryAppender.EVENT_NAME_PREFIX+'/testEvent'); }); }); \ No newline at end of file