diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-standalone-span/init.js similarity index 76% rename from dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/init.js rename to dev-packages/browser-integration-tests/suites/tracing/request/fetch-standalone-span/init.js index e94191654e74..d4ad9bb47ce9 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-standalone-span/init.js @@ -2,8 +2,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window._sentryTransactionsCount = 0; - Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', // disable auto span creation @@ -16,8 +14,4 @@ Sentry.init({ tracePropagationTargets: ['http://example.com'], tracesSampleRate: 1, autoSessionTracking: false, - beforeSendTransaction() { - window._sentryTransactionsCount++; - return null; - }, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-standalone-span/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-standalone-span/subject.js new file mode 100644 index 000000000000..ab34b15730e4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-standalone-span/subject.js @@ -0,0 +1 @@ +fetch('http://example.com/0'); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-standalone-span/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-standalone-span/test.ts new file mode 100644 index 000000000000..ef59fbc810dd --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-standalone-span/test.ts @@ -0,0 +1,96 @@ +import type { SpanEnvelope } from '@sentry/types'; +import { sentryTest } from '../../../../utils/fixtures'; +import { + getFirstSentryEnvelopeRequest, + properFullEnvelopeRequestParser, + shouldSkipTracingTest, +} from '../../../../utils/helpers'; + +import { expect } from '@playwright/test'; + +sentryTest( + "should create standalone span for fetch requests if there's no active span and should attach tracing headers", + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + let sentryTraceHeader = ''; + let baggageHeader = ''; + + await page.route('http://example.com/**', route => { + sentryTraceHeader = route.request().headers()['sentry-trace']; + baggageHeader = route.request().headers()['baggage']; + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const spanEnvelopePromise = getFirstSentryEnvelopeRequest( + page, + undefined, + properFullEnvelopeRequestParser, + ); + + await page.goto(url); + + const spanEnvelope = await spanEnvelopePromise; + + const spanEnvelopeHeaders = spanEnvelope[0]; + const spanEnvelopeItem = spanEnvelope[1][0][1]; + + const traceId = spanEnvelopeHeaders.trace!.trace_id; + const spanId = spanEnvelopeItem.span_id; + + expect(traceId).toMatch(/[a-f0-9]{32}/); + expect(spanId).toMatch(/[a-f0-9]{16}/); + + expect(spanEnvelopeHeaders).toEqual({ + sent_at: expect.any(String), + trace: { + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: traceId, + transaction: 'GET http://example.com/0', + }, + }); + + expect(spanEnvelopeItem).toEqual({ + data: expect.objectContaining({ + 'http.method': 'GET', + 'http.response.status_code': 200, + 'http.response_content_length': expect.any(Number), + 'http.url': 'http://example.com/0', + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.browser', + 'sentry.sample_rate': 1, + 'sentry.source': 'custom', + 'server.address': 'example.com', + type: 'fetch', + url: 'http://example.com/0', + }), + description: 'GET http://example.com/0', + op: 'http.client', + origin: 'auto.http.browser', + status: 'ok', + trace_id: traceId, + span_id: spanId, + segment_id: spanId, + is_segment: true, + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }); + + // the standalone span was sampled, so we propagate the positive sampling decision + expect(sentryTraceHeader).toBe(`${traceId}-${spanId}-1`); + expect(baggageHeader).toBe( + `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${traceId},sentry-sample_rate=1,sentry-transaction=GET%20http%3A%2F%2Fexample.com%2F0,sentry-sampled=true`, + ); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/subject.js deleted file mode 100644 index f62499b1e9c5..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/subject.js +++ /dev/null @@ -1 +0,0 @@ -fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2'))); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/test.ts deleted file mode 100644 index acaf0e98d693..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { expect } from '@playwright/test'; - -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; - -sentryTest( - 'should not create span for fetch requests with no active span but should attach sentry-trace header', - async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - const sentryTraceHeaders: string[] = []; - - await page.route('http://example.com/**', route => { - const sentryTraceHeader = route.request().headers()['sentry-trace']; - if (sentryTraceHeader) { - sentryTraceHeaders.push(sentryTraceHeader); - } - - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({}), - }); - }); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - await Promise.all([page.goto(url), ...[0, 1, 2].map(idx => page.waitForRequest(`http://example.com/${idx}`))]); - - expect(await page.evaluate('window._sentryTransactionsCount')).toBe(0); - - expect(sentryTraceHeaders).toHaveLength(3); - expect(sentryTraceHeaders).toEqual([ - expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/), - expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/), - expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/), - ]); - }, -); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-standalone-span/init.js similarity index 76% rename from dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/init.js rename to dev-packages/browser-integration-tests/suites/tracing/request/xhr-standalone-span/init.js index e94191654e74..d4ad9bb47ce9 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-standalone-span/init.js @@ -2,8 +2,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window._sentryTransactionsCount = 0; - Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', // disable auto span creation @@ -16,8 +14,4 @@ Sentry.init({ tracePropagationTargets: ['http://example.com'], tracesSampleRate: 1, autoSessionTracking: false, - beforeSendTransaction() { - window._sentryTransactionsCount++; - return null; - }, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-standalone-span/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-standalone-span/subject.js new file mode 100644 index 000000000000..a487cfac3676 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-standalone-span/subject.js @@ -0,0 +1,3 @@ +const xhr_1 = new XMLHttpRequest(); +xhr_1.open('GET', 'http://example.com/0'); +xhr_1.send(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-standalone-span/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-standalone-span/test.ts new file mode 100644 index 000000000000..46c1c5d616a3 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-standalone-span/test.ts @@ -0,0 +1,95 @@ +import { expect } from '@playwright/test'; + +import type { SpanEnvelope } from '@sentry/types'; +import { sentryTest } from '../../../../utils/fixtures'; +import { + getFirstSentryEnvelopeRequest, + properFullEnvelopeRequestParser, + shouldSkipTracingTest, +} from '../../../../utils/helpers'; + +sentryTest( + "should create standalone span for XHR requests if there's no active span and should attach tracing headers", + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + let sentryTraceHeader = ''; + let baggageHeader = ''; + + await page.route('http://example.com/**', route => { + sentryTraceHeader = route.request().headers()['sentry-trace']; + baggageHeader = route.request().headers()['baggage']; + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const spanEnvelopePromise = getFirstSentryEnvelopeRequest( + page, + undefined, + properFullEnvelopeRequestParser, + ); + + await page.goto(url); + + const spanEnvelope = await spanEnvelopePromise; + + const spanEnvelopeHeaders = spanEnvelope[0]; + const spanEnvelopeItem = spanEnvelope[1][0][1]; + + const traceId = spanEnvelopeHeaders.trace!.trace_id; + const spanId = spanEnvelopeItem.span_id; + + expect(traceId).toMatch(/[a-f0-9]{32}/); + expect(spanId).toMatch(/[a-f0-9]{16}/); + + expect(spanEnvelopeHeaders).toEqual({ + sent_at: expect.any(String), + trace: { + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: traceId, + transaction: 'GET http://example.com/0', + }, + }); + + expect(spanEnvelopeItem).toEqual({ + data: { + 'http.method': 'GET', + 'http.response.status_code': 200, + 'http.url': 'http://example.com/0', + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.browser', + 'sentry.sample_rate': 1, + 'sentry.source': 'custom', + 'server.address': 'example.com', + type: 'xhr', + url: 'http://example.com/0', + }, + description: 'GET http://example.com/0', + op: 'http.client', + origin: 'auto.http.browser', + status: 'ok', + trace_id: traceId, + span_id: spanId, + segment_id: spanId, + is_segment: true, + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }); + + // the standalone span was sampled, so we propagate the positive sampling decision + expect(sentryTraceHeader).toBe(`${traceId}-${spanId}-1`); + expect(baggageHeader).toBe( + `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${traceId},sentry-sample_rate=1,sentry-transaction=GET%20http%3A%2F%2Fexample.com%2F0,sentry-sampled=true`, + ); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/subject.js deleted file mode 100644 index 5790c230aa66..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/subject.js +++ /dev/null @@ -1,11 +0,0 @@ -const xhr_1 = new XMLHttpRequest(); -xhr_1.open('GET', 'http://example.com/0'); -xhr_1.send(); - -const xhr_2 = new XMLHttpRequest(); -xhr_2.open('GET', 'http://example.com/1'); -xhr_2.send(); - -const xhr_3 = new XMLHttpRequest(); -xhr_3.open('GET', 'http://example.com/2'); -xhr_3.send(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/test.ts deleted file mode 100644 index d3c5f91362c9..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { expect } from '@playwright/test'; - -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; - -sentryTest( - 'should not create span for xhr requests with no active span but should attach sentry-trace header', - async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - const sentryTraceHeaders: string[] = []; - - await page.route('http://example.com/**', route => { - const sentryTraceHeader = route.request().headers()['sentry-trace']; - if (sentryTraceHeader) { - sentryTraceHeaders.push(sentryTraceHeader); - } - - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({}), - }); - }); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - await Promise.all([page.goto(url), ...[0, 1, 2].map(idx => page.waitForRequest(`http://example.com/${idx}`))]); - - expect(await page.evaluate('window._sentryTransactionsCount')).toBe(0); - - expect(sentryTraceHeaders).toHaveLength(3); - expect(sentryTraceHeaders).toEqual([ - expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/), - expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/), - expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/), - ]); - }, -); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/test.ts index cd5c25237eb7..c3e4bf936288 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/test.ts @@ -1,11 +1,12 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/types'; +import type { Event, SpanEnvelope } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import type { EventAndTraceHeader } from '../../../../utils/helpers'; import { eventAndTraceHeaderRequestParser, getFirstSentryEnvelopeRequest, getMultipleSentryEnvelopeRequests, + properFullEnvelopeRequestParser, shouldSkipTracingTest, } from '../../../../utils/helpers'; @@ -185,7 +186,7 @@ sentryTest('error during navigation has new navigation traceId', async ({ getLoc }); sentryTest( - 'outgoing fetch request after navigation has navigation traceId in headers', + 'outgoing fetch request after navigation has navigation traceId in headers and standalone span', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); @@ -213,6 +214,8 @@ sentryTest( const navigationTraceContext = navigationEvent.contexts?.trace; expect(navigationEvent.type).toEqual('transaction'); + const navigationTraceId = navigationTraceContext?.trace_id; + expect(navigationTraceContext).toMatchObject({ op: 'navigation', trace_id: expect.stringMatching(/^[0-9a-f]{32}$/), @@ -225,17 +228,31 @@ sentryTest( public_key: 'public', sample_rate: '1', sampled: 'true', - trace_id: navigationTraceContext?.trace_id, + trace_id: navigationTraceId, }); + const spanEnvelopePromise = getFirstSentryEnvelopeRequest( + page, + undefined, + properFullEnvelopeRequestParser, + ); const requestPromise = page.waitForRequest('http://example.com/*'); await page.locator('#fetchBtn').click(); - const request = await requestPromise; + const [request, spanEnvelope] = await Promise.all([requestPromise, spanEnvelopePromise]); const headers = request.headers(); + const spanEnvelopeTraceHeader = spanEnvelope[0].trace; + const spanEnvelopeItem = spanEnvelope[1][0][1]; + + expect(spanEnvelopeTraceHeader).toEqual(navigationTraceHeader); + + expect(spanEnvelopeItem).toMatchObject({ + trace_id: navigationTraceContext?.trace_id, + span_id: expect.stringMatching(/^[0-9a-f]{16}$/), + }); + // sampling decision and DSC are continued from navigation span, even after it ended - const navigationTraceId = navigationTraceContext?.trace_id; - expect(headers['sentry-trace']).toMatch(new RegExp(`^${navigationTraceId}-[0-9a-f]{16}-1$`)); + expect(headers['sentry-trace']).toEqual(`${navigationTraceId}-${spanEnvelopeItem.span_id}-1`); expect(headers['baggage']).toEqual( `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId},sentry-sample_rate=1,sentry-sampled=true`, ); @@ -305,7 +322,7 @@ sentryTest( ); sentryTest( - 'outgoing XHR request after navigation has navigation traceId in headers', + 'outgoing XHR request after navigation has navigation traceId in headers and in span envelope', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); @@ -331,8 +348,10 @@ sentryTest( ); const navigationTraceContext = navigationEvent.contexts?.trace; - expect(navigationEvent.type).toEqual('transaction'); + + const navigationTraceId = navigationTraceContext?.trace_id; + expect(navigationTraceContext).toMatchObject({ op: 'navigation', trace_id: expect.stringMatching(/^[0-9a-f]{32}$/), @@ -349,12 +368,26 @@ sentryTest( }); const xhrPromise = page.waitForRequest('http://example.com/*'); + const spanEnvelopePromise = getFirstSentryEnvelopeRequest( + page, + undefined, + properFullEnvelopeRequestParser, + ); await page.locator('#xhrBtn').click(); - const request = await xhrPromise; + const [request, spanEnvelope] = await Promise.all([xhrPromise, spanEnvelopePromise]); const headers = request.headers(); + const spanEnvelopeTraceHeader = spanEnvelope[0].trace; + const spanEnvelopeItem = spanEnvelope[1][0][1]; + + expect(spanEnvelopeTraceHeader).toEqual(navigationTraceHeader); + + expect(spanEnvelopeItem).toMatchObject({ + trace_id: navigationTraceContext?.trace_id, + span_id: expect.stringMatching(/^[0-9a-f]{16}$/), + }); + // sampling decision and DSC are continued from navigation span, even after it ended - const navigationTraceId = navigationTraceContext?.trace_id; expect(headers['sentry-trace']).toMatch(new RegExp(`^${navigationTraceId}-[0-9a-f]{16}-1$`)); expect(headers['baggage']).toEqual( `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId},sentry-sample_rate=1,sentry-sampled=true`, diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/test.ts index 0495c9003579..f206e9292b9f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/test.ts @@ -1,10 +1,12 @@ import { expect } from '@playwright/test'; +import type { SpanEnvelope } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import type { EventAndTraceHeader } from '../../../../utils/helpers'; import { eventAndTraceHeaderRequestParser, getFirstSentryEnvelopeRequest, getMultipleSentryEnvelopeRequests, + properFullEnvelopeRequestParser, shouldSkipTracingTest, } from '../../../../utils/helpers'; @@ -190,7 +192,7 @@ sentryTest('error during tag pageload has pageload traceId', async ({ get }); sentryTest( - 'outgoing fetch request after tag pageload has pageload traceId in headers', + 'outgoing fetch request after tag pageload has pageload traceId in headers and span envelope', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); @@ -230,10 +232,26 @@ sentryTest( }); const requestPromise = page.waitForRequest('http://example.com/*'); + const spanEnvelopePromise = getFirstSentryEnvelopeRequest( + page, + undefined, + properFullEnvelopeRequestParser, + ); await page.locator('#fetchBtn').click(); - const request = await requestPromise; + const [request, spanEnvelope] = await Promise.all([requestPromise, spanEnvelopePromise]); const headers = request.headers(); + const spanEnvelopeTraceHeader = spanEnvelope[0].trace; + const spanEnvelopeItem = spanEnvelope[1][0][1]; + + expect(spanEnvelopeTraceHeader).toEqual(pageloadTraceHeader); + + expect(spanEnvelopeItem).toMatchObject({ + parent_span_id: META_TAG_PARENT_SPAN_ID, + span_id: expect.stringMatching(/^[0-9a-f]{16}$/), + trace_id: META_TAG_TRACE_ID, + }); + // sampling decision is propagated from meta tag's sentry-trace sampled flag expect(headers['sentry-trace']).toMatch(new RegExp(`^${META_TAG_TRACE_ID}-[0-9a-f]{16}-1$`)); expect(headers['baggage']).toBe(META_TAG_BAGGAGE); @@ -333,10 +351,26 @@ sentryTest( }); const requestPromise = page.waitForRequest('http://example.com/*'); + const spanEnvelopePromise = getFirstSentryEnvelopeRequest( + page, + undefined, + properFullEnvelopeRequestParser, + ); await page.locator('#xhrBtn').click(); - const request = await requestPromise; + const [request, spanEnvelope] = await Promise.all([requestPromise, spanEnvelopePromise]); const headers = request.headers(); + const spanEnvelopeTraceHeader = spanEnvelope[0].trace; + const spanEnvelopeItem = spanEnvelope[1][0][1]; + + expect(spanEnvelopeTraceHeader).toEqual(pageloadTraceHeader); + + expect(spanEnvelopeItem).toMatchObject({ + parent_span_id: META_TAG_PARENT_SPAN_ID, + span_id: expect.stringMatching(/^[0-9a-f]{16}$/), + trace_id: META_TAG_TRACE_ID, + }); + // sampling decision is propagated from meta tag's sentry-trace sampled flag expect(headers['sentry-trace']).toMatch(new RegExp(`^${META_TAG_TRACE_ID}-[0-9a-f]{16}-1$`)); expect(headers['baggage']).toBe(META_TAG_BAGGAGE); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload/test.ts index e167c63d626e..4bfe8c26d5cb 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload/test.ts @@ -1,10 +1,12 @@ import { expect } from '@playwright/test'; +import type { SpanEnvelope } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import type { EventAndTraceHeader } from '../../../../utils/helpers'; import { eventAndTraceHeaderRequestParser, getFirstSentryEnvelopeRequest, getMultipleSentryEnvelopeRequests, + properFullEnvelopeRequestParser, shouldSkipTracingTest, } from '../../../../utils/helpers'; @@ -181,7 +183,7 @@ sentryTest('error during pageload has pageload traceId', async ({ getLocalTestUr }); sentryTest( - 'outgoing fetch request after pageload has pageload traceId in headers', + 'outgoing fetch request after pageload has pageload traceId in headers and span envelope', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); @@ -222,10 +224,25 @@ sentryTest( }); const requestPromise = page.waitForRequest('http://example.com/*'); + const spanEnvelopePromise = getFirstSentryEnvelopeRequest( + page, + undefined, + properFullEnvelopeRequestParser, + ); await page.locator('#fetchBtn').click(); - const request = await requestPromise; + const [request, spanEnvelope] = await Promise.all([requestPromise, spanEnvelopePromise]); const headers = request.headers(); + const spanEnvelopeTraceHeader = spanEnvelope[0].trace; + const spanEnvelopeItem = spanEnvelope[1][0][1]; + + expect(spanEnvelopeTraceHeader).toEqual(pageloadTraceHeader); + + expect(spanEnvelopeItem).toMatchObject({ + trace_id: pageloadTraceId, + span_id: expect.stringMatching(/^[0-9a-f]{16}$/), + }); + // sampling decision and DSC are continued from the pageload span even after it ended expect(headers['sentry-trace']).toMatch(new RegExp(`^${pageloadTraceId}-[0-9a-f]{16}-1$`)); expect(headers['baggage']).toEqual( @@ -290,56 +307,74 @@ sentryTest( }, ); -sentryTest('outgoing XHR request after pageload has pageload traceId in headers', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +sentryTest( + 'outgoing XHR request after pageload has pageload traceId in headers and span envelope', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } - const url = await getLocalTestUrl({ testDir: __dirname }); + const url = await getLocalTestUrl({ testDir: __dirname }); - await page.route('http://example.com/**', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({}), + await page.route('http://example.com/**', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); }); - }); - const [pageloadEvent, pageloadTraceHeader] = await getFirstSentryEnvelopeRequest( - page, - url, - eventAndTraceHeaderRequestParser, - ); - const pageloadTraceContext = pageloadEvent.contexts?.trace; - const pageloadTraceId = pageloadTraceContext?.trace_id; + const [pageloadEvent, pageloadTraceHeader] = await getFirstSentryEnvelopeRequest( + page, + url, + eventAndTraceHeaderRequestParser, + ); + const pageloadTraceContext = pageloadEvent.contexts?.trace; + const pageloadTraceId = pageloadTraceContext?.trace_id; - expect(pageloadEvent.type).toEqual('transaction'); - expect(pageloadTraceContext).toMatchObject({ - op: 'pageload', - trace_id: expect.stringMatching(/^[0-9a-f]{32}$/), - span_id: expect.stringMatching(/^[0-9a-f]{16}$/), - }); - expect(pageloadTraceContext).not.toHaveProperty('parent_span_id'); + expect(pageloadEvent.type).toEqual('transaction'); + expect(pageloadTraceContext).toMatchObject({ + op: 'pageload', + trace_id: expect.stringMatching(/^[0-9a-f]{32}$/), + span_id: expect.stringMatching(/^[0-9a-f]{16}$/), + }); + expect(pageloadTraceContext).not.toHaveProperty('parent_span_id'); - expect(pageloadTraceHeader).toEqual({ - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: pageloadTraceId, - }); + expect(pageloadTraceHeader).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: pageloadTraceId, + }); - const requestPromise = page.waitForRequest('http://example.com/*'); - await page.locator('#xhrBtn').click(); - const request = await requestPromise; - const headers = request.headers(); + const requestPromise = page.waitForRequest('http://example.com/*'); + const spanEnvelopePromise = getFirstSentryEnvelopeRequest( + page, + undefined, + properFullEnvelopeRequestParser, + ); + await page.locator('#xhrBtn').click(); + const [request, spanEnvelope] = await Promise.all([requestPromise, spanEnvelopePromise]); + const headers = request.headers(); - // sampling decision and DSC are continued from the pageload span even after it ended - expect(headers['sentry-trace']).toMatch(new RegExp(`^${pageloadTraceId}-[0-9a-f]{16}-1$`)); - expect(headers['baggage']).toEqual( - `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sample_rate=1,sentry-sampled=true`, - ); -}); + const spanEnvelopeTraceHeader = spanEnvelope[0].trace; + const spanEnvelopeItem = spanEnvelope[1][0][1]; + + expect(spanEnvelopeTraceHeader).toEqual(pageloadTraceHeader); + + expect(spanEnvelopeItem).toMatchObject({ + trace_id: pageloadTraceId, + span_id: expect.stringMatching(/^[0-9a-f]{16}$/), + }); + + // sampling decision and DSC are continued from the pageload span even after it ended + expect(headers['sentry-trace']).toMatch(new RegExp(`^${pageloadTraceId}-[0-9a-f]{16}-1$`)); + expect(headers['baggage']).toEqual( + `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sample_rate=1,sentry-sampled=true`, + ); + }, +); sentryTest( 'outgoing XHR request during pageload has pageload traceId in headers', diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index 45b373c2468e..909acb647f17 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -327,21 +327,23 @@ export function xhrCallback( const fullUrl = getFullURL(sentryXhrData.url); const host = fullUrl ? parseUrl(fullUrl).host : undefined; - const span = - shouldCreateSpanResult && hasParent - ? startInactiveSpan({ - name: `${sentryXhrData.method} ${sentryXhrData.url}`, - attributes: { - type: 'xhr', - 'http.method': sentryXhrData.method, - 'http.url': fullUrl, - url: sentryXhrData.url, - 'server.address': host, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client', - }, - }) - : new SentryNonRecordingSpan(); + const span = shouldCreateSpanResult + ? startInactiveSpan({ + name: `${sentryXhrData.method} ${sentryXhrData.url}`, + attributes: { + type: 'xhr', + 'http.method': sentryXhrData.method, + 'http.url': fullUrl, + url: sentryXhrData.url, + 'server.address': host, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client', + }, + experimental: { + standalone: !hasParent, + }, + }) + : new SentryNonRecordingSpan(); xhr.__sentry_xhr_span_id__ = span.spanContext().spanId; spans[xhr.__sentry_xhr_span_id__] = span; @@ -352,11 +354,9 @@ export function xhrCallback( addTracingHeadersToXhrRequest( xhr, client, - // In the following cases, we do not want to use the span as base for the trace headers, - // which means that the headers will be generated from the scope: - // - If tracing is disabled (TWP) - // - If the span has no parent span - which means we ran into `onlyIfParent` check - hasTracingEnabled() && hasParent ? span : undefined, + // If performance is disabled (TWP), we do not want to use the span as base for the trace headers, + // which means that the headers will be generated from the scope and the sampling decision is deferred + hasTracingEnabled() ? span : undefined, ); } diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts index 394f70d7fa47..5766c7d1d7bd 100644 --- a/packages/core/src/fetch.ts +++ b/packages/core/src/fetch.ts @@ -72,21 +72,23 @@ export function instrumentFetchRequest( const fullUrl = getFullURL(url); const host = fullUrl ? parseUrl(fullUrl).host : undefined; - const span = - shouldCreateSpanResult && hasParent - ? startInactiveSpan({ - name: `${method} ${url}`, - attributes: { - url, - type: 'fetch', - 'http.method': method, - 'http.url': fullUrl, - 'server.address': host, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client', - }, - }) - : new SentryNonRecordingSpan(); + const span = shouldCreateSpanResult + ? startInactiveSpan({ + name: `${method} ${url}`, + attributes: { + url, + type: 'fetch', + 'http.method': method, + 'http.url': fullUrl, + 'server.address': host, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin, + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client', + }, + experimental: { + standalone: !hasParent, + }, + }) + : new SentryNonRecordingSpan(); handlerData.fetchData.__span = span.spanContext().spanId; spans[span.spanContext().spanId] = span; @@ -105,11 +107,9 @@ export function instrumentFetchRequest( client, scope, options, - // In the following cases, we do not want to use the span as base for the trace headers, - // which means that the headers will be generated from the scope: - // - If tracing is disabled (TWP) - // - If the span has no parent span - which means we ran into `onlyIfParent` check - hasTracingEnabled() && hasParent ? span : undefined, + // If performance is disabled (TWP), we do not want to use the span as base for the trace headers, + // which means that the headers will be generated from the scope and the sampling decision is deferred + hasTracingEnabled() ? span : undefined, ); } diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts index 874edd8d2eda..358f60ccee18 100644 --- a/packages/types/src/envelope.ts +++ b/packages/types/src/envelope.ts @@ -106,7 +106,7 @@ type CheckInEnvelopeHeaders = { trace?: DynamicSamplingContext }; type ClientReportEnvelopeHeaders = BaseEnvelopeHeaders; type ReplayEnvelopeHeaders = BaseEnvelopeHeaders; type StatsdEnvelopeHeaders = BaseEnvelopeHeaders; -type SpanEnvelopeHeaders = BaseEnvelopeHeaders; +type SpanEnvelopeHeaders = BaseEnvelopeHeaders & { trace?: DynamicSamplingContext }; export type EventEnvelope = BaseEnvelope< EventEnvelopeHeaders,