Skip to content

Commit

Permalink
ref(node): Split up nest integration into multiple files (#13172)
Browse files Browse the repository at this point in the history
The file implementing the nest integration in node got a bit annoying to
work with, so splitting it up.
  • Loading branch information
nicohrubec committed Aug 5, 2024
1 parent 4ebac94 commit 7ad5054
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 221 deletions.
2 changes: 1 addition & 1 deletion packages/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export { mongooseIntegration } from './integrations/tracing/mongoose';
export { mysqlIntegration } from './integrations/tracing/mysql';
export { mysql2Integration } from './integrations/tracing/mysql2';
export { redisIntegration } from './integrations/tracing/redis';
export { nestIntegration, setupNestErrorHandler } from './integrations/tracing/nest';
export { nestIntegration, setupNestErrorHandler } from './integrations/tracing/nest/nest';
export { postgresIntegration } from './integrations/tracing/postgres';
export { prismaIntegration } from './integrations/tracing/prisma';
export { hapiIntegration, setupHapiErrorHandler } from './integrations/tracing/hapi';
Expand Down
2 changes: 1 addition & 1 deletion packages/node/src/integrations/tracing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { instrumentMongo, mongoIntegration } from './mongo';
import { instrumentMongoose, mongooseIntegration } from './mongoose';
import { instrumentMysql, mysqlIntegration } from './mysql';
import { instrumentMysql2, mysql2Integration } from './mysql2';
import { instrumentNest, nestIntegration } from './nest';
import { instrumentNest, nestIntegration } from './nest/nest';
import { instrumentPostgres, postgresIntegration } from './postgres';
import { instrumentRedis, redisIntegration } from './redis';

Expand Down
34 changes: 34 additions & 0 deletions packages/node/src/integrations/tracing/nest/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import { addNonEnumerableProperty } from '@sentry/utils';
import type { InjectableTarget } from './types';

const sentryPatched = 'sentryPatched';

/**
* Helper checking if a concrete target class is already patched.
*
* 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.
* This check might not be necessary, but better to play it safe.
*/
export function isPatched(target: InjectableTarget): boolean {
if (target.sentryPatched) {
return true;
}

addNonEnumerableProperty(target, sentryPatched, true);
return false;
}

/**
* Returns span options for nest middleware spans.
*/
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function getMiddlewareSpanOptions(target: InjectableTarget) {
return {
name: target.name,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nestjs',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nestjs',
},
};
}
123 changes: 123 additions & 0 deletions packages/node/src/integrations/tracing/nest/nest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core';
import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
captureException,
defineIntegration,
getClient,
getDefaultIsolationScope,
getIsolationScope,
spanToJSON,
} from '@sentry/core';
import type { IntegrationFn, Span } from '@sentry/types';
import { logger } from '@sentry/utils';
import { generateInstrumentOnce } from '../../../otel/instrument';
import { SentryNestInstrumentation } from './sentry-nest-instrumentation';
import type { MinimalNestJsApp, NestJsErrorFilter } from './types';

const INTEGRATION_NAME = 'Nest';

const instrumentNestCore = generateInstrumentOnce('Nest-Core', () => {
return new NestInstrumentation();
});

const instrumentNestCommon = generateInstrumentOnce('Nest-Common', () => {
return new SentryNestInstrumentation();
});

export const instrumentNest = Object.assign(
(): void => {
instrumentNestCore();
instrumentNestCommon();
},
{ id: INTEGRATION_NAME },
);

const _nestIntegration = (() => {
return {
name: INTEGRATION_NAME,
setupOnce() {
instrumentNest();
},
};
}) satisfies IntegrationFn;

/**
* Nest framework integration
*
* Capture tracing data for nest.
*/
export const nestIntegration = defineIntegration(_nestIntegration);

/**
* Setup an error handler for Nest.
*/
export function setupNestErrorHandler(app: MinimalNestJsApp, baseFilter: NestJsErrorFilter): void {
// Sadly, NestInstrumentation has no requestHook, so we need to add the attributes here
// We register this hook in this method, because if we register it in the integration `setup`,
// it would always run even for users that are not even using Nest.js
const client = getClient();
if (client) {
client.on('spanStart', span => {
addNestSpanAttributes(span);
});
}

app.useGlobalInterceptors({
intercept(context, next) {
if (getIsolationScope() === getDefaultIsolationScope()) {
logger.warn('Isolation scope is still the default isolation scope, skipping setting transactionName.');
return next.handle();
}

if (context.getType() === 'http') {
const req = context.switchToHttp().getRequest();
if (req.route) {
getIsolationScope().setTransactionName(`${req.method?.toUpperCase() || 'GET'} ${req.route.path}`);
}
}

return next.handle();
},
});

const wrappedFilter = new Proxy(baseFilter, {
get(target, prop, receiver) {
if (prop === 'catch') {
const originalCatch = Reflect.get(target, prop, receiver);

return (exception: unknown, host: unknown) => {
const status_code = (exception as { status?: number }).status;

// don't report expected errors
if (status_code !== undefined) {
return originalCatch.apply(target, [exception, host]);
}

captureException(exception);
return originalCatch.apply(target, [exception, host]);
};
}
return Reflect.get(target, prop, receiver);
},
});

app.useGlobalFilters(wrappedFilter);
}

function addNestSpanAttributes(span: Span): void {
const attributes = spanToJSON(span).data || {};

// this is one of: app_creation, request_context, handler
const type = attributes['nestjs.type'];

// If this is already set, or we have no nest.js span, no need to process again...
if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || !type) {
return;
}

span.setAttributes({
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.nestjs',
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.nestjs`,
});
}
Loading

0 comments on commit 7ad5054

Please sign in to comment.