diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/instrument-options.mjs b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/instrument-options.mjs index 6908a019faa3..d283ccff7951 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/instrument-options.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/instrument-options.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, @@ -12,11 +11,11 @@ Sentry.init({ Sentry.httpIntegration({ onSpanCreated: (span, req, res) => { span.setAttribute('onSpanCreated', 'yes'); - Sentry.setExtra('onSpanCreatedCalled', { - reqUrl: req.url, - reqMethod: req.method, - resUrl: res.req.url, - resMethod: res.req.method, + span.setAttributes({ + 'onSpanCreated.reqUrl': req.url, + 'onSpanCreated.reqMethod': req.method, + 'onSpanCreated.resUrl': res.req.url, + 'onSpanCreated.resMethod': res.req.method, }); }, }), diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreIncomingRequests.js b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreIncomingRequests.js index fa075766b2b0..bd5d776d3a66 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreIncomingRequests.js +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreIncomingRequests.js @@ -2,7 +2,6 @@ const { loggingTransport } = require('@sentry-internal/node-integration-tests'); const Sentry = require('@sentry/node'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, @@ -11,6 +10,9 @@ Sentry.init({ integrations: [ Sentry.httpIntegration({ ignoreIncomingRequests: (url, request) => { + if (request.url === '/flush') { + return true; + } if (url.includes('/liveness')) { return true; } @@ -32,6 +34,11 @@ const app = express(); app.use(cors()); +app.get('/flush', async (_req, res) => { + await Sentry.flush(); + res.send({ response: 'flushed' }); +}); + app.get('/test', (_req, res) => { res.send({ response: 'response 1' }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreOutgoingRequests.js b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreOutgoingRequests.js index 79f6023fe441..9ff591659f39 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreOutgoingRequests.js +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreOutgoingRequests.js @@ -4,7 +4,6 @@ const Sentry = require('@sentry/node'); const url = process.env.SERVER_URL; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, @@ -40,6 +39,7 @@ app.use(cors()); app.get('/testUrl', (_req, response) => { makeHttpRequest(`${url}/blockUrl`).then(() => { makeHttpRequest(`${url}/pass`).then(() => { + Sentry.captureMessage('outgoing requests completed'); response.send({ response: 'done' }); }); }); @@ -48,6 +48,7 @@ app.get('/testUrl', (_req, response) => { app.get('/testRequest', (_req, response) => { makeHttpRequest(`${url}/blockRequest`).then(() => { makeHttpRequest(`${url}/pass`).then(() => { + Sentry.captureMessage('outgoing requests completed'); response.send({ response: 'done' }); }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreStaticAssets.js b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreStaticAssets.js index d6c17309f66b..edbe00116bf2 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreStaticAssets.js +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreStaticAssets.js @@ -2,13 +2,16 @@ const { loggingTransport } = require('@sentry-internal/node-integration-tests'); const Sentry = require('@sentry/node'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, transport: loggingTransport, // Test default for ignoreStaticAssets: true - integrations: [Sentry.httpIntegration()], + integrations: [ + Sentry.httpIntegration({ + ignoreIncomingRequests: (_url, request) => request.url === '/flush', + }), + ], }); const express = require('express'); @@ -19,6 +22,11 @@ const app = express(); app.use(cors()); +app.get('/flush', async (_req, res) => { + await Sentry.flush(); + res.send({ response: 'flushed' }); +}); + app.get('/test', (_req, res) => { res.send({ response: 'ok' }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-outgoingHooks.js b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-outgoingHooks.js index 23a9006a63b5..7ddda2264933 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-outgoingHooks.js +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-outgoingHooks.js @@ -4,7 +4,6 @@ const Sentry = require('@sentry/node'); const url = process.env.SERVER_URL; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-traceStaticAssets.js b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-traceStaticAssets.js index e1fd410500a1..9c2b3afd24ae 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-traceStaticAssets.js +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-traceStaticAssets.js @@ -2,7 +2,6 @@ const { loggingTransport } = require('@sentry-internal/node-integration-tests'); const Sentry = require('@sentry/node'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts index 1225b5891e8f..d0508d189bb3 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts @@ -1,4 +1,4 @@ -import { HTTP_SERVER } from '@sentry/conventions/op'; +import { HTTP_CLIENT, HTTP_SERVER } from '@sentry/conventions/op'; import { createTestServer } from '@sentry-internal/test-utils'; import { HTTP_RESPONSE_STATUS_CODE, SENTRY_OP, URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; import { afterAll, describe, expect, test } from 'vitest'; @@ -27,28 +27,25 @@ describe('httpIntegration', () => { test('allows to configure onSpanCreated', async () => { const runner = createRunner() .expect({ - transaction: { - contexts: { - trace: { + span: { + items: expect.arrayContaining([ + expect.objectContaining({ + is_segment: true, span_id: expect.stringMatching(/[a-f\d]{16}/), trace_id: expect.stringMatching(/[a-f\d]{32}/), - data: { - 'url.full': expect.stringMatching(/\/test$/), - 'http.response.status_code': 200, - onSpanCreated: 'yes', - }, - op: 'http.server', status: 'ok', - }, - }, - extra: expect.objectContaining({ - onSpanCreatedCalled: { - reqUrl: expect.stringMatching(/\/test$/), - reqMethod: 'GET', - resUrl: expect.stringMatching(/\/test$/), - resMethod: 'GET', - }, - }), + attributes: expect.objectContaining({ + [URL_FULL]: { type: 'string', value: expect.stringMatching(/\/test$/) }, + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [SENTRY_OP]: { type: 'string', value: HTTP_SERVER }, + onSpanCreated: { type: 'string', value: 'yes' }, + 'onSpanCreated.reqUrl': { type: 'string', value: expect.stringMatching(/\/test$/) }, + 'onSpanCreated.reqMethod': { type: 'string', value: 'GET' }, + 'onSpanCreated.resUrl': { type: 'string', value: expect.stringMatching(/\/test$/) }, + 'onSpanCreated.resMethod': { type: 'string', value: 'GET' }, + }), + }), + ]), }, }) .start(); @@ -67,15 +64,15 @@ describe('httpIntegration', () => { const runner = createRunner(__dirname, 'server-outgoingHooks.js') .withEnv({ SERVER_URL }) .expect({ - transaction: event => { - const clientSpans = event.spans?.filter(span => span.op === 'http.client'); + span: container => { + const clientSpans = container.items.filter(span => span.attributes[SENTRY_OP]?.value === HTTP_CLIENT); expect(clientSpans).toHaveLength(1); // All three hooks run before the span ends, so every attribute has to survive to the envelope. - const data = clientSpans![0]?.data; - expect(data?.['outgoingRequestHook']).toBe('GET'); - expect(data?.['outgoingResponseHook']).toBe(200); - expect(data?.['outgoingRequestApplyCustomAttributes']).toBe('GET 200'); + const attributes = clientSpans[0]?.attributes; + expect(attributes?.['outgoingRequestHook']).toEqual({ type: 'string', value: 'GET' }); + expect(attributes?.['outgoingResponseHook']).toEqual({ type: 'integer', value: 200 }); + expect(attributes?.['outgoingRequestApplyCustomAttributes']).toEqual({ type: 'string', value: 'GET 200' }); }, }) .start(); @@ -455,52 +452,58 @@ describe('httpIntegration', () => { test('via the url param', async () => { const runner = createRunner(__dirname, 'server-ignoreIncomingRequests.js') .expect({ - transaction: { - contexts: { - trace: { + span: { + items: expect.arrayContaining([ + expect.objectContaining({ + name: 'GET /test', + is_segment: true, span_id: expect.stringMatching(/[a-f\d]{16}/), trace_id: expect.stringMatching(/[a-f\d]{32}/), - data: { - 'url.full': expect.stringMatching(/\/test$/), - 'http.response.status_code': 200, - }, - op: 'http.server', status: 'ok', - }, - }, - transaction: 'GET /test', + attributes: expect.objectContaining({ + [URL_FULL]: { type: 'string', value: expect.stringMatching(/\/test$/) }, + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [SENTRY_OP]: { type: 'string', value: HTTP_SERVER }, + }), + }), + ]), }, }) .start(); - runner.makeRequest('get', '/liveness'); // should be ignored - runner.makeRequest('get', '/test'); + await runner.makeRequest('get', '/liveness'); // should be ignored + // Flush so any span from the ignored request fails the expectation before /test runs. + await runner.makeRequest('get', '/flush'); + await runner.makeRequest('get', '/test'); await runner.completed(); }); test('via the request param', async () => { const runner = createRunner(__dirname, 'server-ignoreIncomingRequests.js') .expect({ - transaction: { - contexts: { - trace: { + span: { + items: expect.arrayContaining([ + expect.objectContaining({ + name: 'GET /test', + is_segment: true, span_id: expect.stringMatching(/[a-f\d]{16}/), trace_id: expect.stringMatching(/[a-f\d]{32}/), - data: { - 'url.full': expect.stringMatching(/\/test$/), - 'http.response.status_code': 200, - }, - op: 'http.server', status: 'ok', - }, - }, - transaction: 'GET /test', + attributes: expect.objectContaining({ + [URL_FULL]: { type: 'string', value: expect.stringMatching(/\/test$/) }, + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [SENTRY_OP]: { type: 'string', value: HTTP_SERVER }, + }), + }), + ]), }, }) .start(); - runner.makeRequest('post', '/readiness'); // should be ignored - runner.makeRequest('get', '/test'); + await runner.makeRequest('post', '/readiness'); // should be ignored + // Flush so any span from the ignored request fails the expectation before /test runs. + await runner.makeRequest('get', '/flush'); + await runner.makeRequest('get', '/test'); await runner.completed(); }); }); @@ -515,18 +518,22 @@ describe('httpIntegration', () => { const runner = createRunner(__dirname, 'server-ignoreOutgoingRequests.js') .withEnv({ SERVER_URL }) .expect({ - transaction: event => { - expect(event.transaction).toBe('GET /testUrl'); - - const requestSpans = event.spans?.filter(span => span.op === 'http.client'); - expect(requestSpans).toHaveLength(1); - expect(requestSpans![0]?.description).toBe(`GET ${SERVER_URL}/pass`); - + event: event => { const breadcrumbs = event.breadcrumbs?.filter(b => b.category === 'http'); expect(breadcrumbs).toHaveLength(1); expect(breadcrumbs![0]?.data?.url).toEqual(`${SERVER_URL}/pass`); }, }) + .expect({ + span: container => { + expect(container.items.find(span => span.is_segment)?.name).toBe('GET /testUrl'); + + const requestSpans = container.items.filter(span => span.attributes[SENTRY_OP]?.value === HTTP_CLIENT); + expect(requestSpans).toHaveLength(1); + expect(requestSpans[0]?.name).toBe('GET localhost'); + expect(requestSpans[0]?.attributes[URL_FULL]).toEqual({ type: 'string', value: `${SERVER_URL}/pass` }); + }, + }) .start(); runner.makeRequest('get', '/testUrl'); await runner.completed(); @@ -542,18 +549,22 @@ describe('httpIntegration', () => { const runner = createRunner(__dirname, 'server-ignoreOutgoingRequests.js') .withEnv({ SERVER_URL }) .expect({ - transaction: event => { - expect(event.transaction).toBe('GET /testRequest'); - - const requestSpans = event.spans?.filter(span => span.op === 'http.client'); - expect(requestSpans).toHaveLength(1); - expect(requestSpans![0]?.description).toBe(`GET ${SERVER_URL}/pass`); - + event: event => { const breadcrumbs = event.breadcrumbs?.filter(b => b.category === 'http'); expect(breadcrumbs).toHaveLength(1); expect(breadcrumbs![0]?.data?.url).toEqual(`${SERVER_URL}/pass`); }, }) + .expect({ + span: container => { + expect(container.items.find(span => span.is_segment)?.name).toBe('GET /testRequest'); + + const requestSpans = container.items.filter(span => span.attributes[SENTRY_OP]?.value === HTTP_CLIENT); + expect(requestSpans).toHaveLength(1); + expect(requestSpans[0]?.name).toBe('GET localhost'); + expect(requestSpans[0]?.attributes[URL_FULL]).toEqual({ type: 'string', value: `${SERVER_URL}/pass` }); + }, + }) .start(); runner.makeRequest('get', '/testRequest'); @@ -565,11 +576,12 @@ describe('httpIntegration', () => { test('ignores static asset requests by default', async () => { const runner = createRunner(__dirname, 'server-ignoreStaticAssets.js') .expect({ - transaction: event => { - expect(event.transaction).toBe('GET /test'); - expect(event.contexts?.trace?.data?.['url.full']).toMatch(/\/test$/); - expect(event.contexts?.trace?.op).toBe('http.server'); - expect(event.contexts?.trace?.status).toBe('ok'); + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('GET /test'); + expect(segment?.attributes[URL_FULL]?.value).toMatch(/\/test$/); + expect(segment?.attributes[SENTRY_OP]).toEqual({ type: 'string', value: HTTP_SERVER }); + expect(segment?.status).toBe('ok'); }, }) .start(); @@ -579,6 +591,9 @@ describe('httpIntegration', () => { await runner.makeRequest('get', '/robots.txt'); await runner.makeRequest('get', '/assets/app.js'); + // Flush so any static-asset span fails the expectation before /test runs. + await runner.makeRequest('get', '/flush'); + // This one should be traced await runner.makeRequest('get', '/test'); @@ -588,11 +603,12 @@ describe('httpIntegration', () => { test('traces static asset requests when ignoreStaticAssets is false', async () => { const runner = createRunner(__dirname, 'server-traceStaticAssets.js') .expect({ - transaction: event => { - expect(event.transaction).toBe('GET /favicon.ico'); - expect(event.contexts?.trace?.data?.['url.full']).toMatch(/\/favicon.ico$/); - expect(event.contexts?.trace?.op).toBe('http.server'); - expect(event.contexts?.trace?.status).toBe('ok'); + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('GET /favicon.ico'); + expect(segment?.attributes[URL_FULL]?.value).toMatch(/\/favicon.ico$/); + expect(segment?.attributes[SENTRY_OP]).toEqual({ type: 'string', value: HTTP_SERVER }); + expect(segment?.status).toBe('ok'); }, }) .start();