{"version":3,"sources":["../../../packages/core/diagnostics/sme-web-telemetry.ts"],"names":[],"mappings":"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","file":"sme-web-telemetry.d.ts","sourcesContent":["// 1DS documentation found at\r\n// https://1dsdocs.azurewebsites.net/getting-started/javascript-getting_started.html\r\n\r\nimport { EMPTY, Observable, of } from 'rxjs';\r\nimport { catchError, delay, retry, shareReplay, take, tap } from 'rxjs/operators';\r\nimport { AppContext } from '../data/app-context';\r\nimport { PowerShellCommand } from '../data/powershell';\r\nimport { NotificationState } from '../notification/notification-state';\r\nimport { RpcNotification } from '../rpc/notification/rpc-notification-model';\r\nimport {\r\n    GatewayInstallationType,\r\n    GatewayInventory,\r\n    GatewayMode,\r\n    GatewayOperationalMode\r\n} from '../shared/gateway-inventory/gateway-inventory';\r\nimport { GatewayInventoryCache } from '../shared/gateway-inventory/gateway-inventory-cache';\r\nimport { Logging } from './logging';\r\nimport {\r\n    CompleteSmePerformanceData,\r\n    IContentUpdateOverrideValues,\r\n    IPageActionOverrideValues,\r\n    IPageViewOverrideValues,\r\n    IPageViewPerformanceOverrideValues,\r\n    LighthousePerformanceMetrics,\r\n    PowershellDetails,\r\n    SmeMetaLabels,\r\n    SmePerformanceData,\r\n    TelemetryActionTypes,\r\n    TelemetryEventBody,\r\n    TelemetryEvents,\r\n    TelemetryEventStates,\r\n    TelemetryEventTypes\r\n} from './sme-web-telemetry-models';\r\n\r\n// placeholder for the oneDS type, introduced when we load script.\r\ndeclare let oneDS: any;\r\n\r\nexport class SmeWebTelemetry {\r\n\r\n    /**\r\n     * The source name to use when logging about this service.\r\n     */\r\n    private static get logSourceName() {\r\n        return 'WebTelemetry';\r\n    }\r\n    /**\r\n     * Gets the name of current shell or module.\r\n     */\r\n    private static get nameOfModule(): string {\r\n        return MsftSme.self().Init.moduleName ? MsftSme.self().Init.moduleName : SmeWebTelemetry.testMode;\r\n    }\r\n\r\n    private static get backlogHasSpace(): boolean {\r\n        return this.eventBacklog.length < 50;\r\n    }\r\n\r\n    private static eventBacklog: [TelemetryEvents, TelemetryEventBody][] = [];\r\n    private static telemetryHandler = null;\r\n\r\n    private static testMode = 'test';\r\n    private static rpcInitAlready = false;\r\n    private static metaTags: { [tag: string]: string } = {};\r\n    private static powershellIdMap: { [id: string]: PowerShellCommand } = {};\r\n    private static isProd = true;\r\n\r\n    public static get isProduction() {\r\n        return this.isProd;\r\n    }\r\n\r\n    // AppId/Tenant Token provided by MarTech support\r\n    // AppId is not used, but it will be mapped to later in the pipeline\r\n    // Full keys are JS:WindowsAdminCenter and o:f78e2b7c9ae4461399c360160d82dcfc for\r\n    // AppId/Tenant token respectively. In Cosmos, only the initial portion of the tenant token is used.\r\n    // private static windowsAdminCenterAppId = 'WindowsAdminCenter';\r\n    private static windowsAdminCenterTenantToken = 'f78e2b7c9ae4461399c360160d82dcfc-9371288c-a1b9-4fdc-a196-0340fc5f9880-7071';\r\n\r\n    /**\r\n     * Map of module versions used in this instance of web telemetry - memoize values here.\r\n     */\r\n    private static moduleVersions = {};\r\n\r\n    /**\r\n     * Get the list of module versions for use in telemetry where the event is sent via RPC to shell\r\n     * from the actual module the event is called in.\r\n     * @returns list of module mappings to versions\r\n     */\r\n    private static getModuleVersion(moduleName: string): string {\r\n        if (this.moduleVersions[moduleName]) {\r\n            return this.moduleVersions[moduleName];\r\n        }\r\n        const environment = MsftSme.self().Environment;\r\n        const moduleObject = environment.modules.find((module) => {\r\n            return module.name === moduleName;\r\n        });\r\n        const version = moduleObject ? moduleObject.version : 'N/A';\r\n        this.moduleVersions[moduleName] = version;\r\n        return version;\r\n    }\r\n\r\n    /**\r\n     * Send any manual events that were triggered prior to telemetry initializing.\r\n     */\r\n    private static sendBacklogEvents(): void {\r\n        this.eventBacklog.forEach(([type, body]: [TelemetryEvents, TelemetryEventBody]) => {\r\n            switch (type) {\r\n                case TelemetryEvents.Performance: {\r\n                    const overrideVals = body.overrideValues as IContentUpdateOverrideValues;\r\n                    this.tracePerformanceData(body.performance, overrideVals ? overrideVals.content : null);\r\n                    break;\r\n                }\r\n                case TelemetryEvents.Lighthouse: {\r\n                    this.traceLighthouseData(body.lighthouse, body.overrideValues as IContentUpdateOverrideValues);\r\n                    break;\r\n                }\r\n                case TelemetryEvents.ModuleOpenPerformance: this.traceModuleOpenPerformance(body.performance); break;\r\n                case TelemetryEvents.Action: {\r\n                    const overrideVals = body.overrideValues as IPageActionOverrideValues;\r\n                    this.traceAction(body.element, overrideVals, body.customProperties);\r\n                    break;\r\n                }\r\n                case TelemetryEvents.ContentUpdate: {\r\n                    const overrideVals = body.overrideValues as IContentUpdateOverrideValues;\r\n                    this.traceContentUpdate(overrideVals, body.customProperties);\r\n                    break;\r\n                }\r\n                case TelemetryEvents.PageView: {\r\n                    const overrideVals = body.overrideValues as IPageViewOverrideValues;\r\n                    this.tracePageView(overrideVals, body.customProperties);\r\n                    break;\r\n                }\r\n                case TelemetryEvents.Notification: this.traceClientNotification(body.notification); break;\r\n                default: return;\r\n            }\r\n        });\r\n        this.eventBacklog = [];\r\n    }\r\n\r\n    /**\r\n     * Helper to combine setting metaTags, initialize handler, and\r\n     * send any telemetry events that occurred prior to initialization\r\n     */\r\n    private static configureAndInitTelemetry(): void {\r\n        this.setMetaInDom();\r\n        this.initTelemetryHandler().subscribe({\r\n            next: () => {\r\n                this.sendBacklogEvents();\r\n            },\r\n            error: (error: Error) => {\r\n                Logging.logError(this.logSourceName, '{0}: {1}'.format(error.name, error.message));\r\n            }\r\n        });\r\n    }\r\n\r\n    /**\r\n     * Helper function to set metaTags in DOM\r\n     */\r\n    private static setMetaInDom(): void {\r\n        // create an indexed table of current meta data.\r\n        const metaList = document.head.getElementsByTagName(\"meta\");\r\n        const metaIndex = {};\r\n        for (let i = 0; i < metaList.length; i++) {\r\n            const metaName = metaList[i].getAttribute(\"name\");\r\n            if (metaName && metaName.indexOf('awa-') === 0) {\r\n                metaIndex[metaName] = metaList[i];\r\n            }\r\n        }\r\n\r\n        // set or replace meta tags in the DOM\r\n        for (const [key, value] of Object.entries(this.metaTags)) {\r\n            const metaName = `awa-${key}`;\r\n            const oldMeta = metaIndex[metaName];\r\n            if (oldMeta) {\r\n                document.head.removeChild(oldMeta);\r\n            }\r\n\r\n            const newMeta = document.createElement('meta');\r\n            newMeta.name = metaName;\r\n            newMeta.content = value;\r\n            document.head.appendChild(newMeta);\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Update Meta-tags and/or initialize telemetry, depending on whether rpc message came before or after app-context init.\r\n     * @param newMetaTags   Primarily contains WAC-Session-Id & extension-version, received from RPC message\r\n     */\r\n    public static updateFromRpcInit(newMetaTags: { [tag: string]: string }): void {\r\n        // All the fields will be updated at the same time here, checking one (wac-session-id is the main one) is enough for the check\r\n        const shouldInitHere = SmeMetaLabels.SessionId in this.metaTags\r\n            && this.metaTags[SmeMetaLabels.SessionId] === 'N/A';\r\n\r\n        this.rpcInitAlready = true;\r\n        MsftSme.deepAssign(this.metaTags, newMetaTags);\r\n\r\n        // If metaTags contains session ID but it's N/A (ie initial init was called already), call the init. If not,\r\n        // just wait for normal init to be called in app-context. Depending on timing RPC update can happen before or after.\r\n        if (shouldInitHere) {\r\n            this.configureAndInitTelemetry();\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Load 1DS if does not already exist.\r\n     * @param appContext App context currently being used\r\n     */\r\n    public static init(appContext: AppContext): Observable<boolean> {\r\n        if (this.telemetryHandler) {\r\n            return EMPTY;\r\n        }\r\n\r\n        // Set fields we don't need gateway for, if connection to gateway fails, initialize telemetry with limited fields\r\n        this.metaTags[SmeMetaLabels.Language] = MsftSme.self().Resources.localeId;\r\n        this.metaTags[SmeMetaLabels.Market] = MsftSme.self().Resources.localeId;\r\n        this.metaTags[SmeMetaLabels.IsProduction] = MsftSme.self().Init.isProduction.toString();\r\n        this.metaTags[SmeMetaLabels.Environment] = MsftSme.self().Init.isProduction.toString();\r\n        this.metaTags[SmeMetaLabels.ExtensionVersion] = MsftSme.self().Environment.version;\r\n        this.metaTags[SmeMetaLabels.SessionId] = MsftSme.sessionId();\r\n        // Instantiate these initially so they aren't empty values\r\n        this.metaTags[SmeMetaLabels.InstallationType] = 'N/A';\r\n        this.metaTags[SmeMetaLabels.Build] = 'N/A';\r\n        this.metaTags[SmeMetaLabels.GatewayMode] = GatewayMode[MsftSme.self().Init.mode];\r\n        this.metaTags[SmeMetaLabels.GatewayOperationalMode] = 'N/A';\r\n        this.isProd = MsftSme.self().Init.isProduction;\r\n\r\n        const gatewayInventoryCache = new GatewayInventoryCache(appContext);\r\n        return gatewayInventoryCache.query({})\r\n            .pipe(\r\n                retry({\r\n                    count: 2,\r\n                    delay: error => error.pipe(\r\n                            delay(1000),\r\n                            tap(() => Logging.logDebug(this.logSourceName, 'Attempting to query gateway again...')))\r\n                }),\r\n                take(1),\r\n                tap((status: any) => {\r\n                    const inventory: GatewayInventory = status.instance;\r\n                    this.isProd = this.isProd\r\n                        && inventory.gatewayOperationalMode === GatewayOperationalMode.Production;\r\n\r\n                    this.metaTags[SmeMetaLabels.InstallationType] = inventory.installationType || GatewayInstallationType.Standard;\r\n                    this.metaTags[SmeMetaLabels.Build] = inventory.gatewayVersion;\r\n                    this.metaTags[SmeMetaLabels.GatewayMode] = GatewayMode[inventory.mode];\r\n                    this.metaTags[SmeMetaLabels.GatewayOperationalMode] = GatewayOperationalMode[inventory.gatewayOperationalMode];\r\n                    this.metaTags[SmeMetaLabels.IsProduction] = this.isProd.toString();\r\n                    this.metaTags[SmeMetaLabels.Environment] = this.isProd.toString();\r\n                    // If not shell, handle in RPC handler, because some fields will init after 1DS inits\r\n                    // If rpc handler has happened already, go ahead with this\r\n                    if (MsftSme.isShell() || this.rpcInitAlready) {\r\n                        this.configureAndInitTelemetry();\r\n                    }\r\n                }),\r\n                catchError((error) => {\r\n                    Logging.logWarning(this.logSourceName, 'Telemetry failed to initialized with error {}'.format(error.message));\r\n                    return of(false);\r\n                })\r\n            );\r\n    }\r\n\r\n    /**\r\n     * Set config and initialize telemetry library handler\r\n     */\r\n    private static initTelemetryHandler(): Observable<any> {\r\n        const observable = new Observable(observer => {\r\n            const script: HTMLScriptElement = document.createElement('script');\r\n            script.type = 'text/javascript';\r\n            script.src = MsftSme.self().Environment.configuration.telemetry.sourceLibraryCdnLink;\r\n            script.integrity = MsftSme.self().Environment.configuration.telemetry.sourceLibraryCdnIntegrityHash;\r\n            script.crossOrigin = 'anonymous';\r\n            script.async = true;\r\n\r\n            script.onload = () => {\r\n                this.telemetryHandler = new oneDS.ApplicationInsights();\r\n                const entryType: string = MsftSme.self().Init.entryPointType;\r\n                const entryName: string = MsftSme.self().Init.entryPointName;\r\n\r\n                const config = {\r\n                    instrumentationKey: this.windowsAdminCenterTenantToken,\r\n                    // post channel configuration\r\n                    channelConfiguration: {\r\n                        eventsLimitInMem: 50\r\n                    },\r\n                    // Properties Plugin configuration\r\n                    propertyConfiguration: {\r\n                        userAgent: 'Windows Admin Center',\r\n                        sessionAsGuid: true\r\n                    },\r\n                    // Web Analytics Plugin configuration\r\n                    webAnalyticsConfiguration: {\r\n                        autoCapture: {\r\n                            scroll: false,\r\n                            pageView: true,\r\n                            onLoad: false,\r\n                            onUnload: true,\r\n                            click: true,\r\n                            resize: true,\r\n                            jsError: true,\r\n                            lineage: true\r\n                        },\r\n                        coreData: {\r\n                            pageName: this.nameOfModule,\r\n                            // to prevent personal data from being sent in URI\r\n                            referrerUri: 'windows.admin.center',\r\n                            requestUri: 'windows.admin.center',\r\n                            pageTags: {\r\n                                entryPointName: entryName || '',\r\n                                entryPointType: entryType || ''\r\n                            }\r\n                        },\r\n                        useShortNameForContentBlob: false\r\n                    }\r\n                };\r\n                this.telemetryHandler.initialize(config, []);\r\n                this.telemetryHandler.addTelemetryInitializer((event: any) => {\r\n                    // If we send an event that originates from a different module (eg notifications in shell)\r\n                    // change the extension version to match module\r\n                    if (event.baseData.name !== this.nameOfModule) {\r\n                        event.baseData.properties.pageTags.metaTags[SmeMetaLabels.ExtensionVersion] =\r\n                            this.getModuleVersion(event.baseData.name);\r\n                    }\r\n                    event.baseData.properties.pageTags.screenResolution = {\r\n                        availableHeight: window.screen.availHeight,\r\n                        availableWidth: window.screen.availWidth\r\n                    };\r\n                    event.baseData.properties.pageTags.frameDetails = { height: window.innerHeight, width: window.innerWidth };\r\n                });\r\n                observer.next();\r\n                observer.complete();\r\n            };\r\n            script.onerror = () => {\r\n                script.remove();\r\n                observer.error(new Error('1DS Script could not be loaded'));\r\n            };\r\n            document.body.appendChild(script);\r\n        }).pipe(shareReplay(1));\r\n        return observable;\r\n    }\r\n\r\n    /**\r\n     * Send a Page-Action event through Web Telemetry.\r\n     * @param element Element action is being executed on\r\n     * @param overrideValues Various values to override within default Web Telemetry page action fields, see Web Telemetry documentation.\r\n     * @param properties     Extra properties in an index signature. These are placed under the data field in partC data.\r\n     */\r\n    public static traceAction(element: Element, overrideValues?: IPageActionOverrideValues, properties?: { [name: string]: any }): void {\r\n        if (!this.telemetryHandler) {\r\n            if (this.backlogHasSpace) {\r\n                this.eventBacklog.push([TelemetryEvents.Action, {\r\n                    element: element, overrideValues: overrideValues,\r\n                    customProperties: properties\r\n                }]);\r\n            }\r\n            return;\r\n        }\r\n        if (!overrideValues.actionType) {\r\n            overrideValues.actionType = TelemetryActionTypes.Automatic;\r\n        }\r\n        this.telemetryHandler.capturePageAction(element, overrideValues, properties);\r\n    }\r\n\r\n    /**\r\n     * Send a Page-View event through Web Telemetry.\r\n     * @param overrideValues Various values to override within default Web Telemetry page view fields, see Web Telemetry documentation.\r\n     * @param properties     Extra properties in an index signature. These are placed under the data field in partC data.\r\n     */\r\n    public static tracePageView(overrideValues: IPageViewOverrideValues, properties?: { [name: string]: any }): void {\r\n        if (!this.telemetryHandler) {\r\n            if (this.backlogHasSpace) {\r\n                this.eventBacklog.push([TelemetryEvents.PageView, { overrideValues: overrideValues }]);\r\n            }\r\n            return;\r\n        }\r\n        this.telemetryHandler.capturePageView(overrideValues, properties);\r\n    }\r\n\r\n    /**\r\n     * Send a Content-Update event through Web Telemetry.\r\n     * @param overrideValues Various values to override within default Web Telemetry content update fields, see Web Telemetry documentation\r\n     * @param properties     Extra properties in an index signature. These are placed under the data field in partC data.\r\n     */\r\n    public static traceContentUpdate(overrideValues: IContentUpdateOverrideValues, properties?: { [name: string]: any }): void {\r\n        if (!this.telemetryHandler) {\r\n            if (this.backlogHasSpace) {\r\n                this.eventBacklog.push([TelemetryEvents.ContentUpdate, { overrideValues: overrideValues, customProperties: properties }]);\r\n            }\r\n            return;\r\n        }\r\n        if (!overrideValues.actionType) {\r\n            overrideValues.actionType = TelemetryActionTypes.Automatic;\r\n        }\r\n        this.telemetryHandler.captureContentUpdate(overrideValues, properties);\r\n    }\r\n\r\n    /**\r\n     * Add standard fields onto performance data\r\n     * @param data SmePerformanceData to be sent\r\n     */\r\n    private static fillStandardPerformanceData(data: SmePerformanceData): CompleteSmePerformanceData {\r\n        const fullDataPayload: CompleteSmePerformanceData = {\r\n            extension: MsftSme.self().Environment.name,\r\n            entryPointName: MsftSme.self().Init.entryPointName || '',\r\n            url: window.location.pathname,\r\n            moduleOpened: false,\r\n            totalLoadTime: '',\r\n            label: data.label,\r\n            sme: {},\r\n            resources: {},\r\n            navigation: {}\r\n        };\r\n        // Truncate all decimals to 5 places to avoid cosmos stream from redacting any\r\n        // Convert PerformanceNavigationTiming to JSON so we can manipulate it via Object.() methods.\r\n        fullDataPayload.totalLoadTime = data.totalLoadTime.toFixed(5);\r\n        const timingData = {\r\n            'sme': data.sme || {},\r\n            'resources': data.resources || {},\r\n            'navigation': data.navigation ? data.navigation.toJSON() : {}\r\n        };\r\n\r\n        for (const [sourceKey, sourceVal] of Object.entries(timingData)) {\r\n            for (const [resourceKey, resourceVal] of Object.entries(sourceVal)) {\r\n                // navigation has some string-string maps, ignore the string values.\r\n                // Convert all numbers back into numbers to preserve types in cosmos stream\r\n                if (!MsftSme.isEmpty(sourceVal) && typeof resourceVal === 'number') {\r\n                    fullDataPayload[sourceKey][resourceKey] = parseFloat(resourceVal.toFixed(7));\r\n                }\r\n            }\r\n        }\r\n\r\n        // Set this here if need - often the appLoad times happen before Init is populated,\r\n        // so check again to make sure if its actually empty.\r\n        if (fullDataPayload.navigation) {\r\n            fullDataPayload.entryPointName = MsftSme.self().Init.entryPointName || '';\r\n        }\r\n        return fullDataPayload;\r\n    }\r\n\r\n    /**\r\n     * Send a content update event. This content update contains an updated sme-specific timings structure with relevant\r\n     * performance data under the data field. The original timings data is also contained under the navigation field.\r\n     * The structure of the event is such:\r\n     * data : { ...,\r\n     *      \"timings\": {\r\n     *          \"extension\": [extension-name],\r\n     *          \"entryPointName\": [entryPointName],\r\n     *          \"url\": [url-endpoint],\r\n     *          \"moduleOpened\" : [isModuleOpened],\r\n     *          \"sme\": {\r\n     *              [sme-mark] : [mark-timestamps],\r\n     *              ...\r\n     *          },\r\n     *          \"resources\": {\r\n     *              [resource-endpoint] : [resource-load-complete-timestamps],\r\n     *              ...\r\n     *          },\r\n     *          \"navigation\": {\r\n     *              [performance-navigation-event]: [navigation-event-timestamps]\r\n     *          }\r\n     *      }, ...\r\n     * }\r\n     * Certain fields are set within this class instead of outside modules since they will always be the same.\r\n     * @param dataPayload performance data to be sent through telemetry.\r\n     * @param contentOverrides any content overriding behavior wanted in the performance event\r\n     */\r\n    public static tracePerformanceData(dataPayload: SmePerformanceData, contentOverrides?: any): void {\r\n        if (!this.telemetryHandler) {\r\n            if (this.backlogHasSpace) {\r\n                this.eventBacklog.push([TelemetryEvents.Performance,\r\n                {\r\n                    performance: dataPayload,\r\n                    overrideValues: { content: contentOverrides } as IContentUpdateOverrideValues\r\n                }]);\r\n            }\r\n            return;\r\n        }\r\n        const data = SmeWebTelemetry.fillStandardPerformanceData(dataPayload);\r\n\r\n        // A for automatic, performance tracking is done automatically.\r\n        const overrideValues: IContentUpdateOverrideValues = {\r\n            actionType: TelemetryActionTypes.Automatic,\r\n            content: contentOverrides || dataPayload\r\n        };\r\n\r\n        const customProps = { timings: data, type: TelemetryEventTypes.Performance };\r\n        this.telemetryHandler.captureContentUpdate(overrideValues, customProps);\r\n    }\r\n\r\n    /**\r\n     * Send a content update event - this event will contain timings for lighthouse calculation in the backend\r\n     * This can potentially be sent a couple times with overlapping data for one page load event, depending\r\n     * on when the TTI is calculated. In this scenario, it will be handled on the backend.\r\n     * @param dataPayload       Lighthouse data\r\n     * @param contentOverrides  any content overriding behavior wanted in the performance event\r\n     */\r\n    public static traceLighthouseData(dataPayload: LighthousePerformanceMetrics, contentOverrides?: any,\r\n        properties?: { [name: string]: any }): void {\r\n        if (!this.telemetryHandler) {\r\n            if (this.backlogHasSpace) {\r\n                this.eventBacklog.push([TelemetryEvents.Lighthouse,\r\n                {\r\n                    lighthouse: dataPayload,\r\n                    overrideValues: { content: contentOverrides } as IContentUpdateOverrideValues\r\n                }]);\r\n            }\r\n            return;\r\n        }\r\n        const overrideValues: IContentUpdateOverrideValues = {\r\n            actionType: TelemetryActionTypes.Automatic,\r\n            content: contentOverrides || dataPayload\r\n        };\r\n\r\n        const customProps = { timings: dataPayload, type: TelemetryEventTypes.Lighthouse };\r\n        MsftSme.deepAssign(customProps, properties);\r\n        this.telemetryHandler.captureContentUpdate(overrideValues, customProps);\r\n    }\r\n\r\n    /**\r\n     * See tracePerformanceData comments, only difference is moduleOpened is true and the contentOverrides\r\n     * @param dataPayload performance data to be sent through telemetry.\r\n     */\r\n    public static traceModuleOpenPerformance(dataPayload: SmePerformanceData): void {\r\n        if (!this.telemetryHandler) {\r\n            if (this.backlogHasSpace) {\r\n                this.eventBacklog.push([TelemetryEvents.ModuleOpenPerformance, { performance: dataPayload }]);\r\n            }\r\n            return;\r\n        }\r\n        const data = SmeWebTelemetry.fillStandardPerformanceData(dataPayload);\r\n        data.moduleOpened = true;\r\n\r\n        const overrideValues: IPageViewPerformanceOverrideValues = {\r\n            customTiming: JSON.stringify(data)\r\n        };\r\n        const customProps = { timings: data, type: TelemetryEventTypes.Performance };\r\n\r\n        this.telemetryHandler.capturePageViewPerformance(overrideValues, customProps);\r\n    }\r\n\r\n    /**\r\n     * Helper to create event boilerplate for notification\r\n     * @param clientNotification Client notification to send event for\r\n     */\r\n    public static traceClientNotification(clientNotification: RpcNotification): void {\r\n        if (!this.telemetryHandler) {\r\n            if (this.backlogHasSpace) {\r\n                this.eventBacklog.push([TelemetryEvents.Notification, { notification: clientNotification }]);\r\n            }\r\n            return;\r\n        }\r\n        const overrideValues: IContentUpdateOverrideValues = {\r\n            content: {\r\n                message: clientNotification.message,\r\n                title: clientNotification.title,\r\n                state: NotificationState[clientNotification.state],\r\n                contentType: TelemetryEventTypes.Notification\r\n            },\r\n            // If source is not shell, replace pagename with proper source.\r\n            pageName: clientNotification.sourceName ? clientNotification.sourceName : this.nameOfModule\r\n        };\r\n        this.traceContentUpdate(overrideValues, { type: TelemetryEventTypes.Notification });\r\n    }\r\n\r\n    /**\r\n     * Helper function to assign a command to an id in a multi-step workitem process\r\n     * @param id        The ID of the powershell session to assign\r\n     * @param command   The command to assign to the session ID\r\n     */\r\n    public static addPowershellId(id: string, command: PowerShellCommand) {\r\n        this.powershellIdMap[id] = command;\r\n    }\r\n\r\n    /**\r\n     * Helper function to assign a command to an id in a multi-step workitem process\r\n     * @param id        The ID of the powershell session to assign\r\n     * @param command   The command to assign to the session ID\r\n     */\r\n    public static removePowershellId(id: string) {\r\n        delete this.powershellIdMap[id];\r\n    }\r\n\r\n    /**\r\n     * Helper to create event boilerplate for powershell event. In the case where command is not available,\r\n     * use an ID (usually ps session ID, which may correlate with work item id)\r\n     * @param command Powershell Command\r\n     * @param state   State of powershell event (start/end/etc)\r\n     * @param details Optional details object - see interface for more detail.\r\n     */\r\n    public static tracePowershellEvent(command: PowerShellCommand, state: TelemetryEventStates, details?: PowershellDetails): void {\r\n        const id = details ? details.id : null;\r\n        const response = details ? details.response : null;\r\n        const psCommand = command ? command : this.powershellIdMap[id];\r\n\r\n        // Ignore any events that are either null or un-named PS commands.\r\n        if ((psCommand && !psCommand.command) || !psCommand) {\r\n            return;\r\n        }\r\n\r\n        // If state is not started and id is provided, remove the ID from the map to save space.\r\n        if (state !== TelemetryEventStates.Started && id) {\r\n            this.removePowershellId(id);\r\n        }\r\n\r\n        const psContent = {\r\n            psCommand: psCommand.command, state: state, psModule: psCommand.module,\r\n            otherData: details && details.otherData ? details.otherData : {}\r\n        };\r\n        const sourceName = this.nameOfModule;\r\n        if (response && response.error) {\r\n            psContent['errorCodes'] = [response.error.code];\r\n        } else if (response && response.errors) {\r\n            psContent['errorCodes'] = response.errors.map((e) => e.errorType);\r\n            psContent.otherData['hResult'] = response.errors.map((e) => e.detailRecord && e.detailRecord.hResult);\r\n        }\r\n        SmeWebTelemetry.traceAction(null, { content: psContent, pageName: sourceName },\r\n            { type: TelemetryEventTypes.Powershell });\r\n    }\r\n\r\n    /**\r\n     * Helper to create event boilerplate for powershell batch event. Batch doesn't deal with work items, so\r\n     * we can ignore the ID and sourceName handling that the above function handles.\r\n     * @param commands Powershell Commands List in properties stringified form. PS Batch events places PS command into a\r\n     *                 '{properties: PSCommand }' string structure.\r\n     * @param state    State of powershell event (start/end/etc)\r\n     * @param details  Optional details object contains various optional fields in powershell event.\r\n     * @returns\r\n     */\r\n    public static tracePowershellBatchEvent(commands: string[], state: TelemetryEventStates, details?: PowershellDetails) {\r\n        // check that at least one command is valid\r\n        if (!commands || commands.length === 0) {\r\n            return;\r\n        }\r\n\r\n        let parsedCommands: string[] = [];\r\n        parsedCommands = commands.reduce((commandList, currentCommand) => {\r\n            if (currentCommand) {\r\n                const parsedCommand = JSON.parse(currentCommand).properties.command;\r\n                if (parsedCommand) {\r\n                    commandList.push(parsedCommand);\r\n                }\r\n                return commandList;\r\n            }\r\n        }, parsedCommands);\r\n\r\n        const response = details ? details.response : null;\r\n\r\n        const psContent = { psCommands: parsedCommands, state: state, otherData: details && details.otherData ? details.otherData : {} };\r\n\r\n        if (response && response.error) {\r\n            psContent['errorCodes'] = [response.error.code];\r\n        } else if (response && response.errors) {\r\n            psContent['errorCodes'] = response.errors.map((e) => e.errorType);\r\n            psContent.otherData['hResult'] = response.errors.map((e) => e.detailRecord && e.detailRecord.hResult);\r\n        }\r\n        SmeWebTelemetry.traceAction(null, { content: psContent },\r\n            { type: TelemetryEventTypes.Powershell });\r\n    }\r\n\r\n    /**\r\n     * Helper to redactGenericModel function\r\n     * Determines whether field should be redacted according to fields provided for redacting and exceptions\r\n     * @param key               The field in question\r\n     * @param keywordsToRedact  List of string inclusions to redact - if the field contains any part of this, it will be redacted\r\n     * @param exceptions        Set of exceptions to the above - if the field matches an exception, it will not be redacted\r\n     * @returns                 True if field should be redacted, false otherwise\r\n     */\r\n    private static fieldShouldBeRedacted(key: string, keywordsToRedact: string[], exceptions: Set<string>): boolean {\r\n        if (exceptions.has(key)) {\r\n            return false;\r\n        }\r\n\r\n        for (const field of keywordsToRedact) {\r\n            if (key.toLowerCase().includes(field.toLowerCase())) {\r\n                return true;\r\n            }\r\n        }\r\n        return false;\r\n    }\r\n\r\n    /**\r\n     * Telemetry utility function to redact a model object. This function will traverse the model in BFS fashion and redact any fields\r\n     * that contain any string in the keywordsToRedact array, unless the field is an exception (in the exceptions array).\r\n     * This function will not affect keys in StringMaps or Maps - if names exist there, they should be handled separately.\r\n     *\r\n     * @param model             Model to redact\r\n     * @param keywordsToRedact  List of string inclusions to redact\r\n     * @param fieldExceptions   List of exceptions to the above\r\n     * @returns                 Redacted model\r\n     */\r\n    public static redactGenericModel(model: any, keywordsToRedact: string[], fieldExceptions: string[]): any {\r\n        const redacted_model: any = JSON.parse(JSON.stringify(model));\r\n        const remainingObjects = [redacted_model];\r\n        const exceptionSet = new Set(fieldExceptions);\r\n\r\n        while (remainingObjects.length > 0) {\r\n            const currentTree = remainingObjects[0];\r\n\r\n            Object.keys(currentTree).forEach(key => {\r\n                if (MsftSme.isPlainObject(currentTree[key])) {\r\n                    remainingObjects.push(currentTree[key]);\r\n                } else if (Array.isArray(currentTree[key]) && currentTree[key].some((field: any) => MsftSme.isPlainObject(field))) {\r\n                    // If array intermixes objects and non-objects, we will push the objects into the queue and ignore the non-objects.\r\n                    // The non-objects will avoid redaction, but this should never be a concern in practice.\r\n                    remainingObjects.push(currentTree[key].filter((field: any) => MsftSme.isPlainObject(field)));\r\n                } else if (this.fieldShouldBeRedacted(key, keywordsToRedact, exceptionSet)) {\r\n                    if (Array.isArray(currentTree[key])) {\r\n                        for (let i = 0; i < currentTree[key].length; ++i) {\r\n                            currentTree[key][i] = '***';\r\n                        }\r\n                    } else {\r\n                        currentTree[key] = '***';\r\n                    }\r\n                }\r\n            });\r\n            remainingObjects.shift();\r\n        }\r\n        return redacted_model;\r\n    }\r\n}\r\n"]}