import { CordovaProjectHelper , IPluginDetails , IProjectType } from "./cordovaProjectHelper" ; import * as fs from "fs" ; import * as path from "path" ; import { Telemetry } from "./telemetry" ; import * as nls from "vscode-nls" ; nls . config ( { messageFormat : nls . MessageFormat . bundle , bundleFormat : nls . BundleFormat . standalone } ) ( ) ; const localize = nls . loadMessageBundle ( ) ; export interface ITelemetryPropertyInfo { value : any ; isPii : boolean ; } export interface ICommandTelemetryProperties { [ propertyName ] : ITelemetryPropertyInfo ; } export interface IExternalTelemetryProvider { sendTelemetry : ( event : string , props : Telemetry . ITelemetryProperties , error ? : Error ) => void ; } interface IDictionary < T > { [ key ] : T ; } interface IHasErrorCode { errorCode : number ; } export abstract class TelemetryGeneratorBase { protected telemetryProperties : ICommandTelemetryProperties = { } ; private componentName : string ; private currentStepStartTime : [ number , number ] ; private currentStep : string = "initialStep" ; private errorIndex : number = - 1 ; constructor ( componentName ) { this . componentName = componentName ; this . currentStepStartTime = process . hrtime ( ) ; } public add ( baseName , value , isPii ) { return this . addWithPiiEvaluator ( baseName , value , ( ) => isPii ) ; } public addWithPiiEvaluator ( baseName , value , piiEvaluator ) { try { if ( Array . isArray ( value ) ) { this . addArray ( baseName , < any [ ] > value , piiEvaluator ) ; } else if ( ! ! value && ( typeof value === "object" || typeof value === "function" ) ) { this . addHash ( baseName , < IDictionary < any > > value , piiEvaluator ) ; } else { this . addString ( baseName , String ( value ) , piiEvaluator ) ; } } catch ( error ) { this . addString ( "telemetryGenerationError." + baseName , String ( error ) , ( ) => false ) ; } return this ; } public addError ( error ) { this . add ( "error.message" + ++ this . errorIndex , error . message , true ) ; let errorWithErrorCode = < IHasErrorCode > < Record < string , any > > error ; if ( errorWithErrorCode . errorCode ) { this . add ( "error.code" + this . errorIndex , errorWithErrorCode . errorCode , false ) ; } return this ; } public time < T > ( name , codeToMeasure ) { let startTime = process . hrtime ( ) ; return codeToMeasure ( ) . finally ( ( ) => this . finishTime ( name , startTime ) ) . catch ( ( reason ) => { this . addError ( reason ) ; throw reason ; } ) ; } public step ( name ) { this . finishTime ( this . currentStep , this . currentStepStartTime ) ; this . sendCurrentStep ( ) ; this . currentStep = name ; this . telemetryProperties = { } ; this . currentStepStartTime = process . hrtime ( ) ; return this ; } public send ( ) { if ( this . currentStep ) { this . add ( "lastStepExecuted" , this . currentStep , false ) ; } this . step ( null ) ; } public getTelemetryProperties ( ) { return this . telemetryProperties ; } protected abstract sendTelemetryEvent ( telemetryEvent ) ; private sendCurrentStep ( ) { this . add ( "step" , this . currentStep , false ) ; let telemetryEvent = new Telemetry . TelemetryEvent ( this . componentName ) ; TelemetryHelper . addTelemetryEventProperties ( telemetryEvent , this . telemetryProperties ) ; this . sendTelemetryEvent ( telemetryEvent ) ; } private addArray ( baseName , array , piiEvaluator ) { let elementIndex = 1 ; array . forEach ( ( element ) => this . addWithPiiEvaluator ( baseName + elementIndex ++ , element , piiEvaluator ) ) ; } private addHash ( baseName , hash , piiEvaluator ) { Object . keys ( hash ) . forEach ( ( key ) => this . addWithPiiEvaluator ( baseName + "." + key , hash [ key ] , piiEvaluator ) ) ; } private addString ( name , value , piiEvaluator ) { this . telemetryProperties [ name ] = TelemetryHelper . telemetryProperty ( value , piiEvaluator ( value , name ) ) ; } private combine ( ... components ) { let nonNullComponents = components . filter ( ( component ) => component !== null ) ; return nonNullComponents . join ( "." ) ; } private finishTime ( name , startTime ) { let endTime = process . hrtime ( startTime ) ; this . add ( this . combine ( name , "time" ) , String ( endTime [ 0 ] * 1000 + endTime [ 1 ] / 1000000 ) , false ) ; } } export class TelemetryGenerator extends TelemetryGeneratorBase { protected sendTelemetryEvent ( telemetryEvent ) { Telemetry . send ( telemetryEvent ) ; } } export class TelemetryHelper { public static createTelemetryEvent ( eventName ) { return new Telemetry . TelemetryEvent ( eventName ) ; } public static createTelemetryActivity ( eventName ) { return new Telemetry . TelemetryActivity ( eventName ) ; } public static determineProjectTypes ( projectRoot ) { let ionicVersions = CordovaProjectHelper . checkIonicVersions ( projectRoot ) ; let meteor = CordovaProjectHelper . exists ( path . join ( projectRoot , ".meteor" ) ) ; let mobilefirst = CordovaProjectHelper . exists ( path . join ( projectRoot , ".project" ) ) ; let phonegap = CordovaProjectHelper . exists ( path . join ( projectRoot , "www" , "res" , ".pgbomit" ) ) ; let cordova = CordovaProjectHelper . exists ( path . join ( projectRoot , "config.xml" ) ) ; return Promise . all ( [ meteor , mobilefirst , phonegap , cordova ] ) . then ( ( [ isMeteor , isMobilefirst , isPhonegap , isCordova ] ) => ( { isIonic1 : ionicVersions . isIonic1 , isIonic2 : ionicVersions . isIonic2 , isIonic3 : ionicVersions . isIonic3 , isIonic4 : ionicVersions . isIonic4 , isIonic5 : ionicVersions . isIonic5 , isMeteor : isMeteor , isMobilefirst : isMobilefirst , isPhonegap : isPhonegap , isCordova : isCordova , } ) ) ; } public static telemetryProperty ( propertyValue , pii ? ) { return { value : String ( propertyValue ) , isPii : pii || false } ; } public static addTelemetryEventProperties ( event , properties ) { if ( ! properties ) { return ; } Object . keys ( properties ) . forEach ( function ( propertyName ) : void { TelemetryHelper . addTelemetryEventProperty ( event , propertyName , properties [ propertyName ] . value , properties [ propertyName ] . isPii ) ; } ) ; } public static addTelemetryEventProperty ( event , propertyName , propertyValue , isPii ) { if ( Array . isArray ( propertyValue ) ) { TelemetryHelper . addMultiValuedTelemetryEventProperty ( event , propertyName , propertyValue , isPii ) ; } else { TelemetryHelper . setTelemetryEventProperty ( event , propertyName , propertyValue , isPii ) ; } } public static generate < T > ( name , codeGeneratingTelemetry ) { let generator = new TelemetryGenerator ( name ) ; return generator . time ( null , ( ) => codeGeneratingTelemetry ( generator ) ) . finally ( ( ) => generator . send ( ) ) ; } public static sendPluginsList ( projectRoot , pluginsList ) { let pluginFilePath = path . join ( projectRoot , ".vscode" , "plugins.json" ) ; let pluginFileJson ; if ( CordovaProjectHelper . existsSync ( pluginFilePath ) ) { try { let pluginFileJsonContents = fs . readFileSync ( pluginFilePath , "utf8" ) . toString ( ) ; pluginFileJson = JSON . parse ( pluginFileJsonContents ) ; } catch ( error ) { console . error ( error ) ; } } let pluginsFileList = new Array < string > ( ) ; if ( pluginFileJson && pluginFileJson . plugins ) { pluginsFileList = pluginFileJson . plugins ; } else { pluginFileJson = new Object ( ) ; } let newPlugins = new Array < string > ( ) ; pluginsList . forEach ( plugin => { if ( pluginsFileList . indexOf ( plugin ) < 0 ) { newPlugins . push ( plugin ) ; pluginsFileList . push ( plugin ) ; } } ) ; if ( newPlugins . length === 0 ) { return ; } let pluginDetails = newPlugins . map ( pluginName => CordovaProjectHelper . getInstalledPluginDetails ( projectRoot , pluginName ) ) . filter ( detail => ! ! detail ) ; let pluginEvent = new Telemetry . TelemetryEvent ( "plugins" , { plugins : JSON . stringify ( pluginDetails ) } ) ; Telemetry . send ( pluginEvent ) ; pluginFileJson . plugins = pluginsFileList ; try { fs . writeFileSync ( pluginFilePath , JSON . stringify ( pluginFileJson ) ) ; } catch ( err ) { throw new Error ( err . message + localize ( "CWDDoesntReferToTheWorkspaceRootDirectory" , " It seems that 'cwd' parameter doesn't refer to the workspace root directory. Please make sure that 'cwd' contains the path to the workspace root directory." ) ) ; } } private static setTelemetryEventProperty ( event , propertyName , propertyValue , isPii ) { if ( isPii ) { event . setPiiProperty ( propertyName , String ( propertyValue ) ) ; } else { event . properties [ propertyName ] = String ( propertyValue ) ; } } private static addMultiValuedTelemetryEventProperty ( event , propertyName , propertyValue , isPii ) { for ( let i = 0 ; i < propertyValue . length ; i ++ ) { TelemetryHelper . setTelemetryEventProperty ( event , propertyName + i , propertyValue [ i ] , isPii ) ; } } } export interface ISimulateTelemetryProperties { platform ? : string ; target : string ; port : number ; simulatePort ? : number ; livereload ? : boolean ; livereloadDelay ? : number ; forcePrepare ? : boolean ; }