From aecbfafc9fca8693bf46b4818be01b0b75bfc08b Mon Sep 17 00:00:00 2001 From: Artur Androsovych Date: Thu, 25 Apr 2024 09:08:42 +0100 Subject: [PATCH] fix(angular): Call `showReportDialog` in root context (#11703) This patch calls `showReportDialog` outside of the Angular context to prevent unnecessary view updates when asynchronous tasks are set up within the `showReportDialog` function. Also updates the documentation around `runOutsideAngular` --- packages/angular/src/errorhandler.ts | 8 +++++-- packages/angular/src/zone.ts | 34 +++++++++++++++------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/packages/angular/src/errorhandler.ts b/packages/angular/src/errorhandler.ts index e8c4350d971e..ff56887ec7ee 100644 --- a/packages/angular/src/errorhandler.ts +++ b/packages/angular/src/errorhandler.ts @@ -121,14 +121,18 @@ class SentryErrorHandler implements AngularErrorHandler { if (client && !this._registeredAfterSendEventHandler) { client.on('afterSendEvent', (event: Event) => { if (!event.type && event.event_id) { - Sentry.showReportDialog({ ...this._options.dialogOptions, eventId: event.event_id }); + runOutsideAngular(() => { + Sentry.showReportDialog({ ...this._options.dialogOptions, eventId: event.event_id! }); + }); } }); // We only want to register this hook once in the lifetime of the error handler this._registeredAfterSendEventHandler = true; } else if (!client) { - Sentry.showReportDialog({ ...this._options.dialogOptions, eventId }); + runOutsideAngular(() => { + Sentry.showReportDialog({ ...this._options.dialogOptions, eventId }); + }); } } } diff --git a/packages/angular/src/zone.ts b/packages/angular/src/zone.ts index 865df43ce773..fdd45bdf8b0c 100644 --- a/packages/angular/src/zone.ts +++ b/packages/angular/src/zone.ts @@ -1,28 +1,30 @@ -// That's the `global.Zone` exposed when the `zone.js` package is used. +// This would be exposed in the global environment 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/no-explicit-any declare const Zone: any; -// There're 2 types of Angular applications: -// 1) zone-full (by default) -// 2) zone-less -// The developer can avoid importing the `zone.js` package and tells Angular that -// he is responsible for running the change detection by himself. This is done by -// "nooping" the zone through `CompilerOptions` when bootstrapping the root module. +// In Angular 17 and future versions, zoneless support is forthcoming. +// Therefore, it's advisable to safely check whether the `run` function is +// available in the `` context. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -const isNgZoneEnabled = typeof Zone !== 'undefined' && !!Zone.current; +const isNgZoneEnabled = typeof Zone !== 'undefined' && Zone.root && Zone.root.run; /** * The function that does the same job as `NgZone.runOutsideAngular`. + * + * ⚠️ Note: All of the Sentry 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 Sentry 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 function runOutsideAngular(callback: () => T): T { - // The `Zone.root.run` basically will run the `callback` in the most parent zone. - // Any asynchronous API used inside the `callback` won't catch Angular's zone - // since `Zone.current` will reference `Zone.root`. - // The Angular's zone is forked from the `Zone.root`. In this case, `zone.js` won't - // trigger change detection, and `ApplicationRef.tick()` will not be run. - // Caretaker note: we're using `Zone.root` except `NgZone.runOutsideAngular` since this - // will require injecting the `NgZone` facade. That will create a breaking change for - // projects already using the `@sentry/angular`. + // 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. This approach is + // necessary because some of the Sentry functionality continues to run in the background. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return isNgZoneEnabled ? Zone.root.run(callback) : callback(); }