diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index d2bf67900f3f..7b9cc43818d0 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -1,5 +1,16 @@ /* eslint-disable max-lines */ -import { AfterViewInit, Directive, Injectable, Input, NgModule, OnDestroy, OnInit } from '@angular/core'; +import { + AfterViewInit, + Directive, + Inject, + Injectable, + Input, + NgModule, + OnDestroy, + OnInit, + Optional, + ViewContainerRef, +} from '@angular/core'; import { ActivatedRouteSnapshot, Event, NavigationEnd, NavigationStart, ResolveEnd, Router } from '@angular/router'; import { getCurrentHub, WINDOW } from '@sentry/browser'; import { Span, Transaction, TransactionContext } from '@sentry/types'; @@ -163,13 +174,15 @@ export class TraceDirective implements OnInit, AfterViewInit { private _tracingSpan?: Span; + public constructor(@Optional() @Inject(ViewContainerRef) private readonly _vcRef: ViewContainerRef) {} + /** * Implementation of OnInit lifecycle method * @inheritdoc */ public ngOnInit(): void { if (!this.componentName) { - this.componentName = UNKNOWN_COMPONENT; + this.componentName = detectComponentName(this._vcRef as EnhancedViewContainerRef); } const activeTransaction = getActiveTransaction(); @@ -192,6 +205,28 @@ export class TraceDirective implements OnInit, AfterViewInit { } } +type EnhancedViewContainerRef = ViewContainerRef & { + _lContainer?: { + localName?: string; + }[][]; +}; + +/** + * Detects the angular component name from the passed ViewContainerReference. + * Specifically, it looks up the selector of the component (e.g. `app-my-component`) or + * falls back to the default component name if the selector is not available. + */ +function detectComponentName(vcRef: EnhancedViewContainerRef | undefined): string { + if (vcRef && vcRef._lContainer && vcRef._lContainer[0] && vcRef._lContainer[0][0]) { + // Alternatively, we could get the class name like so: + // const className = Object.getPrototypeOf(vcRef._lContainer[0][8]).constructor.name; + + const selectorName = vcRef._lContainer[0][0].localName; + return selectorName || UNKNOWN_COMPONENT; + } + return UNKNOWN_COMPONENT; +} + /** * A module serves as a single compilation unit for the `TraceDirective` and can be re-used by any other module. */ diff --git a/packages/angular/test/tracing.test.ts b/packages/angular/test/tracing.test.ts index d5a88c9bb082..96e580323bab 100644 --- a/packages/angular/test/tracing.test.ts +++ b/packages/angular/test/tracing.test.ts @@ -12,6 +12,7 @@ const defaultStartTransaction = (ctx: any) => { transaction = { ...ctx, setName: jest.fn(name => (transaction.name = name)), + startChild: jest.fn(), }; return transaction; @@ -256,12 +257,15 @@ describe('Angular Tracing', () => { describe('TraceDirective', () => { it('should create an instance', () => { - const directive = new TraceDirective(); + const directive = new TraceDirective({} as unknown as any); expect(directive).toBeTruthy(); }); - it('should create a child tracingSpan on init', async () => { - const directive = new TraceDirective(); + it("should auto detect the component's selector.", async () => { + const directive = new TraceDirective({ + _lContainer: [[{ localName: 'app-test' }]], + } as unknown as any); + const customStartTransaction = jest.fn(defaultStartTransaction); const env = await TestEnv.setup({ @@ -276,14 +280,58 @@ describe('Angular Tracing', () => { expect(transaction.startChild).toHaveBeenCalledWith({ op: 'ui.angular.init', - description: '', + description: '', }); env.destroy(); }); - it('should use component name as span description', async () => { - const directive = new TraceDirective(); + it.each([ + {}, + { + _lContainer: [], + }, + { + _lContainer: [[]], + }, + { + _lContainer: [[{}]], + }, + { + _lContainer: [[{ localName: undefined }]], + }, + ])( + "should fall back to the default component name if auto-detection doesn't work and no custom name is given", + async (containerViewRef: any) => { + const directive = new TraceDirective(containerViewRef); + const customStartTransaction = jest.fn(defaultStartTransaction); + + const env = await TestEnv.setup({ + components: [TraceDirective], + customStartTransaction, + useTraceService: false, + }); + + const finishSpan = jest.fn(); + transaction.startChild = jest.fn().mockReturnValue({ finish: finishSpan }); + transaction.finish = jest.fn(); + + directive.ngOnInit(); + + expect(transaction.startChild).toHaveBeenCalledWith({ + op: 'ui.angular.init', + description: '', + }); + + directive.ngAfterViewInit(); + expect(finishSpan).toHaveBeenCalledTimes(1); + + env.destroy(); + }, + ); + + it('should use the custom component name as span description if one is passed', async () => { + const directive = new TraceDirective({} as unknown as any); const finishMock = jest.fn(); const customStartTransaction = jest.fn(defaultStartTransaction); @@ -309,7 +357,7 @@ describe('Angular Tracing', () => { }); it('should finish tracingSpan after view init', async () => { - const directive = new TraceDirective(); + const directive = new TraceDirective({} as unknown as any); const finishMock = jest.fn(); const customStartTransaction = jest.fn(defaultStartTransaction);