diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts index 337e98decc31..816ae0bb6602 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts @@ -55,7 +55,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { data: { 'sentry.origin': 'manual', 'fastify.type': 'middleware', - 'plugin.name': 'fastify -> @fastify/middie', + 'plugin.name': '@fastify/middie', 'hook.name': 'onRequest', }, description: 'middleware - runMiddie', @@ -71,7 +71,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { data: { 'sentry.origin': 'auto.http.otel.fastify', 'sentry.op': 'request_handler.fastify', - 'plugin.name': 'fastify -> @fastify/middie', + 'plugin.name': '@fastify/middie', 'fastify.type': 'request_handler', 'http.route': '/test-transaction', }, diff --git a/dev-packages/e2e-tests/test-applications/node-fastify/.gitignore b/dev-packages/e2e-tests/test-applications/node-fastify-3/.gitignore similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-fastify/.gitignore rename to dev-packages/e2e-tests/test-applications/node-fastify-3/.gitignore diff --git a/dev-packages/e2e-tests/test-applications/node-fastify/.npmrc b/dev-packages/e2e-tests/test-applications/node-fastify-3/.npmrc similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-fastify/.npmrc rename to dev-packages/e2e-tests/test-applications/node-fastify-3/.npmrc diff --git a/dev-packages/e2e-tests/test-applications/node-fastify/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json similarity index 92% rename from dev-packages/e2e-tests/test-applications/node-fastify/package.json rename to dev-packages/e2e-tests/test-applications/node-fastify-3/package.json index 9b9f584cc359..25b5881905d2 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify/package.json +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json @@ -1,5 +1,5 @@ { - "name": "node-fastify", + "name": "node-fastify-3", "version": "1.0.0", "private": true, "scripts": { @@ -15,7 +15,7 @@ "@sentry/core": "latest || *", "@sentry/opentelemetry": "latest || *", "@types/node": "^18.19.1", - "fastify": "4.23.2", + "fastify": "3.29.5", "typescript": "~5.0.0", "ts-node": "10.9.1" }, diff --git a/dev-packages/e2e-tests/test-applications/node-fastify/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-fastify-3/playwright.config.mjs similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-fastify/playwright.config.mjs rename to dev-packages/e2e-tests/test-applications/node-fastify-3/playwright.config.mjs diff --git a/dev-packages/e2e-tests/test-applications/node-fastify/src/app.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-fastify/src/app.ts rename to dev-packages/e2e-tests/test-applications/node-fastify-3/src/app.ts diff --git a/dev-packages/e2e-tests/test-applications/node-fastify/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-fastify-3/start-event-proxy.mjs similarity index 75% rename from dev-packages/e2e-tests/test-applications/node-fastify/start-event-proxy.mjs rename to dev-packages/e2e-tests/test-applications/node-fastify-3/start-event-proxy.mjs index 814357a4d413..17d53a9596f3 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/start-event-proxy.mjs @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, - proxyServerName: 'node-fastify', + proxyServerName: 'node-fastify-3', }); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/errors.test.ts similarity index 93% rename from dev-packages/e2e-tests/test-applications/node-fastify/tests/errors.test.ts rename to dev-packages/e2e-tests/test-applications/node-fastify-3/tests/errors.test.ts index 1b63fe0e0c55..1a37fc244413 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/errors.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForError } from '@sentry-internal/test-utils'; test('Sends correct error event', async ({ baseURL }) => { - const errorEventPromise = waitForError('node-fastify', event => { + const errorEventPromise = waitForError('node-fastify-3', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/propagation.test.ts similarity index 98% rename from dev-packages/e2e-tests/test-applications/node-fastify/tests/propagation.test.ts rename to dev-packages/e2e-tests/test-applications/node-fastify-3/tests/propagation.test.ts index af2cfddded9a..ec20963d86e7 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/propagation.test.ts @@ -6,14 +6,14 @@ import { SpanJSON } from '@sentry/core'; test('Propagates trace for outgoing http requests', async ({ baseURL }) => { const id = crypto.randomUUID(); - const inboundTransactionPromise = waitForTransaction('node-fastify', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-fastify-3', transactionEvent => { return ( transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-inbound-headers/${id}` ); }); - const outboundTransactionPromise = waitForTransaction('node-fastify', transactionEvent => { + const outboundTransactionPromise = waitForTransaction('node-fastify-3', transactionEvent => { return ( transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-http/${id}` @@ -120,14 +120,14 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { const id = crypto.randomUUID(); - const inboundTransactionPromise = waitForTransaction('node-fastify', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-fastify-3', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-inbound-headers/${id}` ); }); - const outboundTransactionPromise = waitForTransaction('node-fastify', transactionEvent => { + const outboundTransactionPromise = waitForTransaction('node-fastify-3', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch/${id}` @@ -232,7 +232,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { }); test('Propagates trace for outgoing external http requests', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('node-fastify', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-fastify-3', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-http-external-allowed` @@ -269,7 +269,7 @@ test('Propagates trace for outgoing external http requests', async ({ baseURL }) }); test('Does not propagate outgoing http requests not covered by tracePropagationTargets', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('node-fastify', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-fastify-3', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-http-external-disallowed` @@ -293,7 +293,7 @@ test('Does not propagate outgoing http requests not covered by tracePropagationT }); test('Propagates trace for outgoing external fetch requests', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('node-fastify', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-fastify-3', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch-external-allowed` @@ -330,7 +330,7 @@ test('Propagates trace for outgoing external fetch requests', async ({ baseURL } }); test('Does not propagate outgoing fetch requests not covered by tracePropagationTargets', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('node-fastify', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-fastify-3', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch-external-disallowed` diff --git a/dev-packages/e2e-tests/test-applications/node-fastify/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/transactions.test.ts similarity index 95% rename from dev-packages/e2e-tests/test-applications/node-fastify/tests/transactions.test.ts rename to dev-packages/e2e-tests/test-applications/node-fastify-3/tests/transactions.test.ts index 01e07538dc72..e7e41cfaa52e 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/transactions.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; test('Sends an API route transaction', async ({ baseURL }) => { - const pageloadTransactionEventPromise = waitForTransaction('node-fastify', transactionEvent => { + const pageloadTransactionEventPromise = waitForTransaction('node-fastify-3', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-transaction' @@ -60,7 +60,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { expect(spans).toContainEqual({ data: { - 'plugin.name': 'fastify -> sentry-fastify-error-handler', + 'plugin.name': 'sentry-fastify-error-handler', 'fastify.type': 'middleware', 'hook.name': 'onRequest', 'sentry.origin': 'auto.http.otel.fastify', @@ -79,7 +79,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { expect(spans).toContainEqual({ data: { - 'plugin.name': 'fastify -> sentry-fastify-error-handler', + 'plugin.name': 'sentry-fastify-error-handler', 'fastify.type': 'request_handler', 'http.route': '/test-transaction', 'sentry.op': 'request_handler.fastify', diff --git a/dev-packages/e2e-tests/test-applications/node-fastify/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-fastify-3/tsconfig.json similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-fastify/tsconfig.json rename to dev-packages/e2e-tests/test-applications/node-fastify-3/tsconfig.json diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/.gitignore b/dev-packages/e2e-tests/test-applications/node-fastify-4/.gitignore new file mode 100644 index 000000000000..1521c8b7652b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/.gitignore @@ -0,0 +1 @@ +dist diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/.npmrc b/dev-packages/e2e-tests/test-applications/node-fastify-4/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json new file mode 100644 index 000000000000..4de665edda9b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json @@ -0,0 +1,29 @@ +{ + "name": "node-fastify-4", + "version": "1.0.0", + "private": true, + "scripts": { + "start": "ts-node src/app.ts", + "test": "playwright test", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "typecheck": "tsc", + "test:build": "pnpm install && pnpm run typecheck", + "test:assert": "pnpm test" + }, + "dependencies": { + "@sentry/node": "latest || *", + "@sentry/core": "latest || *", + "@sentry/opentelemetry": "latest || *", + "@types/node": "^18.19.1", + "fastify": "4.29.0", + "typescript": "5.6.3", + "ts-node": "10.9.2" + }, + "devDependencies": { + "@playwright/test": "~1.50.0", + "@sentry-internal/test-utils": "link:../../../test-utils" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-fastify-4/playwright.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/playwright.config.mjs @@ -0,0 +1,7 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app.ts new file mode 100644 index 000000000000..275dfa786ca3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app.ts @@ -0,0 +1,153 @@ +import type * as S from '@sentry/node'; +const Sentry = require('@sentry/node') as typeof S; + +// We wrap console.warn to find out if a warning is incorrectly logged +console.warn = new Proxy(console.warn, { + apply: function (target, thisArg, argumentsList) { + const msg = argumentsList[0]; + if (typeof msg === 'string' && msg.startsWith('[Sentry]')) { + console.error(`Sentry warning was triggered: ${msg}`); + process.exit(1); + } + + return target.apply(thisArg, argumentsList); + }, +}); + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + integrations: [], + tracesSampleRate: 1, + tunnel: 'http://localhost:3031/', // proxy server + tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], +}); + +import type * as H from 'http'; +import type * as F from 'fastify'; + +// Make sure fastify is imported after Sentry is initialized +const { fastify } = require('fastify') as typeof F; +const http = require('http') as typeof H; + +const app = fastify(); +const port = 3030; +const port2 = 3040; + +Sentry.setupFastifyErrorHandler(app); + +app.get('/test-success', function (_req, res) { + res.send({ version: 'v1' }); +}); + +app.get<{ Params: { param: string } }>('/test-param/:param', function (req, res) { + res.send({ paramWas: req.params.param }); +}); + +app.get<{ Params: { id: string } }>('/test-inbound-headers/:id', function (req, res) { + const headers = req.headers; + + res.send({ headers, id: req.params.id }); +}); + +app.get<{ Params: { id: string } }>('/test-outgoing-http/:id', async function (req, res) { + const id = req.params.id; + const data = await makeHttpRequest(`http://localhost:3030/test-inbound-headers/${id}`); + + res.send(data); +}); + +app.get<{ Params: { id: string } }>('/test-outgoing-fetch/:id', async function (req, res) { + const id = req.params.id; + const response = await fetch(`http://localhost:3030/test-inbound-headers/${id}`); + const data = await response.json(); + + res.send(data); +}); + +app.get('/test-transaction', async function (req, res) { + Sentry.startSpan({ name: 'test-span' }, () => { + Sentry.startSpan({ name: 'child-span' }, () => {}); + }); + + res.send({}); +}); + +app.get('/test-error', async function (req, res) { + const exceptionId = Sentry.captureException(new Error('This is an error')); + + await Sentry.flush(2000); + + res.send({ exceptionId }); +}); + +app.get<{ Params: { id: string } }>('/test-exception/:id', async function (req, res) { + throw new Error(`This is an exception with id ${req.params.id}`); +}); + +app.get('/test-outgoing-fetch-external-allowed', async function (req, res) { + const fetchResponse = await fetch(`http://localhost:${port2}/external-allowed`); + const data = await fetchResponse.json(); + + res.send(data); +}); + +app.get('/test-outgoing-fetch-external-disallowed', async function (req, res) { + const fetchResponse = await fetch(`http://localhost:${port2}/external-disallowed`); + const data = await fetchResponse.json(); + + res.send(data); +}); + +app.get('/test-outgoing-http-external-allowed', async function (req, res) { + const data = await makeHttpRequest(`http://localhost:${port2}/external-allowed`); + res.send(data); +}); + +app.get('/test-outgoing-http-external-disallowed', async function (req, res) { + const data = await makeHttpRequest(`http://localhost:${port2}/external-disallowed`); + res.send(data); +}); + +app.listen({ port: port }); + +// A second app so we can test header propagation between external URLs +const app2 = fastify(); +app2.get('/external-allowed', function (req, res) { + const headers = req.headers; + + res.send({ headers, route: '/external-allowed' }); +}); + +app2.get('/external-disallowed', function (req, res) { + const headers = req.headers; + + res.send({ headers, route: '/external-disallowed' }); +}); + +app2.listen({ port: port2 }); + +function makeHttpRequest(url: string) { + return new Promise(resolve => { + const data: any[] = []; + + http + .request(url, httpRes => { + httpRes.on('data', chunk => { + data.push(chunk); + }); + httpRes.on('error', error => { + resolve({ error: error.message, url }); + }); + httpRes.on('end', () => { + try { + const json = JSON.parse(Buffer.concat(data).toString()); + resolve(json); + } catch { + resolve({ data: Buffer.concat(data).toString(), url }); + } + }); + }) + .end(); + }); +} diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-fastify-4/start-event-proxy.mjs new file mode 100644 index 000000000000..e3b3c77e1bc2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'node-fastify-4', +}); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/errors.test.ts new file mode 100644 index 000000000000..1f80020b5a8d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/errors.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Sends correct error event', async ({ baseURL }) => { + const errorEventPromise = waitForError('node-fastify-4', event => { + return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; + }); + + await fetch(`${baseURL}/test-exception/123`); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123'); + + expect(errorEvent.request).toEqual({ + method: 'GET', + cookies: {}, + headers: expect.any(Object), + url: 'http://localhost:3030/test-exception/123', + }); + + expect(errorEvent.transaction).toEqual('GET /test-exception/:id'); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/propagation.test.ts new file mode 100644 index 000000000000..965a47b9aba6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/propagation.test.ts @@ -0,0 +1,354 @@ +import crypto from 'crypto'; +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { SpanJSON } from '@sentry/core'; + +test('Propagates trace for outgoing http requests', async ({ baseURL }) => { + const id = crypto.randomUUID(); + + const inboundTransactionPromise = waitForTransaction('node-fastify-4', transactionEvent => { + return ( + transactionEvent.contexts?.trace?.op === 'http.server' && + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-inbound-headers/${id}` + ); + }); + + const outboundTransactionPromise = waitForTransaction('node-fastify-4', transactionEvent => { + return ( + transactionEvent.contexts?.trace?.op === 'http.server' && + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-http/${id}` + ); + }); + + const response = await fetch(`${baseURL}/test-outgoing-http/${id}`); + const data = await response.json(); + + const inboundTransaction = await inboundTransactionPromise; + const outboundTransaction = await outboundTransactionPromise; + + const traceId = outboundTransaction?.contexts?.trace?.trace_id; + const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client') as SpanJSON | undefined; + + expect(outgoingHttpSpan).toBeDefined(); + + const outgoingHttpSpanId = outgoingHttpSpan?.span_id; + + expect(traceId).toEqual(expect.any(String)); + + // data is passed through from the inbound request, to verify we have the correct headers set + const inboundHeaderSentryTrace = data.headers?.['sentry-trace']; + const inboundHeaderBaggage = data.headers?.['baggage']; + + expect(inboundHeaderSentryTrace).toEqual(`${traceId}-${outgoingHttpSpanId}-1`); + expect(inboundHeaderBaggage).toBeDefined(); + + const baggage = (inboundHeaderBaggage || '').split(','); + expect(baggage).toEqual( + expect.arrayContaining([ + 'sentry-environment=qa', + `sentry-trace_id=${traceId}`, + expect.stringMatching(/sentry-public_key=/), + ]), + ); + + expect(outboundTransaction.contexts?.trace).toEqual({ + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.op': 'http.server', + 'sentry.sample_rate': 1, + url: `http://localhost:3030/test-outgoing-http/${id}`, + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + 'http.url': `http://localhost:3030/test-outgoing-http/${id}`, + 'http.host': 'localhost:3030', + 'net.host.name': 'localhost', + 'http.method': 'GET', + 'http.scheme': 'http', + 'http.target': `/test-outgoing-http/${id}`, + 'http.user_agent': 'node', + 'http.flavor': '1.1', + 'net.transport': 'ip_tcp', + 'net.host.ip': expect.any(String), + 'net.host.port': expect.any(Number), + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'http.status_code': 200, + 'http.status_text': 'OK', + 'http.route': '/test-outgoing-http/:id', + }, + op: 'http.server', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + status: 'ok', + trace_id: traceId, + origin: 'auto.http.otel.http', + }); + + expect(inboundTransaction.contexts?.trace).toEqual({ + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.op': 'http.server', + url: `http://localhost:3030/test-inbound-headers/${id}`, + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + 'http.url': `http://localhost:3030/test-inbound-headers/${id}`, + 'http.host': 'localhost:3030', + 'net.host.name': 'localhost', + 'http.method': 'GET', + 'http.scheme': 'http', + 'http.target': `/test-inbound-headers/${id}`, + 'http.flavor': '1.1', + 'net.transport': 'ip_tcp', + 'net.host.ip': expect.any(String), + 'net.host.port': expect.any(Number), + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'http.status_code': 200, + 'http.status_text': 'OK', + 'http.route': '/test-inbound-headers/:id', + }, + op: 'http.server', + parent_span_id: outgoingHttpSpanId, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + status: 'ok', + trace_id: traceId, + origin: 'auto.http.otel.http', + }); +}); + +test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { + const id = crypto.randomUUID(); + + const inboundTransactionPromise = waitForTransaction('node-fastify-4', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-inbound-headers/${id}` + ); + }); + + const outboundTransactionPromise = waitForTransaction('node-fastify-4', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch/${id}` + ); + }); + + const response = await fetch(`${baseURL}/test-outgoing-fetch/${id}`); + const data = await response.json(); + + const inboundTransaction = await inboundTransactionPromise; + const outboundTransaction = await outboundTransactionPromise; + + const traceId = outboundTransaction?.contexts?.trace?.trace_id; + const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client') as SpanJSON | undefined; + + expect(outgoingHttpSpan).toBeDefined(); + + const outgoingHttpSpanId = outgoingHttpSpan?.span_id; + + expect(traceId).toEqual(expect.any(String)); + + // data is passed through from the inbound request, to verify we have the correct headers set + const inboundHeaderSentryTrace = data.headers?.['sentry-trace']; + const inboundHeaderBaggage = data.headers?.['baggage']; + + expect(inboundHeaderSentryTrace).toEqual(`${traceId}-${outgoingHttpSpanId}-1`); + expect(inboundHeaderBaggage).toBeDefined(); + + const baggage = (inboundHeaderBaggage || '').split(','); + expect(baggage).toEqual( + expect.arrayContaining([ + 'sentry-environment=qa', + `sentry-trace_id=${traceId}`, + expect.stringMatching(/sentry-public_key=/), + ]), + ); + + expect(outboundTransaction.contexts?.trace).toEqual({ + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.op': 'http.server', + 'sentry.sample_rate': 1, + url: `http://localhost:3030/test-outgoing-fetch/${id}`, + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + 'http.url': `http://localhost:3030/test-outgoing-fetch/${id}`, + 'http.host': 'localhost:3030', + 'net.host.name': 'localhost', + 'http.method': 'GET', + 'http.scheme': 'http', + 'http.target': `/test-outgoing-fetch/${id}`, + 'http.user_agent': 'node', + 'http.flavor': '1.1', + 'net.transport': 'ip_tcp', + 'net.host.ip': expect.any(String), + 'net.host.port': expect.any(Number), + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'http.status_code': 200, + 'http.status_text': 'OK', + 'http.route': '/test-outgoing-fetch/:id', + }, + op: 'http.server', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + status: 'ok', + trace_id: traceId, + origin: 'auto.http.otel.http', + }); + + expect(inboundTransaction.contexts?.trace).toEqual({ + data: expect.objectContaining({ + 'sentry.source': 'route', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.op': 'http.server', + url: `http://localhost:3030/test-inbound-headers/${id}`, + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + 'http.url': `http://localhost:3030/test-inbound-headers/${id}`, + 'http.host': 'localhost:3030', + 'net.host.name': 'localhost', + 'http.method': 'GET', + 'http.scheme': 'http', + 'http.target': `/test-inbound-headers/${id}`, + 'http.flavor': '1.1', + 'net.transport': 'ip_tcp', + 'net.host.ip': expect.any(String), + 'net.host.port': expect.any(Number), + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'http.status_code': 200, + 'http.status_text': 'OK', + 'http.route': '/test-inbound-headers/:id', + }), + op: 'http.server', + parent_span_id: outgoingHttpSpanId, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + status: 'ok', + trace_id: traceId, + origin: 'auto.http.otel.http', + }); +}); + +test('Propagates trace for outgoing external http requests', async ({ baseURL }) => { + const inboundTransactionPromise = waitForTransaction('node-fastify-4', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-http-external-allowed` + ); + }); + + const response = await fetch(`${baseURL}/test-outgoing-http-external-allowed`); + const data = await response.json(); + + const inboundTransaction = await inboundTransactionPromise; + + const traceId = inboundTransaction?.contexts?.trace?.trace_id; + const spanId = inboundTransaction?.spans?.find(span => span.op === 'http.client')?.span_id; + + expect(traceId).toEqual(expect.any(String)); + expect(spanId).toEqual(expect.any(String)); + + expect(data).toEqual({ + route: '/external-allowed', + headers: expect.objectContaining({ + 'sentry-trace': `${traceId}-${spanId}-1`, + baggage: expect.any(String), + }), + }); + + const baggage = (data.headers.baggage || '').split(','); + expect(baggage).toEqual( + expect.arrayContaining([ + 'sentry-environment=qa', + `sentry-trace_id=${traceId}`, + expect.stringMatching(/sentry-public_key=/), + ]), + ); +}); + +test('Does not propagate outgoing http requests not covered by tracePropagationTargets', async ({ baseURL }) => { + const inboundTransactionPromise = waitForTransaction('node-fastify-4', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-http-external-disallowed` + ); + }); + + const response = await fetch(`${baseURL}/test-outgoing-http-external-disallowed`); + const data = await response.json(); + + const inboundTransaction = await inboundTransactionPromise; + + const traceId = inboundTransaction?.contexts?.trace?.trace_id; + const spanId = inboundTransaction?.spans?.find(span => span.op === 'http.client')?.span_id; + + expect(traceId).toEqual(expect.any(String)); + expect(spanId).toEqual(expect.any(String)); + + expect(data.route).toBe('/external-disallowed'); + expect(data.headers?.['sentry-trace']).toBeUndefined(); + expect(data.headers?.baggage).toBeUndefined(); +}); + +test('Propagates trace for outgoing external fetch requests', async ({ baseURL }) => { + const inboundTransactionPromise = waitForTransaction('node-fastify-4', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch-external-allowed` + ); + }); + + const response = await fetch(`${baseURL}/test-outgoing-fetch-external-allowed`); + const data = await response.json(); + + const inboundTransaction = await inboundTransactionPromise; + + const traceId = inboundTransaction?.contexts?.trace?.trace_id; + const spanId = inboundTransaction?.spans?.find(span => span.op === 'http.client')?.span_id; + + expect(traceId).toEqual(expect.any(String)); + expect(spanId).toEqual(expect.any(String)); + + expect(data).toEqual({ + route: '/external-allowed', + headers: expect.objectContaining({ + 'sentry-trace': `${traceId}-${spanId}-1`, + baggage: expect.any(String), + }), + }); + + const baggage = (data.headers.baggage || '').split(','); + expect(baggage).toEqual( + expect.arrayContaining([ + 'sentry-environment=qa', + `sentry-trace_id=${traceId}`, + expect.stringMatching(/sentry-public_key=/), + ]), + ); +}); + +test('Does not propagate outgoing fetch requests not covered by tracePropagationTargets', async ({ baseURL }) => { + const inboundTransactionPromise = waitForTransaction('node-fastify-4', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch-external-disallowed` + ); + }); + + const response = await fetch(`${baseURL}/test-outgoing-fetch-external-disallowed`); + const data = await response.json(); + + const inboundTransaction = await inboundTransactionPromise; + + const traceId = inboundTransaction?.contexts?.trace?.trace_id; + const spanId = inboundTransaction?.spans?.find(span => span.op === 'http.client')?.span_id; + + expect(traceId).toEqual(expect.any(String)); + expect(spanId).toEqual(expect.any(String)); + + expect(data.route).toBe('/external-disallowed'); + expect(data.headers?.['sentry-trace']).toBeUndefined(); + expect(data.headers?.baggage).toBeUndefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/transactions.test.ts new file mode 100644 index 000000000000..50753e41b3ca --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/transactions.test.ts @@ -0,0 +1,109 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends an API route transaction', async ({ baseURL }) => { + const pageloadTransactionEventPromise = waitForTransaction('node-fastify-4', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-transaction' + ); + }); + + await fetch(`${baseURL}/test-transaction`); + + const transactionEvent = await pageloadTransactionEventPromise; + + expect(transactionEvent.contexts?.trace).toEqual({ + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.op': 'http.server', + 'sentry.sample_rate': 1, + url: 'http://localhost:3030/test-transaction', + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + 'http.url': 'http://localhost:3030/test-transaction', + 'http.host': 'localhost:3030', + 'net.host.name': 'localhost', + 'http.method': 'GET', + 'http.scheme': 'http', + 'http.target': '/test-transaction', + 'http.user_agent': 'node', + 'http.flavor': '1.1', + 'net.transport': 'ip_tcp', + 'net.host.ip': expect.any(String), + 'net.host.port': expect.any(Number), + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'http.status_code': 200, + 'http.status_text': 'OK', + 'http.route': '/test-transaction', + }, + op: 'http.server', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + status: 'ok', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.http.otel.http', + }); + + expect(transactionEvent).toEqual( + expect.objectContaining({ + transaction: 'GET /test-transaction', + type: 'transaction', + transaction_info: { + source: 'route', + }, + }), + ); + + const spans = transactionEvent.spans || []; + + expect(spans).toContainEqual({ + data: { + 'fastify.type': 'hook', + 'hook.callback.name': 'anonymous', + 'hook.name': 'fastify -> @fastify/otel -> sentry-fastify-error-handler - onRequest', + 'sentry.op': 'hook.fastify', + 'sentry.origin': 'auto.http.otel.fastify', + 'service.name': 'fastify', + }, + description: 'sentry-fastify-error-handler - onRequest', + op: 'hook.fastify', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.http.otel.fastify', + }); + + + expect(spans).toContainEqual({ + data: { + 'sentry.origin': 'manual', + }, + description: 'test-span', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'manual', + }); + + expect(spans).toContainEqual({ + data: { + 'sentry.origin': 'manual', + }, + description: 'child-span', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'manual', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-fastify-4/tsconfig.json new file mode 100644 index 000000000000..6b69bfaa593b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["node"], + "esModuleInterop": true, + "lib": ["dom", "dom.iterable", "esnext"], + "strict": true, + "outDir": "dist" + }, + "include": ["./src/*.ts"] +} diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json index f9f4f726eb0e..9c802849f5a2 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json @@ -15,7 +15,7 @@ "@sentry/core": "latest || *", "@sentry/opentelemetry": "latest || *", "@types/node": "^18.19.1", - "fastify": "5.0.0", + "fastify": "5.2.1", "typescript": "5.6.3", "ts-node": "10.9.2" }, diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/transactions.test.ts index d226009dcc1f..ec0d9f6505c4 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/transactions.test.ts @@ -60,14 +60,15 @@ test('Sends an API route transaction', async ({ baseURL }) => { expect(spans).toContainEqual({ data: { - 'plugin.name': 'fastify -> sentry-fastify-error-handler', - 'fastify.type': 'middleware', - 'hook.name': 'onRequest', + 'fastify.type': 'hook', + 'hook.callback.name': 'anonymous', + 'hook.name': 'fastify -> @fastify/otel -> sentry-fastify-error-handler - onRequest', + 'sentry.op': 'hook.fastify', 'sentry.origin': 'auto.http.otel.fastify', - 'sentry.op': 'middleware.fastify', + 'service.name': 'fastify', }, - description: 'sentry-fastify-error-handler', - op: 'middleware.fastify', + description: 'sentry-fastify-error-handler - onRequest', + op: 'hook.fastify', parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), start_timestamp: expect.any(Number), @@ -77,24 +78,6 @@ test('Sends an API route transaction', async ({ baseURL }) => { origin: 'auto.http.otel.fastify', }); - expect(spans).toContainEqual({ - data: { - 'plugin.name': 'fastify -> sentry-fastify-error-handler', - 'fastify.type': 'request_handler', - 'http.route': '/test-transaction', - 'sentry.op': 'request_handler.fastify', - 'sentry.origin': 'auto.http.otel.fastify', - }, - description: 'sentry-fastify-error-handler', - op: 'request_handler.fastify', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.http.otel.fastify', - }); expect(spans).toContainEqual({ data: { diff --git a/packages/node/package.json b/packages/node/package.json index 74e887ecfc78..6650979f0f1d 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -65,6 +65,7 @@ "access": "public" }, "dependencies": { + "@fastify/otel": "0.4.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1", "@opentelemetry/core": "^1.30.1", @@ -73,7 +74,6 @@ "@opentelemetry/instrumentation-connect": "0.43.1", "@opentelemetry/instrumentation-dataloader": "0.16.1", "@opentelemetry/instrumentation-express": "0.47.1", - "@opentelemetry/instrumentation-fastify": "0.44.2", "@opentelemetry/instrumentation-fs": "0.19.1", "@opentelemetry/instrumentation-generic-pool": "0.43.1", "@opentelemetry/instrumentation-graphql": "0.47.1", diff --git a/packages/node/src/integrations/tracing/fastify-v3/constants.ts b/packages/node/src/integrations/tracing/fastify-v3/constants.ts new file mode 100644 index 000000000000..238a33435d1c --- /dev/null +++ b/packages/node/src/integrations/tracing/fastify-v3/constants.ts @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const spanRequestSymbol = Symbol('opentelemetry.instrumentation.fastify.request_active_span'); + +// The instrumentation creates a span for invocations of lifecycle hook handlers +// that take `(request, reply, ...[, done])` arguments. Currently this is all +// lifecycle hooks except `onRequestAbort`. +// https://fastify.dev/docs/latest/Reference/Hooks +export const hooksNamesToWrap = new Set([ + 'onTimeout', + 'onRequest', + 'preParsing', + 'preValidation', + 'preSerialization', + 'preHandler', + 'onSend', + 'onResponse', + 'onError', +]); diff --git a/packages/node/src/integrations/tracing/fastify-v3/enums/AttributeNames.ts b/packages/node/src/integrations/tracing/fastify-v3/enums/AttributeNames.ts new file mode 100644 index 000000000000..3626fb8fab40 --- /dev/null +++ b/packages/node/src/integrations/tracing/fastify-v3/enums/AttributeNames.ts @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export enum AttributeNames { + FASTIFY_NAME = 'fastify.name', + FASTIFY_TYPE = 'fastify.type', + HOOK_NAME = 'hook.name', + PLUGIN_NAME = 'plugin.name', +} + +export enum FastifyTypes { + MIDDLEWARE = 'middleware', + REQUEST_HANDLER = 'request_handler', +} + +export enum FastifyNames { + MIDDLEWARE = 'middleware', + REQUEST_HANDLER = 'request handler', +} diff --git a/packages/node/src/integrations/tracing/fastify-v3/instrumentation.ts b/packages/node/src/integrations/tracing/fastify-v3/instrumentation.ts new file mode 100644 index 000000000000..1235a989f3c4 --- /dev/null +++ b/packages/node/src/integrations/tracing/fastify-v3/instrumentation.ts @@ -0,0 +1,285 @@ +/* eslint-disable @typescript-eslint/no-this-alias */ +/* eslint-disable jsdoc/require-jsdoc */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { type Attributes, SpanStatusCode, context, trace } from '@opentelemetry/api'; +import { RPCType, getRPCMetadata } from '@opentelemetry/core'; +import { + InstrumentationBase, + InstrumentationNodeModuleDefinition, + safeExecuteInTheMiddle, +} from '@opentelemetry/instrumentation'; +import { SEMATTRS_HTTP_ROUTE } from '@opentelemetry/semantic-conventions'; + +import { AttributeNames, FastifyNames, FastifyTypes } from './enums/AttributeNames'; + +import type { + FastifyErrorCodes, + FastifyInstance, + FastifyReply, + FastifyRequest, + HandlerOriginal, + HookHandlerDoneFunction, + PluginFastifyReply, +} from './internal-types'; + +import type { FastifyInstrumentationConfig } from './types'; +import { endSpan, safeExecuteInTheMiddleMaybePromise, startSpan } from './utils'; +/** @knipignore */ + +const PACKAGE_VERSION = '0.1.0'; + +const PACKAGE_NAME = '@sentry/instrumentation-fastify-v3'; +const ANONYMOUS_NAME = 'anonymous'; + +// The instrumentation creates a span for invocations of lifecycle hook handlers +// that take `(request, reply, ...[, done])` arguments. Currently this is all +// lifecycle hooks except `onRequestAbort`. +// https://fastify.dev/docs/latest/Reference/Hooks +const hooksNamesToWrap = new Set([ + 'onTimeout', + 'onRequest', + 'preParsing', + 'preValidation', + 'preSerialization', + 'preHandler', + 'onSend', + 'onResponse', + 'onError', +]); + +/** + * Fastify instrumentation for OpenTelemetry + */ +export class FastifyInstrumentationV3 extends InstrumentationBase { + public constructor(config: FastifyInstrumentationConfig = {}) { + super(PACKAGE_NAME, PACKAGE_VERSION, config); + } + + public init(): InstrumentationNodeModuleDefinition[] { + return [ + new InstrumentationNodeModuleDefinition('fastify', ['>=3.0.0 <4'], moduleExports => { + return this._patchConstructor(moduleExports); + }), + ]; + } + + private _hookOnRequest() { + const instrumentation = this; + + return function onRequest(request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) { + if (!instrumentation.isEnabled()) { + return done(); + } + instrumentation._wrap(reply, 'send', instrumentation._patchSend()); + + const anyRequest = request as any; + + const rpcMetadata = getRPCMetadata(context.active()); + const routeName = anyRequest.routeOptions + ? anyRequest.routeOptions.url // since fastify@4.10.0 + : request.routerPath; + if (routeName && rpcMetadata?.type === RPCType.HTTP) { + rpcMetadata.route = routeName; + } + done(); + }; + } + + private _wrapHandler( + pluginName: string, + hookName: string, + original: HandlerOriginal, + syncFunctionWithDone: boolean, + ): () => Promise { + const instrumentation = this; + this._diag.debug('Patching fastify route.handler function'); + + return function (this: any, ...args: unknown[]): Promise { + if (!instrumentation.isEnabled()) { + return original.apply(this, args); + } + + const name = original.name || pluginName || ANONYMOUS_NAME; + const spanName = `${FastifyNames.MIDDLEWARE} - ${name}`; + + const reply = args[1] as PluginFastifyReply; + + const span = startSpan(reply, instrumentation.tracer, spanName, { + [AttributeNames.FASTIFY_TYPE]: FastifyTypes.MIDDLEWARE, + [AttributeNames.PLUGIN_NAME]: pluginName, + [AttributeNames.HOOK_NAME]: hookName, + }); + + const origDone = syncFunctionWithDone && (args[args.length - 1] as HookHandlerDoneFunction); + if (origDone) { + args[args.length - 1] = function (...doneArgs: Parameters) { + endSpan(reply); + origDone.apply(this, doneArgs); + }; + } + + return context.with(trace.setSpan(context.active(), span), () => { + return safeExecuteInTheMiddleMaybePromise( + () => { + return original.apply(this, args); + }, + err => { + if (err instanceof Error) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: err.message, + }); + span.recordException(err); + } + // async hooks should end the span as soon as the promise is resolved + if (!syncFunctionWithDone) { + endSpan(reply); + } + }, + ); + }); + }; + } + + private _wrapAddHook(): (original: FastifyInstance['addHook']) => () => FastifyInstance { + const instrumentation = this; + this._diag.debug('Patching fastify server.addHook function'); + + // biome-ignore lint/complexity/useArrowFunction: + return function (original: FastifyInstance['addHook']): () => FastifyInstance { + return function wrappedAddHook(this: any, ...args: any) { + const name = args[0] as string; + const handler = args[1] as HandlerOriginal; + const pluginName = this.pluginName; + if (!hooksNamesToWrap.has(name)) { + return original.apply(this, args); + } + + const syncFunctionWithDone = + typeof args[args.length - 1] === 'function' && handler.constructor.name !== 'AsyncFunction'; + + return original.apply(this, [ + name, + instrumentation._wrapHandler(pluginName, name, handler, syncFunctionWithDone), + ] as never); + }; + }; + } + + private _patchConstructor(moduleExports: { + fastify: () => FastifyInstance; + errorCodes: FastifyErrorCodes | undefined; + }): () => FastifyInstance { + const instrumentation = this; + + function fastify(this: FastifyInstance, ...args: any) { + const app: FastifyInstance = moduleExports.fastify.apply(this, args); + app.addHook('onRequest', instrumentation._hookOnRequest()); + app.addHook('preHandler', instrumentation._hookPreHandler()); + + instrumentation._wrap(app, 'addHook', instrumentation._wrapAddHook()); + + return app; + } + + if (moduleExports.errorCodes !== undefined) { + fastify.errorCodes = moduleExports.errorCodes; + } + fastify.fastify = fastify; + fastify.default = fastify; + return fastify; + } + + private _patchSend() { + const instrumentation = this; + this._diag.debug('Patching fastify reply.send function'); + + return function patchSend(original: () => FastifyReply): () => FastifyReply { + return function send(this: FastifyReply, ...args: any) { + const maybeError: any = args[0]; + + if (!instrumentation.isEnabled()) { + return original.apply(this, args); + } + + return safeExecuteInTheMiddle( + () => { + return original.apply(this, args); + }, + err => { + if (!err && maybeError instanceof Error) { + // eslint-disable-next-line no-param-reassign + err = maybeError; + } + endSpan(this, err); + }, + ); + }; + }; + } + + private _hookPreHandler() { + const instrumentation = this; + this._diag.debug('Patching fastify preHandler function'); + + return function preHandler(this: any, request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) { + if (!instrumentation.isEnabled()) { + return done(); + } + const anyRequest = request as any; + + const handler = anyRequest.routeOptions?.handler || anyRequest.context?.handler; + + const handlerName = handler?.name.startsWith('bound ') ? handler.name.substring(6) : handler?.name; + const spanName = `${FastifyNames.REQUEST_HANDLER} - ${handlerName || this.pluginName || ANONYMOUS_NAME}`; + + const spanAttributes: Attributes = { + [AttributeNames.PLUGIN_NAME]: this.pluginName, + [AttributeNames.FASTIFY_TYPE]: FastifyTypes.REQUEST_HANDLER, + // eslint-disable-next-line deprecation/deprecation + [SEMATTRS_HTTP_ROUTE]: anyRequest.routeOptions + ? anyRequest.routeOptions.url // since fastify@4.10.0 + : request.routerPath, + }; + if (handlerName) { + spanAttributes[AttributeNames.FASTIFY_NAME] = handlerName; + } + const span = startSpan(reply, instrumentation.tracer, spanName, spanAttributes); + + const { requestHook } = instrumentation.getConfig(); + if (requestHook) { + safeExecuteInTheMiddle( + () => requestHook(span, { request }), + e => { + if (e) { + instrumentation._diag.error('request hook failed', e); + } + }, + true, + ); + } + + return context.with(trace.setSpan(context.active(), span), () => { + done(); + }); + }; + } +} diff --git a/packages/node/src/integrations/tracing/fastify-v3/internal-types.ts b/packages/node/src/integrations/tracing/fastify-v3/internal-types.ts new file mode 100644 index 000000000000..14b8fa652390 --- /dev/null +++ b/packages/node/src/integrations/tracing/fastify-v3/internal-types.ts @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { Span } from '@opentelemetry/api'; + +export type FastifyError = any; + +export type HookHandlerDoneFunction = (err?: TError) => void; + +export type FastifyErrorCodes = any; + +export type FastifyPlugin = ( + instance: FastifyInstance, + opts: any, + done: HookHandlerDoneFunction, +) => unknown | Promise; + +export interface FastifyInstance { + version: string; + register: ( + plugin: FastifyPlugin, + ) => FastifyInstance; + after: (listener?: (err: Error) => void) => FastifyInstance; + addHook( + name: + | 'onRequest' + | 'preHandler' + | 'preParsing' + | 'preValidation' + | 'preSerialization' + | 'preHandler' + | 'onSend' + | 'onResponse' + | 'onError' + | 'onTimeout', + handler: HandlerOriginal, + ): FastifyInstance; +} + +export interface FastifyReply { + send: () => FastifyReply; +} +export interface FastifyRequest { + routeOptions?: { + url?: string; + }; + routerPath?: string; +} + +import type { spanRequestSymbol } from './constants'; + +export type HandlerOriginal = + | ((request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => Promise) + | ((request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => void); + +export type PluginFastifyReply = FastifyReply & { + [spanRequestSymbol]?: Span[]; +}; diff --git a/packages/node/src/integrations/tracing/fastify-v3/types.ts b/packages/node/src/integrations/tracing/fastify-v3/types.ts new file mode 100644 index 000000000000..5da1e521ab7a --- /dev/null +++ b/packages/node/src/integrations/tracing/fastify-v3/types.ts @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Span } from '@opentelemetry/api'; +import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; + +export interface FastifyRequestInfo { + request: any; // FastifyRequest object from fastify package +} + +/** + * Function that can be used to add custom attributes to the current span + * @param span - The Fastify handler span. + * @param info - The Fastify request info object. + */ +export interface FastifyCustomAttributeFunction { + (span: Span, info: FastifyRequestInfo): void; +} + +/** + * Options available for the Fastify Instrumentation + */ +export interface FastifyInstrumentationConfig extends InstrumentationConfig { + /** Function for adding custom attributes to each handler span */ + requestHook?: FastifyCustomAttributeFunction; +} diff --git a/packages/node/src/integrations/tracing/fastify-v3/utils.ts b/packages/node/src/integrations/tracing/fastify-v3/utils.ts new file mode 100644 index 000000000000..668da85669e7 --- /dev/null +++ b/packages/node/src/integrations/tracing/fastify-v3/utils.ts @@ -0,0 +1,137 @@ +/* eslint-disable jsdoc/require-jsdoc */ +/* eslint-disable @typescript-eslint/no-dynamic-delete */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { type Attributes, type Span, SpanStatusCode, type Tracer } from '@opentelemetry/api'; +import { spanRequestSymbol } from './constants'; + +import type { PluginFastifyReply } from './internal-types'; + +/** + * Starts Span + * @param reply - reply function + * @param tracer - tracer + * @param spanName - span name + * @param spanAttributes - span attributes + */ +export function startSpan( + reply: PluginFastifyReply, + tracer: Tracer, + spanName: string, + spanAttributes: Attributes = {}, +) { + const span = tracer.startSpan(spanName, { attributes: spanAttributes }); + + const spans: Span[] = reply[spanRequestSymbol] || []; + spans.push(span); + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + Object.defineProperty(reply, spanRequestSymbol, { + enumerable: false, + configurable: true, + value: spans, + }); + + return span; +} + +/** + * Ends span + * @param reply - reply function + * @param err - error + */ +export function endSpan(reply: PluginFastifyReply, err?: any) { + const spans = reply[spanRequestSymbol] || []; + // there is no active span, or it has already ended + if (!spans.length) { + return; + } + // biome-ignore lint/complexity/noForEach: + spans.forEach((span: Span) => { + if (err) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: err.message, + }); + span.recordException(err); + } + span.end(); + }); + delete reply[spanRequestSymbol]; +} + +// @TODO after approve add this to instrumentation package and replace usage +// when it will be released + +/** + * This function handles the missing case from instrumentation package when + * execute can either return a promise or void. And using async is not an + * option as it is producing unwanted side effects. + * @param execute - function to be executed + * @param onFinish - function called when function executed + * @param preventThrowingError - prevent to throw error when execute + * function fails + */ +export function safeExecuteInTheMiddleMaybePromise( + execute: () => Promise, + onFinish: (e: unknown, result?: T) => void, + preventThrowingError?: boolean, +): Promise; +export function safeExecuteInTheMiddleMaybePromise( + execute: () => T, + onFinish: (e: unknown, result?: T) => void, + preventThrowingError?: boolean, +): T; +export function safeExecuteInTheMiddleMaybePromise( + execute: () => T | Promise, + onFinish: (e: unknown, result?: T) => void, + preventThrowingError?: boolean, +): T | Promise | undefined { + let error: unknown; + let result: T | Promise | undefined = undefined; + try { + result = execute(); + + if (isPromise(result)) { + result.then( + res => onFinish(undefined, res), + err => onFinish(err), + ); + } + } catch (e) { + error = e; + } finally { + if (!isPromise(result)) { + onFinish(error, result); + if (error && !preventThrowingError) { + // eslint-disable-next-line no-unsafe-finally + throw error; + } + } + // eslint-disable-next-line no-unsafe-finally + return result; + } +} + +function isPromise(val: T | Promise): val is Promise { + return ( + (typeof val === 'object' && val && typeof Object.getOwnPropertyDescriptor(val, 'then')?.value === 'function') || + false + ); +} diff --git a/packages/node/src/integrations/tracing/fastify.ts b/packages/node/src/integrations/tracing/fastify.ts index 2920b134d82d..445e5225b4d5 100644 --- a/packages/node/src/integrations/tracing/fastify.ts +++ b/packages/node/src/integrations/tracing/fastify.ts @@ -1,4 +1,4 @@ -import { FastifyInstrumentation } from '@opentelemetry/instrumentation-fastify'; +import FastifyOtelInstrumentation from '@fastify/otel'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, @@ -6,23 +6,19 @@ import { defineIntegration, getClient, getIsolationScope, + logger, spanToJSON, } from '@sentry/core'; import type { IntegrationFn, Span } from '@sentry/core'; import { generateInstrumentOnce } from '../../otel/instrument'; -import { ensureIsWrapped } from '../../utils/ensureIsWrapped'; - -// We inline the types we care about here -interface Fastify { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - register: (plugin: any) => void; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - addHook: (hook: string, handler: (request: any, reply: any, error: Error) => void) => void; -} +import { FastifyInstrumentationV3 } from './fastify-v3/instrumentation'; +import * as diagnosticsChannel from 'node:diagnostics_channel'; +import type { FastifyInstance } from './fastify-v3/internal-types'; +import { DEBUG_BUILD } from '../../debug-build'; /** * Minimal request type containing properties around route information. - * Works for Fastify 3, 4 and presumably 5. + * Works for Fastify 3, 4 and 5. */ interface FastifyRequestRouteInfo { method?: string; @@ -34,22 +30,42 @@ interface FastifyRequestRouteInfo { } const INTEGRATION_NAME = 'Fastify'; +const INTEGRATION_NAME_V3 = 'Fastify-V3'; -export const instrumentFastify = generateInstrumentOnce( - INTEGRATION_NAME, +export const instrumentFastifyV3 = generateInstrumentOnce( + INTEGRATION_NAME_V3, () => - // eslint-disable-next-line deprecation/deprecation - new FastifyInstrumentation({ + new FastifyInstrumentationV3({ requestHook(span) { addFastifySpanAttributes(span); }, }), ); +export const instrumentFastify = generateInstrumentOnce(INTEGRATION_NAME, () => { + // FastifyOtelInstrumentation does not have a `requestHook` + // so we can't use `addFastifySpanAttributes` here for now + const fastifyOtelInstrumentationInstance = new FastifyOtelInstrumentation(); + const plugin = fastifyOtelInstrumentationInstance.plugin(); + + diagnosticsChannel.subscribe('fastify.initialization', message => { + const fastifyInstance = (message as { fastify?: FastifyInstance }).fastify; + + fastifyInstance?.register(plugin).after(err => { + if (err) { + DEBUG_BUILD && logger.error('Failed to setup Fastify instrumentation', err); + } + }); + }); + + return fastifyOtelInstrumentationInstance; +}); + const _fastifyIntegration = (() => { return { name: INTEGRATION_NAME, setupOnce() { + instrumentFastifyV3(); instrumentFastify(); }, }; @@ -92,9 +108,9 @@ export const fastifyIntegration = defineIntegration(_fastifyIntegration); * app.listen({ port: 3000 }); * ``` */ -export function setupFastifyErrorHandler(fastify: Fastify): void { +export function setupFastifyErrorHandler(fastify: FastifyInstance): void { const plugin = Object.assign( - function (fastify: Fastify, _options: unknown, done: () => void): void { + function (fastify: FastifyInstance, _options: unknown, done: () => void): void { fastify.addHook('onError', async (_request, _reply, error) => { captureException(error); }); @@ -121,8 +137,6 @@ export function setupFastifyErrorHandler(fastify: Fastify): void { }, ); - fastify.register(plugin); - // Sadly, middleware spans do not go through `requestHook`, so we handle those 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 fastify @@ -133,7 +147,7 @@ export function setupFastifyErrorHandler(fastify: Fastify): void { }); } - ensureIsWrapped(fastify.addHook, 'fastify'); + fastify.register(plugin); } function addFastifySpanAttributes(span: Span): void { @@ -155,7 +169,11 @@ function addFastifySpanAttributes(span: Span): void { // Also update the name, we don't need to "middleware - " prefix const name = attributes['fastify.name'] || attributes['plugin.name'] || attributes['hook.name']; if (typeof name === 'string') { - // Also remove `fastify -> ` prefix - span.updateName(name.replace(/^fastify -> /, '')); + // Try removing `fastify -> ` and `@fastify/otel -> ` prefixes + // This is a bit of a hack, and not always working for all spans + // But it's the best we can do without a proper API + const updatedName = name.replace(/^fastify -> /, '').replace(/^@fastify\/otel -> /, ''); + + span.updateName(updatedName); } } diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index 2873a2643617..d1f0f0f7153c 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -4,7 +4,7 @@ import { instrumentOtelHttp } from '../http'; import { amqplibIntegration, instrumentAmqplib } from './amqplib'; import { connectIntegration, instrumentConnect } from './connect'; import { expressIntegration, instrumentExpress, instrumentExpressV5 } from './express'; -import { fastifyIntegration, instrumentFastify } from './fastify'; +import { fastifyIntegration, instrumentFastify, instrumentFastifyV3 } from './fastify'; import { genericPoolIntegration, instrumentGenericPool } from './genericPool'; import { graphqlIntegration, instrumentGraphql } from './graphql'; import { hapiIntegration, instrumentHapi } from './hapi'; @@ -61,6 +61,7 @@ export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) => instrumentExpressV5, instrumentConnect, instrumentFastify, + instrumentFastifyV3, instrumentHapi, instrumentKafka, instrumentKoa, diff --git a/yarn.lock b/yarn.lock index d58829b21c3a..8c9e527706d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4122,6 +4122,15 @@ resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.0.0.tgz#f22824caff3ae506b18207bad4126dbc6ccdb6b8" integrity sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ== +"@fastify/otel@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@fastify/otel/-/otel-0.4.0.tgz#ba2488b05f77eb11c9fcb4bfaf64f0b845b56a17" + integrity sha512-BPBb4zxH4Sw025sUwapjkkD5xskv1vRl2IHFgiDpD2zIw8wKSWPSQvwfy/yYweFJ2jqc/g9yvL03rwlObLLHxA== + dependencies: + "@opentelemetry/core" "^1.29.0" + "@opentelemetry/instrumentation" "^0.57.0" + "@opentelemetry/semantic-conventions" "^1.28.0" + "@gar/promisify@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" @@ -5872,7 +5881,7 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/context-base/-/context-base-0.12.0.tgz#4906ae27359d3311e3dea1b63770a16f60848550" integrity sha512-UXwSsXo3F3yZ1dIBOG9ID8v2r9e+bqLWoizCtTb8rXtwF+N5TM7hzzvQz72o3nBU+zrI/D5e+OqAYK8ZgDd3DA== -"@opentelemetry/core@1.30.1", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.26.0", "@opentelemetry/core@^1.30.1", "@opentelemetry/core@^1.8.0": +"@opentelemetry/core@1.30.1", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.26.0", "@opentelemetry/core@^1.29.0", "@opentelemetry/core@^1.30.1", "@opentelemetry/core@^1.8.0": version "1.30.1" resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.30.1.tgz#a0b468bb396358df801881709ea38299fc30ab27" integrity sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ== @@ -5942,15 +5951,6 @@ "@opentelemetry/instrumentation" "^0.57.1" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-fastify@0.44.2": - version "0.44.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.44.2.tgz#80bb33fa266560b0a7474f7bebcdb77eb49fc1c3" - integrity sha512-arSp97Y4D2NWogoXRb8CzFK3W2ooVdvqRRtQDljFt9uC3zI6OuShgey6CVFC0JxT1iGjkAr1r4PDz23mWrFULQ== - dependencies: - "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.57.1" - "@opentelemetry/semantic-conventions" "^1.27.0" - "@opentelemetry/instrumentation-fs@0.19.1": version "0.19.1" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.19.1.tgz#ebfe40781949574a66a82b8511d9bcd414dbfe98" @@ -6115,7 +6115,7 @@ "@opentelemetry/core" "^1.8.0" "@opentelemetry/instrumentation" "^0.57.1" -"@opentelemetry/instrumentation@0.57.2", "@opentelemetry/instrumentation@^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0", "@opentelemetry/instrumentation@^0.57.1", "@opentelemetry/instrumentation@^0.57.2": +"@opentelemetry/instrumentation@0.57.2", "@opentelemetry/instrumentation@^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0", "@opentelemetry/instrumentation@^0.57.0", "@opentelemetry/instrumentation@^0.57.1", "@opentelemetry/instrumentation@^0.57.2": version "0.57.2" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz#8924549d7941ba1b5c6f04d5529cf48330456d1d" integrity sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==