Skip to content

Commit 925c114

Browse files
fix(angular): provide a single compilation unit for the trace directive (#3617)
Co-authored-by: Kamil Ogórek <[email protected]>
1 parent 20f3bcb commit 925c114

File tree

5 files changed

+52
-56
lines changed

5 files changed

+52
-56
lines changed

packages/angular/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,11 @@ To track Angular components as part of your transactions, you have 3 options.
155155
_TraceDirective:_ used to track a duration between `OnInit` and `AfterViewInit` lifecycle hooks in template:
156156

157157
```javascript
158-
import { TraceDirective } from '@sentry/angular';
158+
import { TraceModule } from '@sentry/angular';
159159

160160
@NgModule({
161161
// ...
162-
declarations: [TraceDirective],
162+
imports: [TraceModule],
163163
// ...
164164
})
165165
export class AppModule {}
@@ -168,9 +168,9 @@ export class AppModule {}
168168
Then inside your components template (keep in mind that directive name attribute is required):
169169

170170
```html
171-
<app-header [trace]="'header'"></app-header>
172-
<articles-list [trace]="'articles-list'"></articles-list>
173-
<app-footer [trace]="'footer'"></app-footer>
171+
<app-header trace="header"></app-header>
172+
<articles-list trace="articles-list"></articles-list>
173+
<app-footer trace="footer"></app-footer>
174174
```
175175

176176
_TraceClassDecorator:_ used to track a duration between `OnInit` and `AfterViewInit` lifecycle hooks in components:

packages/angular/src/errorhandler.ts

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,7 @@ import { HttpErrorResponse } from '@angular/common/http';
22
import { ErrorHandler as AngularErrorHandler, Injectable } from '@angular/core';
33
import * as Sentry from '@sentry/browser';
44

5-
// That's the `global.Zone` exposed when the `zone.js` package is used.
6-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7-
declare const Zone: any;
8-
9-
// There're 2 types of Angular applications:
10-
// 1) zone-full (by default)
11-
// 2) zone-less
12-
// The developer can avoid importing the `zone.js` package and tells Angular that
13-
// he is responsible for running the change detection by himself. This is done by
14-
// "nooping" the zone through `CompilerOptions` when bootstrapping the root module.
15-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
16-
const isNgZoneEnabled = typeof Zone !== 'undefined' && !!Zone.current;
5+
import { runOutsideAngular } from './zone';
176

187
/**
198
* Options used to configure the behavior of the Angular ErrorHandler.
@@ -51,16 +40,7 @@ class SentryErrorHandler implements AngularErrorHandler {
5140
const extractedError = this._extractError(error) || 'Handled unknown error';
5241

5342
// Capture handled exception and send it to Sentry.
54-
const eventId = isNgZoneEnabled
55-
? // The `Zone.root.run` basically will capture the exception in the most parent zone.
56-
// The Angular's zone is forked from the `Zone.root`. In this case, `zone.js` won't
57-
// trigger change detection, and `ApplicationRef.tick()` will not be run.
58-
// Caretaker note: we're using `Zone.root` except `NgZone.runOutsideAngular` since this
59-
// will require injecting the `NgZone` facade. That will create a breaking change for
60-
// projects already using the `SentryErrorHandler`.
61-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
62-
Zone.root.run(() => Sentry.captureException(extractedError))
63-
: Sentry.captureException(extractedError);
43+
const eventId = runOutsideAngular(() => Sentry.captureException(extractedError));
6444

6545
// When in development mode, log the error to console for immediate feedback.
6646
if (this._options.logErrors) {

packages/angular/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ export {
1010
TraceClassDecorator,
1111
TraceMethodDecorator,
1212
TraceDirective,
13+
TraceModule,
1314
TraceService,
1415
} from './tracing';

packages/angular/src/tracing.ts

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,12 @@
1-
import { AfterViewInit, Directive, Injectable, Input, OnDestroy, OnInit } from '@angular/core';
1+
import { AfterViewInit, Directive, Injectable, Input, NgModule, OnDestroy, OnInit } from '@angular/core';
22
import { Event, NavigationEnd, NavigationStart, Router } from '@angular/router';
33
import { getCurrentHub } from '@sentry/browser';
44
import { Span, Transaction, TransactionContext } from '@sentry/types';
55
import { logger, stripUrlQueryAndFragment, timestampWithMs } from '@sentry/utils';
66
import { Observable, Subscription } from 'rxjs';
77
import { filter, tap } from 'rxjs/operators';
88

9-
// That's the `global.Zone` exposed when the `zone.js` package is used.
10-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
11-
declare const Zone: any;
12-
13-
// There're 2 types of Angular applications:
14-
// 1) zone-full (by default)
15-
// 2) zone-less
16-
// The developer can avoid importing the `zone.js` package and tells Angular that
17-
// he is responsible for running the change detection by himself. This is done by
18-
// "nooping" the zone through `CompilerOptions` when bootstrapping the root module.
19-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
20-
const isNgZoneEnabled = typeof Zone !== 'undefined' && !!Zone.current;
9+
import { runOutsideAngular } from './zone';
2110

2211
let instrumentationInitialized: boolean;
2312
let stashedStartTransaction: (context: TransactionContext) => Transaction | undefined;
@@ -106,21 +95,10 @@ export class TraceService implements OnDestroy {
10695
filter(event => event instanceof NavigationEnd),
10796
tap(() => {
10897
if (this._routingSpan) {
109-
if (isNgZoneEnabled) {
110-
// The `Zone.root.run` basically will finish the transaction in the most parent zone.
111-
// The Angular's zone is forked from the `Zone.root`. In this case, `zone.js` won't
112-
// trigger change detection, and `ApplicationRef.tick()` will not be run.
113-
// Caretaker note: we're using `Zone.root` except `NgZone.runOutsideAngular` since this
114-
// will require injecting the `NgZone` facade. That will create a breaking change for
115-
// projects already using the `TraceService`.
116-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
117-
Zone.root.run(() => {
118-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
119-
this._routingSpan!.finish();
120-
});
121-
} else {
122-
this._routingSpan.finish();
123-
}
98+
runOutsideAngular(() => {
99+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
100+
this._routingSpan!.finish();
101+
});
124102
this._routingSpan = null;
125103
}
126104
}),
@@ -136,7 +114,7 @@ export class TraceService implements OnDestroy {
136114

137115
/**
138116
* This is used to prevent memory leaks when the root view is created and destroyed multiple times,
139-
* since `subscribe` callbacks captures `this` and prevent many resources from being GC'd.
117+
* since `subscribe` callbacks capture `this` and prevent many resources from being GC'd.
140118
*/
141119
public ngOnDestroy(): void {
142120
this._subscription.unsubscribe();
@@ -179,6 +157,15 @@ export class TraceDirective implements OnInit, AfterViewInit {
179157
}
180158
}
181159

160+
/**
161+
* A module serves as a single compilation unit for the `TraceDirective` and can be re-used by any other module.
162+
*/
163+
@NgModule({
164+
declarations: [TraceDirective],
165+
exports: [TraceDirective],
166+
})
167+
export class TraceModule {}
168+
182169
/**
183170
* Decorator function that can be used to capture initialization lifecycle of the whole component.
184171
*/

packages/angular/src/zone.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// That's the `global.Zone` exposed when the `zone.js` package is used.
2+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3+
declare const Zone: any;
4+
5+
// There're 2 types of Angular applications:
6+
// 1) zone-full (by default)
7+
// 2) zone-less
8+
// The developer can avoid importing the `zone.js` package and tells Angular that
9+
// he is responsible for running the change detection by himself. This is done by
10+
// "nooping" the zone through `CompilerOptions` when bootstrapping the root module.
11+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
12+
const isNgZoneEnabled = typeof Zone !== 'undefined' && !!Zone.current;
13+
14+
/**
15+
* The function that does the same job as `NgZone.runOutsideAngular`.
16+
*/
17+
export function runOutsideAngular<T>(callback: () => T): T {
18+
// The `Zone.root.run` basically will run the `callback` in the most parent zone.
19+
// Any asynchronous API used inside the `callback` won't catch Angular's zone
20+
// since `Zone.current` will reference `Zone.root`.
21+
// The Angular's zone is forked from the `Zone.root`. In this case, `zone.js` won't
22+
// trigger change detection, and `ApplicationRef.tick()` will not be run.
23+
// Caretaker note: we're using `Zone.root` except `NgZone.runOutsideAngular` since this
24+
// will require injecting the `NgZone` facade. That will create a breaking change for
25+
// projects already using the `@sentry/angular`.
26+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
27+
return isNgZoneEnabled ? Zone.root.run(callback) : callback();
28+
}

0 commit comments

Comments
 (0)