-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(Angular): Add URL Parameterization of Transaction Names #5416
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
17190a0
f9ab851
634e906
190963d
3ce61c8
1395a34
23242aa
ca02d49
9d5b14e
856ba22
be90bdc
35b9eca
195fe33
ffaf7f1
a8b44b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| /* eslint-disable max-lines */ | ||
| import { AfterViewInit, Directive, Injectable, Input, NgModule, OnDestroy, OnInit } from '@angular/core'; | ||
| import { Event, NavigationEnd, NavigationStart, Router } from '@angular/router'; | ||
| import { ActivatedRouteSnapshot, Event, NavigationEnd, NavigationStart, ResolveEnd, Router } from '@angular/router'; | ||
| import { getCurrentHub } from '@sentry/browser'; | ||
| import { Span, Transaction, TransactionContext } from '@sentry/types'; | ||
| import { getGlobalObject, logger, stripUrlQueryAndFragment, timestampWithMs } from '@sentry/utils'; | ||
|
|
@@ -10,6 +11,8 @@ import { ANGULAR_INIT_OP, ANGULAR_OP, ANGULAR_ROUTING_OP } from './constants'; | |
| import { IS_DEBUG_BUILD } from './flags'; | ||
| import { runOutsideAngular } from './zone'; | ||
|
|
||
| type ParamMap = { [key: string]: string[] }; | ||
|
|
||
| let instrumentationInitialized: boolean; | ||
| let stashedStartTransaction: (context: TransactionContext) => Transaction | undefined; | ||
| let stashedStartTransactionOnLocationChange: boolean; | ||
|
|
@@ -101,6 +104,39 @@ export class TraceService implements OnDestroy { | |
| }), | ||
| ); | ||
|
|
||
| // The ResolveEnd event is fired when the Angular router has resolved the URL and | ||
| // the parameter<->value mapping. It holds the new resolved router state with | ||
| // the mapping and the new URL. | ||
| // Only After this event, the route is activated, meaning that the transaction | ||
| // can be updated with the parameterized route name before e.g. the route's root | ||
| // component is initialized. This should be early enough before outgoing requests | ||
| // are made from the new route, with the exceptions of requests being made during | ||
| // a navigation. | ||
| public resEnd$: Observable<Event> = this._router.events.pipe( | ||
| filter(event => event instanceof ResolveEnd), | ||
| tap(event => { | ||
| const ev = event as ResolveEnd; | ||
|
|
||
| const params = getParamsOfRoute(ev.state.root); | ||
Lms24 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // ev.urlAfterRedirects is the one we prefer because it should hold the most recent | ||
| // one that holds information about a redirect to another route if this was specified | ||
| // in the Angular router config. In case this doesn't exist (for whatever reason), | ||
| // we fall back to ev.url which holds the primarily resolved URL before a potential | ||
| // redirect. | ||
| const url = ev.urlAfterRedirects || ev.url; | ||
|
|
||
| const route = getParameterizedRouteFromUrlAndParams(url, params); | ||
|
|
||
| const transaction = getActiveTransaction(); | ||
| // TODO (v8 / #5416): revisit the source condition. Do we want to make the parameterized route the default? | ||
| if (transaction && transaction.metadata.source === 'url') { | ||
Lms24 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| transaction.setName(route); | ||
| transaction.setMetadata({ source: 'route' }); | ||
| } | ||
| }), | ||
| ); | ||
|
|
||
| public navEnd$: Observable<Event> = this._router.events.pipe( | ||
| filter(event => event instanceof NavigationEnd), | ||
| tap(() => { | ||
|
|
@@ -115,10 +151,12 @@ export class TraceService implements OnDestroy { | |
| ); | ||
|
|
||
| private _routingSpan: Span | null = null; | ||
|
|
||
| private _subscription: Subscription = new Subscription(); | ||
|
|
||
| public constructor(private readonly _router: Router) { | ||
| this._subscription.add(this.navStart$.subscribe()); | ||
| this._subscription.add(this.resEnd$.subscribe()); | ||
| this._subscription.add(this.navEnd$.subscribe()); | ||
| } | ||
|
|
||
|
|
@@ -241,3 +279,44 @@ export function TraceMethodDecorator(): MethodDecorator { | |
| return descriptor; | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Recursively traverses the routerstate and its children to collect a map of parameters on the entire route. | ||
Lms24 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * | ||
| * Because Angular supports child routes (e.g. when creating nested routes or lazy loaded routes), we have | ||
| * to not only visit the root router snapshot but also its children to get all parameters of the entire route. | ||
| * | ||
| * @param activatedRouteSnapshot the root router snapshot | ||
| * @returns a map of params, mapping from a key to an array of values | ||
| */ | ||
| export function getParamsOfRoute(activatedRouteSnapshot: ActivatedRouteSnapshot): ParamMap { | ||
| function getParamsOfRouteH(routeSnapshot: ActivatedRouteSnapshot, accParams: ParamMap): ParamMap { | ||
|
||
| Object.keys(routeSnapshot.params).forEach(key => { | ||
| accParams[key] = [...(accParams[key] || []), routeSnapshot.params[key]]; | ||
| }); | ||
| routeSnapshot.children.forEach(child => getParamsOfRouteH(child, accParams)); | ||
| return accParams; | ||
| } | ||
|
|
||
| return getParamsOfRouteH(activatedRouteSnapshot, {}); | ||
| } | ||
|
|
||
| /** | ||
| * Takes a raw URl and a map of params and replaces each values occuring in the raw URL with | ||
| * the name of the parameter. | ||
| * | ||
| * @param url raw URL (e.g. /user/1234/details) | ||
| * @param params a map of type ParamMap | ||
| * @returns the parameterized URL (e.g. /user/:userId/details) | ||
| */ | ||
| export function getParameterizedRouteFromUrlAndParams(url: string, params: ParamMap): string { | ||
| if (params) { | ||
| return Object.keys(params).reduce((prevUrl: string, paramName: string) => { | ||
| return prevUrl | ||
| .split('/') | ||
| .map(segment => (params[paramName].find(p => p === segment) ? `:${paramName}` : segment)) | ||
| .join('/'); | ||
Lms24 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }, url); | ||
| } | ||
| return url; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.