import { DebugProtocol } from 'vscode-debugprotocol' ; import { OutputEvent } from 'vscode-debugadapter' ; import { fillErrorDetails } from './utils' ; export type ExceptionType = 'uncaughtException' | 'unhandledRejection' | 'firstChance' ; export interface IExecutionResultTelemetryProperties { successful ? : 'true' | 'false' ; exceptionType ? : ExceptionType ; exceptionMessage ? : string ; exceptionName ? : string ; exceptionStack ? : string ; exceptionId ? : string ; startTime ? : string ; timeTakenInMilliseconds ? : string ; } const prefixTelemetryKeys = [ 'exceptionStack' , 'exceptionMessage' , 'exceptionId' ] ; export interface ITelemetryReporter { reportEvent ( name , data ? ) : void ; setupEventHandler ( _sendEvent ) : void ; } export class TelemetryReporter implements ITelemetryReporter { private _sendEvent : ( event : DebugProtocol . Event ) => void ; private _globalTelemetryProperties : any = { } ; reportEvent ( name , data ? ) { if ( ! this . _sendEvent ) { return ; } const combinedData = Object . assign ( { } , this . _globalTelemetryProperties , data ) ; for ( const key of prefixTelemetryKeys ) { if ( combinedData . hasOwnProperty ( key ) ) { combinedData [ `!${ key }` ] = combinedData [ key ] ; delete combinedData [ key ] ; } } const event = new OutputEvent ( name , 'telemetry' , combinedData ) ; this . _sendEvent ( event ) ; } setupEventHandler ( _sendEvent ) { this . _sendEvent = _sendEvent ; } public addCustomGlobalProperty ( additionalGlobalTelemetryProperties ) { Object . assign ( this . _globalTelemetryProperties , additionalGlobalTelemetryProperties ) ; } } export class AsyncGlobalPropertiesTelemetryReporter implements ITelemetryReporter { private _actionsQueue = Promise . resolve ( ) as Promise < any > ; constructor ( private _telemetryReporter ) { } public reportEvent ( name , data ? ) { this . _telemetryReporter . reportEvent ( name , data ) ; } public setupEventHandler ( _sendEvent ) { this . _telemetryReporter . setupEventHandler ( _sendEvent ) ; } public addCustomGlobalProperty ( additionalGlobalPropertiesPromise ) { const reportedPropertyP = Promise . resolve ( additionalGlobalPropertiesPromise ) . then ( property => this . _telemetryReporter . addCustomGlobalProperty ( property ) , rejection => this . reportErrorWhileWaitingForProperty ( rejection ) ) ; this . _actionsQueue = Promise . all ( [ this . _actionsQueue , reportedPropertyP ] ) ; } private reportErrorWhileWaitingForProperty ( rejection ) { let properties = { } ; properties . successful = 'false' ; properties . exceptionType = 'firstChance' ; fillErrorDetails ( properties , rejection ) ; this . _telemetryReporter . reportEvent ( 'error-while-adding-custom-global-property' , properties ) ; } } export class NullTelemetryReporter implements ITelemetryReporter { reportEvent ( name , data ? ) { } setupEventHandler ( _sendEvent ) { } } export const DefaultTelemetryIntervalInMilliseconds = 10000 ; export class BatchTelemetryReporter { private _eventBuckets : { [ eventName ] : any ; } ; private _timer : NodeJS . Timer ; public constructor ( private _telemetryReporter , private _cadenceInMilliseconds = DefaultTelemetryIntervalInMilliseconds ) { this . reset ( ) ; this . setup ( ) ; } public reportEvent ( name , data ? ) { if ( ! this . _eventBuckets [ name ] ) { this . _eventBuckets [ name ] = [ ] ; } this . _eventBuckets [ name ] . push ( data ) ; } public finalize ( ) { this . send ( ) ; clearInterval ( this . _timer ) ; } private setup ( ) { this . _timer = setInterval ( ( ) => this . send ( ) , this . _cadenceInMilliseconds ) ; } private reset ( ) { this . _eventBuckets = { } ; } private send ( ) { for ( const eventName in this . _eventBuckets ) { const bucket = this . _eventBuckets [ eventName ] ; let properties = BatchTelemetryReporter . transfromBucketData ( bucket ) ; this . _telemetryReporter . reportEvent ( eventName , properties ) ; } this . reset ( ) ; } /**
     * Transfrom the bucket of events data from the form:
     * [{
     *  p1: v1,
     *  p2: v2
     * },
     * {
     *  p1: w1,
     *  p2: w2
     *  p3: w3
     * }]
     *
     * to
     * {
     *   p1: [v1,   w1],
     *   p2: [v2,   w2],
     *   p3: [null, w3]
     * }
     *
     *
     * The later form is easier for downstream telemetry analysis.
     */ private static transfromBucketData ( bucketForEventType ) { const allPropertyNamesInTheBucket = BatchTelemetryReporter . collectPropertyNamesFromAllEvents ( bucketForEventType ) ; let properties = { } ; for ( const key of allPropertyNamesInTheBucket ) { properties [ `aggregated.${ key }` ] = [ ] ; } for ( const event of bucketForEventType ) { for ( const propertyName of allPropertyNamesInTheBucket ) { properties [ `aggregated.${ propertyName }` ] . push ( event [ propertyName ] === undefined ? null : event [ propertyName ] ) ; } } for ( const propertyName of allPropertyNamesInTheBucket ) { properties [ `aggregated.${ propertyName }` ] = JSON . stringify ( properties [ `aggregated.${ propertyName }` ] ) ; } return properties ; } /**
     * Get the property keys from all the entries of a event bucket:
     *
     * So
     * [{
     *  p1: v1,
     *  p2: v2
     * },
     * {
     *  p1: w1,
     *  p2: w2
     *  p3: w3
     * }]
     *
     * will return ['p1', 'p2', 'p3']
     */ private static collectPropertyNamesFromAllEvents ( bucket ) { let propertyNamesSet = { } ; for ( const entry of bucket ) { for ( const key of Object . keys ( entry ) ) { propertyNamesSet [ key ] = true ; } } return Object . keys ( propertyNamesSet ) ; } } export interface ITelemetryPropertyCollector { getProperties ( ) : { [ propertyName ] : string ; } ; addTelemetryProperty ( propertyName , value ) : void ; } export class TelemetryPropertyCollector implements ITelemetryPropertyCollector { private _properties : { [ propertyName ] : string ; } = { } ; public getProperties ( ) { return this . _properties ; } public addTelemetryProperty ( propertyName , value ) { this . _properties [ propertyName ] = value ; } } export const telemetry = new AsyncGlobalPropertiesTelemetryReporter ( new TelemetryReporter ( ) ) ;