diff --git a/projects/applicationinsights-angularplugin-js/src/lib/applicationinsights-angularplugin-js.component.ts b/projects/applicationinsights-angularplugin-js/src/lib/applicationinsights-angularplugin-js.component.ts index ac53ede..e6ef6cd 100644 --- a/projects/applicationinsights-angularplugin-js/src/lib/applicationinsights-angularplugin-js.component.ts +++ b/projects/applicationinsights-angularplugin-js/src/lib/applicationinsights-angularplugin-js.component.ts @@ -15,6 +15,7 @@ import { Subscription } from "rxjs"; import { AnalyticsPlugin } from "@microsoft/applicationinsights-analytics-js"; import {objDeepFreeze} from "@nevware21/ts-utils"; import { PropertiesPlugin } from "@microsoft/applicationinsights-properties-js"; +import { runOutsideAngular } from "./run-outside-angular"; interface IAngularExtensionConfig { /** @@ -113,7 +114,7 @@ export class AngularPlugin extends BaseTelemetryPlugin { const pageViewTelemetry: IPageViewTelemetry = { uri: _angularCfg.router.url }; - _self.trackPageView(pageViewTelemetry); + runOutsideAngular(() => _self.trackPageView(pageViewTelemetry)); } // subscribe to new router events @@ -129,7 +130,7 @@ export class AngularPlugin extends BaseTelemetryPlugin { uri: _angularCfg.router.url, properties: { duration: 0 } // SPA route change loading durations are undefined, so send 0 }; - _self.trackPageView(pvt); + runOutsideAngular(() => _self.trackPageView(pvt)); } } }); @@ -150,7 +151,7 @@ export class AngularPlugin extends BaseTelemetryPlugin { _propertiesPlugin.context.telemetryTrace.traceID = generateW3CId(); _propertiesPlugin.context.telemetryTrace.name = location && location.pathname || "_unknown_"; } - _analyticsPlugin.trackPageView(pageView); + runOutsideAngular(() => _analyticsPlugin.trackPageView(pageView)); } else { _throwInternal(_self.diagLog(), // eslint-disable-next-line max-len @@ -186,7 +187,7 @@ export class AngularPlugin extends BaseTelemetryPlugin { * @param event The event that needs to be processed */ processTelemetry(event: ITelemetryItem, itemCtx?: IProcessTelemetryContext) { - this.processNext(event, itemCtx); + runOutsideAngular(() => this.processNext(event, itemCtx)); } diff --git a/projects/applicationinsights-angularplugin-js/src/lib/run-outside-angular.ts b/projects/applicationinsights-angularplugin-js/src/lib/run-outside-angular.ts new file mode 100644 index 0000000..88b53fe --- /dev/null +++ b/projects/applicationinsights-angularplugin-js/src/lib/run-outside-angular.ts @@ -0,0 +1,24 @@ +// This would be exposed on the `globalThis` whenever `zone.js` is +// included in the `polyfills` configuration property. Starting from Angular 17, +// users can opt-in to use zoneless change detection. +// eslint-disable-next-line @typescript-eslint/naming-convention +declare const Zone: any; + +/** + * The function that does the same job as `NgZone.runOutsideAngular`. + * It doesn't require an injection context to be specified. + * + * ⚠️ Note: Most of the Application Insights functionality called from + * inside the Angular execution context must be wrapped in this function. + * Angular's rendering relies on asynchronous tasks being scheduled within + * its execution context. + * Since the plugin schedules tasks that do not interact with Angular's rendering, + * it may prevent Angular from functioning reliably. Consequently, it may disrupt + * processes such as server-side rendering or client-side hydration. + */ +export const runOutsideAngular = (callback: () => T): T => + // Running the `callback` within the root execution context enables Angular + // processes (such as SSR and hydration) to continue functioning normally without + // timeouts and delays that could affect the user experience. + typeof Zone !== "undefined" ? Zone.root.run(callback) : callback() +;