diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 68e0732e342e..8c0fe42434d8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -126,6 +126,7 @@ jobs: node: - *shared - 'packages/node/**' + - 'packages/node-experimental/**' - 'dev-packages/node-integration-tests/**' deno: - *shared diff --git a/packages/node-experimental/src/index.ts b/packages/node-experimental/src/index.ts index 3b208ea1d2d8..9338fae2183d 100644 --- a/packages/node-experimental/src/index.ts +++ b/packages/node-experimental/src/index.ts @@ -1,7 +1,20 @@ import { Integrations as CoreIntegrations } from '@sentry/core'; import * as NodeExperimentalIntegrations from './integrations'; +export { expressIntegration } from './integrations/express'; +export { fastifyIntegration } from './integrations/fastify'; +export { graphqlIntegration } from './integrations/graphql'; +export { httpIntegration } from './integrations/http'; +export { mongoIntegration } from './integrations/mongo'; +export { mongooseIntegration } from './integrations/mongoose'; +export { mysqlIntegration } from './integrations/mysql'; +export { mysql2Integration } from './integrations/mysql2'; +export { nestIntegration } from './integrations/nest'; +export { nativeNodeFetchIntegration } from './integrations/node-fetch'; +export { postgresIntegration } from './integrations/postgres'; +export { prismaIntegration } from './integrations/prisma'; +/** @deprecated Import the integration function directly, e.g. `inboundFiltersIntegration()` instead of `new Integrations.InboundFilter(). */ export const Integrations = { // eslint-disable-next-line deprecation/deprecation ...CoreIntegrations, diff --git a/packages/node-experimental/src/integrations/express.ts b/packages/node-experimental/src/integrations/express.ts index 0bbe3a19a11d..1931038da714 100644 --- a/packages/node-experimental/src/integrations/express.ts +++ b/packages/node-experimental/src/integrations/express.ts @@ -1,14 +1,36 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _expressIntegration = (() => { + return { + name: 'Express', + setupOnce() { + registerInstrumentations({ + instrumentations: [ + new ExpressInstrumentation({ + requestHook(span) { + addOriginToSpan(span, 'auto.http.otel.express'); + }, + }), + ], + }); + }, + }; +}) satisfies IntegrationFn; + +export const expressIntegration = defineIntegration(_expressIntegration); + /** * Express integration * * Capture tracing data for express. + * @deprecated Use `expressIntegration()` instead. */ export class Express extends NodePerformanceIntegration implements Integration { /** @@ -23,6 +45,7 @@ export class Express extends NodePerformanceIntegration implements Integra public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = Express.id; } diff --git a/packages/node-experimental/src/integrations/fastify.ts b/packages/node-experimental/src/integrations/fastify.ts index 4d32037887b1..b34d267934aa 100644 --- a/packages/node-experimental/src/integrations/fastify.ts +++ b/packages/node-experimental/src/integrations/fastify.ts @@ -1,14 +1,37 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { FastifyInstrumentation } from '@opentelemetry/instrumentation-fastify'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _fastifyIntegration = (() => { + return { + name: 'Fastify', + setupOnce() { + registerInstrumentations({ + instrumentations: [ + new FastifyInstrumentation({ + requestHook(span) { + addOriginToSpan(span, 'auto.http.otel.fastify'); + }, + }), + ], + }); + }, + }; +}) satisfies IntegrationFn; + +export const fastifyIntegration = defineIntegration(_fastifyIntegration); + /** * Express integration * * Capture tracing data for fastify. + * + * @deprecated Use `fastifyIntegration()` instead. */ export class Fastify extends NodePerformanceIntegration implements Integration { /** @@ -23,6 +46,7 @@ export class Fastify extends NodePerformanceIntegration implements Integra public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = Fastify.id; } diff --git a/packages/node-experimental/src/integrations/getAutoPerformanceIntegrations.ts b/packages/node-experimental/src/integrations/getAutoPerformanceIntegrations.ts index 1a4200ab6fb0..77d772ce005b 100644 --- a/packages/node-experimental/src/integrations/getAutoPerformanceIntegrations.ts +++ b/packages/node-experimental/src/integrations/getAutoPerformanceIntegrations.ts @@ -1,73 +1,32 @@ import type { Integration } from '@sentry/types'; -import type { NodePerformanceIntegration } from './NodePerformanceIntegration'; -import { Express } from './express'; -import { Fastify } from './fastify'; -import { GraphQL } from './graphql'; -import { Hapi } from './hapi'; -import { Mongo } from './mongo'; -import { Mongoose } from './mongoose'; -import { Mysql } from './mysql'; -import { Mysql2 } from './mysql2'; -import { Nest } from './nest'; -import { Postgres } from './postgres'; -import { Prisma } from './prisma'; - -const INTEGRATIONS: (() => NodePerformanceIntegration)[] = [ - () => { - return new Express(); - }, - () => { - return new Fastify(); - }, - () => { - return new GraphQL(); - }, - () => { - return new Mongo(); - }, - () => { - return new Mongoose(); - }, - () => { - return new Mysql(); - }, - () => { - return new Mysql2(); - }, - () => { - return new Postgres(); - }, - () => { - return new Prisma(); - }, - () => { - return new Nest(); - }, - () => { - return new Hapi(); - }, -]; +import { expressIntegration } from './express'; +import { fastifyIntegration } from './fastify'; +import { graphqlIntegration } from './graphql'; +import { hapiIntegration } from './hapi'; +import { mongoIntegration } from './mongo'; +import { mongooseIntegration } from './mongoose'; +import { mysqlIntegration } from './mysql'; +import { mysql2Integration } from './mysql2'; +import { nestIntegration } from './nest'; +import { postgresIntegration } from './postgres'; +import { prismaIntegration } from './prisma'; /** - * Get auto-dsicovered performance integrations. - * Note that due to the way OpenTelemetry instrumentation works, this will generally still return Integrations - * for stuff that may not be installed. This is because Otel only instruments when the module is imported/required, - * so if the package is not required at all it will not be patched, and thus not instrumented. - * But the _Sentry_ Integration will still be added. - * This _may_ be a bit confusing because it shows all integrations as being installed in the debug logs, but this is - * technically not wrong because we install it (it just doesn't do anything). + * With OTEL, all performance integrations will be added, as OTEL only initializes them when the patched package is actually required. */ export function getAutoPerformanceIntegrations(): Integration[] { - const loadedIntegrations = INTEGRATIONS.map(tryLoad => { - try { - const integration = tryLoad(); - const isLoaded = integration.loadInstrumentations(); - return isLoaded ? integration : false; - } catch (_) { - return false; - } - }).filter(integration => !!integration) as Integration[]; - - return loadedIntegrations; + return [ + expressIntegration(), + fastifyIntegration(), + graphqlIntegration(), + mongoIntegration(), + mongooseIntegration(), + mysqlIntegration(), + mysql2Integration(), + postgresIntegration(), + prismaIntegration(), + nestIntegration(), + hapiIntegration(), + ]; } diff --git a/packages/node-experimental/src/integrations/graphql.ts b/packages/node-experimental/src/integrations/graphql.ts index b4a529df713e..576d049c44b9 100644 --- a/packages/node-experimental/src/integrations/graphql.ts +++ b/packages/node-experimental/src/integrations/graphql.ts @@ -1,14 +1,38 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _graphqlIntegration = (() => { + return { + name: 'Graphql', + setupOnce() { + registerInstrumentations({ + instrumentations: [ + new GraphQLInstrumentation({ + ignoreTrivialResolveSpans: true, + responseHook(span) { + addOriginToSpan(span, 'auto.graphql.otel.graphql'); + }, + }), + ], + }); + }, + }; +}) satisfies IntegrationFn; + +export const graphqlIntegration = defineIntegration(_graphqlIntegration); + /** * GraphQL integration * * Capture tracing data for GraphQL. + * + * @deprecated Use `graphqlIntegration()` instead. */ export class GraphQL extends NodePerformanceIntegration implements Integration { /** @@ -23,6 +47,7 @@ export class GraphQL extends NodePerformanceIntegration implements Integra public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = GraphQL.id; } diff --git a/packages/node-experimental/src/integrations/hapi.ts b/packages/node-experimental/src/integrations/hapi.ts index 3f486e07961c..1376bcb49ccf 100644 --- a/packages/node-experimental/src/integrations/hapi.ts +++ b/packages/node-experimental/src/integrations/hapi.ts @@ -1,13 +1,30 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { HapiInstrumentation } from '@opentelemetry/instrumentation-hapi'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _hapiIntegration = (() => { + return { + name: 'Hapi', + setupOnce() { + registerInstrumentations({ + instrumentations: [new HapiInstrumentation()], + }); + }, + }; +}) satisfies IntegrationFn; + +export const hapiIntegration = defineIntegration(_hapiIntegration); + /** * Hapi integration * * Capture tracing data for Hapi. + * + * @deprecated Use `hapiIntegration()` instead. */ export class Hapi extends NodePerformanceIntegration implements Integration { /** @@ -22,6 +39,7 @@ export class Hapi extends NodePerformanceIntegration implements Integratio public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = Hapi.id; } diff --git a/packages/node-experimental/src/integrations/http.ts b/packages/node-experimental/src/integrations/http.ts index 66606bbf8258..6894ddc4ab2e 100644 --- a/packages/node-experimental/src/integrations/http.ts +++ b/packages/node-experimental/src/integrations/http.ts @@ -3,9 +3,9 @@ import type { Span } from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; -import { addBreadcrumb, hasTracingEnabled, isSentryRequestUrl } from '@sentry/core'; +import { addBreadcrumb, defineIntegration, hasTracingEnabled, isSentryRequestUrl } from '@sentry/core'; import { _INTERNAL, getClient, getSpanKind, setSpanMetadata } from '@sentry/opentelemetry'; -import type { EventProcessor, Hub, Integration } from '@sentry/types'; +import type { EventProcessor, Hub, Integration, IntegrationFn } from '@sentry/types'; import { stringMatchesSomePattern } from '@sentry/utils'; import { getIsolationScope, setIsolationScope } from '../sdk/api'; @@ -14,6 +14,97 @@ import { addOriginToSpan } from '../utils/addOriginToSpan'; import { getRequestUrl } from '../utils/getRequestUrl'; interface HttpOptions { + /** + * Whether breadcrumbs should be recorded for requests. + * Defaults to true + */ + breadcrumbs?: boolean; + + /** + * Do not capture spans or breadcrumbs for outgoing HTTP requests to URLs where the given callback returns `true`. + * This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled. + */ + ignoreOutgoingRequests?: (url: string) => boolean; + + /** + * Do not capture spans or breadcrumbs for incoming HTTP requests to URLs where the given callback returns `true`. + * This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled. + */ + ignoreIncomingRequests?: (url: string) => boolean; +} + +const _httpIntegration = ((options: HttpOptions = {}) => { + const _breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs; + const _ignoreOutgoingRequests = options.ignoreOutgoingRequests; + const _ignoreIncomingRequests = options.ignoreIncomingRequests; + + return { + name: 'Http', + setupOnce() { + const instrumentations = [ + new HttpInstrumentation({ + ignoreOutgoingRequestHook: request => { + const url = getRequestUrl(request); + + if (!url) { + return false; + } + + if (isSentryRequestUrl(url, getClient())) { + return true; + } + + if (_ignoreOutgoingRequests && _ignoreOutgoingRequests(url)) { + return true; + } + + return false; + }, + + ignoreIncomingRequestHook: request => { + const url = getRequestUrl(request); + + const method = request.method?.toUpperCase(); + // We do not capture OPTIONS/HEAD requests as transactions + if (method === 'OPTIONS' || method === 'HEAD') { + return true; + } + + if (_ignoreIncomingRequests && _ignoreIncomingRequests(url)) { + return true; + } + + return false; + }, + + requireParentforOutgoingSpans: true, + requireParentforIncomingSpans: false, + requestHook: (span, req) => { + _updateSpan(span, req); + + // Update the isolation scope, isolate this request + if (getSpanKind(span) === SpanKind.SERVER) { + setIsolationScope(getIsolationScope().clone()); + } + }, + responseHook: (span, res) => { + if (_breadcrumbs) { + _addRequestBreadcrumb(span, res); + } + }, + }), + ]; + + registerInstrumentations({ + instrumentations, + }); + }, + }; +}) satisfies IntegrationFn; + +export const httpIntegration = defineIntegration(_httpIntegration); + +interface OldHttpOptions { /** * Whether breadcrumbs should be recorded for requests * Defaults to true @@ -39,6 +130,8 @@ interface HttpOptions { * * Create spans for outgoing requests * * Note that this integration is also needed for the Express integration to work! + * + * @deprecated Use `httpIntegration()` instead. */ export class Http implements Integration { /** @@ -65,7 +158,8 @@ export class Http implements Integration { /** * @inheritDoc */ - public constructor(options: HttpOptions = {}) { + public constructor(options: OldHttpOptions = {}) { + // eslint-disable-next-line deprecation/deprecation this.name = Http.id; this._breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs; this._spans = typeof options.spans === 'undefined' ? undefined : options.spans; @@ -127,7 +221,7 @@ export class Http implements Integration { requireParentforOutgoingSpans: true, requireParentforIncomingSpans: false, requestHook: (span, req) => { - this._updateSpan(span, req); + _updateSpan(span, req); // Update the isolation scope, isolate this request if (getSpanKind(span) === SpanKind.SERVER) { @@ -135,7 +229,9 @@ export class Http implements Integration { } }, responseHook: (span, res) => { - this._addRequestBreadcrumb(span, res); + if (this._breadcrumbs) { + _addRequestBreadcrumb(span, res); + } }, }), ], @@ -148,39 +244,39 @@ export class Http implements Integration { public unregister(): void { this._unload?.(); } +} - /** Update the span with data we need. */ - private _updateSpan(span: Span, request: ClientRequest | IncomingMessage): void { - addOriginToSpan(span, 'auto.http.otel.http'); +/** Update the span with data we need. */ +function _updateSpan(span: Span, request: ClientRequest | IncomingMessage): void { + addOriginToSpan(span, 'auto.http.otel.http'); - if (getSpanKind(span) === SpanKind.SERVER) { - setSpanMetadata(span, { request }); - } + if (getSpanKind(span) === SpanKind.SERVER) { + setSpanMetadata(span, { request }); } +} - /** Add a breadcrumb for outgoing requests. */ - private _addRequestBreadcrumb(span: Span, response: IncomingMessage | ServerResponse): void { - if (!this._breadcrumbs || getSpanKind(span) !== SpanKind.CLIENT) { - return; - } +/** Add a breadcrumb for outgoing requests. */ +function _addRequestBreadcrumb(span: Span, response: IncomingMessage | ServerResponse): void { + if (getSpanKind(span) !== SpanKind.CLIENT) { + return; + } - const data = _INTERNAL.getRequestSpanData(span); - addBreadcrumb( - { - category: 'http', - data: { - status_code: response.statusCode, - ...data, - }, - type: 'http', + const data = _INTERNAL.getRequestSpanData(span); + addBreadcrumb( + { + category: 'http', + data: { + status_code: response.statusCode, + ...data, }, - { - event: 'response', - // TODO FN: Do we need access to `request` here? - // If we do, we'll have to use the `applyCustomAttributesOnSpan` hook instead, - // but this has worse context semantics than request/responseHook. - response, - }, - ); - } + type: 'http', + }, + { + event: 'response', + // TODO FN: Do we need access to `request` here? + // If we do, we'll have to use the `applyCustomAttributesOnSpan` hook instead, + // but this has worse context semantics than request/responseHook. + response, + }, + ); } diff --git a/packages/node-experimental/src/integrations/index.ts b/packages/node-experimental/src/integrations/index.ts index 7279f45c2dfc..a37c0f3b615e 100644 --- a/packages/node-experimental/src/integrations/index.ts +++ b/packages/node-experimental/src/integrations/index.ts @@ -23,6 +23,7 @@ export { LocalVariables, }; +/* eslint-disable deprecation/deprecation */ export { Express } from './express'; export { Http } from './http'; export { NodeFetch } from './node-fetch'; diff --git a/packages/node-experimental/src/integrations/mongo.ts b/packages/node-experimental/src/integrations/mongo.ts index f8be482be946..bcfaaaf1bc62 100644 --- a/packages/node-experimental/src/integrations/mongo.ts +++ b/packages/node-experimental/src/integrations/mongo.ts @@ -1,14 +1,37 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _mongoIntegration = (() => { + return { + name: 'Mongo', + setupOnce() { + registerInstrumentations({ + instrumentations: [ + new MongoDBInstrumentation({ + responseHook(span) { + addOriginToSpan(span, 'auto.db.otel.mongo'); + }, + }), + ], + }); + }, + }; +}) satisfies IntegrationFn; + +export const mongoIntegration = defineIntegration(_mongoIntegration); + /** * MongoDB integration * * Capture tracing data for MongoDB. + * + * @deprecated Use `mongoIntegration()` instead. */ export class Mongo extends NodePerformanceIntegration implements Integration { /** @@ -23,6 +46,7 @@ export class Mongo extends NodePerformanceIntegration implements Integrati public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = Mongo.id; } diff --git a/packages/node-experimental/src/integrations/mongoose.ts b/packages/node-experimental/src/integrations/mongoose.ts index a5361a620bc2..a14c7d54a266 100644 --- a/packages/node-experimental/src/integrations/mongoose.ts +++ b/packages/node-experimental/src/integrations/mongoose.ts @@ -1,14 +1,37 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { MongooseInstrumentation } from '@opentelemetry/instrumentation-mongoose'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _mongooseIntegration = (() => { + return { + name: 'Mongoose', + setupOnce() { + registerInstrumentations({ + instrumentations: [ + new MongooseInstrumentation({ + responseHook(span) { + addOriginToSpan(span, 'auto.db.otel.mongoose'); + }, + }), + ], + }); + }, + }; +}) satisfies IntegrationFn; + +export const mongooseIntegration = defineIntegration(_mongooseIntegration); + /** * Mongoose integration * * Capture tracing data for Mongoose. + * + * @deprecated Use `mongooseIntegration()` instead. */ export class Mongoose extends NodePerformanceIntegration implements Integration { /** @@ -23,6 +46,7 @@ export class Mongoose extends NodePerformanceIntegration implements Integr public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = Mongoose.id; } diff --git a/packages/node-experimental/src/integrations/mysql.ts b/packages/node-experimental/src/integrations/mysql.ts index 3973f07f4685..3cf0f4e42c87 100644 --- a/packages/node-experimental/src/integrations/mysql.ts +++ b/packages/node-experimental/src/integrations/mysql.ts @@ -1,13 +1,30 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { MySQLInstrumentation } from '@opentelemetry/instrumentation-mysql'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _mysqlIntegration = (() => { + return { + name: 'Mysql', + setupOnce() { + registerInstrumentations({ + instrumentations: [new MySQLInstrumentation({})], + }); + }, + }; +}) satisfies IntegrationFn; + +export const mysqlIntegration = defineIntegration(_mysqlIntegration); + /** * MySQL integration * * Capture tracing data for mysql. + * + * @deprecated Use `mysqlIntegration()` instead. */ export class Mysql extends NodePerformanceIntegration implements Integration { /** @@ -22,6 +39,7 @@ export class Mysql extends NodePerformanceIntegration implements Integrati public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = Mysql.id; } diff --git a/packages/node-experimental/src/integrations/mysql2.ts b/packages/node-experimental/src/integrations/mysql2.ts index 9a87de98fd66..bb89d0aa01cb 100644 --- a/packages/node-experimental/src/integrations/mysql2.ts +++ b/packages/node-experimental/src/integrations/mysql2.ts @@ -1,14 +1,37 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { MySQL2Instrumentation } from '@opentelemetry/instrumentation-mysql2'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _mysql2Integration = (() => { + return { + name: 'Mysql2', + setupOnce() { + registerInstrumentations({ + instrumentations: [ + new MySQL2Instrumentation({ + responseHook(span) { + addOriginToSpan(span, 'auto.db.otel.mysql2'); + }, + }), + ], + }); + }, + }; +}) satisfies IntegrationFn; + +export const mysql2Integration = defineIntegration(_mysql2Integration); + /** * MySQL2 integration * * Capture tracing data for mysql2 + * + * @deprecated Use `mysql2Integration()` instead. */ export class Mysql2 extends NodePerformanceIntegration implements Integration { /** @@ -23,6 +46,7 @@ export class Mysql2 extends NodePerformanceIntegration implements Integrat public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = Mysql2.id; } diff --git a/packages/node-experimental/src/integrations/nest.ts b/packages/node-experimental/src/integrations/nest.ts index b7e47b2f49c8..c03955f71193 100644 --- a/packages/node-experimental/src/integrations/nest.ts +++ b/packages/node-experimental/src/integrations/nest.ts @@ -1,13 +1,30 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _nestIntegration = (() => { + return { + name: 'Nest', + setupOnce() { + registerInstrumentations({ + instrumentations: [new NestInstrumentation({})], + }); + }, + }; +}) satisfies IntegrationFn; + +export const nestIntegration = defineIntegration(_nestIntegration); + /** * Nest framework integration * * Capture tracing data for nest. + * + * @deprecated Use `nestIntegration()` instead. */ export class Nest extends NodePerformanceIntegration implements Integration { /** @@ -22,6 +39,7 @@ export class Nest extends NodePerformanceIntegration implements Integratio public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = Nest.id; } diff --git a/packages/node-experimental/src/integrations/node-fetch.ts b/packages/node-experimental/src/integrations/node-fetch.ts index a2b7b61bdfc0..35bf982d286e 100644 --- a/packages/node-experimental/src/integrations/node-fetch.ts +++ b/packages/node-experimental/src/integrations/node-fetch.ts @@ -1,9 +1,10 @@ import type { Span } from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; import type { Instrumentation } from '@opentelemetry/instrumentation'; -import { addBreadcrumb, hasTracingEnabled } from '@sentry/core'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; +import { addBreadcrumb, defineIntegration, hasTracingEnabled } from '@sentry/core'; import { _INTERNAL, getClient, getSpanKind } from '@sentry/opentelemetry'; -import type { Integration } from '@sentry/types'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { parseSemver } from '@sentry/utils'; import type { NodeExperimentalClient } from '../types'; @@ -13,6 +14,70 @@ import { NodePerformanceIntegration } from './NodePerformanceIntegration'; const NODE_VERSION: ReturnType = parseSemver(process.versions.node); interface NodeFetchOptions { + /** + * Whether breadcrumbs should be recorded for requests. + * Defaults to true + */ + breadcrumbs?: boolean; + + /** + * Do not capture spans or breadcrumbs for outgoing fetch requests to URLs where the given callback returns `true`. + * This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled. + */ + ignoreOutgoingRequests?: (url: string) => boolean; +} + +const _nativeNodeFetchIntegration = ((options: NodeFetchOptions = {}) => { + const _breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs; + const _ignoreOutgoingRequests = options.ignoreOutgoingRequests; + + function getInstrumentation(): [Instrumentation] | void { + // Only add NodeFetch if Node >= 16, as previous versions do not support it + if (!NODE_VERSION.major || NODE_VERSION.major < 16) { + return; + } + + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { FetchInstrumentation } = require('opentelemetry-instrumentation-fetch-node'); + return [ + new FetchInstrumentation({ + ignoreRequestHook: (request: { origin?: string }) => { + const url = request.origin; + return _ignoreOutgoingRequests && url && _ignoreOutgoingRequests(url); + }, + + onRequest: ({ span }: { span: Span }) => { + _updateSpan(span); + + if (_breadcrumbs) { + _addRequestBreadcrumb(span); + } + }, + }), + ]; + } catch (error) { + // Could not load instrumentation + } + } + + return { + name: 'NodeFetch', + setupOnce() { + const instrumentations = getInstrumentation(); + + if (instrumentations) { + registerInstrumentations({ + instrumentations, + }); + } + }, + }; +}) satisfies IntegrationFn; + +export const nativeNodeFetchIntegration = defineIntegration(_nativeNodeFetchIntegration); + +interface OldNodeFetchOptions { /** * Whether breadcrumbs should be recorded for requests * Defaults to true @@ -31,8 +96,10 @@ interface NodeFetchOptions { * This instrumentation does two things: * * Create breadcrumbs for outgoing requests * * Create spans for outgoing requests + * + * @deprecated Use `nativeNodeFetchIntegration()` instead. */ -export class NodeFetch extends NodePerformanceIntegration implements Integration { +export class NodeFetch extends NodePerformanceIntegration implements Integration { /** * @inheritDoc */ @@ -55,9 +122,10 @@ export class NodeFetch extends NodePerformanceIntegration impl /** * @inheritDoc */ - public constructor(options: NodeFetchOptions = {}) { + public constructor(options: OldNodeFetchOptions = {}) { super(options); + // eslint-disable-next-line deprecation/deprecation this.name = NodeFetch.id; this._breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs; this._spans = typeof options.spans === 'undefined' ? undefined : options.spans; @@ -79,8 +147,11 @@ export class NodeFetch extends NodePerformanceIntegration impl return [ new FetchInstrumentation({ onRequest: ({ span }: { span: Span }) => { - this._updateSpan(span); - this._addRequestBreadcrumb(span); + _updateSpan(span); + + if (this._breadcrumbs) { + _addRequestBreadcrumb(span); + } }, }), ]; @@ -109,25 +180,25 @@ export class NodeFetch extends NodePerformanceIntegration impl public unregister(): void { this._unload?.(); } +} - /** Update the span with data we need. */ - private _updateSpan(span: Span): void { - addOriginToSpan(span, 'auto.http.otel.node_fetch'); - } - - /** Add a breadcrumb for outgoing requests. */ - private _addRequestBreadcrumb(span: Span): void { - if (!this._breadcrumbs || getSpanKind(span) !== SpanKind.CLIENT) { - return; - } +/** Update the span with data we need. */ +function _updateSpan(span: Span): void { + addOriginToSpan(span, 'auto.http.otel.node_fetch'); +} - const data = _INTERNAL.getRequestSpanData(span); - addBreadcrumb({ - category: 'http', - data: { - ...data, - }, - type: 'http', - }); +/** Add a breadcrumb for outgoing requests. */ +function _addRequestBreadcrumb(span: Span): void { + if (getSpanKind(span) !== SpanKind.CLIENT) { + return; } + + const data = _INTERNAL.getRequestSpanData(span); + addBreadcrumb({ + category: 'http', + data: { + ...data, + }, + type: 'http', + }); } diff --git a/packages/node-experimental/src/integrations/postgres.ts b/packages/node-experimental/src/integrations/postgres.ts index 85584f8a6507..91a6a710ffdd 100644 --- a/packages/node-experimental/src/integrations/postgres.ts +++ b/packages/node-experimental/src/integrations/postgres.ts @@ -1,14 +1,38 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { PgInstrumentation } from '@opentelemetry/instrumentation-pg'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _postgresIntegration = (() => { + return { + name: 'Postgres', + setupOnce() { + registerInstrumentations({ + instrumentations: [ + new PgInstrumentation({ + requireParentSpan: true, + requestHook(span) { + addOriginToSpan(span, 'auto.db.otel.postgres'); + }, + }), + ], + }); + }, + }; +}) satisfies IntegrationFn; + +export const postgresIntegration = defineIntegration(_postgresIntegration); + /** * Postgres integration * * Capture tracing data for pg. + * + * @deprecated Use `postgresIntegration()` instead. */ export class Postgres extends NodePerformanceIntegration implements Integration { /** @@ -23,6 +47,7 @@ export class Postgres extends NodePerformanceIntegration implements Integr public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = Postgres.id; } diff --git a/packages/node-experimental/src/integrations/prisma.ts b/packages/node-experimental/src/integrations/prisma.ts index 203e9d8ed6b1..9edd6ce9d02d 100644 --- a/packages/node-experimental/src/integrations/prisma.ts +++ b/packages/node-experimental/src/integrations/prisma.ts @@ -1,9 +1,27 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { PrismaInstrumentation } from '@prisma/instrumentation'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _prismaIntegration = (() => { + return { + name: 'Prisma', + setupOnce() { + registerInstrumentations({ + instrumentations: [ + // does not have a hook to adjust spans & add origin + new PrismaInstrumentation({}), + ], + }); + }, + }; +}) satisfies IntegrationFn; + +export const prismaIntegration = defineIntegration(_prismaIntegration); + /** * Prisma integration * @@ -12,6 +30,8 @@ import { NodePerformanceIntegration } from './NodePerformanceIntegration'; * previewFeatures = ["tracing"] * For the prisma client. * See https://www.prisma.io/docs/concepts/components/prisma-client/opentelemetry-tracing for more details. + * + * @deprecated Use `prismaIntegration()` instead. */ export class Prisma extends NodePerformanceIntegration implements Integration { /** @@ -26,6 +46,7 @@ export class Prisma extends NodePerformanceIntegration implements Integrat public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = Prisma.id; } diff --git a/packages/node-experimental/src/sdk/init.ts b/packages/node-experimental/src/sdk/init.ts index 8472bcf17d6e..d617845b9e2e 100644 --- a/packages/node-experimental/src/sdk/init.ts +++ b/packages/node-experimental/src/sdk/init.ts @@ -18,8 +18,8 @@ import { import { DEBUG_BUILD } from '../debug-build'; import { getAutoPerformanceIntegrations } from '../integrations/getAutoPerformanceIntegrations'; -import { Http } from '../integrations/http'; -import { NodeFetch } from '../integrations/node-fetch'; +import { httpIntegration } from '../integrations/http'; +import { nativeNodeFetchIntegration } from '../integrations/node-fetch'; import { setOpenTelemetryContextAsyncContextStrategy } from '../otel/asyncContextStrategy'; import type { NodeExperimentalClientOptions, NodeExperimentalOptions } from '../types'; import { getClient, getCurrentScope, getGlobalScope, getIsolationScope } from './api'; @@ -34,16 +34,16 @@ const ignoredDefaultIntegrations = ['Http', 'Undici']; export const defaultIntegrations: Integration[] = [ // eslint-disable-next-line deprecation/deprecation ...defaultNodeIntegrations.filter(i => !ignoredDefaultIntegrations.includes(i.name)), - new Http(), - new NodeFetch(), + httpIntegration(), + nativeNodeFetchIntegration(), ]; /** Get the default integrations for the Node Experimental SDK. */ export function getDefaultIntegrations(options: Options): Integration[] { return [ ...getDefaultNodeIntegrations(options).filter(i => !ignoredDefaultIntegrations.includes(i.name)), - new Http(), - new NodeFetch(), + httpIntegration(), + nativeNodeFetchIntegration(), ...(hasTracingEnabled(options) ? getAutoPerformanceIntegrations() : []), ]; } diff --git a/packages/node-experimental/src/sdk/spanProcessor.ts b/packages/node-experimental/src/sdk/spanProcessor.ts index 226add7753cf..a85085077e94 100644 --- a/packages/node-experimental/src/sdk/spanProcessor.ts +++ b/packages/node-experimental/src/sdk/spanProcessor.ts @@ -33,12 +33,15 @@ export class NodeExperimentalSentrySpanProcessor extends SentrySpanProcessor { /** @inheritDoc */ protected _shouldSendSpanToSentry(span: Span): boolean { const client = getClient(); + // eslint-disable-next-line deprecation/deprecation const httpIntegration = client ? client.getIntegrationByName('Http') : undefined; + // eslint-disable-next-line deprecation/deprecation const fetchIntegration = client ? client.getIntegrationByName('NodeFetch') : undefined; // If we encounter a client or server span with url & method, we assume this comes from the http instrumentation // In this case, if `shouldCreateSpansForRequests` is false, we want to _record_ the span but not _sample_ it, // So we can generate a breadcrumb for it but no span will be sent + // TODO v8: Remove this if ( (span.kind === SpanKind.CLIENT || span.kind === SpanKind.SERVER) && span.attributes[SemanticAttributes.HTTP_URL] && diff --git a/packages/node-experimental/test/integration/transactions.test.ts b/packages/node-experimental/test/integration/transactions.test.ts index 929b286452f3..d379070c4ee1 100644 --- a/packages/node-experimental/test/integration/transactions.test.ts +++ b/packages/node-experimental/test/integration/transactions.test.ts @@ -540,6 +540,7 @@ describe('Integration | Transactions', () => { if (name === 'Http') { return { shouldCreateSpansForRequests: false, + // eslint-disable-next-line deprecation/deprecation } as Http; } @@ -604,6 +605,7 @@ describe('Integration | Transactions', () => { if (name === 'NodeFetch') { return { shouldCreateSpansForRequests: false, + // eslint-disable-next-line deprecation/deprecation } as NodeFetch; }