diff --git a/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/init.js b/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/init.js new file mode 100644 index 000000000000..9c7cdb7e11b6 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false })], + tracesSampleRate: 1, + tracePropagationTargets: ['example.com'], + release: '1.1.1', +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/subject.js b/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/subject.js new file mode 100644 index 000000000000..163082710a13 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/subject.js @@ -0,0 +1,34 @@ +const btnStartSpan = document.getElementById('btnStartSpan'); +const btnUpdateName = document.getElementById('btnUpdateName'); +const btnMakeRequest = document.getElementById('btnMakeRequest'); +const btnCaptureError = document.getElementById('btnCaptureError'); +const btnEndSpan = document.getElementById('btnEndSpan'); + +btnStartSpan.addEventListener('click', () => { + Sentry.startSpanManual( + { name: 'test-root-span', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } }, + async span => { + window.__traceId = span.spanContext().traceId; + await new Promise(resolve => { + btnEndSpan.addEventListener('click', resolve); + }); + span.end(); + }, + ); +}); + +let updateCnt = 0; +btnUpdateName.addEventListener('click', () => { + const span = Sentry.getActiveSpan(); + const rootSpan = Sentry.getRootSpan(span); + rootSpan.updateName(`updated-root-span-${++updateCnt}`); + rootSpan.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); +}); + +btnMakeRequest.addEventListener('click', () => { + fetch('https://example.com/api'); +}); + +btnCaptureError.addEventListener('click', () => { + Sentry.captureException(new Error('test-error')); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/template.html b/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/template.html new file mode 100644 index 000000000000..9ad1d0cfe584 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/template.html @@ -0,0 +1,5 @@ + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts b/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts new file mode 100644 index 000000000000..e8c21a66647f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts @@ -0,0 +1,185 @@ +import { expect } from '@playwright/test'; +import type { Page } from '@playwright/test'; +import type { DynamicSamplingContext } from '@sentry/types'; +import { sentryTest } from '../../../utils/fixtures'; +import type { EventAndTraceHeader } from '../../../utils/helpers'; +import { + eventAndTraceHeaderRequestParser, + getMultipleSentryEnvelopeRequests, + shouldSkipTracingTest, +} from '../../../utils/helpers'; + +sentryTest('updates the DSC when the txn name is updated and high-quality', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + /* + Test Steps: + 1. Start new span with LQ txn name (source: url) + 2. Make request and check that baggage has no transaction name + 3. Capture error and check that envelope trace header has no transaction name + 4. Update span name and source to HQ (source: route) + 5. Make request and check that baggage has HQ txn name + 6. Capture error and check that envelope trace header has HQ txn name + 7. Update span name again with HQ name (source: route) + 8. Make request and check that baggage has updated HQ txn name + 9. Capture error and check that envelope trace header has updated HQ txn name + 10. End span and check that envelope trace header has updated HQ txn name + 11. Make another request and check that there's no span information in baggage + 12. Capture an error and check that envelope trace header has no span information + */ + + // 1 + await page.locator('#btnStartSpan').click(); + const traceId = await page.evaluate(() => { + return (window as any).__traceId; + }); + + expect(traceId).toMatch(/^[0-9a-f]{32}$/); + + // 2 + const baggageItems = await makeRequestAndGetBaggageItems(page); + expect(baggageItems).toEqual([ + 'sentry-environment=production', + 'sentry-public_key=public', + 'sentry-release=1.1.1', + 'sentry-sample_rate=1', + 'sentry-sampled=true', + `sentry-trace_id=${traceId}`, + ]); + + // 3 + const errorEnvelopeTraceHeader = await captureErrorAndGetEnvelopeTraceHeader(page); + expect(errorEnvelopeTraceHeader).toEqual({ + environment: 'production', + public_key: 'public', + release: '1.1.1', + sample_rate: '1', + sampled: 'true', + trace_id: traceId, + }); + + // 4 + await page.locator('#btnUpdateName').click(); + + // 5 + const baggageItemsAfterUpdate = await makeRequestAndGetBaggageItems(page); + expect(baggageItemsAfterUpdate).toEqual([ + 'sentry-environment=production', + 'sentry-public_key=public', + 'sentry-release=1.1.1', + 'sentry-sample_rate=1', + 'sentry-sampled=true', + `sentry-trace_id=${traceId}`, + 'sentry-transaction=updated-root-span-1', + ]); + + // 6 + const errorEnvelopeTraceHeaderAfterUpdate = await captureErrorAndGetEnvelopeTraceHeader(page); + expect(errorEnvelopeTraceHeaderAfterUpdate).toEqual({ + environment: 'production', + public_key: 'public', + release: '1.1.1', + sample_rate: '1', + sampled: 'true', + trace_id: traceId, + transaction: 'updated-root-span-1', + }); + + // 7 + await page.locator('#btnUpdateName').click(); + + // 8 + const baggageItemsAfterSecondUpdate = await makeRequestAndGetBaggageItems(page); + expect(baggageItemsAfterSecondUpdate).toEqual([ + 'sentry-environment=production', + 'sentry-public_key=public', + 'sentry-release=1.1.1', + 'sentry-sample_rate=1', + 'sentry-sampled=true', + `sentry-trace_id=${traceId}`, + 'sentry-transaction=updated-root-span-2', + ]); + + // 9 + const errorEnvelopeTraceHeaderAfterSecondUpdate = await captureErrorAndGetEnvelopeTraceHeader(page); + expect(errorEnvelopeTraceHeaderAfterSecondUpdate).toEqual({ + environment: 'production', + public_key: 'public', + release: '1.1.1', + sample_rate: '1', + sampled: 'true', + trace_id: traceId, + transaction: 'updated-root-span-2', + }); + + // 10 + const txnEventPromise = getMultipleSentryEnvelopeRequests( + page, + 1, + { envelopeType: 'transaction' }, + eventAndTraceHeaderRequestParser, + ); + + await page.locator('#btnEndSpan').click(); + + const [txnEvent, txnEnvelopeTraceHeader] = (await txnEventPromise)[0]; + expect(txnEnvelopeTraceHeader).toEqual({ + environment: 'production', + public_key: 'public', + release: '1.1.1', + sample_rate: '1', + sampled: 'true', + trace_id: traceId, + transaction: 'updated-root-span-2', + }); + + expect(txnEvent.transaction).toEqual('updated-root-span-2'); + + // 11 + const baggageItemsAfterEnd = await makeRequestAndGetBaggageItems(page); + expect(baggageItemsAfterEnd).toEqual([ + 'sentry-environment=production', + 'sentry-public_key=public', + 'sentry-release=1.1.1', + `sentry-trace_id=${traceId}`, + ]); + + // 12 + const errorEnvelopeTraceHeaderAfterEnd = await captureErrorAndGetEnvelopeTraceHeader(page); + expect(errorEnvelopeTraceHeaderAfterEnd).toEqual({ + environment: 'production', + public_key: 'public', + release: '1.1.1', + trace_id: traceId, + }); +}); + +async function makeRequestAndGetBaggageItems(page: Page): Promise { + const requestPromise = page.waitForRequest('https://example.com/*'); + await page.locator('#btnMakeRequest').click(); + const request = await requestPromise; + + const baggage = await request.headerValue('baggage'); + + return baggage?.split(',').sort() ?? []; +} + +async function captureErrorAndGetEnvelopeTraceHeader(page: Page): Promise { + const errorEventPromise = getMultipleSentryEnvelopeRequests( + page, + 1, + { envelopeType: 'event' }, + eventAndTraceHeaderRequestParser, + ); + + await page.locator('#btnCaptureError').click(); + + const [, errorEnvelopeTraceHeader] = (await errorEventPromise)[0]; + return errorEnvelopeTraceHeader; +} diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/subject.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/subject.js new file mode 100644 index 000000000000..9528f861a723 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/subject.js @@ -0,0 +1,16 @@ +const errorBtn = document.getElementById('errorBtn'); +errorBtn.addEventListener('click', () => { + throw new Error(`Sentry Test Error ${Math.random()}`); +}); + +const fetchBtn = document.getElementById('fetchBtn'); +fetchBtn.addEventListener('click', async () => { + await fetch('http://example.com'); +}); + +const xhrBtn = document.getElementById('xhrBtn'); +xhrBtn.addEventListener('click', () => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', 'http://example.com'); + xhr.send(); +}); 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 2d0933002e7f..5ef3cb81ad28 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 @@ -294,6 +294,149 @@ sentryTest( }, ); +// sentryTest( +// 'outgoing fetch request after pageload has pageload traceId in headers', +// async ({ getLocalTestUrl, page }) => { +// if (shouldSkipTracingTest()) { +// sentryTest.skip(); +// } + +// const url = await getLocalTestUrl({ testDir: __dirname }); + +// await page.route('http://example.com/**', route => { +// return route.fulfill({ +// status: 200, +// contentType: 'application/json', +// body: JSON.stringify({}), +// }); +// }); + +// const pageloadEventPromise = getFirstSentryEnvelopeRequest( +// page, +// undefined, +// eventAndTraceHeaderRequestParser, +// ); +// await page.goto(url); +// const [pageloadEvent, pageloadTraceHeader] = await pageloadEventPromise; + +// 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(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(); + +// // sampling decision is propagated from active span sampling decision +// 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( +// 'custom span and request headers after pageload have pageload traceId ', +// async ({ getLocalTestUrl, page }) => { +// if (shouldSkipTracingTest()) { +// sentryTest.skip(); +// } + +// const url = await getLocalTestUrl({ testDir: __dirname }); + +// await page.route('http://example.com/**', route => { +// return route.fulfill({ +// status: 200, +// contentType: 'application/json', +// body: JSON.stringify({}), +// }); +// }); + +// const pageloadEventPromise = getFirstSentryEnvelopeRequest( +// page, +// undefined, +// eventAndTraceHeaderRequestParser, +// ); + +// await page.goto(url); + +// const [pageloadEvent, pageloadTraceHeader] = await pageloadEventPromise; + +// 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(pageloadTraceHeader).toEqual({ +// environment: 'production', +// public_key: 'public', +// sample_rate: '1', +// sampled: 'true', +// trace_id: pageloadTraceId, +// }); + +// const requestPromise = page.waitForRequest('http://example.com/**'); +// const customTransactionEventPromise = getFirstSentryEnvelopeRequest( +// page, +// undefined, +// eventAndTraceHeaderRequestParser, +// ); + +// await page.locator('#spanAndFetchBtn').click(); + +// const [[customTransactionEvent, customTransactionTraceHeader], request] = await Promise.all([ +// customTransactionEventPromise, +// requestPromise, +// ]); + +// const customTransactionTraceContext = customTransactionEvent.contexts?.trace; + +// expect(customTransactionEvent.type).toEqual('transaction'); +// expect(customTransactionTraceContext).toMatchObject({ +// trace_id: pageloadTraceId, +// }); + +// expect(customTransactionTraceHeader).toEqual({ +// environment: 'production', +// public_key: 'public', +// sample_rate: '1', +// sampled: 'true', +// trace_id: pageloadTraceId, +// }); + +// const headers = request.headers(); + +// // sampling decision is propagated from active span sampling decision +// 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('user feedback event after pageload has pageload traceId in headers', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest() || shouldSkipFeedbackTest()) { sentryTest.skip(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/subject.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/subject.js index 9528f861a723..a2f6271463ce 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/subject.js +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/subject.js @@ -14,3 +14,10 @@ xhrBtn.addEventListener('click', () => { xhr.open('GET', 'http://example.com'); xhr.send(); }); + +const spanAndFetchBtn = document.getElementById('spanAndFetchBtn'); +spanAndFetchBtn.addEventListener('click', () => { + Sentry.startSpan({ name: 'custom-root-span' }, async () => { + await fetch('http://example.com'); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/template.html b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/template.html index a3c17f442605..a112e5c46771 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/template.html +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/template.html @@ -7,5 +7,6 @@ + diff --git a/dev-packages/node-integration-tests/suites/tracing/dsc-txn-name-update/scenario-events.ts b/dev-packages/node-integration-tests/suites/tracing/dsc-txn-name-update/scenario-events.ts new file mode 100644 index 000000000000..892167fa55b4 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/dsc-txn-name-update/scenario-events.ts @@ -0,0 +1,29 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +Sentry.startSpan( + { name: 'initial-name', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } }, + async span => { + Sentry.captureMessage('message-1'); + + span.updateName('updated-name-1'); + span.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + + Sentry.captureMessage('message-2'); + + span.updateName('updated-name-2'); + span.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom'); + + Sentry.captureMessage('message-3'); + + span.end(); + }, +); diff --git a/dev-packages/node-integration-tests/suites/tracing/dsc-txn-name-update/scenario-headers.ts b/dev-packages/node-integration-tests/suites/tracing/dsc-txn-name-update/scenario-headers.ts new file mode 100644 index 000000000000..8c9c01e21444 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/dsc-txn-name-update/scenario-headers.ts @@ -0,0 +1,45 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +import * as http from 'http'; + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +Sentry.startSpan( + { name: 'initial-name', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } }, + async span => { + await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`); + + span.updateName('updated-name-1'); + span.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + + await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`); + + span.updateName('updated-name-2'); + span.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom'); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`); + + span.end(); + }, +); + +function makeHttpRequest(url: string): Promise { + return new Promise(resolve => { + http + .request(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }) + .end(); + }); +} diff --git a/dev-packages/node-integration-tests/suites/tracing/dsc-txn-name-update/test.ts b/dev-packages/node-integration-tests/suites/tracing/dsc-txn-name-update/test.ts new file mode 100644 index 000000000000..cefaba1ad97f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/dsc-txn-name-update/test.ts @@ -0,0 +1,123 @@ +import { createRunner } from '../../../utils/runner'; +import { createTestServer } from '../../../utils/server'; + +test('adds current transaction name to baggage when the txn name is high-quality', done => { + expect.assertions(5); + + let traceId: string | undefined; + + createTestServer(done) + .get('/api/v0', headers => { + const baggageItems = getBaggageHeaderItems(headers); + traceId = baggageItems.find(item => item.startsWith('sentry-trace_id='))?.split('=')[1] as string; + + expect(traceId).toMatch(/^[0-9a-f]{32}$/); + + expect(baggageItems).toEqual([ + 'sentry-environment=production', + 'sentry-public_key=public', + 'sentry-release=1.0', + 'sentry-sample_rate=1', + 'sentry-sampled=true', + `sentry-trace_id=${traceId}`, + ]); + }) + .get('/api/v1', headers => { + expect(getBaggageHeaderItems(headers)).toEqual([ + 'sentry-environment=production', + 'sentry-public_key=public', + 'sentry-release=1.0', + 'sentry-sample_rate=1', + 'sentry-sampled=true', + `sentry-trace_id=${traceId}`, + 'sentry-transaction=updated-name-1', + ]); + }) + .get('/api/v2', headers => { + expect(getBaggageHeaderItems(headers)).toEqual([ + 'sentry-environment=production', + 'sentry-public_key=public', + 'sentry-release=1.0', + 'sentry-sample_rate=1', + 'sentry-sampled=true', + `sentry-trace_id=${traceId}`, + 'sentry-transaction=updated-name-2', + ]); + }) + .start() + .then(([SERVER_URL, closeTestServer]) => { + createRunner(__dirname, 'scenario-headers.ts') + .withEnv({ SERVER_URL }) + .expect({ + transaction: {}, + }) + .start(closeTestServer); + }); +}); + +test('adds current transaction name to trace envelope header when the txn name is high-quality', done => { + expect.assertions(4); + + createRunner(__dirname, 'scenario-events.ts') + .expectHeader({ + event: { + trace: { + environment: 'production', + public_key: 'public', + release: '1.0', + sample_rate: '1', + sampled: 'true', + trace_id: expect.any(String), + }, + }, + }) + .expectHeader({ + event: { + trace: { + environment: 'production', + public_key: 'public', + release: '1.0', + sample_rate: '1', + sampled: 'true', + trace_id: expect.any(String), + transaction: 'updated-name-1', + }, + }, + }) + .expectHeader({ + event: { + trace: { + environment: 'production', + public_key: 'public', + release: '1.0', + sample_rate: '1', + sampled: 'true', + trace_id: expect.any(String), + transaction: 'updated-name-2', + }, + }, + }) + .expectHeader({ + transaction: { + trace: { + environment: 'production', + public_key: 'public', + release: '1.0', + sample_rate: '1', + sampled: 'true', + trace_id: expect.any(String), + transaction: 'updated-name-2', + }, + }, + }) + .start(done); +}); + +function getBaggageHeaderItems(headers: Record) { + const baggage = headers['baggage'] as string; + const baggageItems = baggage + .split(',') + .map(b => b.trim()) + .sort(); + return baggageItems; +}