Skip to content

Commit 7ad5054

Browse files
authored
ref(node): Split up nest integration into multiple files (getsentry#13172)
The file implementing the nest integration in node got a bit annoying to work with, so splitting it up.
1 parent 4ebac94 commit 7ad5054

File tree

7 files changed

+223
-221
lines changed

7 files changed

+223
-221
lines changed

packages/node/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export { mongooseIntegration } from './integrations/tracing/mongoose';
1818
export { mysqlIntegration } from './integrations/tracing/mysql';
1919
export { mysql2Integration } from './integrations/tracing/mysql2';
2020
export { redisIntegration } from './integrations/tracing/redis';
21-
export { nestIntegration, setupNestErrorHandler } from './integrations/tracing/nest';
21+
export { nestIntegration, setupNestErrorHandler } from './integrations/tracing/nest/nest';
2222
export { postgresIntegration } from './integrations/tracing/postgres';
2323
export { prismaIntegration } from './integrations/tracing/prisma';
2424
export { hapiIntegration, setupHapiErrorHandler } from './integrations/tracing/hapi';

packages/node/src/integrations/tracing/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { instrumentMongo, mongoIntegration } from './mongo';
1111
import { instrumentMongoose, mongooseIntegration } from './mongoose';
1212
import { instrumentMysql, mysqlIntegration } from './mysql';
1313
import { instrumentMysql2, mysql2Integration } from './mysql2';
14-
import { instrumentNest, nestIntegration } from './nest';
14+
import { instrumentNest, nestIntegration } from './nest/nest';
1515
import { instrumentPostgres, postgresIntegration } from './postgres';
1616
import { instrumentRedis, redisIntegration } from './redis';
1717

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
2+
import { addNonEnumerableProperty } from '@sentry/utils';
3+
import type { InjectableTarget } from './types';
4+
5+
const sentryPatched = 'sentryPatched';
6+
7+
/**
8+
* Helper checking if a concrete target class is already patched.
9+
*
10+
* We already guard duplicate patching with isWrapped. However, isWrapped checks whether a file has been patched, whereas we use this check for concrete target classes.
11+
* This check might not be necessary, but better to play it safe.
12+
*/
13+
export function isPatched(target: InjectableTarget): boolean {
14+
if (target.sentryPatched) {
15+
return true;
16+
}
17+
18+
addNonEnumerableProperty(target, sentryPatched, true);
19+
return false;
20+
}
21+
22+
/**
23+
* Returns span options for nest middleware spans.
24+
*/
25+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
26+
export function getMiddlewareSpanOptions(target: InjectableTarget) {
27+
return {
28+
name: target.name,
29+
attributes: {
30+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nestjs',
31+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nestjs',
32+
},
33+
};
34+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core';
2+
import {
3+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
4+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
5+
captureException,
6+
defineIntegration,
7+
getClient,
8+
getDefaultIsolationScope,
9+
getIsolationScope,
10+
spanToJSON,
11+
} from '@sentry/core';
12+
import type { IntegrationFn, Span } from '@sentry/types';
13+
import { logger } from '@sentry/utils';
14+
import { generateInstrumentOnce } from '../../../otel/instrument';
15+
import { SentryNestInstrumentation } from './sentry-nest-instrumentation';
16+
import type { MinimalNestJsApp, NestJsErrorFilter } from './types';
17+
18+
const INTEGRATION_NAME = 'Nest';
19+
20+
const instrumentNestCore = generateInstrumentOnce('Nest-Core', () => {
21+
return new NestInstrumentation();
22+
});
23+
24+
const instrumentNestCommon = generateInstrumentOnce('Nest-Common', () => {
25+
return new SentryNestInstrumentation();
26+
});
27+
28+
export const instrumentNest = Object.assign(
29+
(): void => {
30+
instrumentNestCore();
31+
instrumentNestCommon();
32+
},
33+
{ id: INTEGRATION_NAME },
34+
);
35+
36+
const _nestIntegration = (() => {
37+
return {
38+
name: INTEGRATION_NAME,
39+
setupOnce() {
40+
instrumentNest();
41+
},
42+
};
43+
}) satisfies IntegrationFn;
44+
45+
/**
46+
* Nest framework integration
47+
*
48+
* Capture tracing data for nest.
49+
*/
50+
export const nestIntegration = defineIntegration(_nestIntegration);
51+
52+
/**
53+
* Setup an error handler for Nest.
54+
*/
55+
export function setupNestErrorHandler(app: MinimalNestJsApp, baseFilter: NestJsErrorFilter): void {
56+
// Sadly, NestInstrumentation has no requestHook, so we need to add the attributes here
57+
// We register this hook in this method, because if we register it in the integration `setup`,
58+
// it would always run even for users that are not even using Nest.js
59+
const client = getClient();
60+
if (client) {
61+
client.on('spanStart', span => {
62+
addNestSpanAttributes(span);
63+
});
64+
}
65+
66+
app.useGlobalInterceptors({
67+
intercept(context, next) {
68+
if (getIsolationScope() === getDefaultIsolationScope()) {
69+
logger.warn('Isolation scope is still the default isolation scope, skipping setting transactionName.');
70+
return next.handle();
71+
}
72+
73+
if (context.getType() === 'http') {
74+
const req = context.switchToHttp().getRequest();
75+
if (req.route) {
76+
getIsolationScope().setTransactionName(`${req.method?.toUpperCase() || 'GET'} ${req.route.path}`);
77+
}
78+
}
79+
80+
return next.handle();
81+
},
82+
});
83+
84+
const wrappedFilter = new Proxy(baseFilter, {
85+
get(target, prop, receiver) {
86+
if (prop === 'catch') {
87+
const originalCatch = Reflect.get(target, prop, receiver);
88+
89+
return (exception: unknown, host: unknown) => {
90+
const status_code = (exception as { status?: number }).status;
91+
92+
// don't report expected errors
93+
if (status_code !== undefined) {
94+
return originalCatch.apply(target, [exception, host]);
95+
}
96+
97+
captureException(exception);
98+
return originalCatch.apply(target, [exception, host]);
99+
};
100+
}
101+
return Reflect.get(target, prop, receiver);
102+
},
103+
});
104+
105+
app.useGlobalFilters(wrappedFilter);
106+
}
107+
108+
function addNestSpanAttributes(span: Span): void {
109+
const attributes = spanToJSON(span).data || {};
110+
111+
// this is one of: app_creation, request_context, handler
112+
const type = attributes['nestjs.type'];
113+
114+
// If this is already set, or we have no nest.js span, no need to process again...
115+
if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || !type) {
116+
return;
117+
}
118+
119+
span.setAttributes({
120+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.nestjs',
121+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.nestjs`,
122+
});
123+
}

0 commit comments

Comments
 (0)