Skip to content

Commit 365c750

Browse files
authored
fix(angular): Stop routing spans on navigation cancel and error events (#8369)
Previously, in our Angular routing instrumentation, we only stopped routing spans when a navigation ended successfully. This was because we only listened to [`NavigationEnd`](https://angular.io/api/router/NavigationEnd) router events. However, the Angular router emits other events in unsuccessful routing attempts: * a routing error (e.g. unknown route) emits [`NavigationError`](https://angular.io/api/router/NavigationCancel) * a cancelled navigation (e.g. due to a route guard that rejected/returned false) emits [`NavigationCancel`](https://angular.io/api/router/NavigationCancel) This fix adjusts our instrumentation to also listen to these events and to stop the span accordingly.
1 parent c8686ff commit 365c750

File tree

3 files changed

+81
-9
lines changed

3 files changed

+81
-9
lines changed

Diff for: packages/angular/src/tracing.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { ActivatedRouteSnapshot, Event, RouterState } from '@angular/router
55
// Duplicated import to work around a TypeScript bug where it'd complain that `Router` isn't imported as a type.
66
// We need to import it as a value to satisfy Angular dependency injection. So:
77
// eslint-disable-next-line @typescript-eslint/consistent-type-imports, import/no-duplicates
8-
import { Router } from '@angular/router';
8+
import { NavigationCancel, NavigationError, Router } from '@angular/router';
99
// eslint-disable-next-line import/no-duplicates
1010
import { NavigationEnd, NavigationStart, ResolveEnd } from '@angular/router';
1111
import { getCurrentHub, WINDOW } from '@sentry/browser';
@@ -131,7 +131,9 @@ export class TraceService implements OnDestroy {
131131
);
132132

133133
public navEnd$: Observable<Event> = this._router.events.pipe(
134-
filter(event => event instanceof NavigationEnd),
134+
filter(
135+
event => event instanceof NavigationEnd || event instanceof NavigationCancel || event instanceof NavigationError,
136+
),
135137
tap(() => {
136138
if (this._routingSpan) {
137139
runOutsideAngular(() => {

Diff for: packages/angular/test/tracing.test.ts

+61-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Component } from '@angular/core';
2-
import type { ActivatedRouteSnapshot } from '@angular/router';
2+
import type { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
33
import type { Hub } from '@sentry/types';
44

55
import { instrumentAngularRouting, TraceClassDecorator, TraceDirective, TraceMethodDecorator } from '../src';
@@ -185,6 +185,66 @@ describe('Angular Tracing', () => {
185185
env.destroy();
186186
});
187187

188+
it('finishes routing span on navigation error', async () => {
189+
const customStartTransaction = jest.fn(defaultStartTransaction);
190+
191+
const env = await TestEnv.setup({
192+
customStartTransaction,
193+
routes: [
194+
{
195+
path: '',
196+
component: AppComponent,
197+
},
198+
],
199+
useTraceService: true,
200+
});
201+
202+
const finishMock = jest.fn();
203+
transaction.startChild = jest.fn(() => ({
204+
finish: finishMock,
205+
}));
206+
207+
await env.navigateInAngular('/somewhere');
208+
209+
expect(finishMock).toHaveBeenCalledTimes(1);
210+
211+
env.destroy();
212+
});
213+
214+
it('finishes routing span on navigation cancel', async () => {
215+
const customStartTransaction = jest.fn(defaultStartTransaction);
216+
217+
class CanActivateGuard implements CanActivate {
218+
canActivate(_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot): boolean {
219+
return false;
220+
}
221+
}
222+
223+
const env = await TestEnv.setup({
224+
customStartTransaction,
225+
routes: [
226+
{
227+
path: 'cancel',
228+
component: AppComponent,
229+
canActivate: [CanActivateGuard],
230+
},
231+
],
232+
useTraceService: true,
233+
additionalProviders: [{ provide: CanActivateGuard, useClass: CanActivateGuard }],
234+
});
235+
236+
const finishMock = jest.fn();
237+
transaction.startChild = jest.fn(() => ({
238+
finish: finishMock,
239+
}));
240+
241+
await env.navigateInAngular('/cancel');
242+
243+
expect(finishMock).toHaveBeenCalledTimes(1);
244+
245+
env.destroy();
246+
});
247+
188248
describe('URL parameterization', () => {
189249
it.each([
190250
[

Diff for: packages/angular/test/utils/index.ts

+16-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Provider } from '@angular/core';
12
import { Component, NgModule } from '@angular/core';
23
import type { ComponentFixture } from '@angular/core/testing';
34
import { TestBed } from '@angular/core/testing';
@@ -47,6 +48,7 @@ export class TestEnv {
4748
startTransactionOnPageLoad?: boolean;
4849
startTransactionOnNavigation?: boolean;
4950
useTraceService?: boolean;
51+
additionalProviders?: Provider[];
5052
}): Promise<TestEnv> {
5153
instrumentAngularRouting(
5254
conf.customStartTransaction || jest.fn(),
@@ -60,14 +62,16 @@ export class TestEnv {
6062
TestBed.configureTestingModule({
6163
imports: [AppModule, RouterTestingModule.withRoutes(routes)],
6264
declarations: [...(conf.components || []), AppComponent],
63-
providers: useTraceService
65+
providers: (useTraceService
6466
? [
6567
{
6668
provide: TraceService,
6769
deps: [Router],
6870
},
71+
...(conf.additionalProviders || []),
6972
]
70-
: [],
73+
: []
74+
).concat(...(conf.additionalProviders || [])),
7175
});
7276

7377
const router: Router = TestBed.inject(Router);
@@ -80,10 +84,16 @@ export class TestEnv {
8084
public async navigateInAngular(url: string): Promise<void> {
8185
return new Promise(resolve => {
8286
return this.fixture.ngZone?.run(() => {
83-
void this.router.navigateByUrl(url).then(() => {
84-
this.fixture.detectChanges();
85-
resolve();
86-
});
87+
void this.router
88+
.navigateByUrl(url)
89+
.then(() => {
90+
this.fixture.detectChanges();
91+
resolve();
92+
})
93+
.catch(() => {
94+
this.fixture.detectChanges();
95+
resolve();
96+
});
8797
});
8898
});
8999
}

0 commit comments

Comments
 (0)