Skip to content

Commit

Permalink
fix(angular): call showReportDialog in root context
Browse files Browse the repository at this point in the history
This commit calls `showReportDialog` outside of the Angular context to prevent
unnecessary view updates when asynchronous tasks are set up within the `showReportDialog` function.
  • Loading branch information
arturovt committed Apr 20, 2024
1 parent 7eb000c commit 34a62e3
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 18 deletions.
3 changes: 3 additions & 0 deletions packages/angular/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ module.exports = {
browser: true,
},
extends: ['../../.eslintrc.js'],
rules: {
'@sentry-internal/sdk/no-optional-chaining': 'off',
},
ignorePatterns: ['setup-test.ts', 'patch-vitest.ts'],
};
8 changes: 6 additions & 2 deletions packages/angular/src/errorhandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
});
}
}
}
Expand Down
34 changes: 18 additions & 16 deletions packages/angular/src/zone.ts
Original file line number Diff line number Diff line change
@@ -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 `<root>` 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?.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<T>(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();
}

0 comments on commit 34a62e3

Please sign in to comment.