Skip to content
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { Controller, Get } from '@nestjs/common';
import * as Sentry from '@sentry/nestjs';

@Controller()
export class AppController {
@Get('/test-transaction')
testTransaction() {
return { message: 'ok' };
}

@Get('/flush')
async flush() {
await Sentry.flush(2000);
return { message: 'ok' };
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { ParseIntPipe, UseGuards, UseInterceptors, UsePipes } from '@nestjs/common';
import { SubscribeMessage, WebSocketGateway, MessageBody } from '@nestjs/websockets';
import * as Sentry from '@sentry/nestjs';
import { ExampleGuard } from './example.guard';
import { ExampleInterceptor } from './example.interceptor';

@WebSocketGateway()
export class AppGateway {
Expand All @@ -17,4 +20,30 @@ export class AppGateway {
}
return { event: 'capture-response', data: { success: true } };
}

@SubscribeMessage('test-guard-instrumentation')
@UseGuards(ExampleGuard)
handleGuardInstrumentation() {
return { event: 'guard-response', data: { success: true } };
}

@SubscribeMessage('test-interceptor-instrumentation')
@UseInterceptors(ExampleInterceptor)
handleInterceptorInstrumentation() {
return { event: 'interceptor-response', data: { success: true } };
}

@SubscribeMessage('test-pipe-instrumentation')
@UsePipes(ParseIntPipe)
handlePipeInstrumentation(@MessageBody() value: number) {
return { event: 'pipe-response', data: { value } };
}

@SubscribeMessage('test-manual-span')
handleManualSpan() {
const result = Sentry.startSpan({ name: 'test-ws-manual-span' }, () => {
return { success: true };
});
return { event: 'manual-span-response', data: result };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import * as Sentry from '@sentry/nestjs';

@Injectable()
export class ExampleGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
Sentry.startSpan({ name: 'test-guard-span' }, () => {});
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import * as Sentry from '@sentry/nestjs';
import { tap } from 'rxjs';

@Injectable()
export class ExampleInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
Sentry.startSpan({ name: 'test-interceptor-span' }, () => {});
return next.handle().pipe(
tap(() => {
Sentry.startSpan({ name: 'test-interceptor-span-after-route' }, () => {});
}),
);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { io, type Socket } from 'socket.io-client';

function connectSocket(baseURL: string): Promise<Socket> {
const socket = io(baseURL);
return new Promise<Socket>(resolve => socket.on('connect', () => resolve(socket)));
}

test('Sends an HTTP transaction', async ({ baseURL }) => {
const txPromise = waitForTransaction('nestjs-websockets', tx => {
Expand All @@ -16,3 +22,140 @@ test('Sends an HTTP transaction', async ({ baseURL }) => {
}),
);
});

test('WebSocket handler with manual Sentry.startSpan() sends a transaction', async ({ baseURL }) => {
const txPromise = waitForTransaction('nestjs-websockets', tx => {
return tx?.transaction === 'test-ws-manual-span';
});

const socket = await connectSocket(baseURL!);
try {
socket.emit('test-manual-span', {});
await fetch(`${baseURL}/flush`);

const tx = await txPromise;
expect(tx.transaction).toBe('test-ws-manual-span');
expect(tx.contexts?.trace).toEqual(
expect.objectContaining({
origin: 'manual',
}),
);
} finally {
socket.disconnect();
}
});

test('WebSocket handler with guard includes guard span and nested manual span', async ({ baseURL }) => {
const txPromise = waitForTransaction('nestjs-websockets', tx => {
return tx?.transaction === 'ExampleGuard';
});

const socket = await connectSocket(baseURL!);
try {
socket.emit('test-guard-instrumentation', {});
await fetch(`${baseURL}/flush`);

const tx = await txPromise;

expect(tx.transaction).toBe('ExampleGuard');
expect(tx.contexts?.trace).toEqual(
expect.objectContaining({
op: 'middleware.nestjs',
origin: 'auto.middleware.nestjs',
}),
);

expect(tx.spans).toEqual(
expect.arrayContaining([
expect.objectContaining({
description: 'test-guard-span',
parent_span_id: tx.contexts?.trace?.span_id,
origin: 'manual',
status: 'ok',
}),
]),
);
} finally {
socket.disconnect();
}
});

test('WebSocket handler with interceptor includes interceptor span, after-route span, and nested manual spans', async ({
baseURL,
}) => {
const txPromise = waitForTransaction('nestjs-websockets', tx => {
return tx?.transaction === 'ExampleInterceptor';
});

const socket = await connectSocket(baseURL!);
try {
socket.emit('test-interceptor-instrumentation', {});
await fetch(`${baseURL}/flush`);

const tx = await txPromise;

expect(tx.transaction).toBe('ExampleInterceptor');
expect(tx.contexts?.trace).toEqual(
expect.objectContaining({
op: 'middleware.nestjs',
origin: 'auto.middleware.nestjs',
}),
);

const rootSpanId = tx.contexts?.trace?.span_id;

expect(tx.spans).toEqual(
expect.arrayContaining([
expect.objectContaining({
description: 'test-interceptor-span',
parent_span_id: rootSpanId,
origin: 'manual',
status: 'ok',
}),
expect.objectContaining({
description: 'Interceptors - After Route',
op: 'middleware.nestjs',
origin: 'auto.middleware.nestjs',
status: 'ok',
}),
]),
);

const afterRouteSpan = tx.spans.find(span => span.description === 'Interceptors - After Route');

expect(tx.spans).toEqual(
expect.arrayContaining([
expect.objectContaining({
description: 'test-interceptor-span-after-route',
parent_span_id: afterRouteSpan?.span_id,
}),
]),
);
} finally {
socket.disconnect();
}
});

test('WebSocket handler with pipe includes pipe span', async ({ baseURL }) => {
const txPromise = waitForTransaction('nestjs-websockets', tx => {
return tx?.transaction === 'ParseIntPipe';
});

const socket = await connectSocket(baseURL!);
try {
socket.emit('test-pipe-instrumentation', '123');
await fetch(`${baseURL}/flush`);

const tx = await txPromise;

expect(tx.transaction).toBe('ParseIntPipe');
expect(tx.contexts?.trace).toEqual(
expect.objectContaining({
op: 'middleware.nestjs',
origin: 'auto.middleware.nestjs',
}),
);
} finally {
socket.disconnect();
}
});
Loading