Skip to content

feat(angular): Auto-detect component name in TraceDirective #6223

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

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions packages/angular/src/tracing.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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();
Expand All @@ -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.
*/
Expand Down
62 changes: 55 additions & 7 deletions packages/angular/test/tracing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const defaultStartTransaction = (ctx: any) => {
transaction = {
...ctx,
setName: jest.fn(name => (transaction.name = name)),
startChild: jest.fn(),
};

return transaction;
Expand Down Expand Up @@ -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({
Expand All @@ -276,14 +280,58 @@ describe('Angular Tracing', () => {

expect(transaction.startChild).toHaveBeenCalledWith({
op: 'ui.angular.init',
description: '<unknown>',
description: '<app-test>',
});

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: '<unknown>',
});

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);

Expand All @@ -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);

Expand Down