diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts index 7e0873355c65..8850a3ede58a 100644 --- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts @@ -19,7 +19,6 @@ const NODE_EXPORTS_IGNORE = [ 'getDefaultIntegrationsWithoutPerformance', 'initWithoutDefaultIntegrations', 'SentryContextManager', - 'validateOpenTelemetrySetup', 'preloadOpenTelemetry', // Experimental, Node-runtime-only opt-in (diagnostics-channel injection); it // registers Node module hooks and is not surfaced through the framework / diff --git a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/.gitignore b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/.gitignore deleted file mode 100644 index 686a0277246c..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -.vscode diff --git a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/package.json b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/package.json deleted file mode 100644 index b26e3981e028..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "node-otel-custom-sampler", - "version": "1.0.0", - "private": true, - "scripts": { - "build": "tsc", - "start": "node dist/app.js", - "test": "playwright test", - "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && pnpm build", - "test:assert": "pnpm test" - }, - "dependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/sdk-trace-node": "^2.6.0", - "@sentry/node": "file:../../packed/sentry-node-packed.tgz", - "@sentry/opentelemetry": "file:../../packed/sentry-opentelemetry-packed.tgz", - "@types/express": "4.17.17", - "@types/node": "^18.19.1", - "express": "^4.21.2", - "typescript": "~5.0.0" - }, - "devDependencies": { - "@playwright/test": "~1.56.0", - "@sentry-internal/test-utils": "link:../../../test-utils" - }, - "volta": { - "extends": "../../package.json" - } -} diff --git a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/playwright.config.mjs deleted file mode 100644 index 31f2b913b58b..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/playwright.config.mjs +++ /dev/null @@ -1,7 +0,0 @@ -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-otel-custom-sampler/src/app.ts b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/src/app.ts deleted file mode 100644 index cc7e98446706..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/src/app.ts +++ /dev/null @@ -1,50 +0,0 @@ -import './instrument'; - -import * as Sentry from '@sentry/node'; -import express from 'express'; - -const PORT = 3030; -const app = express(); - -const wait = (duration: number) => { - return new Promise(res => { - setTimeout(() => res(), duration); - }); -}; - -app.get('/task', async (_req, res) => { - await Sentry.startSpan({ name: 'Long task', op: 'custom.op' }, async () => { - await wait(200); - }); - res.send('ok'); -}); - -app.get('/unsampled/task', async (_req, res) => { - await wait(200); - res.send('ok'); -}); - -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('/test-exception/:id', function (req, _res) { - throw new Error(`This is an exception with id ${req.params.id}`); -}); - -Sentry.setupExpressErrorHandler(app); - -app.use(function onError(err: unknown, req: any, res: any, next: any) { - // The error id is attached to `res.sentry` to be returned - // and optionally displayed to the user for support. - res.statusCode = 500; - res.end(res.sentry + '\n'); -}); - -app.listen(PORT, () => { - console.log('App listening on ', PORT); -}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/src/custom-sampler.ts b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/src/custom-sampler.ts deleted file mode 100644 index cbaaac57c8ea..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/src/custom-sampler.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Attributes, Context, Link, SpanKind } from '@opentelemetry/api'; -import { Sampler, SamplingResult } from '@opentelemetry/sdk-trace-node'; -import { wrapSamplingDecision } from '@sentry/opentelemetry'; - -export class CustomSampler implements Sampler { - public shouldSample( - context: Context, - _traceId: string, - _spanName: string, - _spanKind: SpanKind, - attributes: Attributes, - _links: Link[], - ): SamplingResult { - const route = attributes['http.route']; - const target = attributes['http.target']; - const decision = - (typeof route === 'string' && route.includes('/unsampled')) || - (typeof target === 'string' && target.includes('/unsampled')) - ? 0 - : 1; - return wrapSamplingDecision({ - decision, - context, - spanAttributes: attributes, - }); - } - - public toString(): string { - return CustomSampler.name; - } -} diff --git a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/src/instrument.ts b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/src/instrument.ts deleted file mode 100644 index de09f0965baa..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/src/instrument.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; -import * as Sentry from '@sentry/node'; -import { SentryPropagator, SentrySpanProcessor } from '@sentry/opentelemetry'; -import { CustomSampler } from './custom-sampler'; - -Sentry.init({ - environment: 'qa', // dynamic sampling bias to keep transactions - dsn: - process.env.E2E_TEST_DSN || - 'https://3b6c388182fb435097f41d181be2b2ba@o4504321058471936.ingest.sentry.io/4504321066008576', - includeLocalVariables: true, - debug: !!process.env.DEBUG, - tunnel: `http://localhost:3031/`, // proxy server - skipOpenTelemetrySetup: true, - // By defining _any_ sample rate, tracing integrations will be added by default - tracesSampleRate: 0, - integrations: [Sentry.httpIntegration({ spans: true })], -}); - -const provider = new NodeTracerProvider({ - sampler: new CustomSampler(), - spanProcessors: [new SentrySpanProcessor()], -}); - -provider.register({ - propagator: new SentryPropagator(), - contextManager: new Sentry.SentryContextManager(), -}); - -Sentry.validateOpenTelemetrySetup(); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/start-event-proxy.mjs deleted file mode 100644 index 4650bf6ecc67..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/start-event-proxy.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import { startEventProxyServer } from '@sentry-internal/test-utils'; - -startEventProxyServer({ - port: 3031, - proxyServerName: 'node-otel-custom-sampler', -}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/tests/errors.test.ts deleted file mode 100644 index 4627c86bf03f..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/tests/errors.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForError } from '@sentry-internal/test-utils'; - -test('Sends correct error event', async ({ baseURL }) => { - const errorEventPromise = waitForError('node-otel-custom-sampler', 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}/), - }); -}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/tests/sampling.test.ts b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/tests/sampling.test.ts deleted file mode 100644 index 04348a3245a4..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/tests/sampling.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; - -test('Sends a sampled API route transaction', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('node-otel-custom-sampler', transactionEvent => { - return transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /task'; - }); - - await fetch(`${baseURL}/task`); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent.contexts?.trace).toEqual({ - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.source': 'route', - 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.otel.http', - url: 'http://localhost:3030/task', - 'sentry.kind': 'server', - 'http.response.status_code': 200, - 'http.url': 'http://localhost:3030/task', - 'url.full': 'http://localhost:3030/task', - 'url.path': '/task', - 'http.host': 'localhost:3030', - 'net.host.name': 'localhost', - 'http.method': 'GET', - 'http.scheme': 'http', - 'http.target': '/task', - 'http.user_agent': 'node', - 'http.flavor': '1.1', - 'net.transport': 'ip_tcp', - 'net.host.ip': expect.any(String), - 'net.host.port': 3030, - 'net.peer.ip': expect.any(String), - 'net.peer.port': expect.any(Number), - 'http.status_code': 200, - 'http.status_text': 'OK', - 'http.route': '/task', - 'http.request.header.accept': '*/*', - 'http.request.header.accept_encoding': 'gzip, deflate', - 'http.request.header.accept_language': '*', - 'http.request.header.connection': 'keep-alive', - 'http.request.header.host': expect.any(String), - 'http.request.header.sec_fetch_mode': 'cors', - 'http.request.header.user_agent': 'node', - }, - origin: 'auto.http.otel.http', - op: 'http.server', - status: 'ok', - }); - - expect(transactionEvent.spans?.length).toBe(4); - - expect(transactionEvent.spans).toContainEqual({ - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.origin': 'auto.http.express', - 'sentry.op': 'middleware.express', - 'express.name': 'query', - 'express.type': 'middleware', - }, - description: 'query', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - op: 'middleware.express', - origin: 'auto.http.express', - }); - - expect(transactionEvent.spans).toContainEqual({ - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.origin': 'auto.http.express', - 'sentry.op': 'middleware.express', - 'express.name': 'expressInit', - 'express.type': 'middleware', - }, - description: 'expressInit', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - op: 'middleware.express', - origin: 'auto.http.express', - }); - - expect(transactionEvent.spans).toContainEqual({ - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.origin': 'auto.http.express', - 'sentry.op': 'request_handler.express', - 'http.route': '/task', - 'express.name': '/task', - 'express.type': 'request_handler', - }, - description: '/task', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - op: 'request_handler.express', - origin: 'auto.http.express', - }); - - expect(transactionEvent.spans).toContainEqual({ - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.origin': 'manual', - 'sentry.op': 'custom.op', - }, - description: 'Long task', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - status: 'ok', - op: 'custom.op', - origin: 'manual', - }); -}); - -test('Does not send an unsampled API route transaction', async ({ baseURL }) => { - const unsampledTransactionEventPromise = waitForTransaction('node-otel-custom-sampler', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /unsampled/task' - ); - }); - - await fetch(`${baseURL}/unsampled/task`); - - const promiseShouldNotResolve = () => - new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - resolve(); // Test passes because promise did not resolve within timeout - }, 1000); - - unsampledTransactionEventPromise.then( - () => { - clearTimeout(timeout); - reject(new Error('Promise should not have resolved')); - }, - () => { - clearTimeout(timeout); - reject(new Error('Promise should not have been rejected')); - }, - ); - }); - - expect(promiseShouldNotResolve()).resolves.not.toThrow(); -}); 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 deleted file mode 100644 index 2887ec11a81d..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "types": ["node"], - "esModuleInterop": true, - "lib": ["es2018"], - "strict": true, - "outDir": "dist", - "skipLibCheck": true - }, - "include": ["src/**/*.ts"] -} diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/.gitignore b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/.gitignore deleted file mode 100644 index 1521c8b7652b..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dist diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json deleted file mode 100644 index 38badec93335..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "node-otel-sdk-node", - "version": "1.0.0", - "private": true, - "scripts": { - "build": "tsc", - "start": "node dist/app.js", - "test": "playwright test", - "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && pnpm build", - "test:assert": "pnpm test" - }, - "dependencies": { - "@opentelemetry/sdk-node": "0.213.0", - "@opentelemetry/exporter-trace-otlp-http": "0.213.0", - "@sentry/node": "file:../../packed/sentry-node-packed.tgz", - "@types/express": "4.17.17", - "@types/node": "^18.19.1", - "express": "^4.21.2", - "typescript": "~5.0.0" - }, - "devDependencies": { - "@playwright/test": "~1.56.0", - "@sentry-internal/test-utils": "link:../../../test-utils" - }, - "volta": { - "extends": "../../package.json" - } -} diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/playwright.config.mjs deleted file mode 100644 index 888e61cfb2dc..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/playwright.config.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import { getPlaywrightConfig } from '@sentry-internal/test-utils'; - -const config = getPlaywrightConfig( - { - startCommand: `pnpm start`, - }, - { - webServer: [ - { - command: `node ./start-event-proxy.mjs`, - port: 3031, - stdout: 'pipe', - stderr: 'pipe', - }, - { - command: `node ./start-otel-proxy.mjs`, - port: 3032, - stdout: 'pipe', - stderr: 'pipe', - }, - { - command: 'pnpm start', - port: 3030, - stdout: 'pipe', - stderr: 'pipe', - env: { - PORT: 3030, - }, - }, - ], - }, -); - -export default config; diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/src/app.ts b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/src/app.ts deleted file mode 100644 index 26779990f6d1..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/src/app.ts +++ /dev/null @@ -1,53 +0,0 @@ -import './instrument'; - -// Other imports below -import * as Sentry from '@sentry/node'; -import express from 'express'; - -const app = express(); -const port = 3030; - -app.get('/test-success', function (req, res) { - res.send({ version: 'v1' }); -}); - -app.get('/test-param/:param', function (req, res) { - res.send({ paramWas: req.params.param }); -}); - -app.get('/test-transaction', function (req, res) { - Sentry.withActiveSpan(null, async () => { - Sentry.startSpan({ name: 'test-transaction', op: 'e2e-test' }, () => { - Sentry.startSpan({ name: 'test-span' }, () => undefined); - }); - - await Sentry.flush(); - - 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('/test-exception/:id', function (req, _res) { - throw new Error(`This is an exception with id ${req.params.id}`); -}); - -Sentry.setupExpressErrorHandler(app); - -app.use(function onError(err: unknown, req: any, res: any, next: any) { - // The error id is attached to `res.sentry` to be returned - // and optionally displayed to the user for support. - res.statusCode = 500; - res.end(res.sentry + '\n'); -}); - -app.listen(port, () => { - console.log(`Example app listening on port ${port}`); -}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/src/instrument.ts b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/src/instrument.ts deleted file mode 100644 index 5cb2e5570db9..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/src/instrument.ts +++ /dev/null @@ -1,36 +0,0 @@ -const opentelemetry = require('@opentelemetry/sdk-node'); -const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http'); -const Sentry = require('@sentry/node'); -const { SentrySpanProcessor, SentryPropagator, SentrySampler } = require('@sentry/opentelemetry'); - -const sentryClient = Sentry.init({ - environment: 'qa', // dynamic sampling bias to keep transactions - dsn: - process.env.E2E_TEST_DSN || - 'https://3b6c388182fb435097f41d181be2b2ba@o4504321058471936.ingest.sentry.io/4504321066008576', - includeLocalVariables: true, - debug: !!process.env.DEBUG, - tunnel: `http://localhost:3031/`, // proxy server - tracesSampleRate: 1, - - skipOpenTelemetrySetup: true, - integrations: [Sentry.httpIntegration({ spans: true })], -}); - -const sdk = new opentelemetry.NodeSDK({ - sampler: sentryClient ? new SentrySampler(sentryClient) : undefined, - textMapPropagator: new SentryPropagator(), - contextManager: new Sentry.SentryContextManager(), - spanProcessors: [ - new SentrySpanProcessor(), - new opentelemetry.node.BatchSpanProcessor( - new OTLPTraceExporter({ - url: 'http://localhost:3032/', - }), - ), - ], -}); - -sdk.start(); - -Sentry.validateOpenTelemetrySetup(); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/start-event-proxy.mjs deleted file mode 100644 index b97bfc4664dd..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/start-event-proxy.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import { startEventProxyServer } from '@sentry-internal/test-utils'; - -startEventProxyServer({ - port: 3031, - proxyServerName: 'node-otel-sdk-node', -}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/start-otel-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/start-otel-proxy.mjs deleted file mode 100644 index c24241310fbb..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/start-otel-proxy.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import { startProxyServer } from '@sentry-internal/test-utils'; - -startProxyServer({ - port: 3032, - proxyServerName: 'node-otel-sdk-node-otel', -}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/errors.test.ts deleted file mode 100644 index bb3c8c70b629..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/errors.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForError } from '@sentry-internal/test-utils'; - -test('Sends correct error event', async ({ baseURL }) => { - const errorEventPromise = waitForError('node-otel-sdk-node', 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}/), - }); -}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/transactions.test.ts deleted file mode 100644 index e9cc9ce152ea..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/transactions.test.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForPlainRequest, waitForTransaction } from '@sentry-internal/test-utils'; - -test('Sends an API route transaction', async ({ baseURL }) => { - const pageloadTransactionEventPromise = waitForTransaction('node-otel-sdk-node', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-transaction' - ); - }); - - // Ensure we also send data to the OTLP endpoint - const otelPromise = waitForPlainRequest('node-otel-sdk-node-otel', data => { - const json = JSON.parse(data) as any; - - return json.resourceSpans.length > 0; - }); - - await fetch(`${baseURL}/test-transaction`); - - const transactionEvent = await pageloadTransactionEventPromise; - - const otelData = await otelPromise; - - // For now we do not test the actual shape of this, but only existence - expect(otelData).toBeDefined(); - - 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', - 'sentry.kind': 'server', - 'http.response.status_code': 200, - 'http.url': 'http://localhost:3030/test-transaction', - 'url.full': 'http://localhost:3030/test-transaction', - 'url.path': '/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', - 'http.request.header.accept': '*/*', - 'http.request.header.accept_encoding': 'gzip, deflate', - 'http.request.header.accept_language': '*', - 'http.request.header.connection': 'keep-alive', - 'http.request.header.host': expect.any(String), - 'http.request.header.sec_fetch_mode': 'cors', - 'http.request.header.user_agent': 'node', - }, - 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: { - 'sentry.origin': 'auto.http.express', - 'sentry.op': 'middleware.express', - 'express.name': 'query', - 'express.type': 'middleware', - }, - description: 'query', - op: 'middleware.express', - origin: 'auto.http.express', - 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}/), - }); - - expect(spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.http.express', - 'sentry.op': 'middleware.express', - 'express.name': 'expressInit', - 'express.type': 'middleware', - }, - description: 'expressInit', - op: 'middleware.express', - origin: 'auto.http.express', - 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}/), - }); - - expect(spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.http.express', - 'sentry.op': 'request_handler.express', - 'http.route': '/test-transaction', - 'express.name': '/test-transaction', - 'express.type': 'request_handler', - }, - description: '/test-transaction', - op: 'request_handler.express', - origin: 'auto.http.express', - 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}/), - }); -}); - -test('Sends an API route transaction for an errored route', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('node-otel-sdk-node', transactionEvent => { - return ( - transactionEvent.contexts?.trace?.op === 'http.server' && - transactionEvent.transaction === 'GET /test-exception/:id' && - transactionEvent.request?.url === 'http://localhost:3030/test-exception/777' - ); - }); - - await fetch(`${baseURL}/test-exception/777`); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent.contexts?.trace?.op).toEqual('http.server'); - expect(transactionEvent.transaction).toEqual('GET /test-exception/:id'); - expect(transactionEvent.contexts?.trace?.status).toEqual('internal_error'); - expect(transactionEvent.contexts?.trace?.data?.['http.status_code']).toEqual(500); - - const spans = transactionEvent.spans || []; - - expect(spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.http.express', - 'sentry.op': 'middleware.express', - 'express.name': 'query', - 'express.type': 'middleware', - }, - description: 'query', - op: 'middleware.express', - origin: 'auto.http.express', - 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}/), - }); - - expect(spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.http.express', - 'sentry.op': 'middleware.express', - 'express.name': 'expressInit', - 'express.type': 'middleware', - }, - description: 'expressInit', - op: 'middleware.express', - origin: 'auto.http.express', - 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}/), - }); - - expect(spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.http.express', - 'sentry.op': 'request_handler.express', - 'http.route': '/test-exception/:id', - 'express.name': '/test-exception/:id', - 'express.type': 'request_handler', - }, - description: '/test-exception/:id', - op: 'request_handler.express', - origin: 'auto.http.express', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'internal_error', - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - }); -}); 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 deleted file mode 100644 index 2887ec11a81d..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "types": ["node"], - "esModuleInterop": true, - "lib": ["es2018"], - "strict": true, - "outDir": "dist", - "skipLibCheck": true - }, - "include": ["src/**/*.ts"] -} diff --git a/dev-packages/e2e-tests/test-applications/node-otel/.gitignore b/dev-packages/e2e-tests/test-applications/node-otel/.gitignore deleted file mode 100644 index 1521c8b7652b..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dist diff --git a/dev-packages/e2e-tests/test-applications/node-otel/package.json b/dev-packages/e2e-tests/test-applications/node-otel/package.json deleted file mode 100644 index 274b141ff2a7..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "node-otel", - "version": "1.0.0", - "private": true, - "scripts": { - "build": "tsc", - "start": "node dist/app.js", - "test": "playwright test", - "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && pnpm build", - "test:assert": "pnpm test" - }, - "dependencies": { - "@opentelemetry/sdk-node": "0.213.0", - "@opentelemetry/exporter-trace-otlp-http": "0.213.0", - "@sentry/node": "file:../../packed/sentry-node-packed.tgz", - "@types/express": "4.17.17", - "@types/node": "^18.19.1", - "express": "^4.21.2", - "typescript": "~5.0.0" - }, - "devDependencies": { - "@playwright/test": "~1.56.0", - "@sentry-internal/test-utils": "link:../../../test-utils" - }, - "volta": { - "extends": "../../package.json" - } -} diff --git a/dev-packages/e2e-tests/test-applications/node-otel/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-otel/playwright.config.mjs deleted file mode 100644 index 888e61cfb2dc..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel/playwright.config.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import { getPlaywrightConfig } from '@sentry-internal/test-utils'; - -const config = getPlaywrightConfig( - { - startCommand: `pnpm start`, - }, - { - webServer: [ - { - command: `node ./start-event-proxy.mjs`, - port: 3031, - stdout: 'pipe', - stderr: 'pipe', - }, - { - command: `node ./start-otel-proxy.mjs`, - port: 3032, - stdout: 'pipe', - stderr: 'pipe', - }, - { - command: 'pnpm start', - port: 3030, - stdout: 'pipe', - stderr: 'pipe', - env: { - PORT: 3030, - }, - }, - ], - }, -); - -export default config; diff --git a/dev-packages/e2e-tests/test-applications/node-otel/src/app.ts b/dev-packages/e2e-tests/test-applications/node-otel/src/app.ts deleted file mode 100644 index 26779990f6d1..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel/src/app.ts +++ /dev/null @@ -1,53 +0,0 @@ -import './instrument'; - -// Other imports below -import * as Sentry from '@sentry/node'; -import express from 'express'; - -const app = express(); -const port = 3030; - -app.get('/test-success', function (req, res) { - res.send({ version: 'v1' }); -}); - -app.get('/test-param/:param', function (req, res) { - res.send({ paramWas: req.params.param }); -}); - -app.get('/test-transaction', function (req, res) { - Sentry.withActiveSpan(null, async () => { - Sentry.startSpan({ name: 'test-transaction', op: 'e2e-test' }, () => { - Sentry.startSpan({ name: 'test-span' }, () => undefined); - }); - - await Sentry.flush(); - - 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('/test-exception/:id', function (req, _res) { - throw new Error(`This is an exception with id ${req.params.id}`); -}); - -Sentry.setupExpressErrorHandler(app); - -app.use(function onError(err: unknown, req: any, res: any, next: any) { - // The error id is attached to `res.sentry` to be returned - // and optionally displayed to the user for support. - res.statusCode = 500; - res.end(res.sentry + '\n'); -}); - -app.listen(port, () => { - console.log(`Example app listening on port ${port}`); -}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel/src/instrument.ts b/dev-packages/e2e-tests/test-applications/node-otel/src/instrument.ts deleted file mode 100644 index bbc5ddf9c30f..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel/src/instrument.ts +++ /dev/null @@ -1,22 +0,0 @@ -const opentelemetry = require('@opentelemetry/sdk-node'); -const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http'); -const Sentry = require('@sentry/node'); - -const sentryClient = Sentry.init({ - environment: 'qa', // dynamic sampling bias to keep transactions - dsn: - process.env.E2E_TEST_DSN || - 'https://3b6c388182fb435097f41d181be2b2ba@o4504321058471936.ingest.sentry.io/4504321066008576', - debug: !!process.env.DEBUG, - tunnel: `http://localhost:3031/`, // proxy server - tracesSampleRate: 1, - - // Additional OTEL options - openTelemetrySpanProcessors: [ - new opentelemetry.node.BatchSpanProcessor( - new OTLPTraceExporter({ - url: 'http://localhost:3032/', - }), - ), - ], -}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-otel/start-event-proxy.mjs deleted file mode 100644 index e82b876a4979..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel/start-event-proxy.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import { startEventProxyServer } from '@sentry-internal/test-utils'; - -startEventProxyServer({ - port: 3031, - proxyServerName: 'node-otel', -}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel/start-otel-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-otel/start-otel-proxy.mjs deleted file mode 100644 index df546bf5ff77..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel/start-otel-proxy.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import { startProxyServer } from '@sentry-internal/test-utils'; - -startProxyServer({ - port: 3032, - proxyServerName: 'node-otel-otel', -}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-otel/tests/errors.test.ts deleted file mode 100644 index e5b2d5ff6836..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel/tests/errors.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForError } from '@sentry-internal/test-utils'; - -test('Sends correct error event', async ({ baseURL }) => { - const errorEventPromise = waitForError('node-otel', 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}/), - }); -}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-otel/tests/transactions.test.ts deleted file mode 100644 index 12f129f18c89..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel/tests/transactions.test.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForPlainRequest, waitForTransaction } from '@sentry-internal/test-utils'; - -test('Sends an API route transaction', async ({ baseURL }) => { - const pageloadTransactionEventPromise = waitForTransaction('node-otel', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-transaction' - ); - }); - - // Ensure we also send data to the OTLP endpoint - const otelPromise = waitForPlainRequest('node-otel-otel', data => { - const json = JSON.parse(data) as any; - - return json.resourceSpans.length > 0; - }); - - await fetch(`${baseURL}/test-transaction`); - - const transactionEvent = await pageloadTransactionEventPromise; - - const otelData = await otelPromise; - - // For now we do not test the actual shape of this, but only existence - expect(otelData).toBeDefined(); - - 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', - 'sentry.kind': 'server', - 'http.response.status_code': 200, - 'http.url': 'http://localhost:3030/test-transaction', - 'url.full': 'http://localhost:3030/test-transaction', - 'url.path': '/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', - 'http.request.header.accept': '*/*', - 'http.request.header.accept_encoding': 'gzip, deflate', - 'http.request.header.accept_language': '*', - 'http.request.header.connection': 'keep-alive', - 'http.request.header.host': expect.any(String), - 'http.request.header.sec_fetch_mode': 'cors', - 'http.request.header.user_agent': 'node', - }, - 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: { - 'sentry.origin': 'auto.http.express', - 'sentry.op': 'middleware.express', - 'express.name': 'query', - 'express.type': 'middleware', - }, - description: 'query', - op: 'middleware.express', - origin: 'auto.http.express', - 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}/), - }); - - expect(spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.http.express', - 'sentry.op': 'middleware.express', - 'express.name': 'expressInit', - 'express.type': 'middleware', - }, - description: 'expressInit', - op: 'middleware.express', - origin: 'auto.http.express', - 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}/), - }); - - expect(spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.http.express', - 'sentry.op': 'request_handler.express', - 'http.route': '/test-transaction', - 'express.name': '/test-transaction', - 'express.type': 'request_handler', - }, - description: '/test-transaction', - op: 'request_handler.express', - origin: 'auto.http.express', - 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}/), - }); -}); - -test('Sends an API route transaction for an errored route', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('node-otel', transactionEvent => { - return ( - transactionEvent.contexts?.trace?.op === 'http.server' && - transactionEvent.transaction === 'GET /test-exception/:id' && - transactionEvent.request?.url === 'http://localhost:3030/test-exception/777' - ); - }); - - await fetch(`${baseURL}/test-exception/777`); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent.contexts?.trace?.op).toEqual('http.server'); - expect(transactionEvent.transaction).toEqual('GET /test-exception/:id'); - expect(transactionEvent.contexts?.trace?.status).toEqual('internal_error'); - expect(transactionEvent.contexts?.trace?.data?.['http.status_code']).toEqual(500); - - const spans = transactionEvent.spans || []; - - expect(spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.http.express', - 'sentry.op': 'middleware.express', - 'express.name': 'query', - 'express.type': 'middleware', - }, - description: 'query', - op: 'middleware.express', - origin: 'auto.http.express', - 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}/), - }); - - expect(spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.http.express', - 'sentry.op': 'middleware.express', - 'express.name': 'expressInit', - 'express.type': 'middleware', - }, - description: 'expressInit', - op: 'middleware.express', - origin: 'auto.http.express', - 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}/), - }); - - expect(spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.http.express', - 'sentry.op': 'request_handler.express', - 'http.route': '/test-exception/:id', - 'express.name': '/test-exception/:id', - 'express.type': 'request_handler', - }, - description: '/test-exception/:id', - op: 'request_handler.express', - origin: 'auto.http.express', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'internal_error', - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - }); -}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-otel/tsconfig.json deleted file mode 100644 index 2887ec11a81d..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "types": ["node"], - "esModuleInterop": true, - "lib": ["es2018"], - "strict": true, - "outDir": "dist", - "skipLibCheck": true - }, - "include": ["src/**/*.ts"] -} diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/instrument-basic-tracer-provider.mjs b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/instrument-basic-tracer-provider.mjs deleted file mode 100644 index 114a06934170..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/instrument-basic-tracer-provider.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - openTelemetryBasicTracerProvider: true, - transport: loggingTransport, -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts index a28ff3aa76c5..e4c4f031dced 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts @@ -11,9 +11,6 @@ const ADDITIONAL_DEPENDENCIES = { prisma: '5.22.0', }; -// Prisma v5 engine spans are minted by Sentry's v5 compatibility shim through Sentry's -// provider-agnostic span APIs, so the same span tree must be produced under both the default -// SentryTracerProvider and the opt-in OTel BasicTracerProvider (`openTelemetryBasicTracerProvider`). function expectPrismaV5Spans(transaction: TransactionEvent): void { expect(transaction.transaction).toBe('Test Transaction'); const spans = transaction.spans || []; @@ -132,24 +129,4 @@ describeWithDockerCompose('Prisma ORM v5', { workingDirectory: [__dirname] }, () }, ); }); - - // The BasicTracerProvider path is opt-in via `openTelemetryBasicTracerProvider: true`; it must produce - // the same Prisma v5 span tree as the default SentryTracerProvider. - describe('Prisma ORM v5 Tests (BasicTracerProvider)', () => { - createEsmAndCjsTests( - __dirname, - 'scenario.mjs', - 'instrument-basic-tracer-provider.mjs', - (createRunner, test) => { - test('should instrument PostgreSQL queries from Prisma ORM', { timeout: 75_000 }, async () => { - await createRunner().expect({ transaction: expectPrismaV5Spans }).start().completed(); - }); - }, - { - additionalDependencies: ADDITIONAL_DEPENDENCIES, - afterSetupCommand: AFTER_SETUP_COMMAND, - copyPaths: ['prisma'], - }, - ); - }); }); diff --git a/packages/core/src/types/options.ts b/packages/core/src/types/options.ts index 3d55c5f17498..1a9aea7463ed 100644 --- a/packages/core/src/types/options.ts +++ b/packages/core/src/types/options.ts @@ -84,17 +84,6 @@ export interface ServerRuntimeOptions { */ clientReportFlushInterval?: number; - /** - * The max. duration in seconds that the SDK will wait for parent spans to be finished before discarding a span. - * The SDK will automatically clean up spans that have no finished parent after this duration. - * This is necessary to prevent memory leaks in case of parent spans that are never finished or otherwise dropped/missing. - * However, if you have very long-running spans in your application, a shorter duration might cause spans to be discarded too early. - * In this case, you can increase this duration to a value that fits your expected data. - * - * Defaults to 300 seconds (5 minutes). - */ - maxSpanWaitDuration?: number; - /** * Callback that is executed when a fatal global error occurs. */ diff --git a/packages/core/test/lib/integrations/http/server-subscription.test.ts b/packages/core/test/lib/integrations/http/server-subscription.test.ts index 37e822c5fc4c..7d019955e38c 100644 --- a/packages/core/test/lib/integrations/http/server-subscription.test.ts +++ b/packages/core/test/lib/integrations/http/server-subscription.test.ts @@ -41,7 +41,7 @@ describe('getHttpServerSubscriptions', () => { await new Promise(resolve => server.close(() => resolve())); }); - async function makeRequest(path: string, method: 'GET' | 'HEAD' = 'GET'): Promise { + async function makeRequest(path: string, method: 'GET' | 'HEAD' | 'OPTIONS' = 'GET'): Promise { const { port } = server.address() as AddressInfo; return new Promise((resolve, reject) => { // Connection: close so the server-side `response.once('close', ...)` @@ -153,6 +153,17 @@ describe('getHttpServerSubscriptions', () => { expect(events.find(e => e.type === 'transaction')).toBeUndefined(); }); + it('skips span creation for OPTIONS requests', async () => { + server = http.createServer((_req, res) => res.end()); + await new Promise(resolve => server.listen(0, '127.0.0.1', () => resolve())); + instrument(true); + + await makeRequest('/anything', 'OPTIONS'); + await new Promise(resolve => setImmediate(resolve)); + + expect(events.find(e => e.type === 'transaction')).toBeUndefined(); + }); + it('skips span creation for static assets by default', async () => { server = http.createServer((_req, res) => res.end()); await new Promise(resolve => server.listen(0, '127.0.0.1', () => resolve())); diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index 943ba1c84f5a..5fc880b64fde 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -101,7 +101,7 @@ export function init(options: VercelEdgeOptions = {}): void { // Next.js's OTel instrumentation samples root spans before the Sentry middleware wrapper can set // `normalizedRequest` on the isolation scope. Seed it from span attributes so `tracesSampler` has access. - client?.on('beforeSampling', ({ spanAttributes }) => { + client.on('beforeSampling', ({ spanAttributes }) => { const spanType = spanAttributes[ATTR_NEXT_SPAN_TYPE]; if (spanType !== 'Middleware.execute' && spanType !== 'BaseServer.handleRequest') { return; @@ -119,7 +119,7 @@ export function init(options: VercelEdgeOptions = {}): void { } }); - client?.on('spanStart', span => { + client.on('spanStart', span => { const spanAttributes = spanToJSON(span).data; const rootSpan = getRootSpan(span); const isRootSpan = span === rootSpan; @@ -154,7 +154,7 @@ export function init(options: VercelEdgeOptions = {}): void { // This handles the legacy (non-streamed) path where the segment span is emitted as a transaction event; // `enhanceMiddlewareRootSpan` is adapted to operate on the event's trace context, which is the segment span's data. // Span streaming bypasses event processors entirely - see the `processSegmentSpan` hook below for that path. - client?.on('preprocessEvent', event => { + client.on('preprocessEvent', event => { if (event.type === 'transaction' && event.contexts?.trace?.data) { enhanceMiddlewareRootSpan({ attributes: event.contexts.trace.data, @@ -173,7 +173,7 @@ export function init(options: VercelEdgeOptions = {}): void { // Streamed-span counterpart of the `preprocessEvent` hook above. Streamed segment spans never become // transaction events, so the same enhancement has to be applied here directly on the span JSON. - client?.on('processSegmentSpan', span => { + client.on('processSegmentSpan', span => { const attributes = (span.attributes ??= {}); enhanceMiddlewareRootSpan({ attributes, @@ -187,7 +187,7 @@ export function init(options: VercelEdgeOptions = {}): void { }); }); - client?.on('spanEnd', span => { + client.on('spanEnd', span => { if (span === getRootSpan(span)) { waitUntil(flushSafelyWithTimeout()); } diff --git a/packages/node/package.json b/packages/node/package.json index 1b6747382cc7..05fc53f1c4fb 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -101,7 +101,6 @@ "@opentelemetry/api": "^1.9.1", "@opentelemetry/core": "^2.9.0", "@opentelemetry/instrumentation": "^0.220.0", - "@opentelemetry/sdk-trace-base": "^2.9.0", "@sentry/conventions": "^0.16.0", "@sentry/core": "10.67.0", "@sentry/opentelemetry": "10.67.0", diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index cbbc614c82fe..f672cb16f30b 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -210,6 +210,5 @@ export { processSessionIntegration } from './integrations/processSession'; export { SentryContextManager } from './otel/contextManager'; export { generateInstrumentOnce } from './otel/instrument'; export { NodeClient } from './sdk/client'; -export { validateOpenTelemetrySetup } from './sdk'; // eslint-disable-next-line typescript/no-deprecated export { anrIntegration, disableAnrDetectionForCallback } from './integrations/anr'; diff --git a/packages/node/src/sdk/client.ts b/packages/node/src/sdk/client.ts index fa9dfeae1e3e..7968fb3595cc 100644 --- a/packages/node/src/sdk/client.ts +++ b/packages/node/src/sdk/client.ts @@ -1,7 +1,6 @@ import * as os from 'node:os'; import type { Tracer } from '@opentelemetry/api'; import { trace } from '@opentelemetry/api'; -import { registerInstrumentations } from '@opentelemetry/instrumentation'; import type { DynamicSamplingContext, Scope, ServerRuntimeClientOptions, TraceContext } from '@sentry/core'; import { _INTERNAL_clearAiProviderSkips, @@ -15,7 +14,7 @@ import { import { type AsyncLocalStorageLookup, getTraceContextForScope, - type OpenTelemetryTracerProvider, + type SentryTracerProvider, } from '@sentry/opentelemetry'; import { isMainThread, threadId } from 'worker_threads'; import { DEBUG_BUILD } from '../debug-build'; @@ -25,7 +24,7 @@ const DEFAULT_CLIENT_REPORT_FLUSH_INTERVAL_MS = 60_000; // 60s was chosen arbitr /** A client for using Sentry with Node & OpenTelemetry. */ export class NodeClient extends ServerRuntimeClient { - public traceProvider: OpenTelemetryTracerProvider | undefined; + public traceProvider: SentryTracerProvider | undefined; public asyncLocalStorageLookup: AsyncLocalStorageLookup | undefined; private _tracer: Tracer | undefined; @@ -47,12 +46,6 @@ export class NodeClient extends ServerRuntimeClient { serverName, }; - if (options.openTelemetryInstrumentations) { - registerInstrumentations({ - instrumentations: options.openTelemetryInstrumentations, - }); - } - applySdkMetadata(clientOptions, 'node'); debug.log(`Initializing Sentry: process: ${process.pid}, thread: ${isMainThread ? 'main' : `worker-${threadId}`}.`); diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index 418603c28ab5..c519b4408269 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -17,7 +17,6 @@ import { } from '@sentry/core'; import { enhanceDscWithOpenTelemetryRootSpanName, - openTelemetrySetupCheck, setOpenTelemetryContextAsyncContextStrategy, setupEventContextTrace, } from '@sentry/opentelemetry'; @@ -280,10 +279,7 @@ function _init( // Add Node SDK specific OpenTelemetry setup if (!clientOptions.skipOpenTelemetrySetup) { - initOpenTelemetry(client, { - spanProcessors: clientOptions.openTelemetrySpanProcessors, - }); - validateOpenTelemetrySetup(); + initOpenTelemetry(client); } // Warn about missing or doubled channel injection. Runs after the client @@ -295,39 +291,6 @@ function _init( return client; } -/** - * Validate that your OpenTelemetry setup is correct. - */ -export function validateOpenTelemetrySetup(): void { - if (!DEBUG_BUILD) { - return; - } - - const setup = openTelemetrySetupCheck(); - - const required: ReturnType = ['SentryContextManager', 'SentryPropagator']; - - const hasSentryTracerProvider = setup.includes('SentryTracerProvider'); - - if (hasSpansEnabled() && !hasSentryTracerProvider) { - required.push('SentrySpanProcessor'); - } - - for (const k of required) { - if (!setup.includes(k)) { - debug.error( - `You have to set up the ${k}. Without this, the OpenTelemetry & Sentry integration will not work properly.`, - ); - } - } - - if (!hasSentryTracerProvider && !setup.includes('SentrySampler')) { - debug.warn( - 'You have to set up the SentrySampler. Without this, the OpenTelemetry & Sentry integration may still work, but sample rates set for the Sentry SDK will not be respected. If you use a custom sampler, make sure to use `wrapSamplingDecision`.', - ); - } -} - function getClientOptions( options: NodeOptions, getDefaultIntegrationsImpl: (options: Options) => Integration[], diff --git a/packages/node/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts index 3b264c993f56..42c3e6fb0c40 100644 --- a/packages/node/src/sdk/initOtel.ts +++ b/packages/node/src/sdk/initOtel.ts @@ -1,7 +1,5 @@ import type { TracerProvider } from '@opentelemetry/api'; import { context, propagation, trace } from '@opentelemetry/api'; -import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; -import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { debug as coreDebug, hasSpanStreamingEnabled } from '@sentry/core'; import { SentryContextManager } from '../otel/contextManager'; import { setupOpenTelemetryLogger } from '../otel/logger'; @@ -12,19 +10,12 @@ import { type AsyncLocalStorageLookup, backfillStreamedSpanDataFromOtel, getSentryResource, - type OpenTelemetryTracerProvider, SentryPropagator, - SentrySampler, - SentrySpanProcessor, SentryTracerProvider, - setIsSetup, } from '@sentry/opentelemetry'; import { DEBUG_BUILD } from '../debug-build'; import { getOpenTelemetryInstrumentationToPreload } from '../integrations/tracing'; -// About 277h - this must fit into new Array(len)! -const MAX_MAX_SPAN_WAIT_DURATION = 1_000_000; - // The global registry of @opentelemetry/api 1.x, shared across all copies of the package const OTEL_API_GLOBAL_KEY = Symbol.for('opentelemetry.js.api.1'); @@ -80,20 +71,15 @@ function registerGlobalTracerProvider(provider: TracerProvider): boolean { return false; } -interface AdditionalOpenTelemetryOptions { - /** Additional SpanProcessor instances that should be used. */ - spanProcessors?: SpanProcessor[]; -} - /** * Initialize OpenTelemetry for Node. */ -export function initOpenTelemetry(client: NodeClient, options: AdditionalOpenTelemetryOptions = {}): void { +export function initOpenTelemetry(client: NodeClient): void { if (client.getOptions().debug) { setupOpenTelemetryLogger(); } - const [provider, asyncLocalStorageLookup] = setupOtel(client, options); + const [provider, asyncLocalStorageLookup] = setupOtel(client); client.traceProvider = provider; client.asyncLocalStorageLookup = asyncLocalStorageLookup; } @@ -142,48 +128,7 @@ function getPreloadMethods(integrationNames?: string[]): ((() => void) & { id: s } /** Just exported for tests. */ -export function setupOtel( - client: NodeClient, - options: AdditionalOpenTelemetryOptions = {}, -): [OpenTelemetryTracerProvider | undefined, AsyncLocalStorageLookup | undefined] { - // Sentry's minimal tracer provider is the default. We fall back to the full OpenTelemetry SDK - // `BasicTracerProvider` when the user explicitly opts in via `openTelemetryBasicTracerProvider`, or - // when they provide custom `openTelemetrySpanProcessors` — those require the SDK span pipeline - // that the minimal provider does not run. - const shouldUseBasicTracerProvider = - client.getOptions().openTelemetryBasicTracerProvider || !!options.spanProcessors?.length; - - if (!shouldUseBasicTracerProvider) { - return setupSentryTracerProvider(client); - } - - // Create and configure NodeTracerProvider - const provider = new BasicTracerProvider({ - sampler: new SentrySampler(client), - resource: getSentryResource('node'), - forceFlushTimeoutMillis: 500, - spanProcessors: [ - new SentrySpanProcessor({ - timeout: _clampSpanProcessorTimeout(client.getOptions().maxSpanWaitDuration), - client, - }), - ...(options.spanProcessors || []), - ], - }); - - // Register as globals - registerGlobalTracerProvider(provider); - propagation.setGlobalPropagator(new SentryPropagator()); - - const ctxManager = new SentryContextManager(); - context.setGlobalContextManager(ctxManager); - - return [provider, ctxManager.getAsyncLocalStorageLookup()]; -} - -function setupSentryTracerProvider( - client: NodeClient, -): [SentryTracerProvider | undefined, AsyncLocalStorageLookup | undefined] { +export function setupOtel(client: NodeClient): [SentryTracerProvider | undefined, AsyncLocalStorageLookup | undefined] { const provider = new SentryTracerProvider({ resource: getSentryResource('node') }); if (!registerGlobalTracerProvider(provider)) { @@ -194,11 +139,6 @@ function setupSentryTracerProvider( return [undefined, undefined]; } - // Only mark the provider as set up once it is actually the registered global - // tracer provider, so setup validation doesn't skip required checks when - // registration failed. - setIsSetup('SentryTracerProvider'); - propagation.setGlobalPropagator(new SentryPropagator()); const ctxManager = new SentryContextManager(); @@ -209,9 +149,6 @@ function setupSentryTracerProvider( }); if (hasSpanStreamingEnabled(client)) { - // Streamed spans skip the exporter, so per-span data inferred from OTel semantic conventions - // (notably `sentry.source` on child spans, which `applyOtelSpanData` only sets on segment roots) - // is backfilled here, reusing the exact inference the OTel SDK `SentrySpanProcessor` applies. client.on('preprocessSpan', backfillStreamedSpanDataFromOtel); } @@ -231,23 +168,3 @@ function setupSentryTracerProvider( return [provider, ctxManager.getAsyncLocalStorageLookup()]; } - -/** Just exported for tests. */ -export function _clampSpanProcessorTimeout(maxSpanWaitDuration: number | undefined): number | undefined { - if (maxSpanWaitDuration == null) { - return undefined; - } - - // We guard for a max. value here, because we create an array with this length - // So if this value is too large, this would fail - if (maxSpanWaitDuration > MAX_MAX_SPAN_WAIT_DURATION) { - DEBUG_BUILD && - coreDebug.warn(`\`maxSpanWaitDuration\` is too high, using the maximum value of ${MAX_MAX_SPAN_WAIT_DURATION}`); - return MAX_MAX_SPAN_WAIT_DURATION; - } else if (maxSpanWaitDuration <= 0 || Number.isNaN(maxSpanWaitDuration)) { - DEBUG_BUILD && coreDebug.warn('`maxSpanWaitDuration` must be a positive number, using default value instead.'); - return undefined; - } - - return maxSpanWaitDuration; -} diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts index 7c431c44abed..d1fddafdfc15 100644 --- a/packages/node/src/types.ts +++ b/packages/node/src/types.ts @@ -1,7 +1,4 @@ -import type { Span as WriteableSpan } from '@opentelemetry/api'; -import type { Instrumentation } from '@opentelemetry/instrumentation'; -import type { ReadableSpan, SpanProcessor } from '@opentelemetry/sdk-trace-base'; -import type { ClientOptions, Options, SamplingContext, Scope, ServerRuntimeOptions, Span } from '@sentry/core'; +import type { ClientOptions, Options, SamplingContext, Scope, ServerRuntimeOptions } from '@sentry/core'; import type { NodeTransportOptions } from './transports'; /** @@ -13,42 +10,10 @@ export interface OpenTelemetryServerRuntimeOptions extends ServerRuntimeOptions /** * If this is set to true, the SDK will not set up OpenTelemetry automatically. * In this case, you _have_ to ensure to set it up correctly yourself, including: - * * The `SentrySpanProcessor` * * The `SentryPropagator` * * The `SentryContextManager` - * * The `SentrySampler` */ skipOpenTelemetrySetup?: boolean; - - /** - * Provide an array of OpenTelemetry Instrumentations that should be registered. - * - * Use this option if you want to register OpenTelemetry instrumentation that the Sentry SDK does not yet have support for. - */ - openTelemetryInstrumentations?: Instrumentation[]; - - /** - * Provide an array of additional OpenTelemetry SpanProcessors that should be registered. - * - * Note: providing this forces the full OpenTelemetry SDK `BasicTracerProvider` instead of Sentry's - * minimal tracer provider, since custom span processors require the SDK span pipeline. See - * {@link OpenTelemetryServerRuntimeOptions.openTelemetryBasicTracerProvider}. - */ - openTelemetrySpanProcessors?: SpanProcessor[]; - - /** - * By default, the SDK uses Sentry's minimal OpenTelemetry tracer provider, which creates native - * Sentry spans directly instead of going through the full OpenTelemetry SDK span pipeline. - * - * Set this to `true` to use the full OpenTelemetry SDK `BasicTracerProvider` instead, e.g. if you - * rely on OpenTelemetry SDK features that the minimal provider does not support. - * - * Note: providing `openTelemetrySpanProcessors` also forces the full OpenTelemetry SDK provider, - * since custom span processors require the SDK span pipeline. - * - * @default false - */ - openTelemetryBasicTracerProvider?: boolean; } /** @@ -138,14 +103,3 @@ export interface CurrentScopes { scope: Scope; isolationScope: Scope; } - -/** - * The base `Span` type is basically a `WriteableSpan`. - * There are places where we basically want to allow passing _any_ span, - * so in these cases we type this as `AbstractSpan` which could be either a regular `Span` or a `ReadableSpan`. - * You'll have to make sur to check relevant fields before accessing them. - * - * Note that technically, the `Span` exported from `@opentelemetry/sdk-trace-base` matches this, - * but we cannot be 100% sure that we are actually getting such a span, so this type is more defensive. - */ -export type AbstractSpan = WriteableSpan | ReadableSpan | Span; diff --git a/packages/node/test/helpers/mockSdkInit.ts b/packages/node/test/helpers/mockSdkInit.ts index 84f172c349ab..a2df938be2e7 100644 --- a/packages/node/test/helpers/mockSdkInit.ts +++ b/packages/node/test/helpers/mockSdkInit.ts @@ -1,15 +1,5 @@ -import { context, propagation, ProxyTracerProvider, trace } from '@opentelemetry/api'; -import { BasicTracerProvider, type SpanProcessor } from '@opentelemetry/sdk-trace-base'; -import { - createTransport, - getClient, - getCurrentScope, - getGlobalScope, - getIsolationScope, - resolvedSyncPromise, -} from '@sentry/core'; -import { SentrySpanProcessor } from '@sentry/opentelemetry'; -import type { NodeClient } from '../../src'; +import { context, propagation, trace } from '@opentelemetry/api'; +import { createTransport, getCurrentScope, getGlobalScope, getIsolationScope, resolvedSyncPromise } from '@sentry/core'; import { init } from '../../src/sdk'; import type { NodeClientOptions } from '../../src/types'; @@ -36,56 +26,9 @@ export function mockSdkInit(options?: Partial) { }); } -export function cleanupOtel(_provider?: BasicTracerProvider): void { - const provider = getProvider(_provider); - - // `getProvider` only resolves the OpenTelemetry SDK `BasicTracerProvider`; the default - // `SentryTracerProvider` is not an instance of it. Flush/shutdown only apply to the SDK provider, - // but the global APIs must always be disabled so the next test can register its own provider. - if (provider) { - void provider.forceFlush(); - void provider.shutdown(); - } - +export function cleanupOtel(): void { // Disable all globally registered APIs trace.disable(); context.disable(); propagation.disable(); } - -export function getSpanProcessor(): SentrySpanProcessor | undefined { - const client = getClient(); - if (!client?.traceProvider) { - return undefined; - } - - const provider = getProvider(client.traceProvider); - if (!provider) { - return undefined; - } - - // Access the span processors from the provider via _activeSpanProcessor - const multiSpanProcessor = provider?.['_activeSpanProcessor'] as - | (SpanProcessor & { _spanProcessors?: SpanProcessor[] }) - | undefined; - - const spanProcessor = multiSpanProcessor?.['_spanProcessors']?.find( - (spanProcessor: SpanProcessor) => spanProcessor instanceof SentrySpanProcessor, - ); - - return spanProcessor; -} - -export function getProvider(_provider?: BasicTracerProvider): BasicTracerProvider | undefined { - let provider = _provider || getClient()?.traceProvider || trace.getTracerProvider(); - - if (provider instanceof ProxyTracerProvider) { - provider = provider.getDelegate(); - } - - if (!(provider instanceof BasicTracerProvider)) { - return undefined; - } - - return provider; -} diff --git a/packages/node/test/integration/transactions.test.ts b/packages/node/test/integration/transactions.test.ts index cef5ac2852cf..572132fb6244 100644 --- a/packages/node/test/integration/transactions.test.ts +++ b/packages/node/test/integration/transactions.test.ts @@ -1,9 +1,9 @@ import { context, trace, TraceFlags } from '@opentelemetry/api'; import type { ErrorEvent, TransactionEvent } from '@sentry/core'; -import { debug, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { afterEach, describe, expect, it, vi } from 'vitest'; import * as Sentry from '../../src'; -import { cleanupOtel, getSpanProcessor, mockSdkInit } from '../helpers/mockSdkInit'; +import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; describe('Integration | Transactions', () => { afterEach(() => { @@ -551,138 +551,6 @@ describe('Integration | Transactions', () => { ]); }); - it('cleans up spans that are not flushed for over 5 mins', async () => { - const beforeSendTransaction = vi.fn(() => null); - - const now = Date.now(); - vi.useFakeTimers(); - vi.setSystemTime(now); - - const logs: unknown[] = []; - vi.spyOn(debug, 'log').mockImplementation(msg => logs.push(msg)); - - // This test inspects the `SentrySpanProcessor`/exporter buffering, which only exists on the - // OpenTelemetry SDK provider, so opt out of the default `SentryTracerProvider`. - mockSdkInit({ tracesSampleRate: 1, beforeSendTransaction, openTelemetryBasicTracerProvider: true }); - - const spanProcessor = getSpanProcessor(); - - const exporter = spanProcessor ? spanProcessor['_exporter'] : undefined; - - if (!exporter) { - throw new Error('No exporter found, aborting test...'); - } - - void Sentry.startSpan({ name: 'test name' }, async () => { - Sentry.startInactiveSpan({ name: 'inner span 1' }).end(); - Sentry.startInactiveSpan({ name: 'inner span 2' }).end(); - - // Pretend this is pending for 10 minutes - await new Promise(resolve => setTimeout(resolve, 10 * 60 * 1000)); - }); - - vi.advanceTimersByTime(1); - - // Child-spans have been added to the exporter, but they are pending since they are waiting for their parent - const finishedSpans1 = []; - exporter['_finishedSpanBuckets'].forEach((bucket: any) => { - if (bucket) { - finishedSpans1.push(...bucket.spans); - } - }); - expect(finishedSpans1.length).toBe(2); - expect(beforeSendTransaction).toHaveBeenCalledTimes(0); - - // Now wait for 5 mins - vi.advanceTimersByTime(5 * 60 * 1_000 + 1); - - // Adding another span will trigger the cleanup - Sentry.startSpan({ name: 'other span' }, () => {}); - - vi.advanceTimersByTime(1); - - // Old spans have been cleared away - const finishedSpans2 = []; - exporter['_finishedSpanBuckets'].forEach((bucket: any) => { - if (bucket) { - finishedSpans2.push(...bucket.spans); - } - }); - expect(finishedSpans2.length).toBe(0); - - // Called once for the 'other span' - expect(beforeSendTransaction).toHaveBeenCalledTimes(1); - - expect(logs).toEqual( - expect.arrayContaining([ - 'SpanExporter dropped 2 spans because they were pending for more than 300 seconds.', - 'SpanExporter exported 1 spans, 0 spans are waiting for their parent spans to finish', - ]), - ); - }); - - it('allows to configure `maxSpanWaitDuration` to capture long running spans', async () => { - const transactions: TransactionEvent[] = []; - const beforeSendTransaction = vi.fn(event => { - transactions.push(event); - return null; - }); - - const now = Date.now(); - vi.useFakeTimers(); - vi.setSystemTime(now); - - const logs: unknown[] = []; - vi.spyOn(debug, 'log').mockImplementation(msg => logs.push(msg)); - - // `maxSpanWaitDuration` configures the `SentrySpanProcessor` timeout, which only exists on the - // OpenTelemetry SDK provider, so opt out of the default `SentryTracerProvider`. - mockSdkInit({ - tracesSampleRate: 1, - beforeSendTransaction, - maxSpanWaitDuration: 100 * 60, - openTelemetryBasicTracerProvider: true, - }); - - Sentry.startSpanManual({ name: 'test name' }, rootSpan => { - const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' }); - subSpan.end(); - - Sentry.startSpanManual({ name: 'inner span 2' }, innerSpan => { - // Child span ends after 10 min - setTimeout( - () => { - innerSpan.end(); - }, - 10 * 60 * 1_000, - ); - }); - - // root span ends after 99 min - setTimeout( - () => { - rootSpan.end(); - }, - 99 * 10 * 1_000, - ); - }); - - // Now wait for 100 mins - vi.advanceTimersByTime(100 * 60 * 1_000); - - expect(beforeSendTransaction).toHaveBeenCalledTimes(1); - expect(transactions).toHaveLength(1); - const transaction = transactions[0]!; - - expect(transaction.transaction).toEqual('test name'); - const spans = transaction.spans || []; - - expect(spans).toHaveLength(2); - - expect(spans).toContainEqual(expect.objectContaining({ description: 'inner span 1' })); - expect(spans).toContainEqual(expect.objectContaining({ description: 'inner span 2' })); - }); - it('withMonitor should use the same traces for each monitor', async () => { const sendEvents: ErrorEvent[] = []; const transactionEvents: TransactionEvent[] = []; diff --git a/packages/node/test/sdk/client.test.ts b/packages/node/test/sdk/client.test.ts index 8dcdf33d4067..09998805c12a 100644 --- a/packages/node/test/sdk/client.test.ts +++ b/packages/node/test/sdk/client.test.ts @@ -1,8 +1,7 @@ import { ProxyTracer } from '@opentelemetry/api'; -import * as opentelemetryInstrumentationPackage from '@opentelemetry/instrumentation'; -import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import type { Event, EventHint, Log } from '@sentry/core'; import { getCurrentScope, getGlobalScope, getIsolationScope, Scope, SDK_VERSION } from '@sentry/core'; +import type { SentryTracerProvider } from '@sentry/opentelemetry'; import { setOpenTelemetryContextAsyncContextStrategy } from '@sentry/opentelemetry'; import * as os from 'os'; import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest'; @@ -298,21 +297,6 @@ describe('NodeClient', () => { }); }); - it('registers instrumentations provided with `openTelemetryInstrumentations`', () => { - const registerInstrumentationsSpy = vi - .spyOn(opentelemetryInstrumentationPackage, 'registerInstrumentations') - .mockImplementationOnce(() => () => undefined); - const instrumentationsArray = ['foobar'] as unknown as opentelemetryInstrumentationPackage.Instrumentation[]; - - new NodeClient(getDefaultNodeClientOptions({ openTelemetryInstrumentations: instrumentationsArray })); - - expect(registerInstrumentationsSpy).toHaveBeenCalledWith( - expect.objectContaining({ - instrumentations: instrumentationsArray, - }), - ); - }); - describe('log capture', () => { it('adds server name to log attributes', () => { const options = getDefaultNodeClientOptions({ enableLogs: true }); @@ -355,7 +339,7 @@ describe('NodeClient', () => { client.traceProvider = { shutdown: shutdownSpy, forceFlush: forceFlushSpy, - } as unknown as BasicTracerProvider; + } as unknown as SentryTracerProvider; const result = await client.close(); diff --git a/packages/node/test/sdk/init.test.ts b/packages/node/test/sdk/init.test.ts index cca2608a2639..c4b0550098fa 100644 --- a/packages/node/test/sdk/init.test.ts +++ b/packages/node/test/sdk/init.test.ts @@ -1,10 +1,8 @@ -import { trace } from '@opentelemetry/api'; -import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import type { Integration } from '@sentry/core'; import { debug, SDK_VERSION } from '@sentry/core'; import * as SentryOpentelemetry from '@sentry/opentelemetry'; import { afterEach, beforeEach, describe, expect, it, type Mock, type MockInstance, vi } from 'vitest'; -import { getClient, NodeClient, validateOpenTelemetrySetup } from '../../src/'; +import { getClient, NodeClient } from '../../src/'; import * as auto from '../../src/integrations/tracing'; import { init } from '../../src/sdk'; import { cleanupOtel } from '../helpers/mockSdkInit'; @@ -230,59 +228,6 @@ describe('init()', () => { expect(client?.traceProvider).toBeInstanceOf(SentryOpentelemetry.SentryTracerProvider); }); - it('uses the OpenTelemetry SDK tracer provider when opted in via `openTelemetryBasicTracerProvider`', () => { - init({ dsn: PUBLIC_DSN, openTelemetryBasicTracerProvider: true }); - - const client = getClient(); - - expect(client?.traceProvider).toBeInstanceOf(BasicTracerProvider); - }); - - it('uses the OpenTelemetry SDK tracer provider when custom span processors are provided', () => { - init({ - dsn: PUBLIC_DSN, - openTelemetrySpanProcessors: [ - { - forceFlush: () => Promise.resolve(), - onStart: () => undefined, - onEnd: () => undefined, - shutdown: () => Promise.resolve(), - }, - ], - }); - - const client = getClient(); - - expect(client?.traceProvider).toBeInstanceOf(BasicTracerProvider); - }); - - it('recreates the OTel API registry when it pre-exists with a different @opentelemetry/api version', () => { - // Simulate a host runtime (e.g. Neon Functions) pre-creating the registry with its own api version - global[OTEL_API_GLOBAL_KEY] = { version: '0.0.1' }; - - init({ dsn: PUBLIC_DSN }); - - const client = getClient(); - const registry = global[OTEL_API_GLOBAL_KEY]; - - expect(client?.traceProvider).toBeInstanceOf(SentryOpentelemetry.SentryTracerProvider); - expect(registry?.version).not.toBe('0.0.1'); - expect(registry?.trace).toBeDefined(); - }); - - it('recreates a version-mismatched OTel API registry also for the OpenTelemetry SDK tracer provider', () => { - global[OTEL_API_GLOBAL_KEY] = { version: '0.0.1' }; - - init({ dsn: PUBLIC_DSN, openTelemetryBasicTracerProvider: true }); - - const client = getClient(); - const registry = global[OTEL_API_GLOBAL_KEY]; - - expect(client?.traceProvider).toBeInstanceOf(BasicTracerProvider); - expect(registry?.version).not.toBe('0.0.1'); - expect(registry?.trace).toBeDefined(); - }); - it('carries non-Sentry slots of a version-mismatched OTel API registry over into the recreated one', () => { // Must be a complete DiagLogger: once carried over, the SDK's api copy resolves it and // calls it for its own diag output. @@ -322,24 +267,6 @@ describe('init()', () => { global[OTEL_API_GLOBAL_KEY] = undefined; }); - - it('does not mark SentryTracerProvider as set up when global registration fails', () => { - // Simulate another OpenTelemetry tracer provider already being registered. - const setGlobalSpy = vi.spyOn(trace, 'setGlobalTracerProvider').mockReturnValue(false); - const setIsSetupSpy = vi.spyOn(SentryOpentelemetry, 'setIsSetup'); - const warnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); - - init({ dsn: PUBLIC_DSN }); - - expect(getClient()?.traceProvider).not.toBeDefined(); - expect(setIsSetupSpy).not.toHaveBeenCalledWith('SentryTracerProvider'); - expect(warnSpy).toHaveBeenCalledWith( - 'Could not register SentryTracerProvider because another OpenTelemetry tracer provider is already registered.', - ); - - setGlobalSpy.mockRestore(); - setIsSetupSpy.mockRestore(); - }); }); it('returns initialized client', () => { @@ -621,70 +548,3 @@ describe('init()', () => { }); }); }); - -describe('validateOpenTelemetrySetup', () => { - afterEach(() => { - global.__SENTRY__ = {}; - cleanupOtel(); - vi.clearAllMocks(); - }); - - it('works with correct setup', () => { - const errorSpy = vi.spyOn(debug, 'error').mockImplementation(() => {}); - const warnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); - - vi.spyOn(SentryOpentelemetry, 'openTelemetrySetupCheck').mockImplementation(() => { - return ['SentryContextManager', 'SentryPropagator', 'SentrySampler']; - }); - - validateOpenTelemetrySetup(); - - expect(errorSpy).toHaveBeenCalledTimes(0); - expect(warnSpy).toHaveBeenCalledTimes(0); - }); - - it('works with missing setup, without tracing', () => { - const errorSpy = vi.spyOn(debug, 'error').mockImplementation(() => {}); - const warnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); - - vi.spyOn(SentryOpentelemetry, 'openTelemetrySetupCheck').mockImplementation(() => { - return []; - }); - - validateOpenTelemetrySetup(); - - // Without tracing, this is expected only twice - expect(errorSpy).toHaveBeenCalledTimes(2); - expect(warnSpy).toHaveBeenCalledTimes(1); - - expect(errorSpy).toBeCalledWith(expect.stringContaining('You have to set up the SentryContextManager.')); - expect(errorSpy).toBeCalledWith(expect.stringContaining('You have to set up the SentryPropagator.')); - expect(warnSpy).toBeCalledWith(expect.stringContaining('You have to set up the SentrySampler.')); - }); - - it('works with missing setup, with tracing', () => { - const errorSpy = vi.spyOn(debug, 'error').mockImplementation(() => {}); - const warnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); - - vi.spyOn(SentryOpentelemetry, 'openTelemetrySetupCheck').mockImplementation(() => { - return []; - }); - - init({ dsn: PUBLIC_DSN, skipOpenTelemetrySetup: true, tracesSampleRate: 1 }); - - validateOpenTelemetrySetup(); - - expect(errorSpy).toHaveBeenCalledTimes(3); - expect(warnSpy).toHaveBeenCalledTimes(1); - - expect(errorSpy).toBeCalledWith(expect.stringContaining('You have to set up the SentryContextManager.')); - expect(errorSpy).toBeCalledWith(expect.stringContaining('You have to set up the SentryPropagator.')); - expect(errorSpy).toBeCalledWith(expect.stringContaining('You have to set up the SentrySpanProcessor.')); - expect(warnSpy).toBeCalledWith(expect.stringContaining('You have to set up the SentrySampler.')); - }); - - // Regression test for https://github.com/getsentry/sentry-javascript/issues/15558 - it('accepts an undefined transport', () => { - init({ dsn: PUBLIC_DSN, transport: undefined }); - }); -}); diff --git a/packages/node/test/sdk/initOtel.test.ts b/packages/node/test/sdk/initOtel.test.ts deleted file mode 100644 index a373d733457b..000000000000 --- a/packages/node/test/sdk/initOtel.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { debug } from '@sentry/core'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { _clampSpanProcessorTimeout } from '../../src/sdk/initOtel'; - -describe('_clampSpanProcessorTimeout', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('works with undefined', () => { - const debugWarnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); - const timeout = _clampSpanProcessorTimeout(undefined); - expect(timeout).toBe(undefined); - expect(debugWarnSpy).not.toHaveBeenCalled(); - }); - - it('works with positive number', () => { - const debugWarnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); - const timeout = _clampSpanProcessorTimeout(10); - expect(timeout).toBe(10); - expect(debugWarnSpy).not.toHaveBeenCalled(); - }); - - it('works with 0', () => { - const debugWarnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); - const timeout = _clampSpanProcessorTimeout(0); - expect(timeout).toBe(undefined); - expect(debugWarnSpy).toHaveBeenCalledTimes(1); - expect(debugWarnSpy).toHaveBeenCalledWith( - '`maxSpanWaitDuration` must be a positive number, using default value instead.', - ); - }); - - it('works with negative number', () => { - const debugWarnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); - const timeout = _clampSpanProcessorTimeout(-10); - expect(timeout).toBe(undefined); - expect(debugWarnSpy).toHaveBeenCalledTimes(1); - expect(debugWarnSpy).toHaveBeenCalledWith( - '`maxSpanWaitDuration` must be a positive number, using default value instead.', - ); - }); - - it('works with -Infinity', () => { - const debugWarnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); - const timeout = _clampSpanProcessorTimeout(-Infinity); - expect(timeout).toBe(undefined); - expect(debugWarnSpy).toHaveBeenCalledTimes(1); - expect(debugWarnSpy).toHaveBeenCalledWith( - '`maxSpanWaitDuration` must be a positive number, using default value instead.', - ); - }); - - it('works with Infinity', () => { - const debugWarnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); - const timeout = _clampSpanProcessorTimeout(Infinity); - expect(timeout).toBe(1_000_000); - expect(debugWarnSpy).toHaveBeenCalledTimes(1); - expect(debugWarnSpy).toHaveBeenCalledWith('`maxSpanWaitDuration` is too high, using the maximum value of 1000000'); - }); - - it('works with large number', () => { - const debugWarnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); - const timeout = _clampSpanProcessorTimeout(1_000_000_000); - expect(timeout).toBe(1_000_000); - expect(debugWarnSpy).toHaveBeenCalledTimes(1); - expect(debugWarnSpy).toHaveBeenCalledWith('`maxSpanWaitDuration` is too high, using the maximum value of 1000000'); - }); - - it('works with NaN', () => { - const debugWarnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); - const timeout = _clampSpanProcessorTimeout(NaN); - expect(timeout).toBe(undefined); - expect(debugWarnSpy).toHaveBeenCalledTimes(1); - expect(debugWarnSpy).toHaveBeenCalledWith( - '`maxSpanWaitDuration` must be a positive number, using default value instead.', - ); - }); -}); diff --git a/packages/opentelemetry/README.md b/packages/opentelemetry/README.md index 3fc8413e6144..eb9665d9d555 100644 --- a/packages/opentelemetry/README.md +++ b/packages/opentelemetry/README.md @@ -28,7 +28,6 @@ Note that `@sentry/opentelemetry` depends on the following peer dependencies: - `@opentelemetry/api` version `1.0.0` or greater - `@opentelemetry/core` version `1.0.0` or greater -- `@opentelemetry/sdk-trace-base` version `1.0.0` or greater, or a package that implements that, like `@opentelemetry/sdk-node`. ## Usage @@ -101,18 +100,9 @@ const span = trace.getTracer('example').startSpan('work'); span.end(); ``` -In `@sentry/node`, this is the default tracer provider. To use the full OpenTelemetry SDK `BasicTracerProvider` -instead, opt out with: +In `@sentry/node`, this is the default tracer provider. -```js -Sentry.init({ - dsn: 'xxx', - openTelemetryBasicTracerProvider: true, -}); -``` - -Providing `openTelemetrySpanProcessors` also falls back to the full OpenTelemetry SDK provider, since custom span -processors require the SDK span pipeline. The `SentryTracerProvider` does not handle OpenTelemetry logs and metrics. +The `SentryTracerProvider` does not handle OpenTelemetry logs and metrics. ## Links diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index fe8bab714e74..10e8ad797db8 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -46,13 +46,11 @@ }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/core": "^1.30.1 || ^2.1.0", - "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" + "@opentelemetry/core": "^1.30.1 || ^2.1.0" }, "devDependencies": { "@opentelemetry/api": "^1.9.1", - "@opentelemetry/core": "^2.9.0", - "@opentelemetry/sdk-trace-base": "^2.9.0" + "@opentelemetry/core": "^2.9.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/opentelemetry/src/asyncLocalStorageContextManager.ts b/packages/opentelemetry/src/asyncLocalStorageContextManager.ts index a7b2abcad9c2..13c32e62d0ac 100644 --- a/packages/opentelemetry/src/asyncLocalStorageContextManager.ts +++ b/packages/opentelemetry/src/asyncLocalStorageContextManager.ts @@ -28,7 +28,6 @@ import { EventEmitter } from 'node:events'; import type { AsyncLocalStorageLookup } from './contextManager'; import { SENTRY_SCOPES_CONTEXT_KEY } from './constants'; import { buildContextWithSentryScopes } from './utils/buildContextWithSentryScopes'; -import { setIsSetup } from './utils/setupCheck'; import { getAsyncContextStrategy, getMainCarrier } from '@sentry/core'; type ListenerFn = (...args: unknown[]) => unknown; @@ -51,7 +50,6 @@ export class SentryAsyncLocalStorageContextManager implements ContextManager { private _wrapped = false; public constructor() { - setIsSetup('SentryContextManager'); // Pick the instance from the async context strategy // this should normally always be there, but if it is not for whatever reason, we fall back to a new instance this._asyncLocalStorage = diff --git a/packages/opentelemetry/src/contextManager.ts b/packages/opentelemetry/src/contextManager.ts index f1c3228c5dfa..6027782e4588 100644 --- a/packages/opentelemetry/src/contextManager.ts +++ b/packages/opentelemetry/src/contextManager.ts @@ -2,7 +2,6 @@ import type { AsyncLocalStorage } from 'node:async_hooks'; import type { Context, ContextManager } from '@opentelemetry/api'; import { SENTRY_SCOPES_CONTEXT_KEY } from './constants'; import { buildContextWithSentryScopes } from './utils/buildContextWithSentryScopes'; -import { setIsSetup } from './utils/setupCheck'; export type AsyncLocalStorageLookup = { asyncLocalStorage: AsyncLocalStorage; @@ -38,10 +37,6 @@ export function wrapContextManagerClass(ClientClass: ClassConstructor): WrappedClassConstructor { // @ts-expect-error We just assume that this is non-abstract, if you pass in an abstract class this would make it non-abstract class OpenTelemetryClient extends ClientClass implements OpenTelemetryClientInterface { - public traceProvider: OpenTelemetryTracerProvider | undefined; + public traceProvider: SentryTracerProvider | undefined; private _tracer: Tracer | undefined; public constructor(...args: any[]) { diff --git a/packages/opentelemetry/src/exports.ts b/packages/opentelemetry/src/exports.ts index 7c3d992dae92..a6fb01dde13f 100644 --- a/packages/opentelemetry/src/exports.ts +++ b/packages/opentelemetry/src/exports.ts @@ -7,14 +7,7 @@ export { wrapClientClass } from './custom/client'; export { getScopesFromContext } from './utils/contextData'; -export { - spanHasAttributes, - spanHasEvents, - spanHasKind, - spanHasName, - spanHasParentId, - spanHasStatus, -} from './utils/spanTypes'; +export { spanHasKind } from './utils/spanTypes'; // Re-export this for backwards compatibility (this used to be a different implementation) export { getDynamicSamplingContextFromSpan } from '@sentry/core'; @@ -41,14 +34,9 @@ export { setupEventContextTrace } from './setupEventContextTrace'; export { wrapContextManagerClass } from './contextManager'; export { SentryPropagator, shouldPropagateTraceForUrl } from './propagator'; -export { SentrySpanProcessor } from './spanProcessor'; -export { SentrySampler, wrapSamplingDecision } from './sampler'; export { applyOtelSpanData } from './applyOtelSpanData'; export { backfillStreamedSpanDataFromOtel } from './utils/backfillStreamedSpanData'; export { SentryTracerProvider } from './tracerProvider'; -export type { OpenTelemetryTracerProvider } from './types'; - -export { openTelemetrySetupCheck, setIsSetup } from './utils/setupCheck'; export { getSentryResource } from './resource'; diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts index c22fde1fe750..2397b3768ddf 100644 --- a/packages/opentelemetry/src/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -26,7 +26,6 @@ import { DEBUG_BUILD } from './debug-build'; import { getScopesFromContext, setScopesOnContext } from './utils/contextData'; import { getSampledForPropagation, getSamplingDecision } from './utils/getSamplingDecision'; import { makeTraceState } from './utils/makeTraceState'; -import { setIsSetup } from './utils/setupCheck'; /** * Injects and extracts `sentry-trace` and `baggage` headers from carriers. @@ -37,7 +36,6 @@ export class SentryPropagator extends W3CBaggagePropagator { public constructor() { super(); - setIsSetup('SentryPropagator'); // We're caching results so we don't have to recompute regexp every time we create a request. this._urlMatchesTargetsMap = new LRUMap(100); diff --git a/packages/opentelemetry/src/resource.ts b/packages/opentelemetry/src/resource.ts index f93404aa9404..6500528502b0 100644 --- a/packages/opentelemetry/src/resource.ts +++ b/packages/opentelemetry/src/resource.ts @@ -16,8 +16,7 @@ const SEMRESATTRS_SERVICE_NAMESPACE = 'service.namespace'; type RawResourceAttribute = [string, AttributeValue | undefined]; /** - * Minimal Resource implementation that satisfies the OpenTelemetry Resource interface - * used by BasicTracerProvider, without depending on `@opentelemetry/resources`. + * Minimal Resource implementation that satisfies the OpenTelemetry Resource interface. */ class SentryResource { private _attributes: Attributes; diff --git a/packages/opentelemetry/src/sampler.ts b/packages/opentelemetry/src/sampler.ts deleted file mode 100644 index ad532a8e4ff9..000000000000 --- a/packages/opentelemetry/src/sampler.ts +++ /dev/null @@ -1,342 +0,0 @@ -/* eslint-disable complexity */ -import type { Context, Span, TraceState as TraceStateInterface } from '@opentelemetry/api'; -import { isSpanContextValid, SpanKind, trace } from '@opentelemetry/api'; -import { TraceState } from './utils/TraceState'; -import type { Sampler, SamplingResult } from '@opentelemetry/sdk-trace-base'; -import { SamplingDecision } from '@opentelemetry/sdk-trace-base'; -import { HTTP_METHOD, HTTP_REQUEST_METHOD, HTTP_URL, URL_FULL } from '@sentry/conventions/attributes'; -import type { Client, SpanAttributes } from '@sentry/core'; -import { - _INTERNAL_safeMathRandom, - baggageHeaderToDynamicSamplingContext, - debug, - hasSpansEnabled, - hasSpanStreamingEnabled, - parseSampleRate, - sampleSpan, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, - shouldIgnoreSpan, -} from '@sentry/core'; -import { - SENTRY_TRACE_STATE_DSC, - SENTRY_TRACE_STATE_CHILD_IGNORED, - SENTRY_TRACE_STATE_SAMPLE_RAND, - SENTRY_TRACE_STATE_SAMPLE_RATE, - SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, - SENTRY_TRACE_STATE_SEGMENT_IGNORED, - SENTRY_TRACE_STATE_URL, -} from './constants'; -import { DEBUG_BUILD } from './debug-build'; -import { getScopesFromContext } from './utils/contextData'; -import { getSamplingDecision } from './utils/getSamplingDecision'; -import { inferSpanData } from './utils/parseSpanDescription'; -import { setIsSetup } from './utils/setupCheck'; - -/** - * A custom OTEL sampler that uses Sentry sampling rates to make its decision - */ -export class SentrySampler implements Sampler { - private _client: Client; - private _isSpanStreaming: boolean; - - public constructor(client: Client) { - this._client = client; - this._isSpanStreaming = hasSpanStreamingEnabled(client); - setIsSetup('SentrySampler'); - } - - /** @inheritDoc */ - public shouldSample( - context: Context, - traceId: string, - spanName: string, - spanKind: SpanKind, - spanAttributes: SpanAttributes, - _links: unknown, - ): SamplingResult { - const options = this._client.getOptions(); - const { ignoreSpans } = options; - - const parentSpan = getValidSpan(context); - const parentContext = parentSpan?.spanContext(); - - if (!hasSpansEnabled(options)) { - return wrapSamplingDecision({ decision: undefined, context, spanAttributes }); - } - - // `HTTP_REQUEST_METHOD` is the new attribute, but we still support the old one, `HTTP_METHOD`, for now. - // eslint-disable-next-line typescript/no-deprecated - const maybeSpanHttpMethod = spanAttributes[HTTP_METHOD] || spanAttributes[HTTP_REQUEST_METHOD]; - - // If we have a http.client span that has no local parent, we never want to sample it - // but we want to leave downstream sampling decisions up to the server. - // Exception: when span streaming is enabled, we always emit these spans. - if (spanKind === SpanKind.CLIENT && maybeSpanHttpMethod && (!parentSpan || parentContext?.isRemote)) { - if (!this._isSpanStreaming) { - this._client.recordDroppedEvent('no_parent_span', 'span'); - return wrapSamplingDecision({ decision: undefined, context, spanAttributes }); - } - } - - const parentSampled = parentSpan ? getParentSampled(parentSpan, traceId, spanName) : undefined; - const isRootSpan = !parentSpan || parentContext?.isRemote; - - // We only sample based on parameters (like tracesSampleRate or tracesSampler) for root spans (which is done in sampleSpan). - // Non-root-spans simply inherit the sampling decision from their parent. - if (!isRootSpan) { - if (this._isSpanStreaming) { - // `ignoreSpans` is only applied at span start for streamed spans. - // Static spans/transactions get filtered at transaction end. - // Likewise, we only record client outcomes for child spans when streaming - if (parentSampled) { - if (ignoreSpans?.length) { - const { description: inferredChildName, op: childOp } = inferSpanData(spanName, spanAttributes); - if ( - shouldIgnoreSpan( - { - description: inferredChildName, - op: spanAttributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] ?? childOp, - attributes: spanAttributes, - }, - ignoreSpans, - ) - ) { - this._client.recordDroppedEvent('ignored', 'span'); - return wrapSamplingDecision({ - decision: SamplingDecision.NOT_RECORD, - context, - spanAttributes, - ignoredChildSpan: true, - }); - } - } - } - - if (!parentSampled) { - const parentSegmentIgnored = parentContext?.traceState?.get(SENTRY_TRACE_STATE_SEGMENT_IGNORED) === '1'; - this._client.recordDroppedEvent(parentSegmentIgnored ? 'ignored' : 'sample_rate', 'span'); - } - } - - return wrapSamplingDecision({ - decision: parentSampled ? SamplingDecision.RECORD_AND_SAMPLED : SamplingDecision.NOT_RECORD, - context, - spanAttributes, - }); - } - - // We want to pass the inferred name & attributes to the sampler method - const { description: inferredSpanName, data: inferredAttributes, op } = inferSpanData(spanName, spanAttributes); - - const mergedAttributes = { - ...inferredAttributes, - ...spanAttributes, - }; - - if (op) { - mergedAttributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] = op; - } - - if ( - this._isSpanStreaming && - ignoreSpans?.length && - shouldIgnoreSpan( - { - description: inferredSpanName, - op: mergedAttributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] ?? op, - attributes: mergedAttributes, - }, - ignoreSpans, - ) - ) { - this._client.recordDroppedEvent('ignored', 'span'); - return wrapSamplingDecision({ - decision: SamplingDecision.NOT_RECORD, - context, - spanAttributes, - ignoredSegmentSpan: true, - }); - } - - const mutableSamplingDecision = { decision: true }; - this._client.emit( - 'beforeSampling', - { - spanAttributes: mergedAttributes, - spanName: inferredSpanName, - parentSampled: parentSampled, - parentContext: parentContext, - }, - mutableSamplingDecision, - ); - if (!mutableSamplingDecision.decision) { - return wrapSamplingDecision({ decision: undefined, context, spanAttributes }); - } - - const { isolationScope } = getScopesFromContext(context) ?? {}; - - const dscString = parentContext?.traceState ? parentContext.traceState.get(SENTRY_TRACE_STATE_DSC) : undefined; - const dsc = dscString ? baggageHeaderToDynamicSamplingContext(dscString) : undefined; - - const sampleRand = parseSampleRate(dsc?.sample_rand) ?? _INTERNAL_safeMathRandom(); - - const [sampled, sampleRate, localSampleRateWasApplied] = sampleSpan( - options, - { - name: inferredSpanName, - attributes: mergedAttributes, - normalizedRequest: isolationScope?.getScopeData().sdkProcessingMetadata.normalizedRequest, - parentSampled, - parentSampleRate: parseSampleRate(dsc?.sample_rate), - }, - sampleRand, - ); - - const method = `${maybeSpanHttpMethod}`.toUpperCase(); - if (method === 'OPTIONS' || method === 'HEAD') { - DEBUG_BUILD && debug.log(`[Tracing] Not sampling span because HTTP method is '${method}' for ${spanName}`); - - return wrapSamplingDecision({ - decision: SamplingDecision.NOT_RECORD, - context, - spanAttributes, - sampleRand, - downstreamTraceSampleRate: 0, // we don't want to sample anything in the downstream trace either - }); - } - - if ( - !sampled && - // We check for `parentSampled === undefined` because we only want to record client reports for spans that are trace roots (ie. when there was incoming trace) - parentSampled === undefined - ) { - DEBUG_BUILD && debug.log('[Tracing] Discarding root span because its trace was not chosen to be sampled.'); - this._client.recordDroppedEvent('sample_rate', this._isSpanStreaming ? 'span' : 'transaction'); - } - - return { - ...wrapSamplingDecision({ - decision: sampled ? SamplingDecision.RECORD_AND_SAMPLED : SamplingDecision.NOT_RECORD, - context, - spanAttributes, - sampleRand, - downstreamTraceSampleRate: localSampleRateWasApplied ? sampleRate : undefined, - }), - attributes: { - // We set the sample rate on the span when a local sample rate was applied to better understand how traces were sampled in Sentry - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: localSampleRateWasApplied ? sampleRate : undefined, - }, - }; - } - - /** Returns the sampler name or short description with the configuration. */ - public toString(): string { - return 'SentrySampler'; - } -} - -function getParentSampled(parentSpan: Span, traceId: string, spanName: string): boolean | undefined { - const parentContext = parentSpan.spanContext(); - - // Only inherit sample rate if `traceId` is the same - // Note for testing: `isSpanContextValid()` checks the format of the traceId/spanId, so we need to pass valid ones - if (isSpanContextValid(parentContext) && parentContext.traceId === traceId) { - if (parentContext.isRemote) { - const parentSampled = getSamplingDecision(parentSpan.spanContext()); - DEBUG_BUILD && - debug.log(`[Tracing] Inheriting remote parent's sampled decision for ${spanName}: ${parentSampled}`); - return parentSampled; - } - - const parentSampled = getSamplingDecision(parentContext); - DEBUG_BUILD && debug.log(`[Tracing] Inheriting parent's sampled decision for ${spanName}: ${parentSampled}`); - return parentSampled; - } - - return undefined; -} - -/** - * Wrap a sampling decision with data that Sentry needs to work properly with it. - * If you pass `decision: undefined`, it will be treated as `NOT_RECORDING`, but in contrast to passing `NOT_RECORDING` - * it will not propagate this decision to downstream Sentry SDKs. - */ -export function wrapSamplingDecision({ - decision, - context, - spanAttributes, - sampleRand, - downstreamTraceSampleRate, - ignoredChildSpan, - ignoredSegmentSpan, -}: { - decision: SamplingDecision | undefined; - context: Context; - spanAttributes: SpanAttributes; - sampleRand?: number; - downstreamTraceSampleRate?: number; - // flags for ignored streamed spans (only set for span streaming) - ignoredChildSpan?: boolean; - ignoredSegmentSpan?: boolean; -}): SamplingResult { - let traceState = getBaseTraceState(context, spanAttributes); - - // We will override the propagated sample rate downstream when - // - the tracesSampleRate is applied - // - the tracesSampler is invoked - // Since unsampled OTEL spans (NonRecordingSpans) cannot hold attributes we need to store this on the (trace)context. - if (downstreamTraceSampleRate !== undefined) { - traceState = traceState.set(SENTRY_TRACE_STATE_SAMPLE_RATE, `${downstreamTraceSampleRate}`); - } - - if (sampleRand !== undefined) { - traceState = traceState.set(SENTRY_TRACE_STATE_SAMPLE_RAND, `${sampleRand}`); - } - - if (ignoredChildSpan) { - traceState = traceState.set(SENTRY_TRACE_STATE_CHILD_IGNORED, '1'); - } - - if (ignoredSegmentSpan) { - traceState = traceState.set(SENTRY_TRACE_STATE_SEGMENT_IGNORED, '1'); - } - - // If the decision is undefined, we treat it as NOT_RECORDING, but we don't propagate this decision to downstream SDKs - // Which is done by not setting `SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING` traceState - if (decision == undefined) { - return { decision: SamplingDecision.NOT_RECORD, traceState }; - } - - if (decision === SamplingDecision.NOT_RECORD) { - return { decision, traceState: traceState.set(SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, '1') }; - } - - return { decision, traceState }; -} - -function getBaseTraceState(context: Context, spanAttributes: SpanAttributes): TraceStateInterface { - const parentSpan = trace.getSpan(context); - const parentContext = parentSpan?.spanContext(); - - let traceState = parentContext?.traceState || new TraceState(); - - // We always keep the URL on the trace state, so we can access it in the propagator - // `URL_FULL` is the new attribute, but we still support the old one, `HTTP_URL`, for now. - // eslint-disable-next-line typescript/no-deprecated - const url = spanAttributes[HTTP_URL] || spanAttributes[URL_FULL]; - if (url && typeof url === 'string') { - traceState = traceState.set(SENTRY_TRACE_STATE_URL, url); - } - - return traceState; -} - -/** - * If the active span is invalid, we want to ignore it as parent. - * This aligns with how otel tracers and default samplers handle these cases. - */ -function getValidSpan(context: Context): Span | undefined { - const span = trace.getSpan(context); - return span && isSpanContextValid(span.spanContext()) ? span : undefined; -} diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts deleted file mode 100644 index 47c8a3d38f5f..000000000000 --- a/packages/opentelemetry/src/spanExporter.ts +++ /dev/null @@ -1,468 +0,0 @@ -/* eslint-disable max-lines */ -import type { Span } from '@opentelemetry/api'; -import { SpanKind } from '@opentelemetry/api'; -import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; -import { HTTP_RESPONSE_STATUS_CODE, HTTP_STATUS_CODE } from '@sentry/conventions/attributes'; -import type { - SpanAttributes, - SpanJSON, - SpanOrigin, - TraceContext, - TransactionEvent, - TransactionSource, -} from '@sentry/core'; -import { - _INTERNAL_safeDateNow, - captureEvent, - convertSpanLinksForEnvelope, - debounce, - debug, - getCapturedScopesOnSpan, - getDynamicSamplingContextFromSpan, - getStatusMessage, - LRUMap, - SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - spanKindToName, - spanTimeInputToSeconds, - timedEventsToMeasurements, -} from '@sentry/core'; -import { DEBUG_BUILD } from './debug-build'; -import { SEMANTIC_ATTRIBUTE_SENTRY_PARENT_IS_REMOTE } from './semanticAttributes'; -import { getParentSpanId } from './utils/getParentSpanId'; -import { getRequestSpanData } from './utils/getRequestSpanData'; -import type { SpanNode } from './utils/groupSpansWithParents'; -import { getLocalParentId, groupSpansWithParents } from './utils/groupSpansWithParents'; -import { mapStatus } from './utils/mapStatus'; -import { parseSpanDescription } from './utils/parseSpanDescription'; - -type SpanNodeCompleted = SpanNode & { span: ReadableSpan }; - -const MAX_SPAN_COUNT = 1000; -const DEFAULT_TIMEOUT = 300; // 5 min -const SENT_SPANS_MAX_SIZE = 10000; - -interface FinishedSpanBucket { - timestampInS: number; - spans: Set; -} - -/** - * A Sentry-specific exporter that converts OpenTelemetry Spans to Sentry Spans & Transactions. - */ -export class SentrySpanExporter { - /* - * A quick explanation on the buckets: We do bucketing of finished spans for efficiency. This span exporter is - * accumulating spans until a root span is encountered and then it flushes all the spans that are descendants of that - * root span. Because it is totally in the realm of possibilities that root spans are never finished, and we don't - * want to accumulate spans indefinitely in memory, we need to periodically evacuate spans. Naively we could simply - * store the spans in an array and each time a new span comes in we could iterate through the entire array and - * evacuate all spans that have an end-timestamp that is older than our limit. This could get quite expensive because - * we would have to iterate a potentially large number of spans every time we evacuate. We want to avoid these large - * bursts of computation. - * - * Instead we go for a bucketing approach and put spans into buckets, based on what second - * (modulo the time limit) the span was put into the exporter. With buckets, when we decide to evacuate, we can - * iterate through the bucket entries instead, which have an upper bound of items, making the evacuation much more - * efficient. Cleaning up also becomes much more efficient since it simply involves de-referencing a bucket within the - * bucket array, and letting garbage collection take care of the rest. - */ - private _finishedSpanBuckets: (FinishedSpanBucket | undefined)[]; - private _finishedSpanBucketSize: number; - private _spansToBucketEntry: WeakMap; - private _lastCleanupTimestampInS: number; - private _sentSpans: LRUMap; - /* Internally, we use a debounced flush to give some wiggle room to the span processor to accumulate more spans. */ - private _debouncedFlush: ReturnType; - - public constructor(options?: { - /** Lower bound of time in seconds until spans that are buffered but have not been sent as part of a transaction get cleared from memory. */ - timeout?: number; - }) { - this._finishedSpanBucketSize = options?.timeout || DEFAULT_TIMEOUT; - this._finishedSpanBuckets = new Array(this._finishedSpanBucketSize).fill(undefined); - this._lastCleanupTimestampInS = Math.floor(_INTERNAL_safeDateNow() / 1000); - this._spansToBucketEntry = new WeakMap(); - this._sentSpans = new LRUMap(SENT_SPANS_MAX_SIZE); - this._debouncedFlush = debounce(this.flush.bind(this), 1, { maxWait: 100 }); - } - - /** - * Export a single span. - * This is called by the span processor whenever a span is ended. - */ - public export(span: ReadableSpan): void { - const currentTimestampInS = Math.floor(_INTERNAL_safeDateNow() / 1000); - - if (this._lastCleanupTimestampInS !== currentTimestampInS) { - let droppedSpanCount = 0; - this._finishedSpanBuckets.forEach((bucket, i) => { - if (bucket && bucket.timestampInS <= currentTimestampInS - this._finishedSpanBucketSize) { - droppedSpanCount += bucket.spans.size; - this._finishedSpanBuckets[i] = undefined; - } - }); - if (droppedSpanCount > 0) { - DEBUG_BUILD && - debug.log( - `SpanExporter dropped ${droppedSpanCount} spans because they were pending for more than ${this._finishedSpanBucketSize} seconds.`, - ); - } - this._lastCleanupTimestampInS = currentTimestampInS; - } - - const currentBucketIndex = currentTimestampInS % this._finishedSpanBucketSize; - const currentBucket = this._finishedSpanBuckets[currentBucketIndex] || { - timestampInS: currentTimestampInS, - spans: new Set(), - }; - this._finishedSpanBuckets[currentBucketIndex] = currentBucket; - currentBucket.spans.add(span); - this._spansToBucketEntry.set(span, currentBucket); - - // If the span doesn't have a local parent ID (it's a root span), we're gonna flush all the ended spans - const localParentId = getLocalParentId(span); - if (!localParentId || this._sentSpans.get(localParentId)) { - this._debouncedFlush(); - } - } - - /** - * Try to flush any pending spans immediately. - * This is called internally by the exporter (via _debouncedFlush), - * but can also be triggered externally if we force-flush. - */ - public flush(): void { - const finishedSpans = this._finishedSpanBuckets.flatMap(bucket => (bucket ? Array.from(bucket.spans) : [])); - - const sentSpans = this._maybeSend(finishedSpans); - - const sentSpanCount = sentSpans.size; - const remainingOpenSpanCount = finishedSpans.length - sentSpanCount; - DEBUG_BUILD && - debug.log( - `SpanExporter exported ${sentSpanCount} spans, ${remainingOpenSpanCount} spans are waiting for their parent spans to finish`, - ); - - for (const span of sentSpans) { - this._sentSpans.set(span.spanContext().spanId, 1); - const bucketEntry = this._spansToBucketEntry.get(span); - if (bucketEntry) { - bucketEntry.spans.delete(span); - } - } - - // Cancel a pending debounced flush, if there is one - // This can be relevant if we directly flush, circumventing the debounce - // in that case, we want to cancel any pending debounced flush - this._debouncedFlush.cancel(); - } - - /** - * Clear the exporter. - * This is called when the span processor is shut down. - */ - public clear(): void { - this._finishedSpanBuckets = this._finishedSpanBuckets.fill(undefined); - this._sentSpans.clear(); - this._debouncedFlush.cancel(); - } - - /** - * Send the given spans, but only if they are part of a finished transaction. - * - * Returns the sent spans. - * Spans remain unsent when their parent span is not yet finished. - * This will happen regularly, as child spans are generally finished before their parents. - * But it _could_ also happen because, for whatever reason, a parent span was lost. - * In this case, we'll eventually need to clean this up. - */ - private _maybeSend(spans: ReadableSpan[]): Set { - const grouped = groupSpansWithParents(spans); - const sentSpans = new Set(); - - const rootNodes = this._getCompletedRootNodes(grouped); - - for (const root of rootNodes) { - const span = root.span; - sentSpans.add(span); - const transactionEvent = createTransactionForOtelSpan(span); - - // Add an attribute to the transaction event to indicate that this transaction is an orphaned transaction - if (root.parentNode && this._sentSpans.get(root.parentNode.id)) { - const traceData = transactionEvent.contexts?.trace?.data; - if (traceData) { - traceData['sentry.parent_span_already_sent'] = true; - } - } - - // We'll recursively add all the child spans to this array - const spans = transactionEvent.spans || []; - - let hasGenAiSpans = false; - for (const child of root.children) { - if (createAndFinishSpanForOtelSpan(child, spans, sentSpans)) { - hasGenAiSpans = true; - } - } - - // spans.sort() mutates the array, but we do not use this anymore after this point - // so we can safely mutate it here - transactionEvent.spans = - spans.length > MAX_SPAN_COUNT - ? spans.sort((a, b) => a.start_timestamp - b.start_timestamp).slice(0, MAX_SPAN_COUNT) - : spans; - - if (hasGenAiSpans) { - transactionEvent.sdkProcessingMetadata = { - ...transactionEvent.sdkProcessingMetadata, - hasGenAiSpans: true, - }; - } - - const measurements = timedEventsToMeasurements(span.events); - if (measurements) { - transactionEvent.measurements = measurements; - } - - captureEvent(transactionEvent); - } - - return sentSpans; - } - - /** Check if a node is a completed root node or a node whose parent has already been sent */ - private _nodeIsCompletedRootNodeOrHasSentParent(node: SpanNode): node is SpanNodeCompleted { - return !!node.span && (!node.parentNode || !!this._sentSpans.get(node.parentNode.id)); - } - - /** Get all completed root nodes from a list of nodes */ - private _getCompletedRootNodes(nodes: SpanNode[]): SpanNodeCompleted[] { - // TODO: We should be able to remove the explicit `node is SpanNodeCompleted` type guard - // once we stop supporting TS < 5.5 - return nodes.filter((node): node is SpanNodeCompleted => this._nodeIsCompletedRootNodeOrHasSentParent(node)); - } -} - -function parseSpan(span: ReadableSpan): { op?: string; origin?: SpanOrigin; source?: TransactionSource } { - const attributes = span.attributes; - - const origin = attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined; - const op = attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] as string | undefined; - const source = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] as TransactionSource | undefined; - - return { origin, op, source }; -} - -/** Exported only for tests. */ -export function createTransactionForOtelSpan(span: ReadableSpan): TransactionEvent { - const { op, description, data, origin = 'manual', source } = getSpanData(span); - const capturedSpanScopes = getCapturedScopesOnSpan(span as unknown as Span); - - const sampleRate = span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE] as number | undefined; - - const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: sampleRate, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin, - ...data, - ...removeSentryAttributes(span.attributes), - }; - - const { links } = span; - const { traceId: trace_id, spanId: span_id } = span.spanContext(); - - // If parentSpanIdFromTraceState is defined at all, we want it to take precedence - // In that case, an empty string should be interpreted as "no parent span id", - // even if `span.parentSpanId` is set - // this is the case when we are starting a new trace, where we have a virtual span based on the propagationContext - // We only want to continue the traceId in this case, but ignore the parent span - const parent_span_id = getParentSpanId(span); - - const status = mapStatus(span); - - const traceContext: TraceContext = { - parent_span_id, - span_id, - trace_id, - data: attributes, - origin, - op, - status: getStatusMessage(status), // As per protocol, span status is allowed to be undefined - links: convertSpanLinksForEnvelope(links), - }; - - const statusCode = attributes[HTTP_RESPONSE_STATUS_CODE]; - const responseContext = typeof statusCode === 'number' ? { response: { status_code: statusCode } } : undefined; - - const transactionEvent: TransactionEvent = { - contexts: { - trace: traceContext, - otel: { - resource: span.resource.attributes, - }, - ...responseContext, - }, - spans: [], - start_timestamp: spanTimeInputToSeconds(span.startTime), - timestamp: spanTimeInputToSeconds(span.endTime), - transaction: description, - type: 'transaction', - sdkProcessingMetadata: { - capturedSpanScope: capturedSpanScopes.scope, - capturedSpanIsolationScope: capturedSpanScopes.isolationScope, - sampleRate, - dynamicSamplingContext: getDynamicSamplingContextFromSpan(span as unknown as Span), - }, - ...(source && { - transaction_info: { - source, - }, - }), - }; - - return transactionEvent; -} - -/** - * Returns `true` if this span or any descendant is a gen_ai span. - */ -function createAndFinishSpanForOtelSpan(node: SpanNode, spans: SpanJSON[], sentSpans: Set): boolean { - const span = node.span; - - if (span) { - sentSpans.add(span); - } - - const shouldDrop = !span; - - // If this span should be dropped, we still want to create spans for the children of this - if (shouldDrop) { - let hasGenAiSpans = false; - node.children.forEach(child => { - if (createAndFinishSpanForOtelSpan(child, spans, sentSpans)) { - hasGenAiSpans = true; - } - }); - return hasGenAiSpans; - } - - const span_id = span.spanContext().spanId; - const trace_id = span.spanContext().traceId; - const parentSpanId = getParentSpanId(span); - - const { attributes, startTime, endTime, links } = span; - - const { op, description, data, origin = 'manual' } = getSpanData(span); - const allData = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op, - ...removeSentryAttributes(attributes), - ...data, - }; - - const status = mapStatus(span); - - const spanJSON: SpanJSON = { - span_id, - trace_id, - data: allData, - description, - parent_span_id: parentSpanId, - start_timestamp: spanTimeInputToSeconds(startTime), - // This is [0,0] by default in OTEL, in which case we want to interpret this as no end time - timestamp: spanTimeInputToSeconds(endTime) || undefined, - status: getStatusMessage(status), // As per protocol, span status is allowed to be undefined - op, - origin, - measurements: timedEventsToMeasurements(span.events), - links: convertSpanLinksForEnvelope(links), - }; - - spans.push(spanJSON); - - let hasGenAiSpans = !!op?.startsWith('gen_ai.'); - node.children.forEach(child => { - if (createAndFinishSpanForOtelSpan(child, spans, sentSpans)) { - hasGenAiSpans = true; - } - }); - return hasGenAiSpans; -} - -function getSpanData(span: ReadableSpan): { - data: Record; - op?: string; - description: string; - source?: TransactionSource; - origin?: SpanOrigin; -} { - const { op: definedOp, source: definedSource, origin } = parseSpan(span); - const { op: inferredOp, description, source: inferredSource, data: inferredData } = parseSpanDescription(span); - - const op = definedOp || inferredOp; - const source = definedSource || inferredSource; - - const data = { ...inferredData, ...getData(span) }; - - return { - op, - description, - source, - origin, - data, - }; -} - -/** - * Remove custom `sentry.` attributes we do not need to send. - * These are more carrier attributes we use inside of the SDK, we do not need to send them to the API. - */ -function removeSentryAttributes(data: Record): Record { - const cleanedData = { ...data }; - - /* eslint-disable @typescript-eslint/no-dynamic-delete */ - delete cleanedData[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]; - delete cleanedData[SEMANTIC_ATTRIBUTE_SENTRY_PARENT_IS_REMOTE]; - delete cleanedData[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]; - /* eslint-enable @typescript-eslint/no-dynamic-delete */ - - return cleanedData; -} - -function getData(span: ReadableSpan): Record { - const attributes = span.attributes; - const data: Record = {}; - - if (span.kind !== SpanKind.INTERNAL) { - data['sentry.kind'] = spanKindToName(span.kind); - } - - // eslint-disable-next-line typescript/no-deprecated - const maybeHttpStatusCodeAttribute = attributes[HTTP_STATUS_CODE]; - if (maybeHttpStatusCodeAttribute) { - data[HTTP_RESPONSE_STATUS_CODE] = maybeHttpStatusCodeAttribute as string; - } - - const requestData = getRequestSpanData(span); - - if (requestData.url) { - data.url = requestData.url; - } - - // TODO(v11): emit `url.query`/`url.fragment` (OTel-standard, no leading `?`/`#`) and drop - // this stripping + the `http.query`/`http.fragment` attributes. `http.query` is actually specced to - // keep the leading `?`, so stripping diverges from our own conventions; `url.query` sidesteps it. - if (requestData['http.query']) { - data['http.query'] = requestData['http.query'].slice(1); - } - if (requestData['http.fragment']) { - data['http.fragment'] = requestData['http.fragment'].slice(1); - } - - return data; -} diff --git a/packages/opentelemetry/src/spanProcessor.ts b/packages/opentelemetry/src/spanProcessor.ts deleted file mode 100644 index f22ef13767f0..000000000000 --- a/packages/opentelemetry/src/spanProcessor.ts +++ /dev/null @@ -1,108 +0,0 @@ -import type { Context } from '@opentelemetry/api'; -import { ROOT_CONTEXT, trace } from '@opentelemetry/api'; -import type { ReadableSpan, Span, SpanProcessor as SpanProcessorInterface } from '@opentelemetry/sdk-trace-base'; -import type { Client } from '@sentry/core'; -import { - addChildSpanToSpan, - getClient, - getDefaultCurrentScope, - getDefaultIsolationScope, - hasSpanStreamingEnabled, - logSpanEnd, - logSpanStart, - setCapturedScopesOnSpan, -} from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_PARENT_IS_REMOTE } from './semanticAttributes'; -import { SentrySpanExporter } from './spanExporter'; -import { backfillStreamedSpanDataFromOtel } from './utils/backfillStreamedSpanData'; -import { getScopesFromContext } from './utils/contextData'; -import { setIsSetup } from './utils/setupCheck'; -/** - * Converts OpenTelemetry Spans to Sentry Spans and sends them to Sentry via - * the Sentry SDK. - */ -export class SentrySpanProcessor implements SpanProcessorInterface { - private _exporter: SentrySpanExporter; - private _client: Client | undefined; - private _unsubscribePreprocessSpan: (() => void) | undefined = undefined; - - public constructor(options?: { timeout?: number; client?: Client }) { - setIsSetup('SentrySpanProcessor'); - this._exporter = new SentrySpanExporter(options); - this._client = options?.client ?? getClient(); - - if (this._client && hasSpanStreamingEnabled(this._client)) { - // Streamed spans skip the exporter, so they don't get op/source/name inferred from OTel - // semantic conventions. We backfill them here, reusing the same inference as the exporter. - // This runs as a `preprocessSpan` subscriber so the inferred data is available to all - // `processSpan`/`processSegmentSpan` hooks (incl. integrations) and `beforeSendSpan`. - this._unsubscribePreprocessSpan = this._client.on('preprocessSpan', backfillStreamedSpanDataFromOtel); - } - } - - /** - * @inheritDoc - */ - public async forceFlush(): Promise { - this._exporter.flush(); - } - - /** - * @inheritDoc - */ - public async shutdown(): Promise { - this._unsubscribePreprocessSpan?.(); - this._exporter.clear(); - } - - /** - * @inheritDoc - */ - public onStart(span: Span, parentContext: Context): void { - // This is a reliable way to get the parent span - because this is exactly how the parent is identified in the OTEL SDK - const parentSpan = trace.getSpan(parentContext); - - let scopes = getScopesFromContext(parentContext); - - // We need access to the parent span in order to be able to move up the span tree for breadcrumbs - if (parentSpan && !parentSpan.spanContext().isRemote) { - addChildSpanToSpan(parentSpan, span); - } - - // We need this in the span exporter - if (parentSpan?.spanContext().isRemote) { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_PARENT_IS_REMOTE, true); - } - - // The root context does not have scopes stored, so we check for this specifically - // As fallback we attach the global scopes - if (parentContext === ROOT_CONTEXT) { - scopes = { - scope: getDefaultCurrentScope(), - isolationScope: getDefaultIsolationScope(), - }; - } - - // We need the scope at time of span creation in order to apply it to the event when the span is finished - if (scopes) { - setCapturedScopesOnSpan(span, scopes.scope, scopes.isolationScope); - } - - logSpanStart(span); - - this._client?.emit('spanStart', span); - } - - /** @inheritDoc */ - public onEnd(span: Span & ReadableSpan): void { - logSpanEnd(span); - - this._client?.emit('spanEnd', span); - - if (this._client && hasSpanStreamingEnabled(this._client)) { - this._client.emit('afterSpanEnd', span); - } else { - this._exporter.export(span); - } - } -} diff --git a/packages/opentelemetry/src/tracer.ts b/packages/opentelemetry/src/tracer.ts index 69dab7b9ef1c..e34b0a96d49d 100644 --- a/packages/opentelemetry/src/tracer.ts +++ b/packages/opentelemetry/src/tracer.ts @@ -1,5 +1,5 @@ import type { Context, Span as OpenTelemetrySpan, SpanOptions, Tracer } from '@opentelemetry/api'; -import { context, trace } from '@opentelemetry/api'; +import { context, isSpanContextValid, trace } from '@opentelemetry/api'; import { isTracingSuppressed } from '@opentelemetry/core'; import { _INTERNAL_safeMathRandom, @@ -30,7 +30,11 @@ export class SentryTracer implements Tracer { /** @inheritdoc */ public startSpan(name: string, options: SpanOptions = {}, ctx?: Context): OpenTelemetrySpan { const parentContext = ctx || context.active(); - const parentSpan = options.root ? undefined : trace.getSpan(parentContext); + const parentSpanCandidate = options.root ? undefined : trace.getSpan(parentContext); + // Ignore an invalid parent (e.g. a malformed incoming trace/span id) and start a fresh trace, + // matching the OTel SDK sampler's `getValidSpan` behaviour. + const parentSpan = + parentSpanCandidate && isSpanContextValid(parentSpanCandidate.spanContext()) ? parentSpanCandidate : undefined; if (isTracingSuppressed(parentContext)) { return this._createNonRecordingSpan(parentSpan); diff --git a/packages/opentelemetry/src/types.ts b/packages/opentelemetry/src/types.ts index 1061d7e00730..a4862346c2c9 100644 --- a/packages/opentelemetry/src/types.ts +++ b/packages/opentelemetry/src/types.ts @@ -1,15 +1,10 @@ -import type { Span as WriteableSpan, SpanKind, Tracer, TracerProvider } from '@opentelemetry/api'; -import type { BasicTracerProvider, ReadableSpan } from '@opentelemetry/sdk-trace-base'; -import type { Scope, Span, StartSpanOptions } from '@sentry/core'; - -export interface OpenTelemetryTracerProvider extends TracerProvider { - forceFlush(): Promise; - shutdown(): Promise; -} +import type { SpanKind, Tracer } from '@opentelemetry/api'; +import type { Scope, StartSpanOptions } from '@sentry/core'; +import type { SentryTracerProvider } from './tracerProvider'; export interface OpenTelemetryClient { tracer: Tracer; - traceProvider: BasicTracerProvider | OpenTelemetryTracerProvider | undefined; + traceProvider: SentryTracerProvider | undefined; } export interface OpenTelemetrySpanContext extends StartSpanOptions { @@ -17,17 +12,6 @@ export interface OpenTelemetrySpanContext extends StartSpanOptions { kind?: SpanKind; } -/** - * The base `Span` type is basically a `WriteableSpan`. - * There are places where we basically want to allow passing _any_ span, - * so in these cases we type this as `AbstractSpan` which could be either a regular `Span` or a `ReadableSpan`. - * You'll have to make sure to check relevant fields before accessing them. - * - * Note that technically, the `Span` exported from `@opentelemetry/sdk-trace-base` matches this, - * but we cannot be 100% sure that we are actually getting such a span, so this type is more defensive. - */ -export type AbstractSpan = WriteableSpan | ReadableSpan | Span; - export interface CurrentScopes { scope: Scope; isolationScope: Scope; diff --git a/packages/opentelemetry/src/utils/getParentSpanId.ts b/packages/opentelemetry/src/utils/getParentSpanId.ts deleted file mode 100644 index 63f4ab0b80f4..000000000000 --- a/packages/opentelemetry/src/utils/getParentSpanId.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; - -/** - * Get the parent span id from a span. - * In OTel v1, the parent span id is accessed as `parentSpanId` - * In OTel v2, the parent span id is accessed as `spanId` on the `parentSpanContext` - */ -export function getParentSpanId(span: ReadableSpan): string | undefined { - if ('parentSpanId' in span) { - return span.parentSpanId as string | undefined; - } else if ('parentSpanContext' in span) { - return (span.parentSpanContext as { spanId?: string } | undefined)?.spanId; - } - - return undefined; -} diff --git a/packages/opentelemetry/src/utils/getRequestSpanData.ts b/packages/opentelemetry/src/utils/getRequestSpanData.ts index e94e8fb2a362..898d65fe294b 100644 --- a/packages/opentelemetry/src/utils/getRequestSpanData.ts +++ b/packages/opentelemetry/src/utils/getRequestSpanData.ts @@ -1,26 +1,21 @@ -import type { Span } from '@opentelemetry/api'; -import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; import { HTTP_METHOD, HTTP_REQUEST_METHOD, HTTP_URL, URL_FULL } from '@sentry/conventions/attributes'; -import type { SanitizedRequestData } from '@sentry/core'; +import type { SanitizedRequestData, Span } from '@sentry/core'; import { getSanitizedUrlString, parseUrl } from '@sentry/core'; -import { spanHasAttributes } from './spanTypes'; +import { getSpanAttributes } from './spanTypes'; /** - * Get sanitizied request data from an OTEL span. + * Get sanitized request data from a span. */ -export function getRequestSpanData(span: Span | ReadableSpan): Partial { - // The base `Span` type has no `attributes`, so we need to guard here against that - if (!spanHasAttributes(span)) { - return {}; - } +export function getRequestSpanData(span: Span): Partial { + const attributes = getSpanAttributes(span); // eslint-disable-next-line typescript/no-deprecated - const maybeUrlAttribute = (span.attributes[URL_FULL] || span.attributes[HTTP_URL]) as string | undefined; + const maybeUrlAttribute = (attributes[URL_FULL] || attributes[HTTP_URL]) as string | undefined; const data: Partial = { url: maybeUrlAttribute, // eslint-disable-next-line typescript/no-deprecated - 'http.method': (span.attributes[HTTP_REQUEST_METHOD] || span.attributes[HTTP_METHOD]) as string | undefined, + 'http.method': (attributes[HTTP_REQUEST_METHOD] || attributes[HTTP_METHOD]) as string | undefined, }; // Default to GET if URL is set but method is not diff --git a/packages/opentelemetry/src/utils/groupSpansWithParents.ts b/packages/opentelemetry/src/utils/groupSpansWithParents.ts deleted file mode 100644 index fcbb635d4b2b..000000000000 --- a/packages/opentelemetry/src/utils/groupSpansWithParents.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; -import { SEMANTIC_ATTRIBUTE_SENTRY_PARENT_IS_REMOTE } from '../semanticAttributes'; -import { getParentSpanId } from './getParentSpanId'; - -export interface SpanNode { - id: string; - span?: ReadableSpan; - parentNode?: SpanNode | undefined; - children: SpanNode[]; -} - -type SpanMap = Map; - -/** - * This function runs through a list of OTEL Spans, and wraps them in an `SpanNode` - * where each node holds a reference to their parent node. - */ -export function groupSpansWithParents(spans: ReadableSpan[]): SpanNode[] { - const nodeMap: SpanMap = new Map(); - - for (const span of spans) { - createOrUpdateSpanNodeAndRefs(nodeMap, span); - } - - return Array.from(nodeMap, function ([_id, spanNode]) { - return spanNode; - }); -} - -/** - * This returns the _local_ parent ID - `parentId` on the span may point to a remote span. - */ -export function getLocalParentId(span: ReadableSpan): string | undefined { - const parentIsRemote = span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_PARENT_IS_REMOTE] === true; - // If the parentId is the trace parent ID, we pretend it's undefined - // As this means the parent exists somewhere else - return !parentIsRemote ? getParentSpanId(span) : undefined; -} - -function createOrUpdateSpanNodeAndRefs(nodeMap: SpanMap, span: ReadableSpan): void { - const id = span.spanContext().spanId; - const parentId = getLocalParentId(span); - - if (!parentId) { - createOrUpdateNode(nodeMap, { id, span, children: [] }); - return; - } - - // Else make sure to create parent node as well - // Note that the parent may not know it's parent _yet_, this may be updated in a later pass - const parentNode = createOrGetParentNode(nodeMap, parentId); - const node = createOrUpdateNode(nodeMap, { id, span, parentNode, children: [] }); - parentNode.children.push(node); -} - -function createOrGetParentNode(nodeMap: SpanMap, id: string): SpanNode { - const existing = nodeMap.get(id); - - if (existing) { - return existing; - } - - return createOrUpdateNode(nodeMap, { id, children: [] }); -} - -function createOrUpdateNode(nodeMap: SpanMap, spanNode: SpanNode): SpanNode { - const existing = nodeMap.get(spanNode.id); - - // If span is already set, nothing to do here - if (existing?.span) { - return existing; - } - - // If it exists but span is not set yet, we update it - if (existing && !existing.span) { - existing.span = spanNode.span; - existing.parentNode = spanNode.parentNode; - return existing; - } - - // Else, we create a new one... - nodeMap.set(spanNode.id, spanNode); - return spanNode; -} diff --git a/packages/opentelemetry/src/utils/isSentryRequest.ts b/packages/opentelemetry/src/utils/isSentryRequest.ts index 012c769b2f5b..d9da86579341 100644 --- a/packages/opentelemetry/src/utils/isSentryRequest.ts +++ b/packages/opentelemetry/src/utils/isSentryRequest.ts @@ -1,19 +1,13 @@ import { HTTP_URL, URL_FULL } from '@sentry/conventions/attributes'; +import type { Span } from '@sentry/core'; import { getClient, isSentryRequestUrl } from '@sentry/core'; -import type { AbstractSpan } from '../types'; -import { spanHasAttributes } from './spanTypes'; +import { getSpanAttributes } from './spanTypes'; /** - * - * @param otelSpan Checks whether a given OTEL Span is an http request to sentry. - * @returns boolean + * Checks whether a given span is an http request to Sentry. */ -export function isSentryRequestSpan(span: AbstractSpan): boolean { - if (!spanHasAttributes(span)) { - return false; - } - - const { attributes } = span; +export function isSentryRequestSpan(span: Span): boolean { + const attributes = getSpanAttributes(span); // `URL_FULL` is the new attribute, but we still support the old one, `HTTP_URL`, for now. // eslint-disable-next-line typescript/no-deprecated diff --git a/packages/opentelemetry/src/utils/mapStatus.ts b/packages/opentelemetry/src/utils/mapStatus.ts index 53a0311eee3e..bafdbb09d6db 100644 --- a/packages/opentelemetry/src/utils/mapStatus.ts +++ b/packages/opentelemetry/src/utils/mapStatus.ts @@ -1,9 +1,6 @@ -import { SpanStatusCode } from '@opentelemetry/api'; import { HTTP_RESPONSE_STATUS_CODE, HTTP_STATUS_CODE, RPC_GRPC_STATUS_CODE } from '@sentry/conventions/attributes'; import type { SpanAttributes, SpanStatus } from '@sentry/core'; -import { getSpanStatusFromHttpCode, isStatusErrorMessageValid, SPAN_STATUS_ERROR, SPAN_STATUS_OK } from '@sentry/core'; -import type { AbstractSpan } from '../types'; -import { spanHasAttributes, spanHasStatus } from './spanTypes'; +import { getSpanStatusFromHttpCode, SPAN_STATUS_ERROR } from '@sentry/core'; // canonicalCodesGrpcMap maps some GRPC codes to Sentry's span statuses. See description in grpc documentation. const canonicalGrpcErrorCodesMap: Record = { @@ -25,49 +22,6 @@ const canonicalGrpcErrorCodesMap: Record = { '16': 'unauthenticated', } as const; -/** - * Get a Sentry span status from an otel span. - */ -export function mapStatus(span: AbstractSpan): SpanStatus { - const attributes = spanHasAttributes(span) ? span.attributes : {}; - const status = spanHasStatus(span) ? span.status : undefined; - - if (status) { - // Since span status OK is not set by default, we give it priority: https://opentelemetry.io/docs/concepts/signals/traces/#span-status - if (status.code === SpanStatusCode.OK) { - return { code: SPAN_STATUS_OK }; - // If the span is already marked as erroneous we return that exact status - } else if (status.code === SpanStatusCode.ERROR) { - if (typeof status.message === 'undefined') { - const inferredStatus = inferStatusFromAttributes(attributes); - if (inferredStatus) { - return inferredStatus; - } - } - - if (status.message && isStatusErrorMessageValid(status.message)) { - return { code: SPAN_STATUS_ERROR, message: status.message }; - } else { - return { code: SPAN_STATUS_ERROR, message: 'internal_error' }; - } - } - } - - // If the span status is UNSET, we try to infer it from HTTP or GRPC status codes. - const inferredStatus = inferStatusFromAttributes(attributes); - - if (inferredStatus) { - return inferredStatus; - } - - // We default to setting the spans status to ok. - if (status?.code === SpanStatusCode.UNSET) { - return { code: SPAN_STATUS_OK }; - } else { - return { code: SPAN_STATUS_ERROR, message: 'unknown_error' }; - } -} - export function inferStatusFromAttributes(attributes: SpanAttributes): SpanStatus | undefined { // If the span status is UNSET, we try to infer it from HTTP or GRPC status codes. diff --git a/packages/opentelemetry/src/utils/parseSpanDescription.ts b/packages/opentelemetry/src/utils/parseSpanDescription.ts index 545bd4fffd0c..b12c4147f0db 100644 --- a/packages/opentelemetry/src/utils/parseSpanDescription.ts +++ b/packages/opentelemetry/src/utils/parseSpanDescription.ts @@ -26,8 +26,6 @@ import { stripUrlQueryAndFragment, } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION } from '../semanticAttributes'; -import type { AbstractSpan } from '../types'; -import { spanHasAttributes, spanHasName } from './spanTypes'; interface SpanDescription { op: string | undefined; @@ -103,21 +101,10 @@ export function inferSpanData(spanName: string, attributes: SpanAttributes): Spa * * Based on https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/7422ce2a06337f68a59b552b8c5a2ac125d6bae5/exporter/sentryexporter/sentry_exporter.go#L306 */ -export function parseSpanDescription(span: AbstractSpan): SpanDescription { - let attributes: Attributes; - let name: string; - - // TODO(v11): Once the OTel SDK provider is removed and SentryTracerProvider is the only path, - // every span is a native Sentry span — drop this `spanHasAttributes` (OTel ReadableSpan) branch - // and keep only the `spanToJSON()` path below. - if (spanHasAttributes(span)) { - attributes = span.attributes; - name = spanHasName(span) ? span.name : ''; - } else { - const json = typeof (span as Span).spanContext === 'function' ? spanToJSON(span as Span) : undefined; - attributes = json?.data || {}; - name = spanHasName(span) ? span.name : json?.description || ''; - } +export function parseSpanDescription(span: Span): SpanDescription { + const json = spanToJSON(span); + const attributes = json.data; + const name = json.description || ''; return inferSpanData(name, attributes); } diff --git a/packages/opentelemetry/src/utils/setupCheck.ts b/packages/opentelemetry/src/utils/setupCheck.ts deleted file mode 100644 index 4ac3e07db1fe..000000000000 --- a/packages/opentelemetry/src/utils/setupCheck.ts +++ /dev/null @@ -1,23 +0,0 @@ -type OpenTelemetryElement = - | 'SentrySpanProcessor' - | 'SentryContextManager' - | 'SentryPropagator' - | 'SentrySampler' - | 'SentryTracerProvider'; - -const setupElements = new Set(); - -/** Get all the OpenTelemetry elements that have been set up. */ -export function openTelemetrySetupCheck(): OpenTelemetryElement[] { - return Array.from(setupElements); -} - -/** Mark an OpenTelemetry element as setup. */ -export function setIsSetup(element: OpenTelemetryElement): void { - setupElements.add(element); -} - -/** Only exported for tests. */ -export function clearOpenTelemetrySetupCheck(): void { - setupElements.clear(); -} diff --git a/packages/opentelemetry/src/utils/spanTypes.ts b/packages/opentelemetry/src/utils/spanTypes.ts index ea3f2c9dddc2..7e50bc0b26f0 100644 --- a/packages/opentelemetry/src/utils/spanTypes.ts +++ b/packages/opentelemetry/src/utils/spanTypes.ts @@ -1,73 +1,19 @@ -import type { SpanKind, SpanStatus } from '@opentelemetry/api'; -import type { ReadableSpan, TimedEvent } from '@opentelemetry/sdk-trace-base'; -import type { AbstractSpan } from '../types'; -import { isObjectLike } from '@sentry/core'; -import { getParentSpanId } from './getParentSpanId'; - -/** - * Check if a given span has attributes. - * This is necessary because the base `Span` type does not have attributes, - * so in places where we are passed a generic span, we need to check if we want to access them. - */ -export function spanHasAttributes( - span: SpanType, -): span is SpanType & { attributes: ReadableSpan['attributes'] } { - const castSpan = span as ReadableSpan; - return isObjectLike(castSpan.attributes); -} - -/** - * Check if a given span has a kind. - * This is necessary because the base `Span` type does not have a kind, - * so in places where we are passed a generic span, we need to check if we want to access it. - */ -export function spanHasKind(span: SpanType): span is SpanType & { kind: SpanKind } { - const castSpan = span as ReadableSpan; - return typeof castSpan.kind === 'number'; -} - -/** - * Check if a given span has a status. - * This is necessary because the base `Span` type does not have a status, - * so in places where we are passed a generic span, we need to check if we want to access it. - */ -export function spanHasStatus( - span: SpanType, -): span is SpanType & { status: SpanStatus } { - const castSpan = span as ReadableSpan; - return !!castSpan.status; -} - -/** - * Check if a given span has a name. - * This is necessary because the base `Span` type does not have a name, - * so in places where we are passed a generic span, we need to check if we want to access it. - */ -export function spanHasName(span: SpanType): span is SpanType & { name: string } { - const castSpan = span as ReadableSpan; - return !!castSpan.name; -} +import type { SpanKind } from '@opentelemetry/api'; +import type { Span, SpanAttributes } from '@sentry/core'; +import { spanToJSON } from '@sentry/core'; /** * Check if a given span has a kind. - * This is necessary because the base `Span` type does not have a kind, - * so in places where we are passed a generic span, we need to check if we want to access it. + * The span `kind` is stamped onto native Sentry spans by the `SentryTracerProvider` and is not part of + * the base `Span` type, so we check for it before accessing it (falling back to `SpanKind.INTERNAL`). */ -export function spanHasParentId( - span: SpanType, -): span is SpanType & { parentSpanId: string } { - const castSpan = span as ReadableSpan; - return !!getParentSpanId(castSpan); +export function spanHasKind(span: SpanType): span is SpanType & { kind: SpanKind } { + return typeof (span as { kind?: unknown }).kind === 'number'; } /** - * Check if a given span has events. - * This is necessary because the base `Span` type does not have events, - * so in places where we are passed a generic span, we need to check if we want to access it. + * Read a Sentry span's attributes. */ -export function spanHasEvents( - span: SpanType, -): span is SpanType & { events: TimedEvent[] } { - const castSpan = span as ReadableSpan; - return Array.isArray(castSpan.events); +export function getSpanAttributes(span: Span): SpanAttributes { + return spanToJSON(span).data; } diff --git a/packages/opentelemetry/test/asyncContextStrategy.test.ts b/packages/opentelemetry/test/asyncContextStrategy.test.ts index 55499676d912..1d32bf34e06c 100644 --- a/packages/opentelemetry/test/asyncContextStrategy.test.ts +++ b/packages/opentelemetry/test/asyncContextStrategy.test.ts @@ -1,5 +1,4 @@ import { context, trace, TraceFlags, type Context } from '@opentelemetry/api'; -import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import type { Scope } from '@sentry/core'; import { addChildSpanToSpan, @@ -20,23 +19,18 @@ import { setNodeOpenTelemetryContextAsyncContextStrategy } from '../src/nodeAsyn import { TraceState } from '../src/utils/TraceState'; import { setupOtel } from './helpers/initOtel'; import { cleanupOtel } from './helpers/mockSdkInit'; -import { getDefaultTestClientOptions, TestClient } from './helpers/TestClient'; describe('asyncContextStrategy', () => { - let provider: BasicTracerProvider | undefined; - beforeEach(() => { getCurrentScope().clear(); getIsolationScope().clear(); - const options = getDefaultTestClientOptions(); - const client = new TestClient(options); - [provider] = setupOtel(client); + setupOtel(); setOpenTelemetryContextAsyncContextStrategy(); }); afterEach(() => { - cleanupOtel(provider); + cleanupOtel(); }); afterAll(() => { diff --git a/packages/opentelemetry/test/helpers/initOtel.ts b/packages/opentelemetry/test/helpers/initOtel.ts index a080679c1e78..2bb8299572cb 100644 --- a/packages/opentelemetry/test/helpers/initOtel.ts +++ b/packages/opentelemetry/test/helpers/initOtel.ts @@ -1,15 +1,13 @@ import { context, diag, DiagLogLevel, propagation, trace } from '@opentelemetry/api'; -import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { debug, getClient } from '@sentry/core'; import { SentryAsyncLocalStorageContextManager } from '../../src/asyncLocalStorageContextManager'; import { DEBUG_BUILD } from '../../src/debug-build'; import { SentryPropagator } from '../../src/propagator'; import { getSentryResource } from '../../src/resource'; -import { SentrySampler } from '../../src/sampler'; import { setupEventContextTrace } from '../../src/setupEventContextTrace'; -import { SentrySpanProcessor } from '../../src/spanProcessor'; import { enhanceDscWithOpenTelemetryRootSpanName } from '../../src/utils/enhanceDscWithOpenTelemetryRootSpanName'; import type { TestClientInterface } from './TestClient'; +import { SentryTracerProvider } from '../../src/tracerProvider'; /** * Initialize OpenTelemetry for Node. @@ -43,25 +41,15 @@ export function initOtel(): void { setupEventContextTrace(client); enhanceDscWithOpenTelemetryRootSpanName(client); - const [provider, spanProcessor] = setupOtel(client); - client.traceProvider = provider; - client.spanProcessor = spanProcessor; + setupOtel(); } -/** Just exported for tests. */ -export function setupOtel(client: TestClientInterface): [BasicTracerProvider, SentrySpanProcessor] { - const spanProcessor = new SentrySpanProcessor(); - // Create and configure NodeTracerProvider - const provider = new BasicTracerProvider({ - sampler: new SentrySampler(client), - resource: getSentryResource('opentelemetry-test'), - forceFlushTimeoutMillis: 500, - spanProcessors: [spanProcessor], - }); +export function setupOtel(): void { + const provider = new SentryTracerProvider({ resource: getSentryResource('node') }); trace.setGlobalTracerProvider(provider); propagation.setGlobalPropagator(new SentryPropagator()); - context.setGlobalContextManager(new SentryAsyncLocalStorageContextManager()); - return [provider, spanProcessor]; + const ctxManager = new SentryAsyncLocalStorageContextManager(); + context.setGlobalContextManager(ctxManager); } diff --git a/packages/opentelemetry/test/helpers/mockSdkInit.ts b/packages/opentelemetry/test/helpers/mockSdkInit.ts index 91b1369eb928..3acbf4a1b56d 100644 --- a/packages/opentelemetry/test/helpers/mockSdkInit.ts +++ b/packages/opentelemetry/test/helpers/mockSdkInit.ts @@ -1,11 +1,7 @@ -import { context, propagation, ProxyTracerProvider, trace } from '@opentelemetry/api'; -import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; +import { context, propagation, trace } from '@opentelemetry/api'; import type { ClientOptions, Options } from '@sentry/core'; -import { flush, getClient, getCurrentScope, getGlobalScope, getIsolationScope } from '@sentry/core'; +import { flush, getCurrentScope, getGlobalScope, getIsolationScope } from '@sentry/core'; import { setOpenTelemetryContextAsyncContextStrategy } from '../../src/asyncContextStrategy'; -import { SentrySpanProcessor } from '../../src/spanProcessor'; -import type { OpenTelemetryClient } from '../../src/types'; -import { clearOpenTelemetrySetupCheck } from '../../src/utils/setupCheck'; import { initOtel } from './initOtel'; import { init as initTestClient } from './TestClient'; @@ -34,16 +30,7 @@ export function mockSdkInit(options?: Partial) { init({ dsn: PUBLIC_DSN, ...options }); } -export async function cleanupOtel(_provider?: BasicTracerProvider): Promise { - clearOpenTelemetrySetupCheck(); - - const provider = getProvider(_provider); - - if (provider) { - await provider.forceFlush(); - await provider.shutdown(); - } - +export async function cleanupOtel(): Promise { // Disable all globally registered APIs trace.disable(); context.disable(); @@ -51,31 +38,3 @@ export async function cleanupOtel(_provider?: BasicTracerProvider): Promise(); - if (!client) { - return undefined; - } - - const spanProcessor = client.spanProcessor; - if (spanProcessor instanceof SentrySpanProcessor) { - return spanProcessor; - } - - return undefined; -} - -export function getProvider(_provider?: BasicTracerProvider): BasicTracerProvider | undefined { - let provider = _provider || getClient()?.traceProvider || trace.getTracerProvider(); - - if (provider instanceof ProxyTracerProvider) { - provider = provider.getDelegate(); - } - - if (!(provider instanceof BasicTracerProvider)) { - return undefined; - } - - return provider; -} diff --git a/packages/opentelemetry/test/integration/scope.test.ts b/packages/opentelemetry/test/integration/scope.test.ts index 56acffb7921e..3c1bc308f1fb 100644 --- a/packages/opentelemetry/test/integration/scope.test.ts +++ b/packages/opentelemetry/test/integration/scope.test.ts @@ -53,7 +53,10 @@ describe('Integration | Scope', () => { scope2.setTag('tag3', 'val3'); startSpan({ name: 'outer' }, span => { - expect(getCapturedScopesOnSpan(span).scope).toBe(tracingEnabled ? scope2 : undefined); + // A recording root span starts a new trace, which forks the active scope, so the captured + // scope is a fork of `scope2`; a non-recording span captures the active scope directly. + // Either way it carries `scope2`'s data. + expect(getCapturedScopesOnSpan(span).scope?.getScopeData().tags).toEqual(scope2.getScopeData().tags); spanId = span.spanContext().spanId; traceId = span.spanContext().traceId; diff --git a/packages/opentelemetry/test/integration/transactions.test.ts b/packages/opentelemetry/test/integration/transactions.test.ts index 7280bc4d9b0c..d2c9966a8f29 100644 --- a/packages/opentelemetry/test/integration/transactions.test.ts +++ b/packages/opentelemetry/test/integration/transactions.test.ts @@ -4,19 +4,17 @@ import { TraceState } from '../../src/utils/TraceState'; import type { Event, TransactionEvent } from '@sentry/core'; import { addBreadcrumb, - debug, getClient, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, setTag, - startSpanManual, withIsolationScope, } from '@sentry/core'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { SENTRY_TRACE_STATE_DSC } from '../../src/constants'; import { startInactiveSpan, startSpan } from '../../src/trace'; import { makeTraceState } from '../../src/utils/makeTraceState'; -import { cleanupOtel, getSpanProcessor, mockSdkInit } from '../helpers/mockSdkInit'; +import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; import type { TestClientInterface } from '../helpers/TestClient'; describe('Integration | Transactions', () => { @@ -86,17 +84,6 @@ describe('Integration | Transactions', () => { { message: 'test breadcrumb 3', timestamp: 123456 }, ]); - expect(transaction.contexts?.otel).toEqual({ - resource: { - 'service.name': 'opentelemetry-test', - 'service.namespace': 'sentry', - 'service.version': expect.any(String), - 'telemetry.sdk.language': 'nodejs', - 'telemetry.sdk.name': 'opentelemetry', - 'telemetry.sdk.version': expect.any(String), - }, - }); - expect(transaction.contexts?.trace).toEqual({ data: { 'sentry.op': 'test op', @@ -112,7 +99,6 @@ describe('Integration | Transactions', () => { origin: 'auto.test', }); - expect(transaction.sdkProcessingMetadata?.sampleRate).toEqual(1); expect(transaction.sdkProcessingMetadata?.dynamicSamplingContext).toEqual({ environment: 'production', public_key: expect.any(String), @@ -430,301 +416,6 @@ describe('Integration | Transactions', () => { ]); }); - it('cleans up spans that are not flushed for over 5 mins', async () => { - const beforeSendTransaction = vi.fn(() => null); - - const now = Date.now(); - vi.useFakeTimers(); - vi.setSystemTime(now); - - const logs: unknown[] = []; - vi.spyOn(debug, 'log').mockImplementation(msg => logs.push(msg)); - - mockSdkInit({ tracesSampleRate: 1, beforeSendTransaction }); - - const spanProcessor = getSpanProcessor(); - - const exporter = spanProcessor ? spanProcessor['_exporter'] : undefined; - - if (!exporter) { - throw new Error('No exporter found, aborting test...'); - } - - void startSpan({ name: 'test name' }, async () => { - startInactiveSpan({ name: 'inner span 1' }).end(); - startInactiveSpan({ name: 'inner span 2' }).end(); - - // Pretend this is pending for 10 minutes - await new Promise(resolve => setTimeout(resolve, 10 * 60 * 1000)); - }); - - // Child-spans have been added to the exporter, but they are pending since they are waiting for their parent - const finishedSpans1 = []; - exporter['_finishedSpanBuckets'].forEach(bucket => { - if (bucket) { - finishedSpans1.push(...bucket.spans); - } - }); - expect(finishedSpans1.length).toBe(2); - expect(beforeSendTransaction).toHaveBeenCalledTimes(0); - - // Now wait for 5 mins - vi.advanceTimersByTime(5 * 60 * 1_000 + 1); - - // Adding another span will trigger the cleanup - startSpan({ name: 'other span' }, () => {}); - - vi.advanceTimersByTime(1); - - // Old spans have been cleared away - const finishedSpans2 = []; - exporter['_finishedSpanBuckets'].forEach(bucket => { - if (bucket) { - finishedSpans2.push(...bucket.spans); - } - }); - expect(finishedSpans2.length).toBe(0); - - // Called once for the 'other span' - expect(beforeSendTransaction).toHaveBeenCalledTimes(1); - - expect(logs).toEqual( - expect.arrayContaining([ - 'SpanExporter dropped 2 spans because they were pending for more than 300 seconds.', - 'SpanExporter exported 1 spans, 0 spans are waiting for their parent spans to finish', - ]), - ); - }); - - it('includes child spans that are finished in the same tick but after their parent span', async () => { - const now = Date.now(); - vi.useFakeTimers(); - vi.setSystemTime(now); - - const logs: unknown[] = []; - vi.spyOn(debug, 'log').mockImplementation(msg => logs.push(msg)); - - const transactions: Event[] = []; - - mockSdkInit({ - tracesSampleRate: 1, - beforeSendTransaction: event => { - transactions.push(event); - return null; - }, - }); - - const spanProcessor = getSpanProcessor(); - - const exporter = spanProcessor ? spanProcessor['_exporter'] : undefined; - - if (!exporter) { - throw new Error('No exporter found, aborting test...'); - } - - startSpanManual({ name: 'test name' }, async span => { - const subSpan = startInactiveSpan({ name: 'inner span 1' }); - subSpan.end(); - - const subSpan2 = startInactiveSpan({ name: 'inner span 2' }); - - span.end(); - subSpan2.end(); - }); - - vi.advanceTimersByTime(1); - - expect(transactions).toHaveLength(1); - expect(transactions[0]?.spans).toHaveLength(2); - - // No spans are pending - const finishedSpans = []; - exporter['_finishedSpanBuckets'].forEach(bucket => { - if (bucket) { - finishedSpans.push(...bucket.spans); - } - }); - expect(finishedSpans.length).toBe(0); - }); - - it('collects child spans that are finished within 5 minutes their parent span has been sent', async () => { - const timeout = 5 * 60 * 1000; - const now = Date.now(); - vi.useFakeTimers(); - vi.setSystemTime(now); - - const logs: unknown[] = []; - vi.spyOn(debug, 'log').mockImplementation(msg => logs.push(msg)); - - const transactions: Event[] = []; - - mockSdkInit({ - tracesSampleRate: 1, - beforeSendTransaction: event => { - transactions.push(event); - return null; - }, - }); - - const spanProcessor = getSpanProcessor(); - - const exporter = spanProcessor ? spanProcessor['_exporter'] : undefined; - - if (!exporter) { - throw new Error('No exporter found, aborting test...'); - } - - startSpanManual({ name: 'test name' }, async span => { - const subSpan = startInactiveSpan({ name: 'inner span 1' }); - subSpan.end(); - - const subSpan2 = startInactiveSpan({ name: 'inner span 2' }); - - span.end(); - - setTimeout(() => { - subSpan2.end(); - }, timeout - 2); - }); - - vi.advanceTimersByTime(timeout - 1); - - expect(transactions).toHaveLength(2); - expect(transactions[0]?.spans).toHaveLength(1); - - expect(transactions[0]?.transaction).toBe('test name'); - expect(transactions[0]?.contexts?.trace?.data).toEqual({ - 'sentry.origin': 'manual', - 'sentry.sample_rate': 1, - 'sentry.source': 'custom', - }); - - expect(transactions[1]?.transaction).toBe('inner span 2'); - expect(transactions[1]?.contexts?.trace?.data).toEqual({ - 'sentry.parent_span_already_sent': true, - 'sentry.origin': 'manual', - 'sentry.source': 'custom', - }); - - const finishedSpans: any = exporter['_finishedSpanBuckets'].flatMap(bucket => - bucket ? Array.from(bucket.spans) : [], - ); - expect(finishedSpans.length).toBe(0); - }); - - it('sends child spans that are finished after 5 minutes as orphaned transactions', async () => { - const timeout = 5 * 60 * 1000; - const now = Date.now(); - vi.useFakeTimers(); - vi.setSystemTime(now); - - const logs: unknown[] = []; - vi.spyOn(debug, 'log').mockImplementation(msg => logs.push(msg)); - - const transactions: Event[] = []; - - mockSdkInit({ - tracesSampleRate: 1, - beforeSendTransaction: event => { - transactions.push(event); - return null; - }, - }); - - startSpanManual({ name: 'test name' }, async span => { - const subSpan = startInactiveSpan({ name: 'inner span 1' }); - subSpan.end(); - - const subSpan2 = startInactiveSpan({ name: 'inner span 2' }); - - span.end(); - - setTimeout(() => { - subSpan2.end(); - }, timeout + 1); - }); - - vi.advanceTimersByTime(timeout + 2); - - expect(transactions).toHaveLength(2); - expect(transactions[0]?.spans).toHaveLength(1); - - expect(transactions[1]?.transaction).toBe('inner span 2'); - expect(transactions[1]?.contexts?.trace?.data).toEqual({ - 'sentry.parent_span_already_sent': true, - 'sentry.origin': 'manual', - 'sentry.source': 'custom', - }); - }); - - it('only considers sent spans, not finished spans, for flushing orphaned spans of sent spans', async () => { - const timeout = 5 * 60 * 1000; - const now = Date.now(); - vi.useFakeTimers(); - vi.setSystemTime(now); - - const logs: unknown[] = []; - vi.spyOn(debug, 'log').mockImplementation(msg => logs.push(msg)); - - const transactions: Event[] = []; - - mockSdkInit({ - tracesSampleRate: 1, - beforeSendTransaction: event => { - transactions.push(event); - return null; - }, - }); - - const spanProcessor = getSpanProcessor(); - - const exporter = spanProcessor ? spanProcessor['_exporter'] : undefined; - - if (!exporter) { - throw new Error('No exporter found, aborting test...'); - } - - /** - * This is our span structure: - * span 1 -------- - * span 2 --- - * span 3 - - * - * Where span 2 is finished before span 3 & span 1 - */ - - const [span1, span3] = startSpanManual({ name: 'span 1' }, span1 => { - const [span2, span3] = startSpanManual({ name: 'span 2' }, span2 => { - const span3 = startInactiveSpan({ name: 'span 3' }); - return [span2, span3]; - }); - - // End span 2 before span 3 - span2.end(); - - return [span1, span3]; - }); - - vi.advanceTimersByTime(1); - - // nothing should be sent yet, as span1 is not yet done - expect(transactions).toHaveLength(0); - - // Now finish span1, should be sent with only span2 but without span3, as that is not yet finished - span1.end(); - vi.advanceTimersByTime(1); - - expect(transactions).toHaveLength(1); - expect(transactions[0]?.spans).toHaveLength(1); - - // now finish span3, which should be sent as transaction too - span3.end(); - vi.advanceTimersByTime(timeout); - - expect(transactions).toHaveLength(2); - expect(transactions[1]?.spans).toHaveLength(0); - }); - it('uses & inherits DSC on span trace state', async () => { const transactionEvents: Event[] = []; const beforeSendTransaction = vi.fn(event => { @@ -764,18 +455,11 @@ describe('Integration | Transactions', () => { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task', }, }, - span => { - expect(span.spanContext().traceState?.get(SENTRY_TRACE_STATE_DSC)).toEqual(dscString); - + () => { const subSpan = startInactiveSpan({ name: 'inner span 1' }); - - expect(subSpan.spanContext().traceState?.get(SENTRY_TRACE_STATE_DSC)).toEqual(dscString); - subSpan.end(); - startSpan({ name: 'inner span 2' }, subSpan => { - expect(subSpan.spanContext().traceState?.get(SENTRY_TRACE_STATE_DSC)).toEqual(dscString); - }); + startSpan({ name: 'inner span 2' }, () => {}); }, ); }); diff --git a/packages/opentelemetry/test/sampler.test.ts b/packages/opentelemetry/test/sampler.test.ts deleted file mode 100644 index 0337c43e8113..000000000000 --- a/packages/opentelemetry/test/sampler.test.ts +++ /dev/null @@ -1,373 +0,0 @@ -import { context, SpanKind, trace, TraceFlags } from '@opentelemetry/api'; -import { TraceState } from '../src/utils/TraceState'; -import { SamplingDecision } from '@opentelemetry/sdk-trace-base'; -import { HTTP_REQUEST_METHOD, SENTRY_KIND } from '@sentry/conventions/attributes'; -import { generateSpanId, generateTraceId } from '@sentry/core'; -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { - SENTRY_TRACE_STATE_CHILD_IGNORED, - SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, - SENTRY_TRACE_STATE_SEGMENT_IGNORED, -} from '../src/constants'; -import { SentrySampler } from '../src/sampler'; -import { cleanupOtel } from './helpers/mockSdkInit'; -import { getDefaultTestClientOptions, TestClient } from './helpers/TestClient'; - -describe('SentrySampler', () => { - afterEach(async () => { - await cleanupOtel(); - }); - - it('samples negatively with tracesSampleRate=0 and records a sample_rate outcome', () => { - const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 0 })); - const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent'); - const sampler = new SentrySampler(client); - - const ctx = context.active(); - const traceId = generateTraceId(); - const spanName = 'test'; - const spanKind = SpanKind.INTERNAL; - const spanAttributes = {}; - const links = undefined; - - const actual = sampler.shouldSample(ctx, traceId, spanName, spanKind, spanAttributes, links); - expect(actual).toEqual( - expect.objectContaining({ - decision: SamplingDecision.NOT_RECORD, - attributes: { 'sentry.sample_rate': 0 }, - }), - ); - expect(actual.traceState?.get('sentry.sampled_not_recording')).toBe('1'); - expect(actual.traceState?.get('sentry.sample_rand')).toEqual(expect.any(String)); - expect(spyOnDroppedEvent).toHaveBeenCalledTimes(1); - expect(spyOnDroppedEvent).toHaveBeenCalledWith('sample_rate', 'transaction'); - - spyOnDroppedEvent.mockReset(); - }); - - it('samples a child span negatively based on tracesSampleRate=0', () => { - const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 0 })); - const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent'); - const sampler = new SentrySampler(client); - - const traceId = generateTraceId(); - const ctx = trace.setSpanContext(context.active(), { - spanId: generateSpanId(), - traceId, - traceFlags: 0, - traceState: new TraceState().set(SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, '1'), - }); - const spanName = 'test'; - const spanKind = SpanKind.INTERNAL; - const spanAttributes = {}; - const links = undefined; - - const actual = sampler.shouldSample(ctx, traceId, spanName, spanKind, spanAttributes, links); - expect(actual).toEqual({ - decision: SamplingDecision.NOT_RECORD, - traceState: new TraceState().set(SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, '1'), - }); - // Does not record a client outcome for child spans when in static trace lifecycle (i.e. transactions) - expect(spyOnDroppedEvent).not.toHaveBeenCalled(); - - spyOnDroppedEvent.mockReset(); - }); - - it('samples positively with tracesSampleRate=1', () => { - const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1 })); - const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent'); - const sampler = new SentrySampler(client); - - const ctx = context.active(); - const traceId = generateTraceId(); - const spanName = 'test'; - const spanKind = SpanKind.INTERNAL; - const spanAttributes = {}; - const links = undefined; - - const actual = sampler.shouldSample(ctx, traceId, spanName, spanKind, spanAttributes, links); - expect(actual).toEqual( - expect.objectContaining({ - decision: SamplingDecision.RECORD_AND_SAMPLED, - attributes: { 'sentry.sample_rate': 1 }, - }), - ); - expect(actual.traceState?.constructor.name).toBe('TraceState'); - expect(spyOnDroppedEvent).toHaveBeenCalledTimes(0); - - spyOnDroppedEvent.mockReset(); - }); - - it('defers sampling with traceSampleRate=undefined', () => { - const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: undefined })); - const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent'); - const sampler = new SentrySampler(client); - - const ctx = context.active(); - const traceId = generateTraceId(); - const spanName = 'test'; - const spanKind = SpanKind.INTERNAL; - const spanAttributes = {}; - const links = undefined; - - const actual = sampler.shouldSample(ctx, traceId, spanName, spanKind, spanAttributes, links); - expect(actual).toEqual({ - decision: SamplingDecision.NOT_RECORD, - traceState: new TraceState(), - }); - expect(spyOnDroppedEvent).toHaveBeenCalledTimes(0); - - spyOnDroppedEvent.mockReset(); - }); - - it('ignores local http client root spans and records no_parent_span client report', () => { - const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 0 })); - const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent'); - const sampler = new SentrySampler(client); - - const ctx = context.active(); - const traceId = generateTraceId(); - const spanName = 'test'; - const spanKind = SpanKind.CLIENT; - const spanAttributes = { - [HTTP_REQUEST_METHOD]: 'GET', - }; - const links = undefined; - - const actual = sampler.shouldSample(ctx, traceId, spanName, spanKind, spanAttributes, links); - expect(actual).toEqual({ - decision: SamplingDecision.NOT_RECORD, - traceState: new TraceState(), - }); - expect(spyOnDroppedEvent).toHaveBeenCalledTimes(1); - expect(spyOnDroppedEvent).toHaveBeenCalledWith('no_parent_span', 'span'); - - spyOnDroppedEvent.mockReset(); - }); - - describe('when span streaming is enabled', () => { - /* - For span streaming, we use the Sampler to "sample" spans based on `ignoreSpans`. In reality though, - we don't apply sampling options (rate, traces_sampler) but just filter spans via `ignoreSpans`. - The sampler allows us to modify context and tracestate to correctly propagate filtering decisions - to potential child spans (e.g. when a segment is ignored, so that all its children are also ignored). - */ - it('returns NOT_RECORD for root span matching ignoreSpans string pattern', () => { - const client = new TestClient( - getDefaultTestClientOptions({ tracesSampleRate: 1, traceLifecycle: 'stream', ignoreSpans: ['GET /health'] }), - ); - const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent'); - const sampler = new SentrySampler(client); - - const ctx = context.active(); - const traceId = generateTraceId(); - const spanName = 'GET /health'; - const spanKind = SpanKind.SERVER; - const spanAttributes = {}; - - const actual = sampler.shouldSample(ctx, traceId, spanName, spanKind, spanAttributes, undefined); - expect(actual.decision).toBe(SamplingDecision.NOT_RECORD); - expect(spyOnDroppedEvent).toHaveBeenCalledWith('ignored', 'span'); - expect(spyOnDroppedEvent).toHaveBeenCalledOnce(); - }); - - it('returns NOT_RECORD for root span matching ignoreSpans regex pattern', () => { - const client = new TestClient( - getDefaultTestClientOptions({ tracesSampleRate: 1, traceLifecycle: 'stream', ignoreSpans: [/health/] }), - ); - const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent'); - const sampler = new SentrySampler(client); - - const ctx = context.active(); - const traceId = generateTraceId(); - const spanName = 'GET /healthcheck'; - const spanKind = SpanKind.SERVER; - const spanAttributes = {}; - - const actual = sampler.shouldSample(ctx, traceId, spanName, spanKind, spanAttributes, undefined); - expect(actual.decision).toBe(SamplingDecision.NOT_RECORD); - expect(spyOnDroppedEvent).toHaveBeenCalledWith('ignored', 'span'); - expect(spyOnDroppedEvent).toHaveBeenCalledOnce(); - }); - - it('returns NOT_RECORD for root span matching ignoreSpans IgnoreSpanFilter with name and op', () => { - const client = new TestClient( - getDefaultTestClientOptions({ - tracesSampleRate: 1, - traceLifecycle: 'stream', - ignoreSpans: [{ name: 'GET /health', op: 'http.server' }], - }), - ); - const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent'); - const sampler = new SentrySampler(client); - - const ctx = context.active(); - const traceId = generateTraceId(); - const spanName = 'GET /health'; - const spanKind = SpanKind.SERVER; - const spanAttributes = { - [HTTP_REQUEST_METHOD]: 'GET', - [SENTRY_KIND]: 'server' as const, - }; - - const actual = sampler.shouldSample(ctx, traceId, spanName, spanKind, spanAttributes, undefined); - expect(actual.decision).toBe(SamplingDecision.NOT_RECORD); - expect(spyOnDroppedEvent).toHaveBeenCalledWith('ignored', 'span'); - expect(spyOnDroppedEvent).toHaveBeenCalledOnce(); - }); - - it("doesn't ignore root span that does not match ignoreSpans", () => { - const client = new TestClient( - getDefaultTestClientOptions({ tracesSampleRate: 1, traceLifecycle: 'stream', ignoreSpans: ['GET /health'] }), - ); - const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent'); - const sampler = new SentrySampler(client); - - const ctx = context.active(); - const traceId = generateTraceId(); - const spanName = 'GET /users'; - const spanKind = SpanKind.SERVER; - const spanAttributes = {}; - - const actual = sampler.shouldSample(ctx, traceId, spanName, spanKind, spanAttributes, undefined); - expect(actual.decision).toBe(SamplingDecision.RECORD_AND_SAMPLED); - expect(spyOnDroppedEvent).not.toHaveBeenCalled(); - }); - - it('returns NOT_RECORD with sentry.ignored traceState for child span matching ignoreSpans', () => { - const client = new TestClient( - getDefaultTestClientOptions({ - tracesSampleRate: 1, - traceLifecycle: 'stream', - ignoreSpans: ['middleware - expressInit'], - }), - ); - const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent'); - const sampler = new SentrySampler(client); - - const traceId = generateTraceId(); - const ctx = trace.setSpanContext(context.active(), { - traceId, - spanId: generateSpanId(), - traceFlags: TraceFlags.SAMPLED, - isRemote: false, - }); - - const actual = sampler.shouldSample(ctx, traceId, 'middleware - expressInit', SpanKind.INTERNAL, {}, undefined); - - expect(actual.decision).toBe(SamplingDecision.NOT_RECORD); - expect(actual.traceState?.get(SENTRY_TRACE_STATE_CHILD_IGNORED)).toBe('1'); - expect(spyOnDroppedEvent).toHaveBeenCalledWith('ignored', 'span'); - expect(spyOnDroppedEvent).toHaveBeenCalledOnce(); - }); - - it("doesn't set SENTRY_TRACE_STATE_CHILD_IGNORED for child span not matching ignoreSpans", () => { - const client = new TestClient( - getDefaultTestClientOptions({ - tracesSampleRate: 1, - traceLifecycle: 'stream', - ignoreSpans: ['middleware - expressInit'], - }), - ); - const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent'); - const sampler = new SentrySampler(client); - - const traceId = generateTraceId(); - const ctx = trace.setSpanContext(context.active(), { - traceId, - spanId: generateSpanId(), - traceFlags: TraceFlags.SAMPLED, - isRemote: false, - }); - - const actual = sampler.shouldSample(ctx, traceId, 'db.query SELECT 1', SpanKind.CLIENT, {}, undefined); - - expect(actual.decision).toBe(SamplingDecision.RECORD_AND_SAMPLED); - expect(actual.traceState?.get(SENTRY_TRACE_STATE_CHILD_IGNORED)).toBeUndefined(); - expect(spyOnDroppedEvent).not.toHaveBeenCalled(); - }); - - it('sets sentry.segment_ignored traceState for a segment span matching ignoreSpans', () => { - const client = new TestClient( - getDefaultTestClientOptions({ tracesSampleRate: 1, traceLifecycle: 'stream', ignoreSpans: ['GET /health'] }), - ); - const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent'); - const sampler = new SentrySampler(client); - - const ctx = context.active(); - const traceId = generateTraceId(); - const spanName = 'GET /health'; - const spanKind = SpanKind.SERVER; - const spanAttributes = {}; - - const actual = sampler.shouldSample(ctx, traceId, spanName, spanKind, spanAttributes, undefined); - expect(actual.decision).toBe(SamplingDecision.NOT_RECORD); - expect(actual.traceState?.get(SENTRY_TRACE_STATE_SEGMENT_IGNORED)).toBe('1'); - expect(actual.traceState?.get(SENTRY_TRACE_STATE_CHILD_IGNORED)).toBeUndefined(); - expect(spyOnDroppedEvent).toHaveBeenCalledWith('ignored', 'span'); - expect(spyOnDroppedEvent).toHaveBeenCalledOnce(); - }); - - it('records ignored outcome for child span of ignored segment', () => { - const client = new TestClient( - getDefaultTestClientOptions({ tracesSampleRate: 1, traceLifecycle: 'stream', ignoreSpans: ['GET /health'] }), - ); - const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent'); - const sampler = new SentrySampler(client); - - const traceId = generateTraceId(); - const ctx = trace.setSpanContext(context.active(), { - spanId: generateSpanId(), - traceId, - traceFlags: 0, - traceState: new TraceState() - .set(SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, '1') - .set(SENTRY_TRACE_STATE_SEGMENT_IGNORED, '1'), - }); - - const actual = sampler.shouldSample(ctx, traceId, 'db.query SELECT 1', SpanKind.CLIENT, {}, undefined); - expect(actual.decision).toBe(SamplingDecision.NOT_RECORD); - expect(spyOnDroppedEvent).toHaveBeenCalledOnce(); - expect(spyOnDroppedEvent).toHaveBeenCalledWith('ignored', 'span'); - }); - - it('records sample_rate outcome for child span of negatively sampled segment', () => { - // For span streaming, we also record a sample_rate outcome for a child span of a negatively sampled trace. - - const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 0, traceLifecycle: 'stream' })); - const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent'); - const sampler = new SentrySampler(client); - - const traceId = generateTraceId(); - const ctx = trace.setSpanContext(context.active(), { - spanId: generateSpanId(), - traceId, - traceFlags: 0, - traceState: new TraceState().set(SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, '1'), - }); - - const actual = sampler.shouldSample(ctx, traceId, 'db.query SELECT 1', SpanKind.CLIENT, {}, undefined); - expect(actual.decision).toBe(SamplingDecision.NOT_RECORD); - expect(spyOnDroppedEvent).toHaveBeenCalledTimes(1); - expect(spyOnDroppedEvent).toHaveBeenCalledWith('sample_rate', 'span'); - }); - - it('always emits streamed http.client spans without a local parent', () => { - const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1, traceLifecycle: 'stream' })); - const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent'); - const sampler = new SentrySampler(client); - - const ctx = context.active(); - const traceId = generateTraceId(); - const spanName = 'GET http://example.com/api'; - const spanKind = SpanKind.CLIENT; - const spanAttributes = { - [HTTP_REQUEST_METHOD]: 'GET', - }; - - const actual = sampler.shouldSample(ctx, traceId, spanName, spanKind, spanAttributes, undefined); - expect(actual.decision).toBe(SamplingDecision.RECORD_AND_SAMPLED); - expect(spyOnDroppedEvent).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/opentelemetry/test/spanExporter.test.ts b/packages/opentelemetry/test/spanExporter.test.ts deleted file mode 100644 index d84d326bac1f..000000000000 --- a/packages/opentelemetry/test/spanExporter.test.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { HTTP_RESPONSE_STATUS_CODE } from '@sentry/conventions/attributes'; -import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_OP, startInactiveSpan, startSpanManual } from '@sentry/core'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { createTransactionForOtelSpan } from '../src/spanExporter'; -import { cleanupOtel, mockSdkInit } from './helpers/mockSdkInit'; - -describe('createTransactionForOtelSpan', () => { - beforeEach(() => { - mockSdkInit({ - tracesSampleRate: 1, - }); - }); - - afterEach(async () => { - await cleanupOtel(); - }); - - it('works with a basic span', () => { - const span = startInactiveSpan({ name: 'test', startTime: 1733821670000 }); - span.end(1733821672000); - - const event = createTransactionForOtelSpan(span as any); - // we do not care about this here - delete event.sdkProcessingMetadata; - - expect(event).toEqual({ - contexts: { - trace: { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.source': 'custom', - 'sentry.sample_rate': 1, - 'sentry.origin': 'manual', - }, - origin: 'manual', - status: 'ok', - }, - otel: { - resource: { - 'service.name': 'opentelemetry-test', - 'telemetry.sdk.language': 'nodejs', - 'telemetry.sdk.name': 'opentelemetry', - 'telemetry.sdk.version': expect.any(String), - 'service.namespace': 'sentry', - 'service.version': SDK_VERSION, - }, - }, - }, - spans: [], - start_timestamp: 1733821670, - timestamp: 1733821672, - transaction: 'test', - type: 'transaction', - transaction_info: { source: 'custom' }, - }); - }); - - it('works with a http.server span', () => { - const span = startInactiveSpan({ - name: 'test', - startTime: 1733821670000, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', - [HTTP_RESPONSE_STATUS_CODE]: 200, - }, - }); - span.end(1733821672000); - - const event = createTransactionForOtelSpan(span as any); - // we do not care about this here - delete event.sdkProcessingMetadata; - - expect(event).toEqual({ - contexts: { - trace: { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - data: { - 'sentry.source': 'custom', - 'sentry.sample_rate': 1, - 'sentry.origin': 'manual', - 'sentry.op': 'http.server', - 'http.response.status_code': 200, - }, - origin: 'manual', - status: 'ok', - op: 'http.server', - }, - otel: { - resource: { - 'service.name': 'opentelemetry-test', - 'telemetry.sdk.language': 'nodejs', - 'telemetry.sdk.name': 'opentelemetry', - 'telemetry.sdk.version': expect.any(String), - 'service.namespace': 'sentry', - 'service.version': SDK_VERSION, - }, - }, - response: { - status_code: 200, - }, - }, - spans: [], - start_timestamp: 1733821670, - timestamp: 1733821672, - transaction: 'test', - type: 'transaction', - transaction_info: { source: 'custom' }, - }); - }); - - it('adds span link to the trace context when adding with addLink()', () => { - const span = startInactiveSpan({ name: 'parent1' }); - span.end(); - - startSpanManual({ name: 'rootSpan' }, rootSpan => { - rootSpan.addLink({ context: span.spanContext(), attributes: { 'sentry.link.type': 'previous_trace' } }); - rootSpan.end(); - - const prevTraceId = span.spanContext().traceId; - const prevSpanId = span.spanContext().spanId; - const event = createTransactionForOtelSpan(rootSpan as any); - - expect(event.contexts?.trace).toEqual( - expect.objectContaining({ - links: [ - expect.objectContaining({ - attributes: { 'sentry.link.type': 'previous_trace' }, - sampled: true, - trace_id: expect.stringMatching(prevTraceId), - span_id: expect.stringMatching(prevSpanId), - }), - ], - }), - ); - }); - }); - - it('adds span link to the trace context when linked in span options', () => { - const span = startInactiveSpan({ name: 'parent1' }); - - const prevTraceId = span.spanContext().traceId; - const prevSpanId = span.spanContext().spanId; - - const linkedSpan = startInactiveSpan({ - name: 'parent2', - links: [{ context: span.spanContext(), attributes: { 'sentry.link.type': 'previous_trace' } }], - }); - - span.end(); - linkedSpan.end(); - - const event = createTransactionForOtelSpan(linkedSpan as any); - - expect(event.contexts?.trace).toEqual( - expect.objectContaining({ - links: [ - expect.objectContaining({ - attributes: { 'sentry.link.type': 'previous_trace' }, - sampled: true, - trace_id: expect.stringMatching(prevTraceId), - span_id: expect.stringMatching(prevSpanId), - }), - ], - }), - ); - }); -}); diff --git a/packages/opentelemetry/test/spanProcessor.test.ts b/packages/opentelemetry/test/spanProcessor.test.ts deleted file mode 100644 index 2e3b0b5b999e..000000000000 --- a/packages/opentelemetry/test/spanProcessor.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { getClient, startInactiveSpan } from '@sentry/core'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { SentrySpanExporter } from '../src/spanExporter'; -import { cleanupOtel, mockSdkInit } from './helpers/mockSdkInit'; - -const exportSpy = vi.spyOn(SentrySpanExporter.prototype, 'export'); - -describe('SentrySpanProcessor', () => { - beforeEach(() => { - exportSpy.mockClear(); - }); - - describe('with traceLifecycle: static (default)', () => { - beforeEach(() => { - mockSdkInit({ tracesSampleRate: 1 }); - }); - - afterEach(async () => { - await cleanupOtel(); - }); - - it('exports spans via the exporter', () => { - const span = startInactiveSpan({ name: 'test' }); - span.end(); - - expect(exportSpy).toHaveBeenCalled(); - }); - }); - - describe('with traceLifecycle: stream', () => { - beforeEach(() => { - mockSdkInit({ tracesSampleRate: 1, traceLifecycle: 'stream' }); - }); - - afterEach(async () => { - await cleanupOtel(); - }); - - it('does not export spans via the exporter', () => { - const span = startInactiveSpan({ name: 'test' }); - span.end(); - - expect(exportSpy).not.toHaveBeenCalled(); - }); - - it('emits afterSpanEnd', () => { - const afterSpanEndCallback = vi.fn(); - const client = getClient()!; - client.on('afterSpanEnd', afterSpanEndCallback); - - const span = startInactiveSpan({ name: 'test' }); - span.end(); - - expect(afterSpanEndCallback).toHaveBeenCalledWith(span); - }); - }); -}); diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index dcc1b1aaf118..a6bf138a847c 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -1,8 +1,7 @@ /* eslint-disable typescript/no-deprecated */ import type { Span, TimeInput } from '@opentelemetry/api'; import { context, ROOT_CONTEXT, trace, TraceFlags } from '@opentelemetry/api'; -import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; -import { HTTP_METHOD, SENTRY_KIND } from '@sentry/conventions/attributes'; +import { SENTRY_KIND } from '@sentry/conventions/attributes'; import type { Event, Scope } from '@sentry/core'; import { getClient, @@ -21,13 +20,10 @@ import { withScope, } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { getParentSpanId } from '../../../packages/opentelemetry/src/utils/getParentSpanId'; import { continueTrace, startInactiveSpan, startNewTrace, startSpan, startSpanManual } from '../src/trace'; -import type { AbstractSpan } from '../src/types'; import { getActiveSpan } from '../src/utils/getActiveSpan'; import { getSamplingDecision } from '../src/utils/getSamplingDecision'; import { makeTraceState } from '../src/utils/makeTraceState'; -import { spanHasAttributes, spanHasName } from '../src/utils/spanTypes'; import { isSpan } from './helpers/isSpan'; import { cleanupOtel, mockSdkInit } from './helpers/mockSdkInit'; @@ -221,7 +217,9 @@ describe('trace', () => { span => { expect(span).toBeDefined(); expect(getSpanAttributes(span)).toEqual({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', }); }, ); @@ -265,7 +263,9 @@ describe('trace', () => { expect(getSpanName(span)).toEqual('outer'); expect(getSpanStartTime(span)).toEqual(date); expect(getSpanAttributes(span)).toEqual({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', test1: 'test 1', test2: 2, [SENTRY_KIND]: 'client', @@ -358,8 +358,7 @@ describe('trace', () => { it('allows to add span links', () => { const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); - // @ts-expect-error links exists on span - expect(rawSpan1?.links).toEqual([]); + expect(spanToJSON(rawSpan1).links).toBeUndefined(); const span1JSON = spanToJSON(rawSpan1); @@ -375,16 +374,10 @@ describe('trace', () => { expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace'); - // @ts-expect-error links and _spanContext exist on span - expect(rawSpan2?.links?.[0].context.traceId).toEqual(rawSpan1._spanContext.traceId); - // @ts-expect-error links and _spanContext exist on span - expect(rawSpan2?.links?.[0].context.traceId).toEqual(span1JSON.trace_id); + expect(span2LinkJSON?.trace_id).toEqual(rawSpan1.spanContext().traceId); expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id); - // @ts-expect-error links and _spanContext exist on span - expect(rawSpan2?.links?.[0].context.spanId).toEqual(rawSpan1?._spanContext.spanId); - // @ts-expect-error links and _spanContext exist on span - expect(rawSpan2?.links?.[0].context.spanId).toEqual(span1JSON.span_id); + expect(span2LinkJSON?.span_id).toEqual(rawSpan1.spanContext().spanId); expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id); }); }); @@ -392,8 +385,7 @@ describe('trace', () => { it('allows to pass span links in span options', () => { const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); - // @ts-expect-error links exists on span - expect(rawSpan1?.links).toEqual([]); + expect(spanToJSON(rawSpan1).links).toBeUndefined(); const span1JSON = spanToJSON(rawSpan1); @@ -412,16 +404,10 @@ describe('trace', () => { expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace'); - // @ts-expect-error links and _spanContext exist on span - expect(rawSpan2?.links?.[0].context.traceId).toEqual(rawSpan1._spanContext.traceId); - // @ts-expect-error links and _spanContext exist on span - expect(rawSpan2?.links?.[0].context.traceId).toEqual(span1JSON.trace_id); + expect(span2LinkJSON?.trace_id).toEqual(rawSpan1.spanContext().traceId); expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id); - // @ts-expect-error links and _spanContext exist on span - expect(rawSpan2?.links?.[0].context.spanId).toEqual(rawSpan1?._spanContext.spanId); - // @ts-expect-error links and _spanContext exist on span - expect(rawSpan2?.links?.[0].context.spanId).toEqual(span1JSON.span_id); + expect(span2LinkJSON?.span_id).toEqual(rawSpan1.spanContext().spanId); expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id); }, ); @@ -538,7 +524,7 @@ describe('trace', () => { return span; }); - expect(isSpan(span)).toBe(false); + expect(span.isRecording()).toBe(false); expect(spyOnDroppedEvent).toHaveBeenCalledWith('no_parent_span', 'span'); expect(spyOnDroppedEvent).toHaveBeenCalledTimes(1); }); @@ -628,7 +614,9 @@ describe('trace', () => { expect(span).toBeDefined(); expect(getSpanAttributes(span)).toEqual({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', }); const span2 = startInactiveSpan({ @@ -666,7 +654,9 @@ describe('trace', () => { expect(getSpanName(span)).toEqual('outer'); expect(getSpanStartTime(span)).toEqual(date); expect(getSpanAttributes(span)).toEqual({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', test1: 'test 1', test2: 2, [SENTRY_KIND]: 'client', @@ -726,8 +716,7 @@ describe('trace', () => { it('allows to pass span links in span options', () => { const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); - // @ts-expect-error links exists on span - expect(rawSpan1?.links).toEqual([]); + expect(spanToJSON(rawSpan1).links).toBeUndefined(); const rawSpan2 = startInactiveSpan({ name: 'GET users/[id]', @@ -745,16 +734,10 @@ describe('trace', () => { expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace'); - // @ts-expect-error links and _spanContext exist on span - expect(rawSpan2?.links?.[0].context.traceId).toEqual(rawSpan1._spanContext.traceId); - // @ts-expect-error links and _spanContext exist on span - expect(rawSpan2?.links?.[0].context.traceId).toEqual(span1JSON.trace_id); + expect(span2LinkJSON?.trace_id).toEqual(rawSpan1.spanContext().traceId); expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id); - // @ts-expect-error links and _spanContext exist on span - expect(rawSpan2?.links?.[0].context.spanId).toEqual(rawSpan1?._spanContext.spanId); - // @ts-expect-error links and _spanContext exist on span - expect(rawSpan2?.links?.[0].context.spanId).toEqual(span1JSON.span_id); + expect(span2LinkJSON?.span_id).toEqual(rawSpan1.spanContext().spanId); expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id); // sampling decision is inherited @@ -865,7 +848,7 @@ describe('trace', () => { const span = startInactiveSpan({ name: 'test span', onlyIfParent: true }); - expect(isSpan(span)).toBe(false); + expect(span.isRecording()).toBe(false); expect(spyOnDroppedEvent).toHaveBeenCalledWith('no_parent_span', 'span'); expect(spyOnDroppedEvent).toHaveBeenCalledTimes(1); }); @@ -924,6 +907,8 @@ describe('trace', () => { withScope(scope => { scope.setTag('scope', 1); span = startInactiveSpan({ name: 'my-span' }); + // Set after the span was started: the span captures a snapshot of the scope at start time, + // so this later mutation is intentionally not reflected on the transaction. scope.setTag('scope_after_span', 2); }); @@ -935,16 +920,10 @@ describe('trace', () => { await client.flush(); expect(beforeSendTransaction).toHaveBeenCalledTimes(1); - expect(beforeSendTransaction).toHaveBeenCalledWith( - expect.objectContaining({ - tags: expect.objectContaining({ - outer: 'foo', - scope: 1, - scope_after_span: 2, - }), - }), - expect.anything(), - ); + const transactionEvent = beforeSendTransaction.mock.calls[0]![0]; + // Only the scope state at span-start is captured: `outer` and `scope: 1`, but not + // `scope_after_span` (set later) or `scope: 2` (a different scope active at `end()`). + expect(transactionEvent.tags).toEqual({ outer: 'foo', scope: 1 }); }); }); @@ -1008,7 +987,9 @@ describe('trace', () => { expect(getSpanName(span)).toEqual('outer'); expect(getSpanStartTime(span)).toEqual(date); expect(getSpanAttributes(span)).toEqual({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', test1: 'test 1', test2: 2, [SENTRY_KIND]: 'client', @@ -1086,8 +1067,7 @@ describe('trace', () => { it('allows to add span links', () => { const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); - // @ts-expect-error links exists on span - expect(rawSpan1?.links).toEqual([]); + expect(spanToJSON(rawSpan1).links).toBeUndefined(); const span1JSON = spanToJSON(rawSpan1); @@ -1103,16 +1083,10 @@ describe('trace', () => { expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace'); - // @ts-expect-error links and _spanContext exist on span - expect(rawSpan2?.links?.[0].context.traceId).toEqual(rawSpan1._spanContext.traceId); - // @ts-expect-error links and _spanContext exist on span - expect(rawSpan2?.links?.[0].context.traceId).toEqual(span1JSON.trace_id); + expect(span2LinkJSON?.trace_id).toEqual(rawSpan1.spanContext().traceId); expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id); - // @ts-expect-error links and _spanContext exist on span - expect(rawSpan2?.links?.[0].context.spanId).toEqual(rawSpan1?._spanContext.spanId); - // @ts-expect-error links and _spanContext exist on span - expect(rawSpan2?.links?.[0].context.spanId).toEqual(span1JSON.span_id); + expect(span2LinkJSON?.span_id).toEqual(rawSpan1.spanContext().spanId); expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id); }); }); @@ -1120,8 +1094,7 @@ describe('trace', () => { it('allows to pass span links in span options', () => { const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); - // @ts-expect-error links exists on span - expect(rawSpan1?.links).toEqual([]); + expect(spanToJSON(rawSpan1).links).toBeUndefined(); const span1JSON = spanToJSON(rawSpan1); @@ -1140,16 +1113,10 @@ describe('trace', () => { expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace'); - // @ts-expect-error links and _spanContext exist on span - expect(rawSpan2?.links?.[0].context.traceId).toEqual(rawSpan1._spanContext.traceId); - // @ts-expect-error links and _spanContext exist on span - expect(rawSpan2?.links?.[0].context.traceId).toEqual(span1JSON.trace_id); + expect(span2LinkJSON?.trace_id).toEqual(rawSpan1.spanContext().traceId); expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id); - // @ts-expect-error links and _spanContext exist on span - expect(rawSpan2?.links?.[0].context.spanId).toEqual(rawSpan1?._spanContext.spanId); - // @ts-expect-error links and _spanContext exist on span - expect(rawSpan2?.links?.[0].context.spanId).toEqual(span1JSON.span_id); + expect(span2LinkJSON?.span_id).toEqual(rawSpan1.spanContext().spanId); expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id); }, ); @@ -1268,7 +1235,7 @@ describe('trace', () => { return span; }); - expect(isSpan(span)).toBe(false); + expect(span.isRecording()).toBe(false); expect(spyOnDroppedEvent).toHaveBeenCalledWith('no_parent_span', 'span'); expect(spyOnDroppedEvent).toHaveBeenCalledTimes(1); }); @@ -1869,58 +1836,6 @@ describe('trace (sampling)', () => { }); }); -describe('HTTP methods (sampling)', () => { - beforeEach(() => { - mockSdkInit({ tracesSampleRate: 1 }); - }); - - afterEach(async () => { - await cleanupOtel(); - }); - - it('does sample when HTTP method is other than OPTIONS or HEAD', () => { - const spanGET = startSpanManual({ name: 'test span', attributes: { [HTTP_METHOD]: 'GET' } }, span => { - return span; - }); - expect(spanIsSampled(spanGET)).toBe(true); - expect(getSamplingDecision(spanGET.spanContext())).toBe(true); - - const spanPOST = startSpanManual({ name: 'test span', attributes: { [HTTP_METHOD]: 'POST' } }, span => { - return span; - }); - expect(spanIsSampled(spanPOST)).toBe(true); - expect(getSamplingDecision(spanPOST.spanContext())).toBe(true); - - const spanPUT = startSpanManual({ name: 'test span', attributes: { [HTTP_METHOD]: 'PUT' } }, span => { - return span; - }); - expect(spanIsSampled(spanPUT)).toBe(true); - expect(getSamplingDecision(spanPUT.spanContext())).toBe(true); - - const spanDELETE = startSpanManual({ name: 'test span', attributes: { [HTTP_METHOD]: 'DELETE' } }, span => { - return span; - }); - expect(spanIsSampled(spanDELETE)).toBe(true); - expect(getSamplingDecision(spanDELETE.spanContext())).toBe(true); - }); - - it('does not sample when HTTP method is OPTIONS', () => { - const span = startSpanManual({ name: 'test span', attributes: { [HTTP_METHOD]: 'OPTIONS' } }, span => { - return span; - }); - expect(spanIsSampled(span)).toBe(false); - expect(getSamplingDecision(span.spanContext())).toBe(false); - }); - - it('does not sample when HTTP method is HEAD', () => { - const span = startSpanManual({ name: 'test span', attributes: { [HTTP_METHOD]: 'HEAD' } }, span => { - return span; - }); - expect(spanIsSampled(span)).toBe(false); - expect(getSamplingDecision(span.spanContext())).toBe(false); - }); -}); - describe('continueTrace', () => { beforeEach(() => { mockSdkInit({ tracesSampleRate: 1 }); @@ -2331,22 +2246,33 @@ describe('startNewTrace', () => { }); }); -function getSpanName(span: AbstractSpan): string | undefined { - return spanHasName(span) ? span.name : undefined; +function getSpanName(span: Span): string | undefined { + return spanToJSON(span).description; +} + +// Native Sentry spans store timestamps in seconds; the tests assert HrTime `[seconds, nanoseconds]`. +// Convert via milliseconds (like OTel's `numberToHrtime`) so the nanosecond part stays exact — +// converting straight from a fractional-seconds double loses precision. +function hrTimeFromSeconds(seconds: number): [number, number] { + const ms = seconds * 1000; + const sec = Math.trunc(ms / 1000); + return [sec, Math.round((ms - sec * 1000) * 1e6)]; } -function getSpanEndTime(span: AbstractSpan): [number, number] | undefined { - return (span as ReadableSpan).endTime; +function getSpanEndTime(span: Span): [number, number] | undefined { + const timestamp = spanToJSON(span).timestamp; + return typeof timestamp === 'number' ? hrTimeFromSeconds(timestamp) : [0, 0]; } -function getSpanStartTime(span: AbstractSpan): [number, number] | undefined { - return (span as ReadableSpan).startTime; +function getSpanStartTime(span: Span): [number, number] | undefined { + const startTimestamp = spanToJSON(span).start_timestamp; + return typeof startTimestamp === 'number' ? hrTimeFromSeconds(startTimestamp) : undefined; } -function getSpanAttributes(span: AbstractSpan): Record | undefined { - return spanHasAttributes(span) ? span.attributes : undefined; +function getSpanAttributes(span: Span): Record | undefined { + return spanToJSON(span).data; } -function getSpanParentSpanId(span: AbstractSpan): string | undefined { - return getParentSpanId(span as ReadableSpan); +function getSpanParentSpanId(span: Span): string | undefined { + return spanToJSON(span).parent_span_id; } diff --git a/packages/opentelemetry/test/utils/getActiveSpan.test.ts b/packages/opentelemetry/test/utils/getActiveSpan.test.ts index 7a3eefaa6b3d..a01eccd84f3c 100644 --- a/packages/opentelemetry/test/utils/getActiveSpan.test.ts +++ b/packages/opentelemetry/test/utils/getActiveSpan.test.ts @@ -1,22 +1,17 @@ import { trace } from '@opentelemetry/api'; -import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { getRootSpan } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { getActiveSpan } from '../../src/utils/getActiveSpan'; import { setupOtel } from '../helpers/initOtel'; import { cleanupOtel } from '../helpers/mockSdkInit'; -import { getDefaultTestClientOptions, TestClient } from '../helpers/TestClient'; describe('getActiveSpan', () => { - let provider: BasicTracerProvider | undefined; - beforeEach(() => { - const client = new TestClient(getDefaultTestClientOptions()); - [provider] = setupOtel(client); + setupOtel(); }); afterEach(() => { - cleanupOtel(provider); + return cleanupOtel(); }); it('returns undefined if no span is active', () => { @@ -24,15 +19,6 @@ describe('getActiveSpan', () => { expect(span).toBeUndefined(); }); - it('returns undefined if no provider is active', async () => { - await provider?.forceFlush(); - await provider?.shutdown(); - provider = undefined; - - const span = getActiveSpan(); - expect(span).toBeUndefined(); - }); - it('returns currently active span', () => { const tracer = trace.getTracer('test'); @@ -93,16 +79,12 @@ describe('getActiveSpan', () => { }); describe('getRootSpan', () => { - let provider: BasicTracerProvider | undefined; - beforeEach(() => { - const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1 })); - [provider] = setupOtel(client); + setupOtel(); }); - afterEach(async () => { - await provider?.forceFlush(); - await provider?.shutdown(); + afterEach(() => { + return cleanupOtel(); }); it('returns currently active root span', () => { diff --git a/packages/opentelemetry/test/utils/getRequestSpanData.test.ts b/packages/opentelemetry/test/utils/getRequestSpanData.test.ts index 6dab75fec8a3..94986a116f83 100644 --- a/packages/opentelemetry/test/utils/getRequestSpanData.test.ts +++ b/packages/opentelemetry/test/utils/getRequestSpanData.test.ts @@ -1,24 +1,18 @@ /* eslint-disable typescript/no-deprecated */ import type { Span } from '@opentelemetry/api'; import { trace } from '@opentelemetry/api'; -import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { HTTP_METHOD, HTTP_URL } from '@sentry/conventions/attributes'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { getRequestSpanData } from '../../src/utils/getRequestSpanData'; -import { setupOtel } from '../helpers/initOtel'; -import { cleanupOtel } from '../helpers/mockSdkInit'; -import { getDefaultTestClientOptions, TestClient } from '../helpers/TestClient'; +import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; describe('getRequestSpanData', () => { - let provider: BasicTracerProvider | undefined; - beforeEach(() => { - const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1 })); - [provider] = setupOtel(client); + mockSdkInit(); }); afterEach(() => { - cleanupOtel(provider); + return cleanupOtel(); }); function createSpan(name: string): Span { diff --git a/packages/opentelemetry/test/utils/groupSpansWithParents.test.ts b/packages/opentelemetry/test/utils/groupSpansWithParents.test.ts deleted file mode 100644 index c71569c322d5..000000000000 --- a/packages/opentelemetry/test/utils/groupSpansWithParents.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { trace } from '@opentelemetry/api'; -import type { BasicTracerProvider, ReadableSpan } from '@opentelemetry/sdk-trace-base'; -import type { Span } from '@sentry/core'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { withActiveSpan } from '../../src/trace'; -import { groupSpansWithParents } from '../../src/utils/groupSpansWithParents'; -import { setupOtel } from '../helpers/initOtel'; -import { cleanupOtel } from '../helpers/mockSdkInit'; -import { getDefaultTestClientOptions, TestClient } from '../helpers/TestClient'; - -describe('groupSpansWithParents', () => { - let provider: BasicTracerProvider | undefined; - - beforeEach(() => { - const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1 })); - [provider] = setupOtel(client); - }); - - afterEach(() => { - cleanupOtel(provider); - }); - - it('works with no spans', () => { - const actual = groupSpansWithParents([]); - expect(actual).toEqual([]); - }); - - it('works with a single root span & in-order spans', () => { - const tracer = trace.getTracer('test'); - const rootSpan = tracer.startSpan('root') as unknown as ReadableSpan; - const parentSpan1 = withActiveSpan( - rootSpan as unknown as Span, - () => tracer.startSpan('parent1') as unknown as ReadableSpan, - ); - const parentSpan2 = withActiveSpan( - rootSpan as unknown as Span, - () => tracer.startSpan('parent2') as unknown as ReadableSpan, - ); - const child1 = withActiveSpan( - parentSpan1 as unknown as Span, - () => tracer.startSpan('child1') as unknown as ReadableSpan, - ); - - const actual = groupSpansWithParents([rootSpan, parentSpan1, parentSpan2, child1]); - expect(actual).toHaveLength(4); - - // Ensure parent & span is correctly set - const rootRef = actual.find(ref => ref.span === rootSpan); - const parent1Ref = actual.find(ref => ref.span === parentSpan1); - const parent2Ref = actual.find(ref => ref.span === parentSpan2); - const child1Ref = actual.find(ref => ref.span === child1); - - expect(rootRef).toBeDefined(); - expect(parent1Ref).toBeDefined(); - expect(parent2Ref).toBeDefined(); - expect(child1Ref).toBeDefined(); - - expect(rootRef?.parentNode).toBeUndefined(); - expect(rootRef?.children).toEqual([parent1Ref, parent2Ref]); - - expect(parent1Ref?.span).toBe(parentSpan1); - expect(parent2Ref?.span).toBe(parentSpan2); - - expect(parent1Ref?.parentNode).toBe(rootRef); - expect(parent2Ref?.parentNode).toBe(rootRef); - - expect(parent1Ref?.children).toEqual([child1Ref]); - expect(parent2Ref?.children).toEqual([]); - - expect(child1Ref?.parentNode).toBe(parent1Ref); - expect(child1Ref?.children).toEqual([]); - }); - - it('works with a spans with missing root span', () => { - const tracer = trace.getTracer('test'); - - // We create this root span here, but we do not pass it to `groupSpansWithParents` below - const rootSpan = tracer.startSpan('root') as unknown as ReadableSpan; - const parentSpan1 = withActiveSpan( - rootSpan as unknown as Span, - () => tracer.startSpan('parent1') as unknown as ReadableSpan, - ); - const parentSpan2 = withActiveSpan( - rootSpan as unknown as Span, - () => tracer.startSpan('parent2') as unknown as ReadableSpan, - ); - const child1 = withActiveSpan( - parentSpan1 as unknown as Span, - () => tracer.startSpan('child1') as unknown as ReadableSpan, - ); - - const actual = groupSpansWithParents([parentSpan1, parentSpan2, child1]); - expect(actual).toHaveLength(4); - - // Ensure parent & span is correctly set - const rootRef = actual.find(ref => ref.id === rootSpan.spanContext().spanId); - const parent1Ref = actual.find(ref => ref.span === parentSpan1); - const parent2Ref = actual.find(ref => ref.span === parentSpan2); - const child1Ref = actual.find(ref => ref.span === child1); - - expect(rootRef).toBeDefined(); - expect(parent1Ref).toBeDefined(); - expect(parent2Ref).toBeDefined(); - expect(child1Ref).toBeDefined(); - - expect(rootRef?.parentNode).toBeUndefined(); - expect(rootRef?.span).toBeUndefined(); - expect(rootRef?.children).toEqual([parent1Ref, parent2Ref]); - - expect(parent1Ref?.span).toBe(parentSpan1); - expect(parent2Ref?.span).toBe(parentSpan2); - - expect(parent1Ref?.parentNode).toBe(rootRef); - expect(parent2Ref?.parentNode).toBe(rootRef); - - expect(parent1Ref?.children).toEqual([child1Ref]); - expect(parent2Ref?.children).toEqual([]); - - expect(child1Ref?.parentNode).toBe(parent1Ref); - expect(child1Ref?.children).toEqual([]); - }); - - it('works with multiple root spans & out-of-order spans', () => { - const tracer = trace.getTracer('test'); - const rootSpan1 = tracer.startSpan('root1') as unknown as ReadableSpan; - const rootSpan2 = tracer.startSpan('root2') as unknown as ReadableSpan; - const parentSpan1 = withActiveSpan( - rootSpan1 as unknown as Span, - () => tracer.startSpan('parent1') as unknown as ReadableSpan, - ); - const parentSpan2 = withActiveSpan( - rootSpan2 as unknown as Span, - () => tracer.startSpan('parent2') as unknown as ReadableSpan, - ); - const childSpan1 = withActiveSpan( - parentSpan1 as unknown as Span, - () => tracer.startSpan('child1') as unknown as ReadableSpan, - ); - - const actual = groupSpansWithParents([childSpan1, parentSpan1, parentSpan2, rootSpan2, rootSpan1]); - expect(actual).toHaveLength(5); - - // Ensure parent & span is correctly set - const root1Ref = actual.find(ref => ref.span === rootSpan1); - const root2Ref = actual.find(ref => ref.span === rootSpan2); - const parent1Ref = actual.find(ref => ref.span === parentSpan1); - const parent2Ref = actual.find(ref => ref.span === parentSpan2); - const child1Ref = actual.find(ref => ref.span === childSpan1); - - expect(root1Ref).toBeDefined(); - expect(root2Ref).toBeDefined(); - expect(parent1Ref).toBeDefined(); - expect(parent2Ref).toBeDefined(); - expect(child1Ref).toBeDefined(); - - expect(root1Ref?.parentNode).toBeUndefined(); - expect(root1Ref?.children).toEqual([parent1Ref]); - - expect(root2Ref?.parentNode).toBeUndefined(); - expect(root2Ref?.children).toEqual([parent2Ref]); - - expect(parent1Ref?.span).toBe(parentSpan1); - expect(parent2Ref?.span).toBe(parentSpan2); - - expect(parent1Ref?.parentNode).toBe(root1Ref); - expect(parent2Ref?.parentNode).toBe(root2Ref); - - expect(parent1Ref?.children).toEqual([child1Ref]); - expect(parent2Ref?.children).toEqual([]); - - expect(child1Ref?.parentNode).toBe(parent1Ref); - expect(child1Ref?.children).toEqual([]); - }); -}); diff --git a/packages/opentelemetry/test/utils/mapStatus.test.ts b/packages/opentelemetry/test/utils/mapStatus.test.ts deleted file mode 100644 index fa137c970870..000000000000 --- a/packages/opentelemetry/test/utils/mapStatus.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -/* eslint-disable typescript/no-deprecated */ -import type { Span } from '@opentelemetry/api'; -import { trace } from '@opentelemetry/api'; -import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; -import { HTTP_STATUS_CODE, RPC_GRPC_STATUS_CODE } from '@sentry/conventions/attributes'; -import type { SpanStatus } from '@sentry/core'; -import { SPAN_STATUS_ERROR, SPAN_STATUS_OK } from '@sentry/core'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { mapStatus } from '../../src/utils/mapStatus'; -import { setupOtel } from '../helpers/initOtel'; -import { cleanupOtel } from '../helpers/mockSdkInit'; -import { getDefaultTestClientOptions, TestClient } from '../helpers/TestClient'; - -describe('mapStatus', () => { - let provider: BasicTracerProvider | undefined; - - beforeEach(() => { - const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1 })); - [provider] = setupOtel(client); - }); - - afterEach(() => { - cleanupOtel(provider); - }); - - function createSpan(name: string): Span { - return trace.getTracer('test').startSpan(name); - } - - const statusTestTable: [undefined | number | string, undefined | string, SpanStatus][] = [ - // http codes - [400, undefined, { code: SPAN_STATUS_ERROR, message: 'invalid_argument' }], - [401, undefined, { code: SPAN_STATUS_ERROR, message: 'unauthenticated' }], - [403, undefined, { code: SPAN_STATUS_ERROR, message: 'permission_denied' }], - [404, undefined, { code: SPAN_STATUS_ERROR, message: 'not_found' }], - [409, undefined, { code: SPAN_STATUS_ERROR, message: 'already_exists' }], - [429, undefined, { code: SPAN_STATUS_ERROR, message: 'resource_exhausted' }], - [499, undefined, { code: SPAN_STATUS_ERROR, message: 'cancelled' }], - [500, undefined, { code: SPAN_STATUS_ERROR, message: 'internal_error' }], - [501, undefined, { code: SPAN_STATUS_ERROR, message: 'unimplemented' }], - [503, undefined, { code: SPAN_STATUS_ERROR, message: 'unavailable' }], - [504, undefined, { code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' }], - [999, undefined, { code: SPAN_STATUS_ERROR, message: 'internal_error' }], - - // grpc codes - [undefined, '1', { code: SPAN_STATUS_ERROR, message: 'cancelled' }], - [undefined, '2', { code: SPAN_STATUS_ERROR, message: 'unknown_error' }], - [undefined, '3', { code: SPAN_STATUS_ERROR, message: 'invalid_argument' }], - [undefined, '4', { code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' }], - [undefined, '5', { code: SPAN_STATUS_ERROR, message: 'not_found' }], - [undefined, '6', { code: SPAN_STATUS_ERROR, message: 'already_exists' }], - [undefined, '7', { code: SPAN_STATUS_ERROR, message: 'permission_denied' }], - [undefined, '8', { code: SPAN_STATUS_ERROR, message: 'resource_exhausted' }], - [undefined, '9', { code: SPAN_STATUS_ERROR, message: 'failed_precondition' }], - [undefined, '10', { code: SPAN_STATUS_ERROR, message: 'aborted' }], - [undefined, '11', { code: SPAN_STATUS_ERROR, message: 'out_of_range' }], - [undefined, '12', { code: SPAN_STATUS_ERROR, message: 'unimplemented' }], - [undefined, '13', { code: SPAN_STATUS_ERROR, message: 'internal_error' }], - [undefined, '14', { code: SPAN_STATUS_ERROR, message: 'unavailable' }], - [undefined, '15', { code: SPAN_STATUS_ERROR, message: 'data_loss' }], - [undefined, '16', { code: SPAN_STATUS_ERROR, message: 'unauthenticated' }], - [undefined, '999', { code: SPAN_STATUS_ERROR, message: 'unknown_error' }], - - // http takes precedence over grpc - [400, '2', { code: SPAN_STATUS_ERROR, message: 'invalid_argument' }], - ]; - - it.each(statusTestTable)('works with httpCode=%s, grpcCode=%s', (httpCode, grpcCode, expected) => { - const span = createSpan('test-span'); - span.setStatus({ code: 0 }); // UNSET - - if (httpCode) { - span.setAttribute(HTTP_STATUS_CODE, httpCode); - } - - if (grpcCode) { - span.setAttribute(RPC_GRPC_STATUS_CODE, grpcCode); - } - - const actual = mapStatus(span); - expect(actual).toEqual(expected); - }); - - it('works with string HTTP_STATUS_CODE', () => { - const span = createSpan('test-span'); - - span.setStatus({ code: 0 }); // UNSET - span.setAttribute(HTTP_STATUS_CODE, '400'); - - const actual = mapStatus(span); - expect(actual).toEqual({ code: SPAN_STATUS_ERROR, message: 'invalid_argument' }); - }); - - it('returns ok span status when is UNSET present on span', () => { - const span = createSpan('test-span'); - span.setStatus({ code: 0 }); // UNSET - expect(mapStatus(span)).toEqual({ code: SPAN_STATUS_OK }); - }); - - it('returns ok span status when already present on span', () => { - const span = createSpan('test-span'); - span.setStatus({ code: 1 }); // OK - expect(mapStatus(span)).toEqual({ code: SPAN_STATUS_OK }); - }); - - it('returns error status when span already has error status', () => { - const span = createSpan('test-span'); - span.setStatus({ code: 2, message: 'invalid_argument' }); // ERROR - expect(mapStatus(span)).toEqual({ code: SPAN_STATUS_ERROR, message: 'invalid_argument' }); - }); - - it('returns error status when span already has error status without message', () => { - const span = createSpan('test-span'); - span.setStatus({ code: 2 }); // ERROR - expect(mapStatus(span)).toEqual({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - }); - - it('infers error status form attributes when span already has error status without message', () => { - const span = createSpan('test-span'); - span.setAttribute(HTTP_STATUS_CODE, 500); - span.setStatus({ code: 2 }); // ERROR - expect(mapStatus(span)).toEqual({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - }); - - it('returns unknown error status when code is unknown', () => { - const span = createSpan('test-span'); - span.setStatus({ code: -1 as 0 }); - expect(mapStatus(span)).toEqual({ code: SPAN_STATUS_ERROR, message: 'unknown_error' }); - }); -}); diff --git a/packages/opentelemetry/test/utils/parseSpanDescription.test.ts b/packages/opentelemetry/test/utils/parseSpanDescription.test.ts index 367f90777955..46229b90ed9a 100644 --- a/packages/opentelemetry/test/utils/parseSpanDescription.test.ts +++ b/packages/opentelemetry/test/utils/parseSpanDescription.test.ts @@ -1,5 +1,4 @@ /* eslint-disable typescript/no-deprecated */ -import type { Span } from '@opentelemetry/api'; import { DB_STATEMENT, DB_SYSTEM, @@ -21,21 +20,11 @@ import { descriptionForHttpMethod, getSanitizedUrl, getUserUpdatedNameAndSource, - parseSpanDescription, + inferSpanData, } from '../../src/utils/parseSpanDescription'; -describe('parseSpanDescription', () => { +describe('inferSpanData', () => { it.each([ - [ - 'works without attributes & name', - undefined, - undefined, - { - description: '', - op: undefined, - source: 'custom', - }, - ], [ 'works with empty attributes', {}, @@ -348,7 +337,7 @@ describe('parseSpanDescription', () => { }, ], ])('%s', (_, attributes, name, expected) => { - const actual = parseSpanDescription({ attributes, name } as unknown as Span); + const actual = inferSpanData(name, attributes); expect(actual).toEqual(expected); }); }); @@ -373,7 +362,7 @@ describe('descriptionForHttpMethod', () => { [HTTP_METHOD]: 'GET', [HTTP_URL]: 'https://www.example.com/my-path', [HTTP_TARGET]: '/my-path', - [SENTRY_KIND]: 'client', + [SENTRY_KIND]: 'client' as const, }, 'test name', { @@ -393,7 +382,7 @@ describe('descriptionForHttpMethod', () => { [HTTP_URL]: 'https://www.example.com/my-path', [HTTP_TARGET]: '/my-path', 'sentry.http.prefetch': true, - [SENTRY_KIND]: 'client', + [SENTRY_KIND]: 'client' as const, }, 'test name', { @@ -412,7 +401,7 @@ describe('descriptionForHttpMethod', () => { [HTTP_METHOD]: 'POST', [HTTP_URL]: 'https://www.example.com/my-path', [HTTP_TARGET]: '/my-path', - [SENTRY_KIND]: 'server', + [SENTRY_KIND]: 'server' as const, }, 'test name', { @@ -432,7 +421,7 @@ describe('descriptionForHttpMethod', () => { [HTTP_URL]: 'https://www.example.com/my-path/123', [HTTP_TARGET]: '/my-path/123', [HTTP_ROUTE]: '/my-path/:id', - [SENTRY_KIND]: 'client', + [SENTRY_KIND]: 'client' as const, }, 'test name', { @@ -471,7 +460,7 @@ describe('descriptionForHttpMethod', () => { [HTTP_TARGET]: '/my-path/123', [HTTP_ROUTE]: '/my-path/:id', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', - [SENTRY_KIND]: 'client', + [SENTRY_KIND]: 'client' as const, }, 'test name', { @@ -493,7 +482,7 @@ describe('descriptionForHttpMethod', () => { [HTTP_ROUTE]: '/my-path/:id', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', - [SENTRY_KIND]: 'client', + [SENTRY_KIND]: 'client' as const, }, 'test name', { @@ -515,7 +504,7 @@ describe('descriptionForHttpMethod', () => { [HTTP_ROUTE]: '/my-path/:id', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', - [SENTRY_KIND]: 'client', + [SENTRY_KIND]: 'client' as const, }, 'test name', { @@ -534,7 +523,7 @@ describe('descriptionForHttpMethod', () => { [HTTP_METHOD]: 'GET', [HTTP_URL]: 'https://www.example.com/my-path?id=1#section', [HTTP_TARGET]: '/my-path?id=1#section', - [SENTRY_KIND]: 'client', + [SENTRY_KIND]: 'client' as const, }, 'test name', { diff --git a/packages/opentelemetry/test/utils/setupCheck.test.ts b/packages/opentelemetry/test/utils/setupCheck.test.ts deleted file mode 100644 index 16533c265793..000000000000 --- a/packages/opentelemetry/test/utils/setupCheck.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { SentrySampler } from '../../src/sampler'; -import { SentrySpanProcessor } from '../../src/spanProcessor'; -import { SentryTracerProvider } from '../../src/tracerProvider'; -import { openTelemetrySetupCheck, setIsSetup } from '../../src/utils/setupCheck'; -import { setupOtel } from '../helpers/initOtel'; -import { cleanupOtel } from '../helpers/mockSdkInit'; -import { getDefaultTestClientOptions, TestClient } from '../helpers/TestClient'; - -describe('openTelemetrySetupCheck', () => { - let provider: BasicTracerProvider | undefined; - - beforeEach(() => { - cleanupOtel(provider); - }); - - afterEach(() => { - cleanupOtel(provider); - }); - - it('returns empty array by default', () => { - const setup = openTelemetrySetupCheck(); - expect(setup).toEqual([]); - }); - - it('returns all setup parts', () => { - const client = new TestClient(getDefaultTestClientOptions()); - [provider] = setupOtel(client); - - const setup = openTelemetrySetupCheck(); - expect(setup).toEqual(['SentrySpanProcessor', 'SentrySampler', 'SentryPropagator', 'SentryContextManager']); - }); - - it('returns partial setup parts', () => { - const client = new TestClient(getDefaultTestClientOptions()); - provider = new BasicTracerProvider({ - sampler: new SentrySampler(client), - spanProcessors: [new SentrySpanProcessor()], - }); - - const setup = openTelemetrySetupCheck(); - expect(setup).toEqual(['SentrySampler', 'SentrySpanProcessor']); - }); - - it('does not mark SentryTracerProvider as set up on construction', () => { - // Construction must not mark setup — that only happens once the provider is - // successfully registered as the global tracer provider. Otherwise setup - // validation would skip required checks even when registration failed. - new SentryTracerProvider(); - - expect(openTelemetrySetupCheck()).toEqual([]); - }); - - it('returns SentryTracerProvider setup once it is marked as set up', () => { - setIsSetup('SentryTracerProvider'); - - const setup = openTelemetrySetupCheck(); - expect(setup).toEqual(['SentryTracerProvider']); - }); -}); diff --git a/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts b/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts index 19c8e178c160..215e4d22dafd 100644 --- a/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts +++ b/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts @@ -1,4 +1,3 @@ -import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { captureException, setCurrentClient } from '@sentry/core'; import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { setupEventContextTrace } from '../../src/setupEventContextTrace'; @@ -12,7 +11,6 @@ const PUBLIC_DSN = 'https://username@domain/123'; describe('setupEventContextTrace', () => { const beforeSend = vi.fn(() => null); let client: TestClientInterface; - let provider: BasicTracerProvider | undefined; beforeEach(() => { client = new TestClient( @@ -29,12 +27,12 @@ describe('setupEventContextTrace', () => { client.init(); setupEventContextTrace(client); - [provider] = setupOtel(client); + setupOtel(); }); afterEach(() => { beforeSend.mockReset(); - cleanupOtel(provider); + return cleanupOtel(); }); afterAll(() => { diff --git a/packages/opentelemetry/test/utils/spanToJSON.test.ts b/packages/opentelemetry/test/utils/spanToJSON.test.ts deleted file mode 100644 index 9c480e04d54c..000000000000 --- a/packages/opentelemetry/test/utils/spanToJSON.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { Span, SpanOptions } from '@opentelemetry/api'; -import { trace } from '@opentelemetry/api'; -import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, - spanToJSON, -} from '@sentry/core'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { setupOtel } from '../helpers/initOtel'; -import { cleanupOtel } from '../helpers/mockSdkInit'; -import { getDefaultTestClientOptions, TestClient } from '../helpers/TestClient'; - -describe('spanToJSON', () => { - describe('OpenTelemetry Span', () => { - let provider: BasicTracerProvider | undefined; - - beforeEach(() => { - const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1 })); - [provider] = setupOtel(client); - }); - - afterEach(() => { - cleanupOtel(provider); - }); - - function createSpan(name: string, params?: SpanOptions): Span { - return trace.getTracer('test').startSpan(name, params); - } - - it('works with a simple span', () => { - const span = createSpan('test span', { startTime: [123, 0] }); - - expect(spanToJSON(span)).toEqual({ - span_id: span.spanContext().spanId, - trace_id: span.spanContext().traceId, - start_timestamp: 123, - description: 'test span', - status: 'ok', - data: { - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, - }, - }); - }); - - it('works with a full span', () => { - const span = createSpan('test span', { startTime: [123, 0] }); - - span.setAttributes({ - attr1: 'value1', - attr2: 2, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test op', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', - }); - - span.setStatus({ code: 2, message: 'unknown_error' }); - span.end([456, 0]); - - expect(spanToJSON(span)).toEqual({ - span_id: span.spanContext().spanId, - trace_id: span.spanContext().traceId, - start_timestamp: 123, - timestamp: 456, - description: 'test span', - op: 'test op', - origin: 'auto', - data: { - attr1: 'value1', - attr2: 2, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test op', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, - }, - status: 'unknown_error', - }); - }); - }); -}); diff --git a/packages/opentelemetry/test/utils/spanTypes.test.ts b/packages/opentelemetry/test/utils/spanTypes.test.ts index 36f88e20c03e..247254978580 100644 --- a/packages/opentelemetry/test/utils/spanTypes.test.ts +++ b/packages/opentelemetry/test/utils/spanTypes.test.ts @@ -1,25 +1,8 @@ -import type { Span } from '@opentelemetry/api'; +import type { Span } from '@sentry/core'; import { describe, expect, it } from 'vitest'; -import { spanHasAttributes, spanHasEvents, spanHasKind, spanHasParentId } from '../../src/utils/spanTypes'; +import { spanHasKind } from '../../src/utils/spanTypes'; describe('spanTypes', () => { - describe('spanHasAttributes', () => { - it.each([ - [{}, false], - [{ attributes: null }, false], - [{ attributes: {} }, true], - ])('works with %j', (span, expected) => { - const castSpan = span as unknown as Span; - const actual = spanHasAttributes(castSpan); - - expect(actual).toBe(expected); - - if (actual) { - expect(castSpan.attributes).toBeDefined(); - } - }); - }); - describe('spanHasKind', () => { it.each([ [{}, false], @@ -34,41 +17,7 @@ describe('spanTypes', () => { expect(actual).toBe(expected); if (actual) { - expect(castSpan.kind).toBeDefined(); - } - }); - }); - - describe('spanHasParentId', () => { - it.each([ - [{}, false], - [{ parentSpanId: null }, false], - [{ parentSpanId: 'TEST_PARENT_ID' }, true], - ])('works with %j', (span, expected) => { - const castSpan = span as unknown as Span; - const actual = spanHasParentId(castSpan); - - expect(actual).toBe(expected); - - if (actual) { - expect(castSpan.parentSpanId).toBeDefined(); - } - }); - }); - - describe('spanHasEvents', () => { - it.each([ - [{}, false], - [{ events: null }, false], - [{ events: [] }, true], - ])('works with %j', (span, expected) => { - const castSpan = span as unknown as Span; - const actual = spanHasEvents(castSpan); - - expect(actual).toBe(expected); - - if (actual) { - expect(castSpan.events).toBeDefined(); + expect((castSpan as { kind?: unknown }).kind).toBeDefined(); } }); }); diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index a6e04692401c..6584a208e85e 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -37,7 +37,6 @@ }, "devDependencies": { "@edge-runtime/types": "4.0.0", - "@opentelemetry/sdk-trace-base": "^2.9.0", "@sentry/opentelemetry": "10.67.0" }, "scripts": { diff --git a/packages/vercel-edge/rollup.npm.config.mjs b/packages/vercel-edge/rollup.npm.config.mjs index e80e03014471..ae01f43703d0 100644 --- a/packages/vercel-edge/rollup.npm.config.mjs +++ b/packages/vercel-edge/rollup.npm.config.mjs @@ -73,32 +73,6 @@ export default makeNPMConfigVariants( } }, }, - { - // `@opentelemetry/sdk-trace-base` is bundled here, and its `BasicTracerProvider` - // statically imports `defaultResource` from `@opentelemetry/resources` as a fallback - // (`mergedConfig.resource ?? defaultResource()`). Sentry always supplies its own - // resource via `getSentryResource()`, so the fallback never runs. Shimming the import - // lets us drop the `@opentelemetry/resources` dependency and avoids bundling its - // node-only resource detectors into the edge runtime. - name: 'otel-resources-shim', - resolveId: source => (source === '@opentelemetry/resources' ? '\0otel_resources_sentry_shim' : null), - load: id => - id === '\0otel_resources_sentry_shim' - ? ` - export function defaultResource() { - return { - attributes: {}, - merge() { - return this; - }, - getRawAttributes() { - return []; - }, - }; - } - ` - : null, - }, ], }, }), diff --git a/packages/vercel-edge/src/client.ts b/packages/vercel-edge/src/client.ts index d72f5f2f6718..6f1dc8165ef7 100644 --- a/packages/vercel-edge/src/client.ts +++ b/packages/vercel-edge/src/client.ts @@ -1,7 +1,7 @@ -import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import type { ServerRuntimeClientOptions } from '@sentry/core'; import { applySdkMetadata, ServerRuntimeClient } from '@sentry/core'; import type { VercelEdgeClientOptions } from './types'; +import type { SentryTracerProvider } from '@sentry/opentelemetry'; declare const process: { env: Record; @@ -14,7 +14,7 @@ declare const process: { * @see ServerRuntimeClient for usage documentation. */ export class VercelEdgeClient extends ServerRuntimeClient { - public traceProvider: BasicTracerProvider | undefined; + public traceProvider: SentryTracerProvider | undefined; /** * Creates a new Vercel Edge Runtime SDK instance. diff --git a/packages/vercel-edge/src/sdk.ts b/packages/vercel-edge/src/sdk.ts index b2b99587e3e2..43bdecdf3931 100644 --- a/packages/vercel-edge/src/sdk.ts +++ b/packages/vercel-edge/src/sdk.ts @@ -1,5 +1,4 @@ import { context, diag, DiagLogLevel, propagation, trace } from '@opentelemetry/api'; -import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import type { Client, Integration, Options } from '@sentry/core'; import { consoleIntegration, @@ -11,7 +10,6 @@ import { getCurrentScope, getIntegrationsToSetup, GLOBAL_OBJ, - hasSpansEnabled, inboundFiltersIntegration, linkedErrorsIntegration, nodeStackLineParser, @@ -21,16 +19,13 @@ import { import { enhanceDscWithOpenTelemetryRootSpanName, getSentryResource, - openTelemetrySetupCheck, SentryPropagator, - SentrySampler, - SentrySpanProcessor, + SentryTracerProvider, setOpenTelemetryContextAsyncContextStrategy, setupEventContextTrace, wrapContextManagerClass, } from '@sentry/opentelemetry'; import { VercelEdgeClient } from './client'; -import { DEBUG_BUILD } from './debug-build'; import { winterCGFetchIntegration } from './integrations/wintercg-fetch'; import { makeEdgeTransport } from './transports'; import type { VercelEdgeOptions } from './types'; @@ -61,7 +56,7 @@ export function getDefaultIntegrations(_options: Options): Integration[] { } /** Inits the Sentry NextJS SDK on the Edge Runtime. */ -export function init(options: VercelEdgeOptions = {}): Client | undefined { +export function init(options: VercelEdgeOptions = {}): Client { // We force skipOpenTelemetrySetup: true here, because this triggers the custom lookup for the AsyncLocalStorage instance // Since we use a custom Context Manager here (because AsyncLocalStorage is looked up differently than in Node), we need to do this setOpenTelemetryContextAsyncContextStrategy({ skipOpenTelemetrySetup: true }); @@ -109,7 +104,6 @@ export function init(options: VercelEdgeOptions = {}): Client | undefined { // There is no way to use this SDK without OpenTelemetry! if (!options.skipOpenTelemetrySetup) { setupOtel(client); - validateOpenTelemetrySetup(); } enhanceDscWithOpenTelemetryRootSpanName(client); @@ -118,34 +112,6 @@ export function init(options: VercelEdgeOptions = {}): Client | undefined { return client; } -function validateOpenTelemetrySetup(): void { - if (!DEBUG_BUILD) { - return; - } - - const setup = openTelemetrySetupCheck(); - - const required: ReturnType = ['SentryContextManager', 'SentryPropagator']; - - if (hasSpansEnabled()) { - required.push('SentrySpanProcessor'); - } - - for (const k of required) { - if (!setup.includes(k)) { - debug.error( - `You have to set up the ${k}. Without this, the OpenTelemetry & Sentry integration will not work properly.`, - ); - } - } - - if (!setup.includes('SentrySampler')) { - debug.warn( - 'You have to set up the SentrySampler. Without this, the OpenTelemetry & Sentry integration may still work, but sample rates set for the Sentry SDK will not be respected. If you use a custom sampler, make sure to use `wrapSamplingDecision`.', - ); - } -} - // exported for tests // eslint-disable-next-line jsdoc/require-jsdoc export function setupOtel(client: VercelEdgeClient): void { @@ -153,25 +119,16 @@ export function setupOtel(client: VercelEdgeClient): void { setupOpenTelemetryLogger(); } - // Create and configure NodeTracerProvider - const provider = new BasicTracerProvider({ - sampler: new SentrySampler(client), - resource: getSentryResource('edge'), - forceFlushTimeoutMillis: 500, - spanProcessors: [ - new SentrySpanProcessor({ - timeout: client.getOptions().maxSpanWaitDuration, - client, - }), - ], - }); - - // eslint-disable-next-line typescript/no-deprecated - const SentryContextManager = wrapContextManagerClass(AsyncLocalStorageContextManager); + const provider = new SentryTracerProvider({ resource: getSentryResource('edge') }); trace.setGlobalTracerProvider(provider); propagation.setGlobalPropagator(new SentryPropagator()); - context.setGlobalContextManager(new SentryContextManager()); + + // oxlint-disable-next-line typescript/no-deprecated + const SentryContextManager = wrapContextManagerClass(AsyncLocalStorageContextManager); + + const ctxManager = new SentryContextManager(); + context.setGlobalContextManager(ctxManager); client.traceProvider = provider; } diff --git a/packages/vercel-edge/src/types.ts b/packages/vercel-edge/src/types.ts index 0c4a4e03a517..8aaad22f4226 100644 --- a/packages/vercel-edge/src/types.ts +++ b/packages/vercel-edge/src/types.ts @@ -50,17 +50,6 @@ export interface BaseVercelEdgeOptions { */ skipOpenTelemetrySetup?: boolean; - /** - * The max. duration in seconds that the SDK will wait for parent spans to be finished before discarding a span. - * The SDK will automatically clean up spans that have no finished parent after this duration. - * This is necessary to prevent memory leaks in case of parent spans that are never finished or otherwise dropped/missing. - * However, if you have very long-running spans in your application, a shorter duration might cause spans to be discarded too early. - * In this case, you can increase this duration to a value that fits your expected data. - * - * Defaults to 300 seconds (5 minutes). - */ - maxSpanWaitDuration?: number; - /** Callback that is executed when a fatal global error occurs. */ onFatalError?(this: void, error: Error): void; } diff --git a/yarn.lock b/yarn.lock index 621abd6e0855..ade4052bb248 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6177,33 +6177,6 @@ import-in-the-middle "^3.0.0" require-in-the-middle "^8.0.0" -"@opentelemetry/resources@2.9.0": - version "2.9.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-2.9.0.tgz#81e1ce946eec661857a9d6c4fa507b3750ae500f" - integrity sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg== - dependencies: - "@opentelemetry/core" "2.9.0" - "@opentelemetry/semantic-conventions" "^1.29.0" - -"@opentelemetry/sdk-trace-base@^2.9.0": - version "2.9.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.9.0.tgz#34e314486127ce0ad596757a10c4c6f8ed435385" - integrity sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag== - dependencies: - "@opentelemetry/core" "2.9.0" - "@opentelemetry/resources" "2.9.0" - "@opentelemetry/sdk-trace" "2.9.0" - "@opentelemetry/semantic-conventions" "^1.29.0" - -"@opentelemetry/sdk-trace@2.9.0": - version "2.9.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace/-/sdk-trace-2.9.0.tgz#c82103a11a712b31ac9281b706fc5dc76d126ae6" - integrity sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw== - dependencies: - "@opentelemetry/core" "2.9.0" - "@opentelemetry/resources" "2.9.0" - "@opentelemetry/semantic-conventions" "^1.29.0" - "@opentelemetry/semantic-conventions@^1.29.0": version "1.43.0" resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz#f3f467e36c27332f0e735ec86cdcd78dd6f27865"