diff --git a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/.gitignore b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/.gitignore deleted file mode 100644 index 1521c8b7652b..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dist diff --git a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json deleted file mode 100644 index 406783aea067..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "node-otel-without-tracing", - "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.1", - "@opentelemetry/sdk-trace-node": "2.6.1", - "@opentelemetry/exporter-trace-otlp-http": "0.214.0", - "@opentelemetry/instrumentation-undici": "0.24.0", - "@opentelemetry/instrumentation-http": "0.214.0", - "@opentelemetry/instrumentation": "0.214.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-without-tracing/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/playwright.config.mjs deleted file mode 100644 index 888e61cfb2dc..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/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-without-tracing/src/app.ts b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/src/app.ts deleted file mode 100644 index fff4754ed672..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/src/app.ts +++ /dev/null @@ -1,86 +0,0 @@ -import './instrument'; - -// Other imports below -import * as Sentry from '@sentry/node'; -import { trace, type Span } from '@opentelemetry/api'; -import express from 'express'; - -const app = express(); -const port = 3030; - -Sentry.setTag('root-level-tag', 'yes'); - -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 fetch('http://localhost:3030/test-success'); - - res.send({}); - }); -}); - -app.get('/test-only-if-parent', function (req, res) { - // Remove the HTTP span from the context to simulate no parent span - Sentry.withActiveSpan(null, () => { - // This should NOT create a span because onlyIfParent is true and there's no parent - Sentry.startSpan({ name: 'test-only-if-parent', onlyIfParent: true }, () => { - // This custom OTel span SHOULD be created and exported - // This tests that custom OTel spans aren't suppressed when onlyIfParent triggers - const customTracer = trace.getTracer('custom-tracer'); - customTracer.startActiveSpan('custom-span-with-only-if-parent', (span: Span) => { - span.end(); - }); - }); - - 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) { - const id = req.params.id; - Sentry.setTag(`param-${id}`, id); - - throw new Error(`This is an exception with id ${id}`); -}); - -app.get('/test-logs/:id', function (req, res) { - const id = req.params.id; - - Sentry.startSpan({ name: `log-operation-${id}` }, () => { - Sentry.logger.info(`test-log-${id}`, { requestId: id }); - }); - - res.send({ ok: true, 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-without-tracing/src/instrument.ts b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/src/instrument.ts deleted file mode 100644 index 104c46770e6b..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/src/instrument.ts +++ /dev/null @@ -1,51 +0,0 @@ -const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http'); -const { NodeTracerProvider, BatchSpanProcessor } = require('@opentelemetry/sdk-trace-node'); -const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http'); -const Sentry = require('@sentry/node'); -const { SentryPropagator } = require('@sentry/opentelemetry'); -const { UndiciInstrumentation } = require('@opentelemetry/instrumentation-undici'); -const { registerInstrumentations } = require('@opentelemetry/instrumentation'); - -Sentry.init({ - traceLifecycle: 'static', - 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 - // Tracing is completely disabled - // Custom OTEL setup - skipOpenTelemetrySetup: true, -}); - -// Create and configure NodeTracerProvider -const provider = new NodeTracerProvider({ - spanProcessors: [ - new BatchSpanProcessor( - new OTLPTraceExporter({ - url: 'http://localhost:3032/', - }), - ), - ], -}); - -// Initialize the provider -provider.register({ - propagator: new SentryPropagator(), - // We make sure to use the context manager that was previously set up in init - // TODO: This is not ideal but it is just an intermediate state until all of this otel linking stuff is gone - contextManager: null, -}); - -registerInstrumentations({ - instrumentations: [ - new UndiciInstrumentation({ - headersToSpanAttributes: { - responseHeaders: ['content-length'], - }, - }), - new HttpInstrumentation(), - ], -}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/start-event-proxy.mjs deleted file mode 100644 index 62073e9a9b6e..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/start-event-proxy.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import { startEventProxyServer } from '@sentry-internal/test-utils'; - -startEventProxyServer({ - port: 3031, - proxyServerName: 'node-otel-without-tracing', -}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/start-otel-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/start-otel-proxy.mjs deleted file mode 100644 index 1e182efd3873..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/start-otel-proxy.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import { startProxyServer } from '@sentry-internal/test-utils'; - -startProxyServer({ - port: 3032, - proxyServerName: 'node-otel-without-tracing-otel', -}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tests/errors.test.ts deleted file mode 100644 index 5e44ed93fa6c..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tests/errors.test.ts +++ /dev/null @@ -1,51 +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-without-tracing', 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', - }); - - // This is unparameterized here because we do not have the express instrumentation - expect(errorEvent.transaction).toEqual('GET /test-exception/123'); - - expect(errorEvent.contexts?.trace).toEqual({ - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - }); -}); - -test('Isolates requests correctly', async ({ baseURL }) => { - const errorEventPromise1 = waitForError('node-otel-without-tracing', event => { - return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 555-a'; - }); - const errorEventPromise2 = waitForError('node-otel-without-tracing', event => { - return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 555-b'; - }); - - fetch(`${baseURL}/test-exception/555-a`); - fetch(`${baseURL}/test-exception/555-b`); - - const errorEvent1 = await errorEventPromise1; - const errorEvent2 = await errorEventPromise2; - - expect(errorEvent1.transaction).toEqual('GET /test-exception/555-a'); - expect(errorEvent1.tags).toEqual({ 'root-level-tag': 'yes', 'param-555-a': '555-a' }); - - expect(errorEvent2.transaction).toEqual('GET /test-exception/555-b'); - expect(errorEvent2.tags).toEqual({ 'root-level-tag': 'yes', 'param-555-b': '555-b' }); -}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tests/logs.test.ts b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tests/logs.test.ts deleted file mode 100644 index f9fa51e9d3dc..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tests/logs.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForEnvelopeItem } from '@sentry-internal/test-utils'; - -test('Logs from different requests have different trace IDs', async ({ baseURL }) => { - const logEnvelopePromise1 = waitForEnvelopeItem('node-otel-without-tracing', async envelopeItem => { - const [itemHeader, itemPayload] = envelopeItem; - if (itemHeader.type === 'log') { - const logItems = itemPayload as { items: Array<{ body: string; trace_id?: string }> }; - return logItems.items.some(item => item.body === 'test-log-1'); - } - return false; - }); - - const logEnvelopePromise2 = waitForEnvelopeItem('node-otel-without-tracing', async envelopeItem => { - const [itemHeader, itemPayload] = envelopeItem; - if (itemHeader.type === 'log') { - const logItems = itemPayload as { items: Array<{ body: string; trace_id?: string }> }; - return logItems.items.some(item => item.body === 'test-log-2'); - } - return false; - }); - - // Make two requests to different routes (each Express route is an isolation scope) - await fetch(`${baseURL}/test-logs/1`); - await fetch(`${baseURL}/test-logs/2`); - - const logEnvelope1 = await logEnvelopePromise1; - const logEnvelope2 = await logEnvelopePromise2; - - const logPayload1 = logEnvelope1[1] as { items: Array<{ body: string; trace_id?: string }> }; - const logPayload2 = logEnvelope2[1] as { items: Array<{ body: string; trace_id?: string }> }; - - const log1 = logPayload1.items.find(item => item.body === 'test-log-1'); - const log2 = logPayload2.items.find(item => item.body === 'test-log-2'); - - const traceId1 = log1?.trace_id; - const traceId2 = log2?.trace_id; - - expect(traceId1).toMatch(/[a-f0-9]{32}/); - expect(traceId2).toMatch(/[a-f0-9]{32}/); - expect(traceId1).not.toBe(traceId2); -}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tests/transactions.test.ts deleted file mode 100644 index 9823d0dc6b12..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tests/transactions.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForPlainRequest, waitForTransaction } from '@sentry-internal/test-utils'; - -test('Sends an API route transaction to OTLP', async ({ baseURL }) => { - waitForTransaction('node-otel-without-tracing', transactionEvent => { - throw new Error('THIS SHOULD NEVER HAPPEN!'); - }); - - // Ensure we send data to the OTLP endpoint - const otelPromise = waitForPlainRequest('node-otel-without-tracing-otel', data => { - const json = JSON.parse(data) as any; - - const scopeSpans = json.resourceSpans?.[0]?.scopeSpans; - - const httpScope = scopeSpans?.find(scopeSpan => scopeSpan.scope.name === '@opentelemetry/instrumentation-http'); - - return ( - httpScope && - httpScope.spans.some(span => - span.attributes.some(attr => attr.key === 'http.target' && attr.value?.stringValue === '/test-transaction'), - ) - ); - }); - - fetch(`${baseURL}/test-transaction`); - - const otelData = await otelPromise; - - expect(otelData).toBeDefined(); - - const json = JSON.parse(otelData); - expect(json.resourceSpans.length).toBe(1); - - const scopeSpans = json.resourceSpans?.[0]?.scopeSpans; - expect(scopeSpans).toBeDefined(); - - // Http server span & undici client spans are emitted. - // Sentry.startSpan() spans are NOT emitted (non-recording when tracing is disabled). - // Our default node-fetch spans are also not emitted. - expect(scopeSpans.length).toEqual(2); - - const httpScopes = scopeSpans?.filter(scopeSpan => scopeSpan.scope.name === '@opentelemetry/instrumentation-http'); - const undiciScopes = scopeSpans?.filter( - scopeSpan => scopeSpan.scope.name === '@opentelemetry/instrumentation-undici', - ); - const startSpanScopes = scopeSpans?.filter(scopeSpan => scopeSpan.scope.name === '@sentry/node'); - - expect(httpScopes.length).toBe(1); - - // Sentry spans should not be exported when tracing is disabled - expect(startSpanScopes.length).toBe(0); - - // Undici spans are emitted correctly - expect(undiciScopes.length).toBe(1); - expect(undiciScopes[0].spans.length).toBe(1); - - expect(undiciScopes[0].spans).toEqual([ - { - traceId: expect.any(String), - spanId: expect.any(String), - name: 'GET', - kind: 3, - startTimeUnixNano: expect.any(String), - endTimeUnixNano: expect.any(String), - attributes: expect.arrayContaining([ - { key: 'http.request.method', value: { stringValue: 'GET' } }, - { key: 'http.request.method_original', value: { stringValue: 'GET' } }, - { key: 'url.full', value: { stringValue: 'http://localhost:3030/test-success' } }, - { key: 'url.path', value: { stringValue: '/test-success' } }, - { key: 'url.query', value: { stringValue: '' } }, - { key: 'url.scheme', value: { stringValue: 'http' } }, - { key: 'server.address', value: { stringValue: 'localhost' } }, - { key: 'server.port', value: { intValue: 3030 } }, - { key: 'user_agent.original', value: { stringValue: 'node' } }, - { key: 'network.peer.address', value: { stringValue: expect.any(String) } }, - { key: 'network.peer.port', value: { intValue: 3030 } }, - { key: 'http.response.status_code', value: { intValue: 200 } }, - { - key: 'http.response.header.content-length', - value: { arrayValue: { values: [{ stringValue: expect.any(String) }] } }, - }, - ]), - droppedAttributesCount: 0, - events: [], - droppedEventsCount: 0, - status: { code: 0 }, - links: [], - droppedLinksCount: 0, - flags: expect.any(Number), - }, - ]); - - // There may be another span from another request, we can ignore that - const httpSpans = httpScopes[0].spans.filter(span => - span.attributes.some(attr => attr.key === 'http.target' && attr.value?.stringValue === '/test-transaction'), - ); - - expect(httpSpans).toEqual([ - { - traceId: expect.any(String), - spanId: expect.any(String), - name: 'GET', - kind: 2, - startTimeUnixNano: expect.any(String), - endTimeUnixNano: expect.any(String), - attributes: expect.arrayContaining([ - { key: 'http.url', value: { stringValue: 'http://localhost:3030/test-transaction' } }, - { key: 'http.host', value: { stringValue: 'localhost:3030' } }, - { key: 'net.host.name', value: { stringValue: 'localhost' } }, - { key: 'http.method', value: { stringValue: 'GET' } }, - { key: 'http.scheme', value: { stringValue: 'http' } }, - { key: 'http.target', value: { stringValue: '/test-transaction' } }, - { key: 'http.user_agent', value: { stringValue: 'node' } }, - { key: 'http.flavor', value: { stringValue: '1.1' } }, - { key: 'net.transport', value: { stringValue: 'ip_tcp' } }, - { key: 'net.host.ip', value: { stringValue: expect.any(String) } }, - { key: 'net.host.port', value: { intValue: 3030 } }, - { key: 'net.peer.ip', value: { stringValue: expect.any(String) } }, - { key: 'net.peer.port', value: { intValue: expect.any(Number) } }, - { key: 'http.status_code', value: { intValue: 200 } }, - { key: 'http.status_text', value: { stringValue: 'OK' } }, - ]), - droppedAttributesCount: 0, - events: [], - droppedEventsCount: 0, - status: { - code: 0, - }, - links: [], - droppedLinksCount: 0, - flags: expect.any(Number), - }, - ]); -}); - -test('Custom OTel spans work with onlyIfParent when no parent exists', async ({ baseURL }) => { - waitForTransaction('node-otel-without-tracing', transactionEvent => { - throw new Error('THIS SHOULD NEVER HAPPEN!'); - }); - - // Ensure we send data to the OTLP endpoint - const otelPromise = waitForPlainRequest('node-otel-without-tracing-otel', data => { - const json = JSON.parse(data) as any; - - const scopeSpans = json.resourceSpans?.[0]?.scopeSpans; - - // Look for the custom span from our custom-tracer - const customScope = scopeSpans?.find(scopeSpan => scopeSpan.scope.name === 'custom-tracer'); - - return customScope && customScope.spans.some(span => span.name === 'custom-span-with-only-if-parent'); - }); - - fetch(`${baseURL}/test-only-if-parent`); - - const otelData = await otelPromise; - - expect(otelData).toBeDefined(); - - const json = JSON.parse(otelData); - expect(json.resourceSpans.length).toBe(1); - - const scopeSpans = json.resourceSpans?.[0]?.scopeSpans; - expect(scopeSpans).toBeDefined(); - - // Should have HTTP instrumentation span but NO Sentry span - const httpScopes = scopeSpans?.filter(scopeSpan => scopeSpan.scope.name === '@opentelemetry/instrumentation-http'); - const sentryScopes = scopeSpans?.filter(scopeSpan => scopeSpan.scope.name === '@sentry/node'); - const customScopes = scopeSpans?.filter(scopeSpan => scopeSpan.scope.name === 'custom-tracer'); - - // HTTP span exists (from the incoming request) - expect(httpScopes.length).toBe(1); - - // Sentry span should NOT exist (onlyIfParent + no parent = suppressed) - expect(sentryScopes.length).toBe(0); - - // Custom OTel span SHOULD exist (this is what we're testing - the fix ensures this works) - expect(customScopes.length).toBe(1); - expect(customScopes[0].spans.length).toBe(1); - expect(customScopes[0].spans[0]).toMatchObject({ - name: 'custom-span-with-only-if-parent', - kind: 1, - status: { code: 0 }, - }); - - // Verify the custom span is recording (not suppressed) - const customSpan = customScopes[0].spans[0]; - expect(customSpan.spanId).not.toBe('0000000000000000'); - expect(customSpan.traceId).not.toBe('00000000000000000000000000000000'); -}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tsconfig.json deleted file mode 100644 index b21139d5e56a..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "types": ["node"], - "esModuleInterop": true, - "lib": ["es2018", "dom"], - "strict": true, - "outDir": "dist", - "skipLibCheck": true - }, - "include": ["src/**/*.ts"] -}