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..1ac095f0d137 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 @@ -53,34 +53,39 @@ test('Sends an API route transaction', async ({ baseURL }) => { span_id: expect.stringMatching(/[a-f0-9]{16}/), trace_id: expect.stringMatching(/[a-f0-9]{32}/), data: { - 'sentry.origin': 'manual', - 'fastify.type': 'middleware', - 'plugin.name': 'fastify -> @fastify/middie', - 'hook.name': 'onRequest', + 'sentry.origin': 'auto.http.otel.fastify', + 'sentry.op': 'hook.fastify', + 'service.name': 'fastify', + 'hook.name': 'fastify -> @fastify/otel -> @fastify/middie - onRequest', + 'fastify.type': 'hook', + 'hook.callback.name': 'runMiddie', }, - description: 'middleware - runMiddie', + description: '@fastify/middie - onRequest', + op: 'hook.fastify', parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), start_timestamp: expect.any(Number), timestamp: expect.any(Number), status: 'ok', - origin: 'manual', + origin: 'auto.http.otel.fastify', }, { span_id: expect.stringMatching(/[a-f0-9]{16}/), trace_id: expect.stringMatching(/[a-f0-9]{32}/), data: { 'sentry.origin': 'auto.http.otel.fastify', - 'sentry.op': 'request_handler.fastify', - 'plugin.name': 'fastify -> @fastify/middie', - 'fastify.type': 'request_handler', + 'sentry.op': 'request-handler.fastify', + 'service.name': 'fastify', + 'hook.name': 'fastify -> @fastify/otel -> @fastify/middie - route-handler', + 'fastify.type': 'request-handler', 'http.route': '/test-transaction', + 'hook.callback.name': 'anonymous', }, - description: '@fastify/middie', + description: '@fastify/middie - route-handler', + op: 'request-handler.fastify', parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), start_timestamp: expect.any(Number), timestamp: expect.any(Number), status: 'ok', - op: 'request_handler.fastify', origin: 'auto.http.otel.fastify', }, { diff --git a/dev-packages/e2e-tests/test-applications/node-connect/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-connect/tsconfig.json index d1f546d06cd1..b7391228c421 100644 --- a/dev-packages/e2e-tests/test-applications/node-connect/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/node-connect/tsconfig.json @@ -4,7 +4,8 @@ "esModuleInterop": true, "lib": ["dom", "dom.iterable", "esnext"], "strict": true, - "noEmit": true + "noEmit": true, + "skipLibCheck": true }, "include": ["src/*.ts"] } diff --git a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/tsconfig.json index 8cb64e989ed9..2887ec11a81d 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/tsconfig.json @@ -4,7 +4,8 @@ "esModuleInterop": true, "lib": ["es2018"], "strict": true, - "outDir": "dist" + "outDir": "dist", + "skipLibCheck": true }, "include": ["src/**/*.ts"] } diff --git a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/tsconfig.json index 8cb64e989ed9..2887ec11a81d 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/tsconfig.json @@ -4,7 +4,8 @@ "esModuleInterop": true, "lib": ["es2018"], "strict": true, - "outDir": "dist" + "outDir": "dist", + "skipLibCheck": true }, "include": ["src/**/*.ts"] } 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-3/src/app.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app.ts new file mode 100644 index 000000000000..73ffafcfd04d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app.ts @@ -0,0 +1,157 @@ +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.post('/test-post', function (req, res) { + res.send({ status: 'ok', body: req.body }); +}); + +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/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-3/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/errors.test.ts new file mode 100644 index 000000000000..1a37fc244413 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/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-3', 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/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 93% 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 f7c0aa7f5b0e..d4c10751f4a5 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,14 +60,14 @@ 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', + 'plugin.name': 'sentry-fastify-error-handler', + 'fastify.type': 'request_handler', + 'http.route': '/test-transaction', 'sentry.origin': 'auto.http.otel.fastify', - 'sentry.op': 'middleware.fastify', + 'sentry.op': 'request_handler.fastify', }, description: 'sentry-fastify-error-handler', - op: 'middleware.fastify', + 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), @@ -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', @@ -126,7 +126,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { }); test('Captures request metadata', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('node-fastify', transactionEvent => { + const transactionEventPromise = waitForTransaction('node-fastify-3', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'POST /test-post' ); 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/src/app.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/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-4/src/app.ts 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/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/errors.test.ts similarity index 92% rename from dev-packages/e2e-tests/test-applications/node-fastify/tests/errors.test.ts rename to dev-packages/e2e-tests/test-applications/node-fastify-4/tests/errors.test.ts index 1ac1d3d88234..8ecdc8975778 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/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-4', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); @@ -34,7 +34,7 @@ test('Does not send 4xx errors by default', async ({ baseURL }) => { // We should only see the 5xx error captured due to shouldHandleError's default behavior // Create a promise to wait for the 500 error - const serverErrorPromise = waitForError('node-fastify', event => { + const serverErrorPromise = waitForError('node-fastify-4', event => { // Looking for a 500 error that should be captured return !!event.exception?.values?.[0]?.value?.includes('This is a 5xx error'); }); 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..1f049b802bca --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/transactions.test.ts @@ -0,0 +1,145 @@ +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 - onRequest', + 'sentry.op': 'hook.fastify', + 'sentry.origin': 'auto.http.otel.fastify', + 'service.name': 'fastify', + }, + description: '@fastify/otel - 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', + }); +}); + +test('Captures request metadata', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('node-fastify-4', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'POST /test-post' + ); + }); + + const res = await fetch(`${baseURL}/test-post`, { + method: 'POST', + body: JSON.stringify({ foo: 'bar', other: 1 }), + headers: { + 'Content-Type': 'application/json', + }, + }); + const resBody = await res.json(); + + expect(resBody).toEqual({ status: 'ok', body: { foo: 'bar', other: 1 } }); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.request).toEqual({ + cookies: {}, + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: expect.objectContaining({ + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/json', + }), + data: JSON.stringify({ + foo: 'bar', + other: 1, + }), + }); + + expect(transactionEvent.user).toEqual(undefined); +}); 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/src/app.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts index 275dfa786ca3..73ffafcfd04d 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts @@ -109,6 +109,10 @@ app.get('/test-outgoing-http-external-disallowed', async function (req, res) { res.send(data); }); +app.post('/test-post', function (req, res) { + res.send({ status: 'ok', body: req.body }); +}); + app.listen({ port: port }); // A second app so we can test header propagation between external URLs 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..e148c8158cd8 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,33 +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 - 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', - 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: { - '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', + description: '@fastify/otel - 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), @@ -124,3 +106,40 @@ test('Sends an API route transaction', async ({ baseURL }) => { origin: 'manual', }); }); + +test('Captures request metadata', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('node-fastify-5', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'POST /test-post' + ); + }); + + const res = await fetch(`${baseURL}/test-post`, { + method: 'POST', + body: JSON.stringify({ foo: 'bar', other: 1 }), + headers: { + 'Content-Type': 'application/json', + }, + }); + const resBody = await res.json(); + + expect(resBody).toEqual({ status: 'ok', body: { foo: 'bar', other: 1 } }); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.request).toEqual({ + cookies: {}, + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: expect.objectContaining({ + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/json', + }), + data: JSON.stringify({ + foo: 'bar', + other: 1, + }), + }); + + expect(transactionEvent.user).toEqual(undefined); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/tsconfig.json index 8cb64e989ed9..2887ec11a81d 100644 --- a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/tsconfig.json @@ -4,7 +4,8 @@ "esModuleInterop": true, "lib": ["es2018"], "strict": true, - "outDir": "dist" + "outDir": "dist", + "skipLibCheck": true }, "include": ["src/**/*.ts"] } diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tsconfig.json index 8cb64e989ed9..2887ec11a81d 100644 --- a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tsconfig.json @@ -4,7 +4,8 @@ "esModuleInterop": true, "lib": ["es2018"], "strict": true, - "outDir": "dist" + "outDir": "dist", + "skipLibCheck": true }, "include": ["src/**/*.ts"] } diff --git a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tsconfig.json index d14f5822baf2..b21139d5e56a 100644 --- a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tsconfig.json @@ -4,7 +4,8 @@ "esModuleInterop": true, "lib": ["es2018", "dom"], "strict": true, - "outDir": "dist" + "outDir": "dist", + "skipLibCheck": true }, "include": ["src/**/*.ts"] } diff --git a/dev-packages/e2e-tests/test-applications/node-otel/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-otel/tsconfig.json index 8cb64e989ed9..2887ec11a81d 100644 --- a/dev-packages/e2e-tests/test-applications/node-otel/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/node-otel/tsconfig.json @@ -4,7 +4,8 @@ "esModuleInterop": true, "lib": ["es2018"], "strict": true, - "outDir": "dist" + "outDir": "dist", + "skipLibCheck": true }, "include": ["src/**/*.ts"] } diff --git a/packages/node/package.json b/packages/node/package.json index bfe1acb03a48..2c918c79ade5 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -65,6 +65,7 @@ "access": "public" }, "dependencies": { + "@fastify/otel": "0.6.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.ts b/packages/node/src/integrations/tracing/fastify.ts deleted file mode 100644 index b74edeb8bcac..000000000000 --- a/packages/node/src/integrations/tracing/fastify.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { FastifyInstrumentation } from '@opentelemetry/instrumentation-fastify'; -import type { IntegrationFn, Span } from '@sentry/core'; -import { - captureException, - defineIntegration, - getClient, - getIsolationScope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - spanToJSON, -} from '@sentry/core'; -import { generateInstrumentOnce } from '../../otel/instrument'; -import { ensureIsWrapped } from '../../utils/ensureIsWrapped'; - -/** - * Minimal request type containing properties around route information. - * Works for Fastify 3, 4 and presumably 5. - * - * Based on https://github.com/fastify/fastify/blob/ce3811f5f718be278bbcd4392c615d64230065a6/types/request.d.ts - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -interface MinimalFastifyRequest extends Record { - method?: string; - // since fastify@4.10.0 - routeOptions?: { - url?: string; - }; - routerPath?: string; -} - -/** - * Minimal reply type containing properties needed for error handling. - * - * Based on https://github.com/fastify/fastify/blob/ce3811f5f718be278bbcd4392c615d64230065a6/types/reply.d.ts - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -interface MinimalFastifyReply extends Record { - statusCode: number; -} - -// We inline the types we care about here -interface Fastify { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - register: (plugin: any) => void; - addHook: (hook: string, handler: (...params: unknown[]) => void) => void; -} - -interface FastifyWithHooks extends Omit { - addHook( - hook: 'onError', - handler: (request: MinimalFastifyRequest, reply: MinimalFastifyReply, error: Error) => void, - ): void; - addHook(hook: 'onRequest', handler: (request: MinimalFastifyRequest, reply: MinimalFastifyReply) => void): void; -} - -interface FastifyHandlerOptions { - /** - * Callback method deciding whether error should be captured and sent to Sentry - * - * @param error Captured Fastify error - * @param request Fastify request (or any object containing at least method, routeOptions.url, and routerPath) - * @param reply Fastify reply (or any object containing at least statusCode) - * - * @example - * - * ```javascript - * setupFastifyErrorHandler(app, { - * shouldHandleError(_error, _request, reply) { - * return reply.statusCode >= 400; - * }, - * }); - * ``` - * - * If using TypeScript, you can cast the request and reply to get full type safety. - * - * ```typescript - * import type { FastifyRequest, FastifyReply } from 'fastify'; - * - * setupFastifyErrorHandler(app, { - * shouldHandleError(error, minimalRequest, minimalReply) { - * const request = minimalRequest as FastifyRequest; - * const reply = minimalReply as FastifyReply; - * return reply.statusCode >= 500; - * }, - * }); - * ``` - */ - shouldHandleError: (error: Error, request: MinimalFastifyRequest, reply: MinimalFastifyReply) => boolean; -} - -const INTEGRATION_NAME = 'Fastify'; - -export const instrumentFastify = generateInstrumentOnce( - INTEGRATION_NAME, - () => - // eslint-disable-next-line deprecation/deprecation - new FastifyInstrumentation({ - requestHook(span) { - addFastifySpanAttributes(span); - }, - }), -); - -const _fastifyIntegration = (() => { - return { - name: INTEGRATION_NAME, - setupOnce() { - instrumentFastify(); - }, - }; -}) satisfies IntegrationFn; - -/** - * Adds Sentry tracing instrumentation for [Fastify](https://fastify.dev/). - * - * If you also want to capture errors, you need to call `setupFastifyErrorHandler(app)` after you set up your Fastify server. - * - * For more information, see the [fastify documentation](https://docs.sentry.io/platforms/javascript/guides/fastify/). - * - * @example - * ```javascript - * const Sentry = require('@sentry/node'); - * - * Sentry.init({ - * integrations: [Sentry.fastifyIntegration()], - * }) - * ``` - */ -export const fastifyIntegration = defineIntegration(_fastifyIntegration); - -/** - * Default function to determine if an error should be sent to Sentry - * - * 3xx and 4xx errors are not sent by default. - */ -function defaultShouldHandleError(_error: Error, _request: MinimalFastifyRequest, reply: MinimalFastifyReply): boolean { - const statusCode = reply.statusCode; - // 3xx and 4xx errors are not sent by default. - return statusCode >= 500 || statusCode <= 299; -} - -/** - * Add an Fastify error handler to capture errors to Sentry. - * - * @param fastify The Fastify instance to which to add the error handler - * @param options Configuration options for the handler - * - * @example - * ```javascript - * const Sentry = require('@sentry/node'); - * const Fastify = require("fastify"); - * - * const app = Fastify(); - * - * Sentry.setupFastifyErrorHandler(app); - * - * // Add your routes, etc. - * - * app.listen({ port: 3000 }); - * ``` - */ -export function setupFastifyErrorHandler(fastify: Fastify, options?: Partial): void { - const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError; - - const plugin = Object.assign( - function (fastify: FastifyWithHooks, _options: unknown, done: () => void): void { - fastify.addHook('onError', async (request, reply, error) => { - if (shouldHandleError(error, request, reply)) { - captureException(error); - } - }); - - // registering `onRequest` hook here instead of using Otel `onRequest` callback b/c `onRequest` hook - // is ironically called in the fastify `preHandler` hook which is called later in the lifecycle: - // https://fastify.dev/docs/latest/Reference/Lifecycle/ - fastify.addHook('onRequest', async (request, _reply) => { - // Taken from Otel Fastify instrumentation: - // https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts#L94-L96 - const routeName = request.routeOptions?.url || request.routerPath; - const method = request.method || 'GET'; - - getIsolationScope().setTransactionName(`${method} ${routeName}`); - }); - - done(); - }, - { - [Symbol.for('skip-override')]: true, - [Symbol.for('fastify.display-name')]: 'sentry-fastify-error-handler', - }, - ); - - 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 - const client = getClient(); - if (client) { - client.on('spanStart', span => { - addFastifySpanAttributes(span); - }); - } - - ensureIsWrapped(fastify.addHook, 'fastify'); -} - -function addFastifySpanAttributes(span: Span): void { - const attributes = spanToJSON(span).data; - - // this is one of: middleware, request_handler - const type = attributes['fastify.type']; - - // If this is already set, or we have no fastify span, no need to process again... - if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || !type) { - return; - } - - span.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.fastify', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.fastify`, - }); - - // 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 -> /, '')); - } -} diff --git a/packages/node/src/integrations/tracing/fastify/index.ts b/packages/node/src/integrations/tracing/fastify/index.ts new file mode 100644 index 000000000000..49140f46edda --- /dev/null +++ b/packages/node/src/integrations/tracing/fastify/index.ts @@ -0,0 +1,226 @@ +import * as diagnosticsChannel from 'node:diagnostics_channel'; +import { FastifyOtelInstrumentation } from '@fastify/otel'; +import type { Instrumentation, InstrumentationConfig } from '@opentelemetry/instrumentation'; +import type { IntegrationFn, Span } from '@sentry/core'; +import { + captureException, + defineIntegration, + getClient, + getIsolationScope, + logger, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + spanToJSON, +} from '@sentry/core'; +import { DEBUG_BUILD } from '../../../debug-build'; +import { generateInstrumentOnce } from '../../../otel/instrument'; +import type { FastifyInstance, FastifyReply, FastifyRequest } from './types'; +import { FastifyInstrumentationV3 } from './v3/instrumentation'; + +interface FastifyHandlerOptions { + /** + * Callback method deciding whether error should be captured and sent to Sentry + * + * @param error Captured Fastify error + * @param request Fastify request (or any object containing at least method, routeOptions.url, and routerPath) + * @param reply Fastify reply (or any object containing at least statusCode) + * + * @example + * + * ```javascript + * setupFastifyErrorHandler(app, { + * shouldHandleError(_error, _request, reply) { + * return reply.statusCode >= 400; + * }, + * }); + * ``` + * + * If using TypeScript, you can cast the request and reply to get full type safety. + * + * ```typescript + * import type { FastifyRequest, FastifyReply } from 'fastify'; + * + * setupFastifyErrorHandler(app, { + * shouldHandleError(error, minimalRequest, minimalReply) { + * const request = minimalRequest as FastifyRequest; + * const reply = minimalReply as FastifyReply; + * return reply.statusCode >= 500; + * }, + * }); + * ``` + */ + shouldHandleError: (error: Error, request: FastifyRequest, reply: FastifyReply) => boolean; +} + +const INTEGRATION_NAME = 'Fastify'; +const INTEGRATION_NAME_V3 = 'Fastify-V3'; + +export const instrumentFastifyV3 = generateInstrumentOnce(INTEGRATION_NAME_V3, () => new FastifyInstrumentationV3()); + +export const instrumentFastify = generateInstrumentOnce(INTEGRATION_NAME, () => { + const fastifyOtelInstrumentationInstance = new FastifyOtelInstrumentation(); + const plugin = fastifyOtelInstrumentationInstance.plugin(); + + // This message handler works for Fastify versions 3, 4 and 5 + 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); + } else { + instrumentClient(); + + if (fastifyInstance) { + instrumentOnRequest(fastifyInstance); + } + } + }); + }); + + // Returning this as unknown not to deal with the internal types of the FastifyOtelInstrumentation + return fastifyOtelInstrumentationInstance as Instrumentation; +}); + +const _fastifyIntegration = (() => { + return { + name: INTEGRATION_NAME, + setupOnce() { + instrumentFastifyV3(); + instrumentFastify(); + }, + }; +}) satisfies IntegrationFn; + +/** + * Adds Sentry tracing instrumentation for [Fastify](https://fastify.dev/). + * + * If you also want to capture errors, you need to call `setupFastifyErrorHandler(app)` after you set up your Fastify server. + * + * For more information, see the [fastify documentation](https://docs.sentry.io/platforms/javascript/guides/fastify/). + * + * @example + * ```javascript + * const Sentry = require('@sentry/node'); + * + * Sentry.init({ + * integrations: [Sentry.fastifyIntegration()], + * }) + * ``` + */ +export const fastifyIntegration = defineIntegration(_fastifyIntegration); + +/** + * Default function to determine if an error should be sent to Sentry + * + * 3xx and 4xx errors are not sent by default. + */ +function defaultShouldHandleError(_error: Error, _request: FastifyRequest, reply: FastifyReply): boolean { + const statusCode = reply.statusCode; + // 3xx and 4xx errors are not sent by default. + return statusCode >= 500 || statusCode <= 299; +} + +/** + * Add an Fastify error handler to capture errors to Sentry. + * + * @param fastify The Fastify instance to which to add the error handler + * @param options Configuration options for the handler + * + * @example + * ```javascript + * const Sentry = require('@sentry/node'); + * const Fastify = require("fastify"); + * + * const app = Fastify(); + * + * Sentry.setupFastifyErrorHandler(app); + * + * // Add your routes, etc. + * + * app.listen({ port: 3000 }); + * ``` + */ +export function setupFastifyErrorHandler(fastify: FastifyInstance, options?: Partial): void { + const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError; + + const plugin = Object.assign( + function (fastify: FastifyInstance, _options: unknown, done: () => void): void { + fastify.addHook('onError', async (request, reply, error) => { + if (shouldHandleError(error, request, reply)) { + captureException(error); + } + }); + + done(); + }, + { + [Symbol.for('skip-override')]: true, + [Symbol.for('fastify.display-name')]: 'sentry-fastify-error-handler', + }, + ); + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + fastify.register(plugin); +} + +function addFastifySpanAttributes(span: Span): void { + const spanJSON = spanToJSON(span); + const spanName = spanJSON.description; + const attributes = spanJSON.data; + + const type = attributes['fastify.type']; + + const isHook = type === 'hook'; + const isHandler = type === spanName?.startsWith('handler -'); + // In @fastify/otel `request-handler` is separated by dash, not underscore + const isRequestHandler = spanName === 'request' || type === 'request-handler'; + + // If this is already set, or we have no fastify span, no need to process again... + if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || (!isHandler && !isRequestHandler && !isHook)) { + return; + } + + const opPrefix = isHook ? 'hook' : isHandler ? 'middleware' : isRequestHandler ? 'request-handler' : ''; + + span.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.fastify', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${opPrefix}.fastify`, + }); + + const attrName = attributes['fastify.name'] || attributes['plugin.name'] || attributes['hook.name']; + if (typeof attrName === 'string') { + // 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 = attrName.replace(/^fastify -> /, '').replace(/^@fastify\/otel -> /, ''); + + span.updateName(updatedName); + } +} + +function instrumentClient(): void { + const client = getClient(); + if (client) { + client.on('spanStart', (span: Span) => { + addFastifySpanAttributes(span); + }); + } +} + +function instrumentOnRequest(fastify: FastifyInstance): void { + fastify.addHook('onRequest', async (request: FastifyRequest & { opentelemetry?: () => { span?: Span } }, _reply) => { + if (request.opentelemetry) { + const { span } = request.opentelemetry(); + + if (span) { + addFastifySpanAttributes(span); + } + } + + const routeName = request.routeOptions?.url; + const method = request.method || 'GET'; + + getIsolationScope().setTransactionName(`${method} ${routeName}`); + }); +} diff --git a/packages/node/src/integrations/tracing/fastify/types.ts b/packages/node/src/integrations/tracing/fastify/types.ts new file mode 100644 index 000000000000..e15c1c85324b --- /dev/null +++ b/packages/node/src/integrations/tracing/fastify/types.ts @@ -0,0 +1,41 @@ +export type HandlerOriginal = + | ((request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => Promise) + | ((request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => void); + +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: any) => FastifyInstance; + after: (listener?: (err: Error) => void) => FastifyInstance; + addHook(hook: string, handler: HandlerOriginal): FastifyInstance; + addHook( + hook: 'onError', + handler: (request: FastifyRequest, reply: FastifyReply, error: Error) => void, + ): FastifyInstance; + addHook(hook: 'onRequest', handler: (request: FastifyRequest, reply: FastifyReply) => void): FastifyInstance; +} + +export interface FastifyReply { + send: () => FastifyReply; + statusCode: number; +} + +export interface FastifyRequest { + method?: string; + // since fastify@4.10.0 + routeOptions?: { + url?: string; + }; + routerPath?: string; +} 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..10a64ed3b420 --- /dev/null +++ b/packages/node/src/integrations/tracing/fastify/v3/constants.ts @@ -0,0 +1,34 @@ +// Vendored from https://github.com/open-telemetry/opentelemetry-js-contrib/blob/407f61591ba69a39a6908264379d4d98a48dbec4/plugins/node/opentelemetry-instrumentation-fastify/src/constants.ts +/* + * 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..343b68747f06 --- /dev/null +++ b/packages/node/src/integrations/tracing/fastify/v3/enums/AttributeNames.ts @@ -0,0 +1,34 @@ +// Vendored from https://github.com/open-telemetry/opentelemetry-js-contrib/blob/407f61591ba69a39a6908264379d4d98a48dbec4/plugins/node/opentelemetry-instrumentation-fastify/src/enums/AttributeNames.ts +// +/* + * 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..7b8035927a11 --- /dev/null +++ b/packages/node/src/integrations/tracing/fastify/v3/instrumentation.ts @@ -0,0 +1,335 @@ +// Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/407f61591ba69a39a6908264379d4d98a48dbec4/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts +/* 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, context, SpanStatusCode, trace } from '@opentelemetry/api'; +import { getRPCMetadata, RPCType } from '@opentelemetry/core'; +import { + InstrumentationBase, + InstrumentationNodeModuleDefinition, + safeExecuteInTheMiddle, +} from '@opentelemetry/instrumentation'; +import { SEMATTRS_HTTP_ROUTE } from '@opentelemetry/semantic-conventions'; +import type { Span } from '@sentry/core'; +import { + getClient, + getIsolationScope, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + spanToJSON, +} from '@sentry/core'; +import type { + FastifyErrorCodes, + FastifyInstance, + FastifyReply, + FastifyRequest, + HandlerOriginal, + HookHandlerDoneFunction, +} from '../types'; +import { AttributeNames, FastifyNames, FastifyTypes } from './enums/AttributeNames'; +import type { 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; + } + + const method = request.method || 'GET'; + + getIsolationScope().setTransactionName(`${method} ${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()); + + instrumentClient(); + + 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); + + addFastifyV3SpanAttributes(span); + + 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(); + }); + }; + } +} + +function instrumentClient(): void { + const client = getClient(); + if (client) { + client.on('spanStart', (span: Span) => { + addFastifyV3SpanAttributes(span); + }); + } +} + +function addFastifyV3SpanAttributes(span: Span): void { + const attributes = spanToJSON(span).data; + + // this is one of: middleware, request_handler + const type = attributes['fastify.type']; + + // If this is already set, or we have no fastify span, no need to process again... + if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || !type) { + return; + } + + span.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.fastify', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.fastify`, + }); + + // 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') { + // 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/fastify/v3/internal-types.ts b/packages/node/src/integrations/tracing/fastify/v3/internal-types.ts new file mode 100644 index 000000000000..f71a92df0685 --- /dev/null +++ b/packages/node/src/integrations/tracing/fastify/v3/internal-types.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +// Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/407f61591ba69a39a6908264379d4d98a48dbec4/plugins/node/opentelemetry-instrumentation-fastify/src/internal-types.ts +import type { Span } from '@opentelemetry/api'; +import type { FastifyReply } from '../types'; +import type { spanRequestSymbol } from './constants'; + +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..b4c30a3dd36f --- /dev/null +++ b/packages/node/src/integrations/tracing/fastify/v3/types.ts @@ -0,0 +1,40 @@ +// Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/407f61591ba69a39a6908264379d4d98a48dbec4/plugins/node/opentelemetry-instrumentation-fastify/src/types.ts +/* + * 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..0e30c5f0bd07 --- /dev/null +++ b/packages/node/src/integrations/tracing/fastify/v3/utils.ts @@ -0,0 +1,137 @@ +// Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/407f61591ba69a39a6908264379d4d98a48dbec4/plugins/node/opentelemetry-instrumentation-fastify/src/utils.ts +/* 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, type Tracer, SpanStatusCode } 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/index.ts b/packages/node/src/integrations/tracing/index.ts index e07a247d7d34..6e27d6f25fef 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -3,7 +3,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'; @@ -60,6 +60,7 @@ export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) => instrumentExpressV5, instrumentConnect, instrumentFastify, + instrumentFastifyV3, instrumentHapi, instrumentKafka, instrumentKoa, diff --git a/yarn.lock b/yarn.lock index 334e255ebb3b..5e36f0b38a78 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3912,6 +3912,15 @@ resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.0.0.tgz#f22824caff3ae506b18207bad4126dbc6ccdb6b8" integrity sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ== +"@fastify/otel@0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@fastify/otel/-/otel-0.6.0.tgz#f86dfa6711804d0087288d7fadc097b41feea5b1" + integrity sha512-lL+36KwGcFiAMcsPOLLsR+GV8ZpQuz5RLVstlgqmecTdQLTXVOe9Z8uwpMg9ktPcV++Ugp3dzzpBKNFWWWelYg== + dependencies: + "@opentelemetry/core" "^2.0.0" + "@opentelemetry/instrumentation" "^0.200.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" @@ -5411,6 +5420,13 @@ dependencies: "@octokit/openapi-types" "^18.0.0" +"@opentelemetry/api-logs@0.200.0": + version "0.200.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.200.0.tgz#f9015fd844920c13968715b3cdccf5a4d4ff907e" + integrity sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q== + dependencies: + "@opentelemetry/api" "^1.3.0" + "@opentelemetry/api-logs@0.52.1": version "0.52.1" resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz#52906375da4d64c206b0c4cb8ffa209214654ecc" @@ -5442,6 +5458,13 @@ dependencies: "@opentelemetry/semantic-conventions" "1.28.0" +"@opentelemetry/core@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-2.0.0.tgz#37e9f0e9ddec4479b267aca6f32d88757c941b3a" + integrity sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ== + dependencies: + "@opentelemetry/semantic-conventions" "^1.29.0" + "@opentelemetry/instrumentation-amqplib@^0.46.1": version "0.46.1" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.46.1.tgz#7101678488d0e942162ca85c9ac6e93e1f3e0008" @@ -5496,15 +5519,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" @@ -5681,6 +5695,17 @@ semver "^7.5.2" shimmer "^1.2.1" +"@opentelemetry/instrumentation@^0.200.0": + version "0.200.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.200.0.tgz#29d1d4f70cbf0cb1ca9f2f78966379b0be96bddc" + integrity sha512-pmPlzfJd+vvgaZd/reMsC8RWgTXn2WY1OWT5RT42m3aOn5532TozwXNDhg1vzqJ+jnvmkREcdLr27ebJEQt0Jg== + dependencies: + "@opentelemetry/api-logs" "0.200.0" + "@types/shimmer" "^1.2.0" + import-in-the-middle "^1.8.1" + require-in-the-middle "^7.1.1" + shimmer "^1.2.1" + "@opentelemetry/instrumentation@^0.52.1": version "0.52.1" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz#2e7e46a38bd7afbf03cf688c862b0b43418b7f48" @@ -5725,10 +5750,10 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz#337fb2bca0453d0726696e745f50064411f646d6" integrity sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA== -"@opentelemetry/semantic-conventions@^1.25.1", "@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.28.0", "@opentelemetry/semantic-conventions@^1.30.0": - version "1.30.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.30.0.tgz#3a42c4c475482f2ec87c12aad98832dc0087dc9a" - integrity sha512-4VlGgo32k2EQ2wcCY3vEU28A0O13aOtHz3Xt2/2U5FAh9EfhD6t6DqL5Z6yAnRCntbTFDU4YfbpyzSlHNWycPw== +"@opentelemetry/semantic-conventions@^1.25.1", "@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.28.0", "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.30.0": + version "1.32.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.32.0.tgz#a15e8f78f32388a7e4655e7f539570e40958ca3f" + integrity sha512-s0OpmpQFSfMrmedAn9Lhg4KWJELHCU6uU9dtIJ28N8UGhf9Y55im5X8fEzwhwDwiSqN+ZPSNrDJF7ivf/AuRPQ== "@opentelemetry/sql-common@^0.40.1": version "0.40.1" @@ -9435,14 +9460,14 @@ ajv@^6.10.0, ajv@^6.11.0, ajv@^6.12.4, ajv@^6.12.5: uri-js "^4.2.2" ajv@^8.0.0, ajv@^8.0.1, ajv@^8.10.0, ajv@^8.8.0: - version "8.12.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" - integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== dependencies: - fast-deep-equal "^3.1.1" + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" json-schema-traverse "^1.0.0" require-from-string "^2.0.2" - uri-js "^4.2.2" amd-name-resolver@^1.3.1: version "1.3.1" @@ -15617,6 +15642,11 @@ fast-text-encoding@^1.0.0: resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== +fast-uri@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" + integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== + fast-xml-parser@4.2.5: version "4.2.5" resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz#a6747a09296a6cb34f2ae634019bf1738f3b421f" @@ -15632,9 +15662,9 @@ fast-xml-parser@^4.4.1: strnum "^1.0.5" fastq@^1.6.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" - integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g== + version "1.19.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5" + integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ== dependencies: reusify "^1.0.4" @@ -17928,9 +17958,9 @@ ipaddr.js@1.9.1: integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== ipaddr.js@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.1.0.tgz#2119bc447ff8c257753b196fc5f1ce08a4cdf39f" - integrity sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ== + version "2.2.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" + integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== iron-webcrypto@^1.1.1: version "1.2.1" @@ -26608,9 +26638,9 @@ split2@^3.2.2: readable-stream "^3.0.0" split2@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/split2/-/split2-4.1.0.tgz#101907a24370f85bb782f08adaabe4e281ecf809" - integrity sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ== + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== split@^1.0.1: version "1.0.1"