Skip to content

Commit dc08b82

Browse files
arturovtLms24
andauthored
fix(angular): Fall back to element tagName when name is not provided to TraceDirective (getsentry#14778)
The `trace` directive should typically be declared on components to validly trace the lifecycle (from `ngOnInit` to `ngAfterViewInit`, when child views are also rendered). If `trace` is mistakenly not provided, we fall back to `tagName` instead of "unknown component". --------- Co-authored-by: Lukas Stracke <lukas.stracke@sentry.io>
1 parent 4443cb7 commit dc08b82

File tree

6 files changed

+86
-37
lines changed

6 files changed

+86
-37
lines changed

dev-packages/e2e-tests/test-applications/angular-17/src/app/component-tracking/component-tracking.components.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import { SampleComponent } from '../sample-component/sample-component.components
66
selector: 'app-cancel',
77
standalone: true,
88
imports: [TraceModule, SampleComponent],
9-
template: `<app-sample-component [trace]="'sample-component'"></app-sample-component>`,
9+
template: `
10+
<app-sample-component [trace]="'sample-component'"></app-sample-component>
11+
<app-sample-component trace></app-sample-component>
12+
`,
1013
})
1114
@TraceClass({ name: 'ComponentTrackingComponent' })
1215
export class ComponentTrackingComponent implements OnInit, AfterViewInit {

dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ test.describe('finish routing span', () => {
191191
});
192192

193193
test.describe('TraceDirective', () => {
194-
test('creates a child tracingSpan with component name as span name on ngOnInit', async ({ page }) => {
194+
test('creates a child span with the component name as span name on ngOnInit', async ({ page }) => {
195195
const navigationTxnPromise = waitForTransaction('angular-17', async transactionEvent => {
196196
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
197197
});
@@ -201,23 +201,36 @@ test.describe('TraceDirective', () => {
201201
// immediately navigate to a different route
202202
const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]);
203203

204-
const traceDirectiveSpan = navigationTxn.spans?.find(
204+
const traceDirectiveSpans = navigationTxn.spans?.filter(
205205
span => span?.data && span?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.ui.angular.trace_directive',
206206
);
207207

208-
expect(traceDirectiveSpan).toBeDefined();
209-
expect(traceDirectiveSpan).toEqual(
210-
expect.objectContaining({
211-
data: {
212-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init',
213-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive',
214-
},
215-
description: '<sample-component>',
216-
op: 'ui.angular.init',
217-
origin: 'auto.ui.angular.trace_directive',
218-
start_timestamp: expect.any(Number),
219-
timestamp: expect.any(Number),
220-
}),
208+
expect(traceDirectiveSpans).toHaveLength(2);
209+
expect(traceDirectiveSpans).toEqual(
210+
expect.arrayContaining([
211+
expect.objectContaining({
212+
data: {
213+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init',
214+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive',
215+
},
216+
description: '<sample-component>', // custom component name passed to trace directive
217+
op: 'ui.angular.init',
218+
origin: 'auto.ui.angular.trace_directive',
219+
start_timestamp: expect.any(Number),
220+
timestamp: expect.any(Number),
221+
}),
222+
expect.objectContaining({
223+
data: {
224+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init',
225+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive',
226+
},
227+
description: '<app-sample-component>', // fallback selector name
228+
op: 'ui.angular.init',
229+
origin: 'auto.ui.angular.trace_directive',
230+
start_timestamp: expect.any(Number),
231+
timestamp: expect.any(Number),
232+
}),
233+
]),
221234
);
222235
});
223236
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873

dev-packages/e2e-tests/test-applications/angular-19/src/app/component-tracking/component-tracking.components.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ import { TraceClass, TraceMethod, TraceModule } from '@sentry/angular';
33
import { SampleComponent } from '../sample-component/sample-component.components';
44

55
@Component({
6-
selector: 'app-cancel',
6+
selector: 'app-component-tracking',
77
standalone: true,
88
imports: [TraceModule, SampleComponent],
9-
template: `<app-sample-component [trace]="'sample-component'"></app-sample-component>`,
9+
template: `
10+
<app-sample-component trace="sample-component"></app-sample-component>
11+
<app-sample-component trace></app-sample-component>
12+
`,
1013
})
1114
@TraceClass({ name: 'ComponentTrackingComponent' })
1215
export class ComponentTrackingComponent implements OnInit, AfterViewInit {

dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ test.describe('finish routing span', () => {
191191
});
192192

193193
test.describe('TraceDirective', () => {
194-
test('creates a child tracingSpan with component name as span name on ngOnInit', async ({ page }) => {
194+
test('creates a child span with the component name as span name on ngOnInit', async ({ page }) => {
195195
const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
196196
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
197197
});
@@ -201,23 +201,36 @@ test.describe('TraceDirective', () => {
201201
// immediately navigate to a different route
202202
const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]);
203203

204-
const traceDirectiveSpan = navigationTxn.spans?.find(
204+
const traceDirectiveSpans = navigationTxn.spans?.filter(
205205
span => span?.data && span?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.ui.angular.trace_directive',
206206
);
207207

208-
expect(traceDirectiveSpan).toBeDefined();
209-
expect(traceDirectiveSpan).toEqual(
210-
expect.objectContaining({
211-
data: {
212-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init',
213-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive',
214-
},
215-
description: '<sample-component>',
216-
op: 'ui.angular.init',
217-
origin: 'auto.ui.angular.trace_directive',
218-
start_timestamp: expect.any(Number),
219-
timestamp: expect.any(Number),
220-
}),
208+
expect(traceDirectiveSpans).toHaveLength(2);
209+
expect(traceDirectiveSpans).toEqual(
210+
expect.arrayContaining([
211+
expect.objectContaining({
212+
data: {
213+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init',
214+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive',
215+
},
216+
description: '<sample-component>', // custom component name passed to trace directive
217+
op: 'ui.angular.init',
218+
origin: 'auto.ui.angular.trace_directive',
219+
start_timestamp: expect.any(Number),
220+
timestamp: expect.any(Number),
221+
}),
222+
expect.objectContaining({
223+
data: {
224+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init',
225+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive',
226+
},
227+
description: '<app-sample-component>', // fallback selector name
228+
op: 'ui.angular.init',
229+
origin: 'auto.ui.angular.trace_directive',
230+
start_timestamp: expect.any(Number),
231+
timestamp: expect.any(Number),
232+
}),
233+
]),
221234
);
222235
});
223236
});

packages/angular/src/tracing.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
2+
import { ElementRef } from '@angular/core';
13
import type { AfterViewInit, OnDestroy, OnInit } from '@angular/core';
24
import { Directive, Injectable, Input, NgModule } from '@angular/core';
35
import type { ActivatedRouteSnapshot, Event, RouterState } from '@angular/router';
@@ -235,24 +237,37 @@ export class TraceService implements OnDestroy {
235237
}
236238
}
237239

238-
const UNKNOWN_COMPONENT = 'unknown';
239-
240240
/**
241-
* A directive that can be used to capture initialization lifecycle of the whole component.
241+
* Captures the initialization lifecycle of the component this directive is applied to.
242+
* Specifically, this directive measures the time between `ngOnInit` and `ngAfterViewInit`
243+
* of the component.
244+
*
245+
* Falls back to the component's selector if no name is provided.
246+
*
247+
* @example
248+
* ```html
249+
* <app-my-component trace="myComponent"></app-my-component>
250+
* ```
242251
*/
243252
@Directive({ selector: '[trace]' })
244253
export class TraceDirective implements OnInit, AfterViewInit {
245254
@Input('trace') public componentName?: string;
246255

247256
private _tracingSpan?: Span;
248257

258+
public constructor(private readonly _host: ElementRef<HTMLElement>) {}
259+
249260
/**
250261
* Implementation of OnInit lifecycle method
251262
* @inheritdoc
252263
*/
253264
public ngOnInit(): void {
254265
if (!this.componentName) {
255-
this.componentName = UNKNOWN_COMPONENT;
266+
// Technically, the `trace` binding should always be provided.
267+
// However, if it is incorrectly declared on the element without a
268+
// value (e.g., `<app-component trace />`), we fall back to using `tagName`
269+
// (which is e.g. `APP-COMPONENT`).
270+
this.componentName = this._host.nativeElement.tagName.toLowerCase();
256271
}
257272

258273
if (getActiveSpan()) {

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy