From a3dfc27504aca70c6df64e3046a8cc6d6ebbae06 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 8 Oct 2026 15:20:26 +0200 Subject: [PATCH 01/84] test(node): Port Prisma integration tests to span streaming (#25152) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port Prisma v5–v7 tests to default span streaming and fold their query-name checks into the existing tests. Part of #24137 --------- Co-authored-by: GPT-6 --- .../instrument-span-streaming.mjs | 10 - .../tracing/prisma-orm-v5/instrument.mjs | 1 - .../suites/tracing/prisma-orm-v5/test.ts | 198 ++++++++---------- .../instrument-span-streaming.mjs | 10 - .../tracing/prisma-orm-v6/instrument.mjs | 1 - .../suites/tracing/prisma-orm-v6/test.ts | 131 ++++++------ .../instrument-span-streaming.mjs | 10 - .../tracing/prisma-orm-v7/instrument.mjs | 1 - .../suites/tracing/prisma-orm-v7/test.ts | 127 +++++------ 9 files changed, 196 insertions(+), 293 deletions(-) delete mode 100644 dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/instrument-span-streaming.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/instrument-span-streaming.mjs delete mode 100644 dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/instrument-span-streaming.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/instrument-span-streaming.mjs b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/instrument-span-streaming.mjs deleted file mode 100644 index 53b9511a21f0..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/instrument-span-streaming.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, - transport: loggingTransport, - traceLifecycle: 'stream', -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/instrument.mjs index 170ad6f6a702..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/instrument.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, 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 33b8c89d0ec3..4c528b7c3aa5 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 @@ -1,4 +1,13 @@ -import type { TransactionEvent } from '@sentry/core'; +import { + DB_QUERY_SUMMARY, + DB_STATEMENT, + DB_SYSTEM, + SENTRY_KIND, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; +import { DB } from '@sentry/conventions/op'; +import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; @@ -11,105 +20,123 @@ const ADDITIONAL_DEPENDENCIES = { prisma: '5.22.0', }; -function expectPrismaV5Spans(transaction: TransactionEvent): void { - expect(transaction.transaction).toBe('Test Transaction'); - const spans = transaction.spans || []; +function expectPrismaV5Spans(container: SerializedStreamedSpanContainer): void { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('Test Transaction'); + const spans = container.items.filter(span => !span.is_segment); expect(spans.length).toBeGreaterThanOrEqual(5); // Valid parents are the transaction root or any other span within the transaction. - const validParentIds = new Set([transaction.contexts?.trace?.span_id, ...spans.map(s => s.span_id)]); + const validParentIds = new Set([segment?.span_id, ...spans.map(s => s.span_id)]); - const operationSpans = spans.filter(s => s.description === 'prisma:client:operation'); + const operationSpans = spans.filter(s => s.name === 'prisma:client:operation'); expect(operationSpans.length).toBeGreaterThanOrEqual(1); // The db-query spans are materialized from the raw engine event; assert they nest inside the // transaction rather than dangling as orphans. - const dbSpans = spans.filter(s => s.op === 'db'); + const dbSpans = spans.filter(s => s.attributes[SENTRY_OP]?.value === DB); expect(dbSpans.length).toBeGreaterThanOrEqual(1); dbSpans.forEach(dbSpan => { expect(validParentIds.has(dbSpan.parent_span_id)).toBe(true); }); - const txSpan = spans.find(s => s.description === 'prisma:client:transaction'); + const txSpan = spans.find(s => s.name === 'prisma:client:transaction'); expect(txSpan).toBeDefined(); expect(spans).toEqual( expect.arrayContaining([ expect.objectContaining({ - data: { - method: 'create', - model: 'User', - name: 'User.create', - 'sentry.origin': 'auto.db.prisma', - }, - description: 'prisma:client:operation', + attributes: expect.objectContaining({ + method: { type: 'string', value: 'create' }, + model: { type: 'string', value: 'User' }, + name: { type: 'string', value: 'User.create' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.prisma' }, + }), + name: 'prisma:client:operation', status: 'ok', }), expect.objectContaining({ - data: { - method: 'findMany', - model: 'User', - name: 'User.findMany', - 'sentry.origin': 'auto.db.prisma', - }, - description: 'prisma:client:operation', + attributes: expect.objectContaining({ + method: { type: 'string', value: 'findMany' }, + model: { type: 'string', value: 'User' }, + name: { type: 'string', value: 'User.findMany' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.prisma' }, + }), + name: 'prisma:client:operation', status: 'ok', }), expect.objectContaining({ - data: { - 'sentry.origin': 'auto.db.prisma', - }, - description: 'prisma:client:serialize', + attributes: expect.objectContaining({ + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.prisma' }, + }), + name: 'prisma:client:serialize', status: 'ok', }), expect.objectContaining({ - data: { - 'sentry.origin': 'auto.db.prisma', - }, - description: 'prisma:client:connect', + attributes: expect.objectContaining({ + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.prisma' }, + }), + name: 'prisma:client:connect', status: 'ok', }), expect.objectContaining({ - data: { - 'db.statement': expect.stringContaining('INSERT INTO'), - 'db.query.summary': 'INSERT "public"."User"', - 'db.system': 'postgresql', - 'sentry.kind': 'client', - 'sentry.op': 'db', - 'sentry.origin': 'auto.db.prisma', - }, - op: 'db', - description: expect.stringContaining('INSERT INTO'), + attributes: expect.objectContaining({ + [DB_STATEMENT]: { type: 'string', value: expect.stringContaining('INSERT INTO') }, + [DB_QUERY_SUMMARY]: { type: 'string', value: 'INSERT "public"."User"' }, + [DB_SYSTEM]: { type: 'string', value: 'postgresql' }, + [SENTRY_KIND]: { type: 'string', value: 'client' }, + [SENTRY_OP]: { type: 'string', value: DB }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.prisma' }, + }), + name: 'INSERT "public"."User"', status: 'ok', }), expect.objectContaining({ - data: { - 'db.statement': expect.stringContaining('SELECT'), - 'db.query.summary': 'SELECT "public"."User"', - 'db.system': 'postgresql', - 'sentry.kind': 'client', - 'sentry.op': 'db', - 'sentry.origin': 'auto.db.prisma', - }, - op: 'db', - description: expect.stringContaining('SELECT'), + attributes: expect.objectContaining({ + [DB_STATEMENT]: { type: 'string', value: expect.stringContaining('SELECT') }, + [DB_QUERY_SUMMARY]: { type: 'string', value: 'SELECT "public"."User"' }, + [DB_SYSTEM]: { type: 'string', value: 'postgresql' }, + [SENTRY_KIND]: { type: 'string', value: 'client' }, + [SENTRY_OP]: { type: 'string', value: DB }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.prisma' }, + }), + name: 'SELECT "public"."User"', status: 'ok', }), expect.objectContaining({ - data: { - 'db.statement': expect.stringContaining('DELETE'), - 'db.query.summary': 'DELETE "public"."User"', - 'db.system': 'postgresql', - 'sentry.kind': 'client', - 'sentry.op': 'db', - 'sentry.origin': 'auto.db.prisma', - }, - op: 'db', - description: expect.stringContaining('DELETE'), + attributes: expect.objectContaining({ + [DB_STATEMENT]: { type: 'string', value: expect.stringContaining('DELETE') }, + [DB_QUERY_SUMMARY]: { type: 'string', value: 'DELETE "public"."User"' }, + [DB_SYSTEM]: { type: 'string', value: 'postgresql' }, + [SENTRY_KIND]: { type: 'string', value: 'client' }, + [SENTRY_OP]: { type: 'string', value: DB }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.prisma' }, + }), + name: 'DELETE "public"."User"', status: 'ok', }), ]), ); + + const querySpans = spans.filter(item => item.attributes[DB_STATEMENT]); + + expect( + querySpans.map(span => ({ + name: span.name, + summary: span.attributes[DB_QUERY_SUMMARY]?.value, + })), + ).toEqual([ + { name: 'INSERT "public"."User"', summary: 'INSERT "public"."User"' }, + { name: 'SELECT "public"."User"', summary: 'SELECT "public"."User"' }, + { name: 'BEGIN', summary: 'BEGIN' }, + { name: 'INSERT "public"."User"', summary: 'INSERT "public"."User"' }, + { name: 'SELECT "public"."User"', summary: 'SELECT "public"."User"' }, + { name: 'COMMIT', summary: 'COMMIT' }, + { name: 'DELETE "public"."User"', summary: 'DELETE "public"."User"' }, + ]); + + // The raw engine span name must never leak through. + expect(spans.map(span => span.name)).not.toContain('prisma:engine:db_query'); } const AFTER_SETUP_COMMAND = 'prisma generate --schema prisma/schema.prisma'; @@ -122,7 +149,7 @@ describeWithDockerCompose('Prisma ORM v5', { workingDirectory: [__dirname] }, () 'instrument.mjs', (createRunner, test) => { test('should instrument PostgreSQL queries from Prisma ORM', { timeout: 75_000 }, async () => { - await createRunner().expect({ transaction: expectPrismaV5Spans }).start().completed(); + await createRunner().expect({ span: expectPrismaV5Spans }).start().completed(); }); }, { @@ -131,54 +158,5 @@ describeWithDockerCompose('Prisma ORM v5', { workingDirectory: [__dirname] }, () copyPaths: ['prisma'], }, ); - - createEsmAndCjsTests( - __dirname, - 'scenario.mjs', - 'instrument-span-streaming.mjs', - (createRunner, test) => { - test( - 'should name db query spans after the query summary with span streaming', - { timeout: 75_000 }, - async () => { - await createRunner() - // Prisma's engine startup can outlast the span buffer's flush interval, so the query spans - // are not guaranteed to be in the first span envelope. - .unordered() - .expect({ - span: container => { - // v5 reports the SQL on the deprecated `db.statement` rather than `db.query.text`. - const querySpans = container.items.filter(item => item.attributes['db.statement']); - - expect( - querySpans.map(span => ({ - name: span.name, - summary: span.attributes['db.query.summary']?.value, - })), - ).toEqual([ - { name: 'INSERT "public"."User"', summary: 'INSERT "public"."User"' }, - { name: 'SELECT "public"."User"', summary: 'SELECT "public"."User"' }, - { name: 'BEGIN', summary: 'BEGIN' }, - { name: 'INSERT "public"."User"', summary: 'INSERT "public"."User"' }, - { name: 'SELECT "public"."User"', summary: 'SELECT "public"."User"' }, - { name: 'COMMIT', summary: 'COMMIT' }, - { name: 'DELETE "public"."User"', summary: 'DELETE "public"."User"' }, - ]); - - // The raw engine span name must never leak through. - expect(container.items.map(span => span.name)).not.toContain('prisma:engine:db_query'); - }, - }) - .start() - .completed(); - }, - ); - }, - { - additionalDependencies: ADDITIONAL_DEPENDENCIES, - afterSetupCommand: AFTER_SETUP_COMMAND, - copyPaths: ['prisma'], - }, - ); }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/instrument-span-streaming.mjs b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/instrument-span-streaming.mjs deleted file mode 100644 index 53b9511a21f0..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/instrument-span-streaming.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, - transport: loggingTransport, - traceLifecycle: 'stream', -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/instrument.mjs index 170ad6f6a702..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/instrument.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, diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts index 542c482a0fa1..18f44727b1bb 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts @@ -1,4 +1,13 @@ -import type { SpanJSON } from '@sentry/core'; +import { + DB_QUERY_SUMMARY, + DB_QUERY_TEXT, + DB_SYSTEM, + SENTRY_KIND, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; +import { DB } from '@sentry/conventions/op'; +import type { SerializedStreamedSpan } from '@sentry/core'; import { afterAll, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; @@ -15,142 +24,122 @@ describeWithDockerCompose('Prisma ORM v6 Tests', { workingDirectory: [__dirname] test('should instrument PostgreSQL queries from Prisma ORM', { timeout: 75_000 }, async () => { await createRunner() .expect({ - transaction: transaction => { - expect(transaction.transaction).toBe('Test Transaction'); + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('Test Transaction'); - const spans = transaction.spans || []; + const spans = container.items.filter(span => !span.is_segment); expect(spans.length).toBeGreaterThanOrEqual(5); // Each operation span is a direct child of the transaction; the db query span is a child of the engine query span. - const rootSpanId = transaction.contexts?.trace?.span_id; + const rootSpanId = segment?.span_id; - const operationSpans = spans.filter(s => s.description === 'prisma:client:operation'); + const operationSpans = spans.filter(s => s.name === 'prisma:client:operation'); expect(operationSpans.length).toBeGreaterThanOrEqual(1); operationSpans.forEach(operation => { expect(operation.parent_span_id).toBe(rootSpanId); }); const dbQuerySpan = spans.find( - s => s.data?.['sentry.origin'] === 'auto.db.prisma' && s.data?.['db.query.text'], + s => s.attributes[SENTRY_ORIGIN]?.value === 'auto.db.prisma' && s.attributes[DB_QUERY_TEXT], ); expect(dbQuerySpan).toBeDefined(); const dbQueryParent = spans.find(s => s.span_id === dbQuerySpan?.parent_span_id); - expect(dbQueryParent?.description).toBe('prisma:engine:query'); + expect(dbQueryParent?.name).toBe('prisma:engine:query'); - function expectPrismaSpanToIncludeSpanWith(span: Partial) { + function expectPrismaSpanToIncludeSpanWith(span: Partial) { expect(spans).toContainEqual( expect.objectContaining({ ...span, - data: { - ...span.data, - 'sentry.origin': 'auto.db.prisma', - }, + attributes: expect.objectContaining({ + ...span.attributes, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.prisma' }, + }), status: 'ok', }), ); } expectPrismaSpanToIncludeSpanWith({ - description: 'prisma:client:detect_platform', + name: 'prisma:client:detect_platform', }); expectPrismaSpanToIncludeSpanWith({ - description: 'prisma:client:load_engine', + name: 'prisma:client:load_engine', }); expectPrismaSpanToIncludeSpanWith({ - description: 'prisma:client:operation', - data: { - method: 'create', - model: 'User', - name: 'User.create', + name: 'prisma:client:operation', + attributes: { + method: { type: 'string', value: 'create' }, + model: { type: 'string', value: 'User' }, + name: { type: 'string', value: 'User.create' }, }, }); expectPrismaSpanToIncludeSpanWith({ - description: 'prisma:client:serialize', + name: 'prisma:client:serialize', }); expectPrismaSpanToIncludeSpanWith({ - description: 'prisma:client:connect', + name: 'prisma:client:connect', }); expectPrismaSpanToIncludeSpanWith({ - description: 'prisma:engine:connect', + name: 'prisma:engine:connect', }); expectPrismaSpanToIncludeSpanWith({ - description: 'prisma:engine:query', + name: 'prisma:engine:query', }); expectPrismaSpanToIncludeSpanWith({ - data: { - 'sentry.op': 'db', - 'db.query.text': - 'SELECT "public"."User"."id", "public"."User"."createdAt", "public"."User"."email", "public"."User"."name" FROM "public"."User" WHERE 1=1 OFFSET $1', - 'db.query.summary': 'SELECT "public"."User"', - 'db.system': 'postgresql', - 'sentry.kind': 'client', + attributes: { + [SENTRY_OP]: { type: 'string', value: DB }, + [DB_QUERY_TEXT]: { + type: 'string', + value: + 'SELECT "public"."User"."id", "public"."User"."createdAt", "public"."User"."email", "public"."User"."name" FROM "public"."User" WHERE 1=1 OFFSET $1', + }, + [DB_QUERY_SUMMARY]: { type: 'string', value: 'SELECT "public"."User"' }, + [DB_SYSTEM]: { type: 'string', value: 'postgresql' }, + [SENTRY_KIND]: { type: 'string', value: 'client' }, }, - description: - 'SELECT "public"."User"."id", "public"."User"."createdAt", "public"."User"."email", "public"."User"."name" FROM "public"."User" WHERE 1=1 OFFSET $1', + name: 'SELECT "public"."User"', }); expectPrismaSpanToIncludeSpanWith({ - data: { - 'sentry.op': 'db', - 'db.query.text': 'DELETE FROM "public"."User" WHERE "public"."User"."email"::text LIKE $1', - 'db.query.summary': 'DELETE "public"."User"', - 'db.system': 'postgresql', - 'sentry.kind': 'client', + attributes: { + [SENTRY_OP]: { type: 'string', value: DB }, + [DB_QUERY_TEXT]: { + type: 'string', + value: 'DELETE FROM "public"."User" WHERE "public"."User"."email"::text LIKE $1', + }, + [DB_QUERY_SUMMARY]: { type: 'string', value: 'DELETE "public"."User"' }, + [DB_SYSTEM]: { type: 'string', value: 'postgresql' }, + [SENTRY_KIND]: { type: 'string', value: 'client' }, }, - description: 'DELETE FROM "public"."User" WHERE "public"."User"."email"::text LIKE $1', + name: 'DELETE "public"."User"', }); - // The db query span name must always be rewritten to the SQL text; the raw engine span + // The db query span name must always be rewritten to the query summary; the raw engine span // name should never leak through. - expect(spans.find(span => span.description === 'prisma:engine:db_query')).toBeUndefined(); - }, - }) - .start() - .completed(); - }); - }, - { - afterSetupCommand: 'prisma generate --schema prisma/schema.prisma', - copyPaths: ['prisma'], - }, - ); + expect(spans.find(span => span.name === 'prisma:engine:db_query')).toBeUndefined(); - createEsmAndCjsTests( - __dirname, - 'scenario.mjs', - 'instrument-span-streaming.mjs', - (createRunner, test) => { - test('should name db query spans after the query summary with span streaming', { timeout: 75_000 }, async () => { - await createRunner() - // Prisma's engine startup can outlast the span buffer's flush interval, so the query spans - // are not guaranteed to be in the first span envelope. - .unordered() - .expect({ - span: container => { - const querySpans = container.items.filter(item => item.attributes['db.query.text']); + const querySpans = spans.filter(item => item.attributes[DB_QUERY_TEXT]); expect( querySpans.map(span => ({ name: span.name, - summary: span.attributes['db.query.summary']?.value, + summary: span.attributes[DB_QUERY_SUMMARY]?.value, })), ).toEqual([ { name: 'INSERT "public"."User"', summary: 'INSERT "public"."User"' }, { name: 'SELECT "public"."User"', summary: 'SELECT "public"."User"' }, { name: 'DELETE "public"."User"', summary: 'DELETE "public"."User"' }, ]); - - // Neither the raw engine span name nor the full statement may end up as a span name. - expect(container.items.map(span => span.name)).not.toContain('prisma:engine:db_query'); querySpans.forEach(span => { - expect(span.name).not.toBe(span.attributes['db.query.text']?.value); + expect(span.name).not.toBe(span.attributes[DB_QUERY_TEXT]?.value); }); }, }) diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/instrument-span-streaming.mjs b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/instrument-span-streaming.mjs deleted file mode 100644 index 53b9511a21f0..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/instrument-span-streaming.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, - transport: loggingTransport, - traceLifecycle: 'stream', -}); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/instrument.mjs index 170ad6f6a702..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/instrument.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, diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/test.ts index b9fd94230350..9e11425ee539 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/test.ts @@ -1,3 +1,11 @@ +import { + DB_QUERY_SUMMARY, + DB_QUERY_TEXT, + DB_SYSTEM_NAME, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; +import { DB } from '@sentry/conventions/op'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; @@ -15,34 +23,35 @@ describe('Prisma ORM v7 Tests', () => { test('should instrument PostgreSQL queries from Prisma ORM', { timeout: 75_000 }, async () => { await createRunner() .expect({ - transaction: transaction => { - expect(transaction.transaction).toBe('Test Transaction'); + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('Test Transaction'); - const spans = transaction.spans || []; + const spans = container.items.filter(span => !span.is_segment); expect(spans.length).toBeGreaterThanOrEqual(5); // Each operation span is a direct child of the transaction; the db query span is a child of its operation span. - const rootSpanId = transaction.contexts?.trace?.span_id; + const rootSpanId = segment?.span_id; - const operationSpans = spans.filter(s => s.description === 'prisma:client:operation'); + const operationSpans = spans.filter(s => s.name === 'prisma:client:operation'); expect(operationSpans.length).toBeGreaterThanOrEqual(1); operationSpans.forEach(operation => { expect(operation.parent_span_id).toBe(rootSpanId); }); const prismaDbQuerySpan = spans.find( - s => s.data?.['sentry.origin'] === 'auto.db.prisma' && s.data?.['db.query.text'], + s => s.attributes[SENTRY_ORIGIN]?.value === 'auto.db.prisma' && s.attributes[DB_QUERY_TEXT], ); expect(prismaDbQuerySpan).toBeDefined(); const dbQueryParent = spans.find(s => s.span_id === prismaDbQuerySpan?.parent_span_id); - expect(dbQueryParent?.description).toBe('prisma:client:operation'); + expect(dbQueryParent?.name).toBe('prisma:client:operation'); // Verify Prisma spans have the correct origin - const prismaSpans = spans.filter(span => span.data && span.data['sentry.origin'] === 'auto.db.prisma'); + const prismaSpans = spans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.db.prisma'); expect(prismaSpans.length).toBeGreaterThanOrEqual(5); // Check for key Prisma span descriptions - const spanDescriptions = prismaSpans.map(span => span.description); + const spanDescriptions = prismaSpans.map(span => span.name); expect(spanDescriptions).toContain('prisma:client:operation'); expect(spanDescriptions).toContain('prisma:client:serialize'); expect(spanDescriptions).toContain('prisma:client:connect'); @@ -50,27 +59,45 @@ describe('Prisma ORM v7 Tests', () => { // Verify the create operation has correct metadata const createSpan = prismaSpans.find( span => - span.description === 'prisma:client:operation' && - span.data?.['method'] === 'create' && - span.data?.['model'] === 'User', + span.name === 'prisma:client:operation' && + span.attributes['method']?.value === 'create' && + span.attributes['model']?.value === 'User', ); expect(createSpan).toBeDefined(); // Verify db_query span has system info and correct op (v7 uses db.system.name). - // The SDK should rewrite the span name to the actual SQL text (same as v5/v6 - // `prisma:engine:db_query`), so we find it via op/origin rather than description. + // The SDK should rewrite the span name to the query summary (same as v5/v6 + // `prisma:engine:db_query`), so we find it via op/origin rather than name. const dbQuerySpan = prismaSpans.find( - span => span.data?.['sentry.op'] === 'db' && span.data?.['db.query.text'], + span => span.attributes[SENTRY_OP]?.value === DB && span.attributes[DB_QUERY_TEXT]?.value, ); expect(dbQuerySpan).toBeDefined(); - expect(dbQuerySpan?.data?.['db.system.name']).toBe('postgresql'); - expect(dbQuerySpan?.op).toBe('db'); - expect(dbQuerySpan?.description).toBe(dbQuerySpan?.data?.['db.query.text']); - expect(dbQuerySpan?.description).not.toBe('prisma:client:db_query'); + expect(dbQuerySpan?.attributes[DB_SYSTEM_NAME]).toEqual({ type: 'string', value: 'postgresql' }); + expect(dbQuerySpan?.attributes[SENTRY_OP]).toEqual({ type: 'string', value: DB }); + expect(dbQuerySpan?.name).toBe(dbQuerySpan?.attributes[DB_QUERY_SUMMARY]?.value); + expect(dbQuerySpan?.name).not.toBe('prisma:client:db_query'); - // The db query span name must always be rewritten to the SQL text; the raw client span + // The db query span name must always be rewritten to the query summary; the raw client span // name should never leak through. - expect(spans.find(span => span.description === 'prisma:client:db_query')).toBeUndefined(); + expect(spans.find(span => span.name === 'prisma:client:db_query')).toBeUndefined(); + + const querySpans = spans.filter( + item => item.attributes[SENTRY_ORIGIN]?.value === 'auto.db.prisma' && item.attributes[DB_QUERY_TEXT], + ); + + expect( + querySpans.map(span => ({ + name: span.name, + summary: span.attributes[DB_QUERY_SUMMARY]?.value, + })), + ).toEqual([ + { name: 'INSERT "public"."User"', summary: 'INSERT "public"."User"' }, + { name: 'SELECT "public"."User"', summary: 'SELECT "public"."User"' }, + { name: 'DELETE "public"."User"', summary: 'DELETE "public"."User"' }, + ]); + querySpans.forEach(span => { + expect(span.name).not.toBe(span.attributes[DB_QUERY_TEXT]?.value); + }); }, }) .start() @@ -89,63 +116,5 @@ describe('Prisma ORM v7 Tests', () => { copyPaths: ['prisma', 'prisma.config.ts'], }, ); - - createEsmAndCjsTests( - __dirname, - 'scenario.mjs', - 'instrument-span-streaming.mjs', - (createRunner, test) => { - test( - 'should name db query spans after the query summary with span streaming', - { timeout: 75_000 }, - async () => { - await createRunner() - // Prisma's engine startup can outlast the span buffer's flush interval, so the query spans - // are not guaranteed to be in the first span envelope. - .unordered() - .expect({ - span: container => { - // v7 runs the queries through the `pg` adapter, whose own spans are named after the full - // statement by a different integration, so they are filtered out here. - const querySpans = container.items.filter( - item => - item.attributes['sentry.origin']?.value === 'auto.db.prisma' && item.attributes['db.query.text'], - ); - - expect( - querySpans.map(span => ({ - name: span.name, - summary: span.attributes['db.query.summary']?.value, - })), - ).toEqual([ - { name: 'INSERT "public"."User"', summary: 'INSERT "public"."User"' }, - { name: 'SELECT "public"."User"', summary: 'SELECT "public"."User"' }, - { name: 'DELETE "public"."User"', summary: 'DELETE "public"."User"' }, - ]); - - // Neither the raw client span name nor the full statement may end up as a span name. - expect(container.items.map(span => span.name)).not.toContain('prisma:client:db_query'); - querySpans.forEach(span => { - expect(span.name).not.toBe(span.attributes['db.query.text']?.value); - }); - }, - }) - .start() - .completed(); - }, - ); - }, - { - additionalDependencies: { - '@prisma/adapter-pg': '7.2.0', - '@prisma/client': '7.2.0', - pg: '^8.11.0', - prisma: '7.2.0', - typescript: '^5.9.0', - }, - afterSetupCommand: 'prisma generate --schema prisma/schema.prisma && tsc -p prisma/tsconfig.json', - copyPaths: ['prisma', 'prisma.config.ts'], - }, - ); }); }); From 96b7733fc4ea1c7e7399c96b0ff230764dc91ea8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:48:36 +0200 Subject: [PATCH 02/84] chore(deps): Bump next from 16.2.11 to 16.3.8 in /dev-packages/e2e-tests/test-applications/nextjs-16 (#25179) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [next](https://github.com/vercel/next.js) from 16.2.11 to 16.3.8.
Release notes

Sourced from next's releases.

v16.3.8

This release contains security fixes for the following advisories:

High:

Medium:

Low:

v16.3.7

[!NOTE] This release is backporting bug fixes. It does not include all pending features/changes on canary.

Core Changes

  • turbo-tasks-backend: fix strongly consistent read hanging on a canceled task (#98931)

Credits

Huge thanks to @​lukesandberg for helping!

v16.3.6

This release contains a security fix for GHSA-vcvr-r3jv-pc5j: Remote Code Execution in next/og ImageResponse

v16.3.5

The following bug fixes have been backported. It does not include all pending features/changes on canary.

  • next/image: Skip 0-byte entries when initializing disk LRU cache (#98185)
  • next/image: Reject empty images when reading/writing to the disk cache (#98186)
  • Emit whole-app server NFTs when output: 'standalone' is used with an adapter (#98167)
  • Add CSP nonce to script tags of loading and template files (#98403)
  • Fix use cache prerender signal retention (#98448)

v16.3.4

Follow-up release to v16.3.3 re-enabling AVIF Image Optimization (#97949).

The following bug fixes have been backported. It does not include all pending features/changes on canary.

  • testmode: Fix infinite recursion in testmode passthrough fetch (#97691)
  • Fix build error when aliasing typescript to @​typescript/typescript6 (#97997)
  • Fix unset crossOrigin in Turbopack manifests (#97930)

Credits

... (truncated)

Commits
  • b0fad0d v16.3.8
  • 719e4c6 [lts-active] Scope response cache keys to their source route (#218)
  • e92db45 [lts-active] Fix metadata propagation for deduplicated nested caches (#223)
  • 40c2ba9 [lts-active] Match Next data paths case-sensitively (#196)
  • 2d9f50a [lts-active] Fix MCP middleware DNS rebinding (#213)
  • bd9214f [lts-active] Fix draft mode leaks through cross-request 'use cache' dedupli...
  • 8db4a62 [lts-active][webpack] Ensure dynamicParams is respected in `opengraph-image...
  • e002ad6 [lts-active] fix(next/image): Pin DNS resolution when fetching external image...
  • 4c20699 v16.3.7
  • 2521aec [backport] turbo-tasks-backend: fix strongly consistent read hanging on a can...
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=next&package-manager=npm_and_yarn&previous-version=16.2.11&new-version=16.3.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dev-packages/e2e-tests/test-applications/nextjs-16/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json index 80f92437f500..3bb7511d8197 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json @@ -36,7 +36,7 @@ "ai": "^3.0.0", "import-in-the-middle": "^2", "ioredis": "5.10.1", - "next": "16.2.11", + "next": "16.3.8", "openai": "5.18.1", "pg": "^8.13.1", "react": "19.1.0", From 032de38948c289596c9c0feeaf7389b1a444af68 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 9 Oct 2026 09:31:13 +0200 Subject: [PATCH 03/84] test(node): Port HTTP propagation and transport suites to streaming (#25197) Run propagation-target, baggage org ID, proxy, and IPv6 coverage with span streaming. Part of #24135 --------- Co-authored-by: GPT-6 --- dev-packages/node-integration-tests/suites/ipv6/scenario.ts | 1 - dev-packages/node-integration-tests/suites/proxy/basic.js | 1 - .../baggage-org-id/server-no-explicit-org-id.ts | 1 - .../baggage-org-id/server-no-org-id.ts | 1 - .../tracing/tracePropagationTargets/baggage-org-id/server.ts | 1 - .../suites/tracing/tracePropagationTargets/scenario.ts | 1 - .../suites/tracing/tracePropagationTargets/test.ts | 4 ++-- 7 files changed, 2 insertions(+), 8 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/ipv6/scenario.ts b/dev-packages/node-integration-tests/suites/ipv6/scenario.ts index 598e487ea990..0023a1bc4b48 100644 --- a/dev-packages/node-integration-tests/suites/ipv6/scenario.ts +++ b/dev-packages/node-integration-tests/suites/ipv6/scenario.ts @@ -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@[2001:db8::1]/1337', defaultIntegrations: false, sendClientReports: false, diff --git a/dev-packages/node-integration-tests/suites/proxy/basic.js b/dev-packages/node-integration-tests/suites/proxy/basic.js index 45a8a43fd880..aaa0a958554e 100644 --- a/dev-packages/node-integration-tests/suites/proxy/basic.js +++ b/dev-packages/node-integration-tests/suites/proxy/basic.js @@ -7,7 +7,6 @@ proxy.listen(0, () => { const proxyPort = proxy.address().port; Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, transportOptions: { proxy: `http://localhost:${proxyPort}`, diff --git a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-explicit-org-id.ts b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-explicit-org-id.ts index 2446388da0fd..40c29de4d5b4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-explicit-org-id.ts +++ b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-explicit-org-id.ts @@ -4,7 +4,6 @@ import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } }; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@o01234987.ingest.sentry.io/1337', release: '1.0', environment: 'prod', diff --git a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-org-id.ts b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-org-id.ts index 82ede5bb97b3..107ba3605c6b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-org-id.ts +++ b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-org-id.ts @@ -4,7 +4,6 @@ import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } }; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@public.ingest.sentry.io/1337', release: '1.0', environment: 'prod', diff --git a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server.ts b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server.ts index 5f9aa1397b38..d2b593da513f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server.ts +++ b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server.ts @@ -4,7 +4,6 @@ import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } }; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@o0000987.ingest.sentry.io/1337', release: '1.0', environment: 'prod', diff --git a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts index bb87c3096aa0..c31007afe9d0 100644 --- a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts @@ -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, diff --git a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/test.ts b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/test.ts index dc5105c9d1bb..4ef1bb6c4bfe 100644 --- a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/test.ts @@ -29,8 +29,8 @@ test('HttpIntegration should instrument correct requests when tracePropagationTa await createRunner(__dirname, 'scenario.ts') .withEnv({ SERVER_URL }) .expect({ - transaction: { - // we're not too concerned with the actual transaction here since this is tested elsewhere + span: { + // Span contents are tested elsewhere. }, }) .start() From c068f721ea0cc4cba1dec1778632be515fbc5d84 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 9 Oct 2026 09:36:32 +0200 Subject: [PATCH 04/84] docs: Scope pre-PR tests to affected suites (#25198) My agent keeps running the full repository test suite locally before opening PRs, which is quite the waste of time and CPU. --------- Co-authored-by: GPT-6 --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index c1ac68c704ff..8d307c72d452 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,7 +55,7 @@ Uses **Git Flow** (see `docs/gitflow.md`). 1. `yarn format` 2. `yarn build:dev` 3. `yarn lint` -4. `yarn test` +4. Run the unit and integration/E2E tests for the affected packages. 5. NEVER push on `develop` ## Pull Requests From 9d6d6f9b010a9cc4513f58f8b728d043d7fb9eb5 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 9 Oct 2026 09:45:19 +0200 Subject: [PATCH 05/84] ci: Run lint steps in parallel (#25187) The Lint job ran its three lint steps one after another, about 59s in total on `develop` (source 10s, ES compatibility 17s, types 32s). They don't depend on each other, so this uses the new [parallel steps](https://github.blog/changelog/2026-06-25-actions-steps-can-now-be-run-in-parallel/) to run them together. The job should take about as long as the slowest step, around 32s. This is the first of several small PRs that use parallel steps in CI. I kept each one separate so a problem with the new syntax only affects one job. Refs [JS-3888](https://linear.app/getsentry/issue/JS-3888/investigate-running-action-steps-in-parallel-for-ci-wall-time) Co-authored-by: Claude Opus 5.5 --- .github/workflows/build.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a37d3618311f..9a4147ac902e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -337,12 +337,13 @@ jobs: uses: ./.github/actions/restore-cache with: dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - - name: Lint source files - run: yarn lint - - name: Lint for ES compatibility - run: yarn lint:es-compatibility - - name: Lint types - run: yarn lint:types + - parallel: + - name: Lint source files + run: yarn lint + - name: Lint for ES compatibility + run: yarn lint:es-compatibility + - name: Lint types + run: yarn lint:types job_check_lockfile: name: Check lockfile From 3a61fbbfd9b7cc0fb30316eb62a8c8a272cf64af Mon Sep 17 00:00:00 2001 From: Sigrid <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:20:43 +0200 Subject: [PATCH 06/84] feat(nuxt): Detect the server API with `@nuxt/kit` 4.6 (#25138) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps `@nuxt/kit` to `^4.6.0` for `resolveServerVariant`. The Vite server and other non-Nitro builders need this server-API detection. Looks like a lot of code changes, but most of it is just moved code. Those are the important parts: - `resolveServerApi()` replaces `getNitroMajorVersion` and the `local-pkg` dependency (we can get the nitro version from there). This is actually the most important and biggest change in the PR. - On the `nuxt` server API (no Nitro), the module skips all Nitro setup. With this, Kit prints no `NUXT_B8024` warnings - `addServerPlugin` → `addNitroPlugin`. Kit 4.6 deprecates the old name, and the behavior is the same - all nitro-related code moves to `nitroServer.ts` Part of https://github.com/getsentry/sentry-javascript/issues/25072 --- packages/nuxt/package.json | 5 +- packages/nuxt/src/module.ts | 108 ++------------- packages/nuxt/src/vite/addServerConfig.ts | 4 +- packages/nuxt/src/vite/databaseConfig.ts | 6 +- packages/nuxt/src/vite/nitroServer.ts | 124 ++++++++++++++++++ packages/nuxt/src/vite/storageConfig.ts | 6 +- packages/nuxt/src/vite/utils.ts | 46 ++----- .../nuxt/test/vite/addServerConfig.test.ts | 23 +++- .../nuxt/test/vite/databaseConfig.test.ts | 2 +- packages/nuxt/test/vite/nitroServer.test.ts | 100 ++++++++++++++ packages/nuxt/test/vite/nitroVersion.test.ts | 107 --------------- packages/nuxt/test/vite/serverApi.test.ts | 67 ++++++++++ yarn.lock | 108 ++++++++------- 13 files changed, 401 insertions(+), 305 deletions(-) create mode 100644 packages/nuxt/src/vite/nitroServer.ts create mode 100644 packages/nuxt/test/vite/nitroServer.test.ts delete mode 100644 packages/nuxt/test/vite/nitroVersion.test.ts create mode 100644 packages/nuxt/test/vite/serverApi.test.ts diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 9aa3adb592a7..b5b642144827 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -53,7 +53,7 @@ } }, "dependencies": { - "@nuxt/kit": "^4.5.2", + "@nuxt/kit": "^4.6.0", "@sentry/browser": "11.6.0", "@sentry/cloudflare": "11.6.0", "@sentry/conventions": "^0.26.0", @@ -62,8 +62,7 @@ "@sentry/bundler-plugins": "11.6.0", "@sentry/server-runtime-injection": "11.6.0", "@sentry/server-utils": "11.6.0", - "@sentry/vue": "11.6.0", - "local-pkg": "^1.1.2" + "@sentry/vue": "11.6.0" }, "devDependencies": { "@nuxt/nitro-server": "^3.21.11", diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index bea829d335fe..f1cd456b84c6 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -1,31 +1,13 @@ -import { - addPlugin, - addPluginTemplate, - addServerPlugin, - addTemplate, - addVitePlugin, - createResolver, - defineNuxtModule, -} from '@nuxt/kit'; +import { addPlugin, addPluginTemplate, addTemplate, addVitePlugin, createResolver, defineNuxtModule } from '@nuxt/kit'; // Needed to make TS evaluate the augmentation of Nitro types (https://github.com/nuxt/nuxt/pull/34039) import type {} from '@nuxt/nitro-server'; -import { consoleSandbox } from '@sentry/core'; import * as path from 'path'; import type { SentryNuxtModuleOptions } from './common/types'; -import { - addDynamicImportEntryFileWrapper, - addServerConfigShimWithWarning, - addSentryTopImport, - addServerConfigPlugin, - addServerConfigToBuild, -} from './vite/addServerConfig'; -import { addDatabaseInstrumentation } from './vite/databaseConfig'; -import { addMiddlewareImports, addMiddlewareInstrumentation } from './vite/middlewareConfig'; +import { setupNitroServer } from './vite/nitroServer'; import { setupOrchestrion } from './vite/orchestrion'; import { setupSourceMaps } from './vite/sourceMaps'; -import { addStorageInstrumentation } from './vite/storageConfig'; -import { addOTelCommonJSImportAlias, findDefaultSdkInitFile, getNitroMajorVersion } from './vite/utils'; +import { addOTelCommonJSImportAlias, findDefaultSdkInitFile, resolveServerApi } from './vite/utils'; export type ModuleOptions = SentryNuxtModuleOptions; type NuxtPageSubset = { file?: string; path: string }; @@ -44,6 +26,8 @@ export default defineNuxtModule({ return; } + const serverApi = resolveServerApi(); + const moduleOptions = { ...moduleOptionsParam, // oxlint-disable-next-line typescript/no-deprecated -- supported until removal @@ -91,7 +75,6 @@ export default defineNuxtModule({ } const serverConfigFile = await findDefaultSdkInitFile('server', nuxt, moduleOptions); - const isNitroV3 = (await getNitroMajorVersion(nuxt.options.rootDir)) >= 3; const nuxtMajor = parseInt((nuxt as unknown as { _version: string })._version?.split('.')[0] ?? '3', 10); const isMinNuxtV4 = nuxtMajor >= 4; @@ -100,43 +83,21 @@ export default defineNuxtModule({ // Cloudflare detection happens inside, keyed off the resolved Nitro preset. setupOrchestrion(nuxt, !!serverConfigFile, moduleOptions.buildTimeInstrumentation); - // The deprecated inject modes replace the default in-bundle initialization until their removal - const usesDeprecatedInjectMode = - moduleOptions.autoInjectServerSentry === 'top-level-import' || - moduleOptions.autoInjectServerSentry === 'experimental_dynamic-import'; - if (serverConfigFile) { - if (!usesDeprecatedInjectMode) { - addServerConfigPlugin(nuxt, serverConfigFile, !isNitroV3); - } - - if (isNitroV3) { - addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/handler.server')); - addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/update-route-name.server')); - } else { - addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/handler-legacy.server')); - addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/update-route-name-legacy.server')); - } - - addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/sentry.server')); + setupNitroServer(nuxt, serverApi, serverConfigFile, moduleOptions); if (isMinNuxtV4) { addPlugin({ src: moduleDirResolver.resolve('./runtime/plugins/route-detector.server'), mode: 'server' }); } else { addPlugin({ src: moduleDirResolver.resolve('./runtime/plugins/route-detector-legacy.server'), mode: 'server' }); } - - // Preps the middleware instrumentation module. - addMiddlewareImports(); - addStorageInstrumentation(nuxt, !isNitroV3); - addDatabaseInstrumentation(nuxt.options.nitro, !isNitroV3, moduleOptions); } if (clientConfigFile || serverConfigFile) { setupSourceMaps(moduleOptions, nuxt, addVitePlugin); } - addOTelCommonJSImportAlias(nuxt, isNitroV3); + addOTelCommonJSImportAlias(nuxt, serverApi === 'nitro3'); let pagesData: NuxtPageSubset[] = []; @@ -155,8 +116,8 @@ export default defineNuxtModule({ // Vite virtual plugin (for the Vite SSR build, where addPlugin mode:'server' plugins are bundled) addVitePlugin({ name: 'sentry-nuxt-pages-data-virtual', - resolveId: id => (id === pagesDataVirtualModuleId ? `\0${pagesDataVirtualModuleId}` : null), - load: id => + resolveId: (id: string) => (id === pagesDataVirtualModuleId ? `\0${pagesDataVirtualModuleId}` : null), + load: (id: string) => id === `\0${pagesDataVirtualModuleId}` ? `export default ${JSON.stringify(pagesData, null, 2)};` : undefined, }); } else { @@ -186,56 +147,5 @@ export default defineNuxtModule({ tsConfig.include.push(relativePath); } }); - - nuxt.hooks.hook('nitro:init', nitro => { - if (nuxt.options?._prepare) { - return; - } - - if (serverConfigFile) { - addMiddlewareInstrumentation(nitro, isNitroV3); - - if (!usesDeprecatedInjectMode) { - addServerConfigShimWithWarning(nitro); - - if (moduleOptions.debug) { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.log( - `[Sentry] Bundled \`${serverConfigFile}\` into the Nitro server build. The SDK initializes itself at server startup — no \`node --import\` preload needed.`, - ); - }); - } - } else { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn( - `[Sentry] \`autoInjectServerSentry: '${moduleOptions.autoInjectServerSentry}'\` is deprecated and will be removed in a future major version. The Sentry server config is bundled into the Nitro server build by default now. Remove the option to use the default behavior.`, - ); - }); - - if (moduleOptions.autoInjectServerSentry === 'top-level-import') { - // Nitro 3 (in Nuxt 5) is not bundled in dev mode, so there is no build to emit into. - if (!(isNitroV3 && nitro.options.dev)) { - addServerConfigToBuild(moduleOptions, nitro, serverConfigFile); - } - addSentryTopImport(moduleOptions, nitro); - } - - if (moduleOptions.autoInjectServerSentry === 'experimental_dynamic-import') { - addDynamicImportEntryFileWrapper(nitro, serverConfigFile, moduleOptions); - - if (moduleOptions.debug) { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.log( - '[Sentry] Wrapping the server entry file with a dynamic `import()`, so Sentry can be preloaded before the server initializes.', - ); - }); - } - } - } - } - }); }, }); diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts index 7b90d568216c..7b0970ef3f6d 100644 --- a/packages/nuxt/src/vite/addServerConfig.ts +++ b/packages/nuxt/src/vite/addServerConfig.ts @@ -1,7 +1,7 @@ import { existsSync } from 'node:fs'; import { basename } from 'node:path'; import { pathToFileURL } from 'node:url'; -import { addServerPlugin, addTemplate, createResolver } from '@nuxt/kit'; +import { addNitroPlugin, addTemplate, createResolver } from '@nuxt/kit'; import type { Nuxt } from '@nuxt/schema'; import { consoleSandbox, debug } from '@sentry/core'; import * as fs from 'fs'; @@ -132,7 +132,7 @@ export function addServerConfigPlugin(nuxt: Nuxt, serverConfigFile: string, isLe `import ${JSON.stringify(runtimeFlagsTemplate.dst)};\nimport ${JSON.stringify(configPath)};\nexport default () => {};\n`, }); - addServerPlugin(configPluginTemplate.dst); + addNitroPlugin(configPluginTemplate.dst); // Nitro v2 treeshakes side-effect-only imports outside its runtime dir, which would silently drop // the top-level `Sentry.init` and the flag assignments. diff --git a/packages/nuxt/src/vite/databaseConfig.ts b/packages/nuxt/src/vite/databaseConfig.ts index b025157339b3..54f83943191f 100644 --- a/packages/nuxt/src/vite/databaseConfig.ts +++ b/packages/nuxt/src/vite/databaseConfig.ts @@ -1,4 +1,4 @@ -import { addServerPlugin, createResolver } from '@nuxt/kit'; +import { addNitroPlugin, createResolver } from '@nuxt/kit'; import { consoleSandbox } from '@sentry/core'; import type { NitroConfig } from 'nitropack/types'; import type { SentryNuxtModuleOptions } from '../common/types'; @@ -43,8 +43,8 @@ export function addDatabaseInstrumentation( }); if (isLegacyNitro) { - addServerPlugin(createResolver(import.meta.url).resolve('./runtime/plugins/database-legacy.server')); + addNitroPlugin(createResolver(import.meta.url).resolve('./runtime/plugins/database-legacy.server')); } else { - addServerPlugin(createResolver(import.meta.url).resolve('./runtime/plugins/database.server')); + addNitroPlugin(createResolver(import.meta.url).resolve('./runtime/plugins/database.server')); } } diff --git a/packages/nuxt/src/vite/nitroServer.ts b/packages/nuxt/src/vite/nitroServer.ts new file mode 100644 index 000000000000..c999b29cd186 --- /dev/null +++ b/packages/nuxt/src/vite/nitroServer.ts @@ -0,0 +1,124 @@ +import { addNitroPlugin, createResolver } from '@nuxt/kit'; +import type { Nuxt } from '@nuxt/schema'; +import { consoleSandbox } from '@sentry/core'; +import type { Nitro } from 'nitropack/types'; +import type { SentryNuxtModuleOptions } from '../common/types'; +import { + addDynamicImportEntryFileWrapper, + addSentryTopImport, + addServerConfigPlugin, + addServerConfigShimWithWarning, + addServerConfigToBuild, +} from './addServerConfig'; +import { addDatabaseInstrumentation } from './databaseConfig'; +import { addMiddlewareImports, addMiddlewareInstrumentation } from './middlewareConfig'; +import { addStorageInstrumentation } from './storageConfig'; +import type { ServerApi } from './utils'; + +type NitroModuleOptions = Omit & + Required>; + +/** + * Sets up the server SDK on the Nitro side: the server config plugin, the server plugins, and the + * middleware, storage and database instrumentation. + * + * A host on the `nuxt` server API runs no Nitro, so it gets none of this. Kit would skip each + * Nitro-only registration there with a `NUXT_B8024` warning. + */ +export function setupNitroServer( + nuxt: Nuxt, + serverApi: ServerApi, + serverConfigFile: string, + moduleOptions: NitroModuleOptions, +): void { + if (serverApi === 'nuxt') { + return; + } + + const isNitroV3 = serverApi === 'nitro3'; + const moduleDirResolver = createResolver(import.meta.url); + // oxlint-disable-next-line typescript/no-deprecated -- supported until removal + const injectMode = moduleOptions.autoInjectServerSentry; + // The deprecated inject modes replace the default in-bundle initialization until their removal + const deprecatedInjectMode = + injectMode === 'top-level-import' || injectMode === 'experimental_dynamic-import' ? injectMode : undefined; + + if (!deprecatedInjectMode) { + addServerConfigPlugin(nuxt, serverConfigFile, !isNitroV3); + } + + if (isNitroV3) { + addNitroPlugin(moduleDirResolver.resolve('./runtime/plugins/handler.server')); + addNitroPlugin(moduleDirResolver.resolve('./runtime/plugins/update-route-name.server')); + } else { + addNitroPlugin(moduleDirResolver.resolve('./runtime/plugins/handler-legacy.server')); + addNitroPlugin(moduleDirResolver.resolve('./runtime/plugins/update-route-name-legacy.server')); + } + + addNitroPlugin(moduleDirResolver.resolve('./runtime/plugins/sentry.server')); + + // Preps the middleware instrumentation module. + addMiddlewareImports(); + addStorageInstrumentation(nuxt, !isNitroV3); + addDatabaseInstrumentation(nuxt.options.nitro, !isNitroV3, moduleOptions); + + nuxt.hooks.hook('nitro:init', nitro => { + if (nuxt.options?._prepare) { + return; + } + + addMiddlewareInstrumentation(nitro, isNitroV3); + + if (deprecatedInjectMode) { + setupDeprecatedInjectMode(nitro, deprecatedInjectMode, isNitroV3, serverConfigFile, moduleOptions); + return; + } + + addServerConfigShimWithWarning(nitro); + + if (moduleOptions.debug) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.log( + `[Sentry] Bundled \`${serverConfigFile}\` into the Nitro server build. The SDK initializes itself at server startup — no \`node --import\` preload needed.`, + ); + }); + } + }); +} + +function setupDeprecatedInjectMode( + nitro: Nitro, + injectMode: 'top-level-import' | 'experimental_dynamic-import', + isNitroV3: boolean, + serverConfigFile: string, + moduleOptions: NitroModuleOptions, +): void { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + `[Sentry] \`autoInjectServerSentry: '${injectMode}'\` is deprecated and will be removed in a future major version. The Sentry server config is bundled into the Nitro server build by default now. Remove the option to use the default behavior.`, + ); + }); + + if (injectMode === 'top-level-import') { + // Nitro 3 (in Nuxt 5) is not bundled in dev mode, so there is no build to emit into. + if (!(isNitroV3 && nitro.options.dev)) { + addServerConfigToBuild(moduleOptions, nitro, serverConfigFile); + } + addSentryTopImport(moduleOptions, nitro); + } + + if (injectMode === 'experimental_dynamic-import') { + addDynamicImportEntryFileWrapper(nitro, serverConfigFile, moduleOptions); + + if (moduleOptions.debug) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.log( + '[Sentry] Wrapping the server entry file with a dynamic `import()`, so Sentry can be preloaded before the server initializes.', + ); + }); + } + } +} diff --git a/packages/nuxt/src/vite/storageConfig.ts b/packages/nuxt/src/vite/storageConfig.ts index 393f8b12e59b..517c7884e36c 100644 --- a/packages/nuxt/src/vite/storageConfig.ts +++ b/packages/nuxt/src/vite/storageConfig.ts @@ -1,4 +1,4 @@ -import { addServerPlugin, createResolver } from '@nuxt/kit'; +import { addNitroPlugin, createResolver } from '@nuxt/kit'; import type { Nuxt } from '@nuxt/schema'; import { addServerTemplate } from '../vendor/server-template'; @@ -18,8 +18,8 @@ export function addStorageInstrumentation(nuxt: Nuxt, isLegacyNitro: boolean): v }); if (isLegacyNitro) { - addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/storage-legacy.server')); + addNitroPlugin(moduleDirResolver.resolve('./runtime/plugins/storage-legacy.server')); } else { - addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/storage.server')); + addNitroPlugin(moduleDirResolver.resolve('./runtime/plugins/storage.server')); } } diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index 4c95658e48b3..cab39bc95f19 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -4,45 +4,19 @@ import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'node:url'; import type { SentryNuxtModuleOptions } from '../common/types'; -import { resolvePath } from '@nuxt/kit'; +import { resolvePath, resolveServerVariant } from '@nuxt/kit'; -/** - * Gets the major version of the Nitro package used by the app's Nuxt installation. - * Returns 2 as the default if the version cannot be determined. - * - * Nitro v2 is published as `nitropack`, v3 as `nitro`. Resolving `nitro` directly is - * unreliable: module resolution walks up the directory tree, so in a monorepo an - * unrelated `nitro` v3 above the app wins even when the app's Nuxt uses `nitropack` v2. - * Instead, follow the dependency chain Nuxt itself imports Nitro through: - * `nuxt` -> (`@nuxt/nitro-server` ->) `nitro` | `nitropack`. - */ -export async function getNitroMajorVersion(rootDir: string): Promise { - try { - const { getPackageInfo } = await import('local-pkg'); - - // `paths` entries must point at a file: for a bare directory, resolution starts at the - // directory's parent and skips the directory's own `node_modules`, so a hoisted copy higher - // up the tree (e.g. a monorepo root) wins over the app's actual dependency. - const fromPackage = (dir: string): { paths: string[] } => ({ paths: [path.join(dir, 'package.json')] }); - - // The package that declares the Nitro dependency: `nuxt` itself, or `@nuxt/nitro-server` (Nuxt >= 3.21) when nuxt delegates to it - let provider = await getPackageInfo('nuxt', fromPackage(rootDir)); - if (provider?.packageJson.dependencies?.['@nuxt/nitro-server']) { - provider = (await getPackageInfo('@nuxt/nitro-server', fromPackage(provider.rootPath))) ?? provider; - } +const SERVER_API_VARIANTS = { nitro2: 'nitro2', nitro3: 'nitro3', nuxt: 'nuxt' } as const; - if (!provider?.packageJson.dependencies?.nitro) { - return 2; - } +/** The upstream server API the host runs. `nuxt` means the host runs no Nitro, for example the Vite server. */ +export type ServerApi = keyof typeof SERVER_API_VARIANTS; - const info = await getPackageInfo('nitro', fromPackage(provider.rootPath)); - const major = parseInt(info?.version?.split('.')[0] ?? '', 10); - // The provider imports `nitro` (not `nitropack`), so it is at least v3 even if the version is unreadable - return isNaN(major) ? 3 : major; - } catch { - // If local-pkg is unavailable or resolution fails, default to v2 - return 2; - } +/** + * Resolves the server API of the current Nuxt host. Call it once in the module setup and pass the result on. + */ +export function resolveServerApi(): ServerApi { + // Kit returns `undefined` only when the host runs none of the given keys, and every host runs one of these. + return resolveServerVariant(SERVER_API_VARIANTS) ?? 'nitro2'; } /** diff --git a/packages/nuxt/test/vite/addServerConfig.test.ts b/packages/nuxt/test/vite/addServerConfig.test.ts index 655c5b96ccb3..d73e66af2530 100644 --- a/packages/nuxt/test/vite/addServerConfig.test.ts +++ b/packages/nuxt/test/vite/addServerConfig.test.ts @@ -114,11 +114,11 @@ describe('wrapEntryWithDynamicImport', () => { }); const addTemplateMock = vi.hoisted(() => vi.fn()); -const addServerPluginMock = vi.hoisted(() => vi.fn()); +const addNitroPluginMock = vi.hoisted(() => vi.fn()); vi.mock('@nuxt/kit', () => ({ addTemplate: addTemplateMock, - addServerPlugin: addServerPluginMock, + addNitroPlugin: addNitroPluginMock, // `@nuxt/kit` resolves rather than joins, which is what lets an absolute layer path win over the base. createResolver: (base: string) => ({ resolve: (input: string) => path.resolve(base, input) }), })); @@ -167,7 +167,7 @@ describe('addServerConfigPlugin', () => { expect(templateContents('sentry-server-config-plugin.mjs')).toBe( `import ${JSON.stringify(flagsDst)};\nimport ${JSON.stringify(APP_CONFIG)};\nexport default () => {};\n`, ); - expect(addServerPluginMock).toHaveBeenCalledWith(pluginDst); + expect(addNitroPluginMock).toHaveBeenCalledWith(pluginDst); }); it('derives the runtime flags from the build-time `import.meta` values', () => { @@ -256,6 +256,23 @@ describe('addServerConfigPlugin', () => { consoleWarnSpy.mockRestore(); }); + it('removes the plugin on `nitro:init` when Nuxt adds the server plugins after `nitro:config` (Nuxt >= 4.6)', () => { + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const { nuxt, hooks } = createFakeNuxt(); + const serverPlugins: string[] = []; + addNitroPluginMock.mockImplementationOnce((plugin: string) => serverPlugins.push(plugin)); + addServerConfigPlugin(nuxt, APP_CONFIG, true); + const nitroConfig: NitroConfig = { preset: 'cloudflare_module', plugins: ['other-plugin.mjs'] }; + + hooks['nitro:config']!(nitroConfig); + // Nuxt's `collectServerRegistrations` runs after `nitro:config`, so the early filter cannot see the plugin + const nitro = { options: { preset: 'cloudflare_module', plugins: [...nitroConfig.plugins!, ...serverPlugins] } }; + hooks['nitro:init']!(nitro as never); + + expect(nitro.options.plugins).toEqual(['other-plugin.mjs']); + consoleWarnSpy.mockRestore(); + }); + it('keeps the plugin when the resolved preset is not Cloudflare', () => { const { nuxt, hooks } = createFakeNuxt(); addServerConfigPlugin(nuxt, APP_CONFIG, true); diff --git a/packages/nuxt/test/vite/databaseConfig.test.ts b/packages/nuxt/test/vite/databaseConfig.test.ts index e987ab3984a5..df66e7f180c3 100644 --- a/packages/nuxt/test/vite/databaseConfig.test.ts +++ b/packages/nuxt/test/vite/databaseConfig.test.ts @@ -8,7 +8,7 @@ vi.mock('@sentry/core', () => ({ })); vi.mock('@nuxt/kit', () => ({ - addServerPlugin: vi.fn(), + addNitroPlugin: vi.fn(), createResolver: vi.fn(() => ({ resolve: vi.fn((path: string) => path), })), diff --git a/packages/nuxt/test/vite/nitroServer.test.ts b/packages/nuxt/test/vite/nitroServer.test.ts new file mode 100644 index 000000000000..a1ab7e7d40e2 --- /dev/null +++ b/packages/nuxt/test/vite/nitroServer.test.ts @@ -0,0 +1,100 @@ +import type { Nuxt } from '@nuxt/schema'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { setupNitroServer } from '../../src/vite/nitroServer'; + +const addNitroPluginMock = vi.hoisted(() => vi.fn()); +const addServerImportsMock = vi.hoisted(() => vi.fn()); +const addTemplateMock = vi.hoisted(() => vi.fn()); +const addServerTemplateMock = vi.hoisted(() => vi.fn()); + +vi.mock('@nuxt/kit', () => ({ + addNitroPlugin: addNitroPluginMock, + addServerImports: addServerImportsMock, + addTemplate: addTemplateMock, + createResolver: () => ({ resolve: (input: string) => input }), +})); + +vi.mock('../../src/vendor/server-template', () => ({ + addServerTemplate: addServerTemplateMock, +})); + +const APP_ROOT = '/my/app'; +const SERVER_CONFIG = `${APP_ROOT}/sentry.server.config.ts`; +const MODULE_OPTIONS = { experimental_entrypointWrappedFunctions: ['default', 'handler', 'server'] }; + +type HookCallback = (arg: unknown) => void; + +function createFakeNuxt(): { + nuxt: Nuxt; + hook: ReturnType; + callHook: (name: string, arg: unknown) => void; +} { + const callbacks = new Map(); + const hook = vi.fn((name: string, callback: HookCallback) => { + callbacks.set(name, [...(callbacks.get(name) ?? []), callback]); + }); + const nuxt = { + options: { rootDir: APP_ROOT, buildDir: `${APP_ROOT}/.nuxt`, nitro: {} }, + hook, + hooks: { hook }, + } as unknown as Nuxt; + + return { nuxt, hook, callHook: (name, arg) => callbacks.get(name)?.forEach(callback => callback(arg)) }; +} + +describe('setupNitroServer', () => { + beforeEach(() => { + vi.clearAllMocks(); + addTemplateMock.mockImplementation((opts: { filename: string }) => ({ dst: `${APP_ROOT}/.nuxt/${opts.filename}` })); + }); + + it('registers nothing on a host without Nitro', () => { + const { nuxt, hook } = createFakeNuxt(); + + setupNitroServer(nuxt, 'nuxt', SERVER_CONFIG, MODULE_OPTIONS); + + expect(addNitroPluginMock).not.toHaveBeenCalled(); + expect(addServerImportsMock).not.toHaveBeenCalled(); + expect(addTemplateMock).not.toHaveBeenCalled(); + expect(addServerTemplateMock).not.toHaveBeenCalled(); + expect(hook).not.toHaveBeenCalled(); + }); + + it.each([ + { + serverApi: 'nitro2', + handler: 'handler-legacy.server', + updateRouteName: 'update-route-name-legacy.server', + storage: 'storage-legacy.server', + }, + { + serverApi: 'nitro3', + handler: 'handler.server', + updateRouteName: 'update-route-name.server', + storage: 'storage.server', + }, + ] as const)('registers the $handler plugins on $serverApi', ({ serverApi, handler, updateRouteName, storage }) => { + const { nuxt } = createFakeNuxt(); + + setupNitroServer(nuxt, serverApi, SERVER_CONFIG, MODULE_OPTIONS); + + expect(addNitroPluginMock).toHaveBeenCalledTimes(5); + expect(addNitroPluginMock).toHaveBeenNthCalledWith(1, `${APP_ROOT}/.nuxt/sentry-server-config-plugin.mjs`); + expect(addNitroPluginMock).toHaveBeenNthCalledWith(2, `./runtime/plugins/${handler}`); + expect(addNitroPluginMock).toHaveBeenNthCalledWith(3, `./runtime/plugins/${updateRouteName}`); + expect(addNitroPluginMock).toHaveBeenNthCalledWith(4, './runtime/plugins/sentry.server'); + expect(addNitroPluginMock).toHaveBeenNthCalledWith(5, `./runtime/plugins/${storage}`); + }); + + it('instruments the middleware and adds the config shim once Nitro initializes', () => { + const { nuxt, callHook } = createFakeNuxt(); + const nitro = { options: { preset: 'node-server', plugins: [] }, hooks: { hook: vi.fn() } }; + setupNitroServer(nuxt, 'nitro2', SERVER_CONFIG, MODULE_OPTIONS); + + callHook('nitro:init', nitro); + + expect(nitro.hooks.hook).toHaveBeenCalledTimes(2); + expect(nitro.hooks.hook).toHaveBeenCalledWith('rollup:before', expect.any(Function)); + expect(nitro.hooks.hook).toHaveBeenCalledWith('close', expect.any(Function)); + }); +}); diff --git a/packages/nuxt/test/vite/nitroVersion.test.ts b/packages/nuxt/test/vite/nitroVersion.test.ts deleted file mode 100644 index a9228231d395..000000000000 --- a/packages/nuxt/test/vite/nitroVersion.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; -import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -import { getNitroMajorVersion } from '../../src/vite/utils'; - -// Real filesystem fixtures instead of mocks: the bug this guards against lives in -// module resolution walking up the directory tree, which mocks cannot reproduce. -let monorepoRoot: string; - -function writePackage(dir: string, packageJson: Record): void { - fs.mkdirSync(dir, { recursive: true }); - fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ main: 'index.js', ...packageJson })); - fs.writeFileSync(path.join(dir, 'index.js'), ''); -} - -function createApp(appName: string, packages: Record>): string { - const appDir = path.join(monorepoRoot, 'apps', appName); - fs.mkdirSync(appDir, { recursive: true }); - for (const [name, packageJson] of Object.entries(packages)) { - writePackage(path.join(appDir, 'node_modules', name), packageJson); - } - return appDir; -} - -beforeAll(() => { - monorepoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-nitro-version-')); - // An unrelated Nitro v3 above the apps, like a monorepo root devDependency - writePackage(path.join(monorepoRoot, 'node_modules', 'nitro'), { name: 'nitro', version: '3.0.0-beta.1' }); -}); - -afterAll(() => { - fs.rmSync(monorepoRoot, { recursive: true, force: true }); -}); - -describe('getNitroMajorVersion', () => { - it('detects v2 when nuxt depends on nitropack directly (Nuxt 3 / <=4.1), ignoring a nitro v3 higher up the tree', async () => { - const appDir = createApp('nuxt-4-old', { - nuxt: { name: 'nuxt', version: '4.1.0', dependencies: { nitropack: '^2.12.0' } }, - nitropack: { name: 'nitropack', version: '2.12.0' }, - }); - - await expect(getNitroMajorVersion(appDir)).resolves.toBe(2); - }); - - it('detects v2 through @nuxt/nitro-server when it depends on nitropack (Nuxt >=3.21 stable)', async () => { - const appDir = createApp('nuxt-4-stable', { - nuxt: { name: 'nuxt', version: '4.5.2', dependencies: { '@nuxt/nitro-server': '4.5.2' } }, - '@nuxt/nitro-server': { name: '@nuxt/nitro-server', version: '4.5.2', dependencies: { nitropack: '^2.13.4' } }, - nitropack: { name: 'nitropack', version: '2.13.4' }, - }); - - await expect(getNitroMajorVersion(appDir)).resolves.toBe(2); - }); - - it('detects v3 through @nuxt/nitro-server when it depends on nitro (Nuxt 5)', async () => { - const appDir = createApp('nuxt-5', { - nuxt: { - name: 'nuxt', - version: '5.0.0', - dependencies: { '@nuxt/nitro-server': 'npm:@nuxt/nitro-server-nightly' }, - }, - '@nuxt/nitro-server': { - name: '@nuxt/nitro-server-nightly', - version: '5.0.0-nightly', - dependencies: { nitro: '^3.0.0-beta' }, - }, - nitro: { name: 'nitro', version: '3.0.0-beta.2' }, - }); - - await expect(getNitroMajorVersion(appDir)).resolves.toBe(3); - }); - - it('detects v3 when nuxt depends on nitro directly, without @nuxt/nitro-server', async () => { - const appDir = createApp('nuxt-direct-nitro', { - nuxt: { name: 'nuxt', version: '5.1.0', dependencies: { nitro: '^3.1.0' } }, - nitro: { name: 'nitro', version: '3.1.0' }, - }); - - await expect(getNitroMajorVersion(appDir)).resolves.toBe(3); - }); - - it('falls back to v3 when the declared nitro package has no readable version (stub package)', async () => { - const appDir = createApp('nuxt-5-stub', { - nuxt: { - name: 'nuxt', - version: '5.0.0', - dependencies: { '@nuxt/nitro-server': 'npm:@nuxt/nitro-server-nightly' }, - }, - '@nuxt/nitro-server': { - name: '@nuxt/nitro-server-nightly', - version: '5.0.0-nightly', - dependencies: { nitro: '^3.0.0-beta' }, - }, - nitro: { name: 'nitro' }, - }); - - await expect(getNitroMajorVersion(appDir)).resolves.toBe(3); - }); - - it('defaults to v2 when nuxt cannot be resolved', async () => { - const appDir = path.join(monorepoRoot, 'apps', 'no-nuxt'); - fs.mkdirSync(appDir, { recursive: true }); - - await expect(getNitroMajorVersion(appDir)).resolves.toBe(2); - }); -}); diff --git a/packages/nuxt/test/vite/serverApi.test.ts b/packages/nuxt/test/vite/serverApi.test.ts new file mode 100644 index 000000000000..db453a699025 --- /dev/null +++ b/packages/nuxt/test/vite/serverApi.test.ts @@ -0,0 +1,67 @@ +import { runWithNuxtContext } from '@nuxt/kit'; +import type { Nuxt } from '@nuxt/schema'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { resolveServerApi } from '../../src/vite/utils'; + +// The real kit, not a mock: on older hosts kit resolves `nitropack` from the project, so the +// fixture gives it a project with only `nitropack` installed. A temp dir has nothing hoisted above it. +let rootDir: string; + +beforeAll(() => { + rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-nuxt-server-api-')); + const nitropackDir = path.join(rootDir, 'node_modules', 'nitropack'); + fs.mkdirSync(nitropackDir, { recursive: true }); + fs.writeFileSync( + path.join(nitropackDir, 'package.json'), + JSON.stringify({ name: 'nitropack', version: '2.13.4', main: 'index.js' }), + ); + fs.writeFileSync(path.join(nitropackDir, 'index.js'), ''); +}); + +afterAll(() => { + fs.rmSync(rootDir, { recursive: true, force: true }); +}); + +function resolveForHost(version: string, options: Record): ReturnType { + const nuxt = { + _version: version, + options: { rootDir, modulesDir: [path.join(rootDir, 'node_modules')], ...options }, + } as unknown as Nuxt; + + return runWithNuxtContext(nuxt, resolveServerApi); +} + +describe('resolveServerApi', () => { + it.each([ + { host: 'Nuxt 3.7', version: '3.7.0', options: {} }, + { + host: 'Nuxt 4.2 to 4.5', + version: '4.5.2', + options: { server: { builder: '@nuxt/nitro-server' } }, + }, + ])( + 'resolves `nitro2` from the installed `nitropack` on $host, which declares no Nitro major', + ({ version, options }) => { + expect(resolveForHost(version, options)).toBe('nitro2'); + }, + ); + + it('resolves `nitro2` when the host declares Nitro v2 (Nuxt 4.6)', () => { + expect(resolveForHost('4.6.0', { server: { builder: '@nuxt/nitro-server' }, _nitroMajor: 2 })).toBe('nitro2'); + }); + + it('resolves `nitro3` when the host declares Nitro v3 (Nuxt 5)', () => { + expect(resolveForHost('5.0.0', { server: { builder: '@nuxt/nitro-server' }, _nitroMajor: 3 })).toBe('nitro3'); + }); + + it('resolves `nuxt` for the Vite server builder', () => { + expect(resolveForHost('4.6.0', { server: { builder: '@nuxt/vite-server' }, _nitroMajor: 2 })).toBe('nuxt'); + }); + + it('resolves `nuxt` for a custom builder object', () => { + expect(resolveForHost('4.6.0', { server: { builder: { bundle() {} } }, _nitroMajor: 2 })).toBe('nuxt'); + }); +}); diff --git a/yarn.lock b/yarn.lock index abf7f58320c1..0feac03098c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5996,32 +5996,29 @@ unctx "^2.5.0" untyped "^2.0.0" -"@nuxt/kit@^4.5.1", "@nuxt/kit@^4.5.2": - version "4.5.2" - resolved "https://registry.yarnpkg.com/@nuxt/kit/-/kit-4.5.2.tgz#fa338b169d14d9e66f973ded551d2376edb72398" - integrity sha512-l66LU9DcJYjmNwqwAj2I5UGRrUbnG2DOKGChnN70zIGtn0eq/z87gi/FRgha6eMb9/FmB1PFHgtx6PWVml1C2Q== +"@nuxt/kit@^4.5.1", "@nuxt/kit@^4.6.0": + version "4.6.0" + resolved "https://sfw.security.sentry.io/npm/@nuxt/kit/-/kit-4.6.0.tgz#bc2d003484d800398927976fa50c104aa9a76f45" + integrity sha512-UXTajJ+MFJgwV9UL1CTNNd2PSw5PC3Pjb2rM7FsSYcr92ySQKXEslBnUKbvnXT+gG3NbZ/EF70+WenoNIHX2tw== dependencies: - c12 "^3.3.4" consola "^3.4.2" defu "^6.1.7" destr "^2.0.5" - errx "^0.1.2" - exsolve "^1.1.1" - ignore "^7.0.6" - jiti "^2.7.0" + errx "^0.2.2" + exsolve "^1.1.3" + ignore "^7.0.12" klona "^2.0.6" - mlly "^1.8.2" - nostics "^1.2.0" - ohash "^2.0.11" + microdiff "^1.6.0" + nostics "^1.3.0" + package-manager-detector "^1.8.0" pathe "^2.0.3" - pkg-types "^2.3.1" - rc9 "^3.0.1" + rc9 "^3.1.0" scule "^1.3.0" + std-env "^4.3.0" + tinyexec "^1.3.1" tinyglobby "^0.2.17" - ufo "^1.6.4" - unctx "^3.0.0" - untyped "^2.0.0" - verkit "^0.3.1" + unctx "^3.0.1" + verkit "^0.5.0" "@nuxt/nitro-server@3.21.11", "@nuxt/nitro-server@^3.21.11": version "3.21.11" @@ -14485,6 +14482,11 @@ errx@^0.1.2: resolved "https://registry.yarnpkg.com/errx/-/errx-0.1.2.tgz#cd870178afe9072700d8486af4eb3c358892f40c" integrity sha512-chfpPHmCerdo/rXr/nNvPZRkV4WwDRwzwnsJ0Uzz3tVi8Z41tDctRjduYy1138ii77AFlts1qvWtX3g/Acg91Q== +errx@^0.2.2: + version "0.2.2" + resolved "https://sfw.security.sentry.io/npm/errx/-/errx-0.2.2.tgz#33a094dc8238e13b2182d20e8d406572a09ae86b" + integrity sha512-HaPN6cxm6iZ9bIbQQ5orChcw7SY5smUzM08ZMc6OtV4UQhrrc3Ms1G8Ww2n/9lXkWqiioNTssF8bCvchrSbFEQ== + es-abstract@^1.23.2, es-abstract@^1.23.5, es-abstract@^1.23.9, es-abstract@^1.24.0: version "1.24.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.24.0.tgz#c44732d2beb0acc1ed60df840869e3106e7af328" @@ -15547,10 +15549,10 @@ express@^4.17.3, express@^4.21.2: utils-merge "1.0.1" vary "~1.1.2" -exsolve@^1.0.7, exsolve@^1.0.8, exsolve@^1.1.0, exsolve@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.1.1.tgz#c055418255459b6ecde4e59de0060a3e97bc7572" - integrity sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g== +exsolve@^1.0.7, exsolve@^1.0.8, exsolve@^1.1.0, exsolve@^1.1.1, exsolve@^1.1.3: + version "1.1.3" + resolved "https://sfw.security.sentry.io/npm/exsolve/-/exsolve-1.1.3.tgz#175a6336d6cb1c9cfee822a092665ee0b79e716d" + integrity sha512-kuzL9zherLQoqDeQaS55AiiIg1kb4KYXaSusTtgZTPlvKP2gGCI82LNmBNh5RjDVUXIcmgx+/Mx7vPwRzBbhJA== extend-shallow@^2.0.1: version "2.0.1" @@ -17404,10 +17406,10 @@ ignore@^5.0.4, ignore@^5.2.0, ignore@^5.2.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== -ignore@^7.0.5, ignore@^7.0.6: - version "7.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.6.tgz#6a57aaef4c90df27ac3590875d29e8f11988c88e" - integrity sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw== +ignore@^7.0.12, ignore@^7.0.5, ignore@^7.0.6: + version "7.0.12" + resolved "https://sfw.security.sentry.io/npm/ignore/-/ignore-7.0.12.tgz#36048ce2e8d8e0f97fd87863a1cb595ea0a5c69f" + integrity sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg== image-meta@^0.2.2: version "0.2.2" @@ -19580,6 +19582,11 @@ methods@^1.1.2, methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= +microdiff@^1.6.0: + version "1.6.0" + resolved "https://sfw.security.sentry.io/npm/microdiff/-/microdiff-1.6.0.tgz#5a205ff44ab63c2eea7761b1578b6c0e2d4d119b" + integrity sha512-w7JWt8Bno6I8h0rEqlxr4lNG4UbT1FVtWo42wWosIiaJ+rP3pXh7shCYDnyqBrYx36LJ1CZ64p8A62aVp0sJpQ== + micromark-core-commonmark@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz#c691630e485021a68cf28dbc2b2ca27ebf678cd4" @@ -20810,10 +20817,10 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -nostics@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/nostics/-/nostics-1.2.0.tgz#1362df6de2ca8456b521914501abf6c52c4d7fb5" - integrity sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg== +nostics@^1.3.0: + version "1.3.0" + resolved "https://sfw.security.sentry.io/npm/nostics/-/nostics-1.3.0.tgz#e1d538463acb0207f9a0ef9ec551b075930559c4" + integrity sha512-wzRsrJwNlFbqIRQt8aCeL2WE8USip/uuvGSOZV1D02CxX8ABTMbw8IywKjH7yWHETMXbWJI4OBpihg9rmz2Vyg== npm-bundled@^1.1.1: version "1.1.2" @@ -21815,6 +21822,11 @@ package-json-from-dist@^1.0.0, package-json-from-dist@^1.0.1: resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== +package-manager-detector@^1.8.0: + version "1.9.0" + resolved "https://sfw.security.sentry.io/npm/package-manager-detector/-/package-manager-detector-1.9.0.tgz#217ed03645bfeb563e9efce4c5bbbfbb652754ab" + integrity sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA== + package-name-regex@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/package-name-regex/-/package-name-regex-2.0.6.tgz#b54bcb04d950e38082b7bb38fa558e01c1679334" @@ -23375,12 +23387,12 @@ rc9@^2.1.2: defu "^6.1.4" destr "^2.0.3" -rc9@^3.0.0, rc9@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/rc9/-/rc9-3.0.1.tgz#3895e5834a2b5c2d8fb76d93e802fbcbc2579bc7" - integrity sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ== +rc9@^3.0.0, rc9@^3.0.1, rc9@^3.1.0: + version "3.1.0" + resolved "https://sfw.security.sentry.io/npm/rc9/-/rc9-3.1.0.tgz#383f44c0f0bdefcf411c1ce9403c528064a8bd1a" + integrity sha512-ufjkNVzbRHKcCOmTahZkmVsyc3W+MSk3jY03m+a7tGHkIsdVMG9l10/3HvFbWkkKzY5VFp3pkRsIo/UYgmFL7Q== dependencies: - defu "^6.1.6" + defu "^6.1.7" destr "^2.0.5" rc@^1.2.8: @@ -25509,10 +25521,10 @@ std-env@^3.7.0: resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.10.0.tgz#d810b27e3a073047b2b5e40034881f5ea6f9c83b" integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg== -std-env@^4.0.0, std-env@^4.0.0-rc.1, std-env@^4.1.0, std-env@^4.2.0: - version "4.2.0" - resolved "https://sfw.security.sentry.io/npm/std-env/-/std-env-4.2.0.tgz#8ebe0ec60485668ab47227b312f4254cdf80c9d3" - integrity sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw== +std-env@^4.0.0, std-env@^4.0.0-rc.1, std-env@^4.1.0, std-env@^4.2.0, std-env@^4.3.0: + version "4.3.0" + resolved "https://sfw.security.sentry.io/npm/std-env/-/std-env-4.3.0.tgz#4e7231b61fd83219aa7f76f71fa8818b07e41321" + integrity sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag== stdin-discarder@^0.2.2: version "0.2.2" @@ -26196,7 +26208,7 @@ tinyexec@^0.3.1: resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2" integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== -tinyexec@^1.0.2, tinyexec@^1.2.4: +tinyexec@^1.0.2, tinyexec@^1.2.4, tinyexec@^1.3.1: version "1.3.1" resolved "https://sfw.security.sentry.io/npm/tinyexec/-/tinyexec-1.3.1.tgz#16a2e3c6e23fafce72640e678e651db36145b9c6" integrity sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA== @@ -26730,10 +26742,10 @@ unctx@^2.4.1, unctx@^2.5.0: magic-string "^0.30.21" unplugin "^2.3.11" -unctx@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/unctx/-/unctx-3.0.0.tgz#82e2f20c8f9db13a443871caddfdd1af7af2eff1" - integrity sha512-DoXdZVeyi2jyEsn86i8MO5RTItm1kffUkH9/+DQORn3Q688AMOy2551CIl6AdGL2UpwD675wtbNOl75wIQN/uA== +unctx@^3.0.1: + version "3.0.1" + resolved "https://sfw.security.sentry.io/npm/unctx/-/unctx-3.0.1.tgz#16dc56fc256a2bf4df485d9f38866634109058a1" + integrity sha512-5RAt2etv7g362RXyd33R82gm9u/kbtQlpoaOs9Bgm4E32GRMJ0xjbZyvdxUTmiuHk3V1IQb1aUNrp5IWY+JaWw== undefsafe@^2.0.5: version "2.0.5" @@ -27265,10 +27277,10 @@ vary@^1, vary@^1.1.2, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= -verkit@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/verkit/-/verkit-0.3.2.tgz#0511ea63310551b62a20c010aa820350c5c350c6" - integrity sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg== +verkit@^0.5.0: + version "0.5.0" + resolved "https://sfw.security.sentry.io/npm/verkit/-/verkit-0.5.0.tgz#8b7617dda9ad2edb7851c6a4ff5f3ca428482401" + integrity sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g== vfile-location@^5.0.0: version "5.0.3" From d489a0828ec15e4babf4926e375704ac0bffc67e Mon Sep 17 00:00:00 2001 From: Sigrid <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:34:53 +0200 Subject: [PATCH 07/84] fix(server-utils): Record amqplib consumer info before the first delivery and instrument the callback API (#25047) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a worker subscribes to a queue that already has messages, amqplib can decode `BasicConsumeOk` and the first message in one socket read. We recorded each consumer's queue and `noAck` flag when `consume()` resolved. That happens too late: ``` one socket read, decoded synchronously │ ├─ BasicConsumeOk ─► registerConsumer(tag) ◄─ now: info recorded here ├─ BasicDeliver ─► dispatchMessage(tag) ◄─ lookup missed here │ └─ decode ends ─► microtasks ─► consume().then() ◄─ before: info recorded here ``` The first message's span got the routing key as its name, and `noAck` spans ended as errors at channel close. Now we record the info in `registerConsumer`, which amqplib always calls before dispatch. This PR also instruments the callback API (`lib/callback_model.js`). It has the same classes and signatures as the promise API. Follow-up on this PR (noticed the bug while working on this): https://github.com/getsentry/sentry-javascript/pull/23652 --- .../tracing/amqplib/scenario-callback-api.mjs | 51 +++++++++ .../tracing/amqplib/scenario-topic-noack.mjs | 63 +++++++++++ .../suites/tracing/amqplib/test.ts | 51 +++++++++ .../server-utils/src/integrations/amqplib.ts | 77 ++++++++++--- .../src/orchestrion/config/amqplib.ts | 106 ++++++++++-------- 5 files changed, 287 insertions(+), 61 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/amqplib/scenario-callback-api.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/amqplib/scenario-topic-noack.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/amqplib/scenario-callback-api.mjs b/dev-packages/node-integration-tests/suites/tracing/amqplib/scenario-callback-api.mjs new file mode 100644 index 000000000000..4870e99da9bd --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/amqplib/scenario-callback-api.mjs @@ -0,0 +1,51 @@ +import * as Sentry from '@sentry/node'; +import amqp from 'amqplib/callback_api.js'; + +const queueName = 'callback-queue'; +const amqpUsername = 'sentry'; +const amqpPassword = 'sentry'; + +const AMQP_URL = `amqp://${amqpUsername}:${amqpPassword}@localhost:5672/`; + +(async () => { + const connection = await connectToRabbitMQ(); + const channel = await toPromise(cb => connection.createChannel(cb)); + await toPromise(cb => channel.assertQueue(queueName, { durable: false, exclusive: true }, cb)); + + // Without the info from `consume`, a `noAck` span stays open until the channel closes and ends as an error. + const received = new Promise((resolve, reject) => { + channel.consume( + queueName, + message => (message ? resolve() : reject(new Error('No message received'))), + { noAck: true }, + err => err && reject(err), + ); + }); + + await Sentry.startSpan({ name: 'root span' }, async () => { + channel.sendToQueue(queueName, Buffer.from(JSON.stringify({ foo: 'bar01' }))); + }); + + await received; + + await toPromise(cb => channel.close(cb)); + await toPromise(cb => connection.close(cb)); +})(); + +async function connectToRabbitMQ() { + // The broker accepts TCP before AMQP handshakes succeed, so early connects can fail while it boots. + let lastError; + for (let attempt = 0; attempt < 5; attempt++) { + try { + return await toPromise(cb => amqp.connect(AMQP_URL, cb)); + } catch (err) { + lastError = err; + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + throw lastError; +} + +function toPromise(fn) { + return new Promise((resolve, reject) => fn((err, result) => (err ? reject(err) : resolve(result)))); +} diff --git a/dev-packages/node-integration-tests/suites/tracing/amqplib/scenario-topic-noack.mjs b/dev-packages/node-integration-tests/suites/tracing/amqplib/scenario-topic-noack.mjs new file mode 100644 index 000000000000..819f1181cb1f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/amqplib/scenario-topic-noack.mjs @@ -0,0 +1,63 @@ +import * as Sentry from '@sentry/node'; +import amqp from 'amqplib'; + +// A topic exchange, so the routing key and the queue name differ. +const exchangeName = 'orders'; +const queueName = 'orders-worker'; +const routingKey = 'order.created.12345'; +const amqpUsername = 'sentry'; +const amqpPassword = 'sentry'; + +const AMQP_URL = `amqp://${amqpUsername}:${amqpPassword}@localhost:5672/`; + +// A `noAck` consumer never settles a message, so its span has to end when dispatch returns. +const NO_ACKNOWLEDGEMENT = { noAck: true }; + +(async () => { + const { connection, channel } = await connectToRabbitMQ(); + + await channel.assertExchange(exchangeName, 'topic', { durable: false, autoDelete: true }); + await channel.assertQueue(queueName, { durable: false, exclusive: true }); + await channel.bindQueue(queueName, exchangeName, 'order.created.#'); + + // Publish before consuming, so the broker sends `BasicConsumeOk` and the delivery back to back. + await Sentry.startSpan({ name: 'root span' }, async () => { + channel.publish(exchangeName, routingKey, Buffer.from(JSON.stringify({ foo: 'bar01' }))); + }); + + await consumeMessageFromQueue(queueName, channel); + + await channel.close(); + await connection.close(); +})(); + +async function connectToRabbitMQ() { + // The broker accepts TCP before AMQP handshakes succeed, so early connects can reject while it boots. + let lastError; + for (let attempt = 0; attempt < 5; attempt++) { + try { + const connection = await amqp.connect(AMQP_URL); + const channel = await connection.createChannel(); + return { connection, channel }; + } catch (err) { + lastError = err; + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + throw lastError; +} + +async function consumeMessageFromQueue(queueName, channel) { + return new Promise((resolve, reject) => { + const onMessage = message => (message ? resolve() : reject(new Error('No message received'))); + + // Invalid `arguments` throw before the RPC, so the channel stays open for the next `consume`. + try { + channel.consume('orders-archive', onMessage, { arguments: 'invalid' }); + } catch { + // Expected. + } + + channel.consume(queueName, onMessage, NO_ACKNOWLEDGEMENT).catch(reject); + }); +} diff --git a/dev-packages/node-integration-tests/suites/tracing/amqplib/test.ts b/dev-packages/node-integration-tests/suites/tracing/amqplib/test.ts index 9e162e07490c..cd86d01459ec 100644 --- a/dev-packages/node-integration-tests/suites/tracing/amqplib/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/amqplib/test.ts @@ -168,5 +168,56 @@ describeWithDockerCompose('amqplib auto-instrumentation', { workingDirectory: [_ }, { additionalDependencies }, ); + + createEsmAndCjsTests( + __dirname, + 'scenario-topic-noack.mjs', + 'instrument.mjs', + (createTestRunner, test) => { + test('ends a noAck consumer span as ok', { timeout: 60_000 }, async () => { + await createTestRunner() + .expect({ + span: container => { + const consumer = container.items.find( + span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.amqplib.consumer', + ); + + expect(consumer).toBeDefined(); + expect(consumer!.name).toBe('process orders'); + expect(consumer!.status).toBe('ok'); + expect(consumer!.attributes[MESSAGING_DESTINATION_NAME]?.value).toBe('orders'); + expect(consumer!.attributes[MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY]?.value).toBe( + 'order.created.12345', + ); + }, + }) + .start() + .completed(); + }); + }, + { additionalDependencies }, + ); + }); + + createEsmAndCjsTests(__dirname, 'scenario-callback-api.mjs', 'instrument.mjs', (createTestRunner, test) => { + test('instruments publish and noAck consume on the callback API', { timeout: 60_000 }, async () => { + await createTestRunner() + .expect({ + span: container => { + const producerSpan = container.items.find( + span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.amqplib.publisher', + ); + expect(producerSpan?.name).toBe('send callback-queue'); + + const consumerSpan = container.items.find( + span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.amqplib.consumer', + ); + expect(consumerSpan?.name).toBe('process callback-queue'); + expect(consumerSpan?.status).toBe('ok'); + }, + }) + .start() + .completed(); + }); }); }); diff --git a/packages/server-utils/src/integrations/amqplib.ts b/packages/server-utils/src/integrations/amqplib.ts index 9b227d7e0e2c..9768cf27a26c 100644 --- a/packages/server-utils/src/integrations/amqplib.ts +++ b/packages/server-utils/src/integrations/amqplib.ts @@ -68,6 +68,7 @@ const MESSAGE_STORED_SPAN: unique symbol = Symbol('sentry.amqplib.message.stored const CHANNEL_SPANS_NOT_ENDED: unique symbol = Symbol('sentry.amqplib.channel.spans-not-ended'); const CHANNEL_CONSUME_TIMEOUT_TIMER: unique symbol = Symbol('sentry.amqplib.channel.consume-timeout-timer'); const CHANNEL_CONSUMER_INFO: unique symbol = Symbol('sentry.amqplib.channel.consumer-info'); +const CHANNEL_PENDING_CONSUMERS: unique symbol = Symbol('sentry.amqplib.channel.pending-consumers'); const CHANNEL_IS_CONFIRM_PUBLISHING: unique symbol = Symbol('sentry.amqplib.channel.is-confirm-publishing'); const CONNECTION_ATTRIBUTES: unique symbol = Symbol('sentry.amqplib.connection.attributes'); @@ -108,12 +109,19 @@ interface ConsumerInfo { queue: string; } +/** A `consume` call waiting for the broker to assign it a consumer tag. */ +interface PendingConsumer { + callback: unknown; + info: ConsumerInfo; +} + interface ChannelLike { connection?: ConnectionLike; on?: (event: string, listener: (...args: unknown[]) => void) => unknown; [CHANNEL_SPANS_NOT_ENDED]?: { msg: ConsumeMessage; timeOfConsume: number }[]; [CHANNEL_CONSUME_TIMEOUT_TIMER]?: ReturnType; [CHANNEL_CONSUMER_INFO]?: Map; + [CHANNEL_PENDING_CONSUMERS]?: PendingConsumer[]; [CHANNEL_IS_CONFIRM_PUBLISHING]?: boolean; } @@ -135,6 +143,11 @@ interface AmqpDispatchContext extends AmqpChannelContext { _sentryNoAck?: boolean; } +interface AmqpConsumeContext extends AmqpChannelContext { + // The entry this call queued, so the error hook can drop it. + _sentryPendingConsumer?: PendingConsumer; +} + interface AmqpConnectContext { arguments?: unknown[]; result?: unknown; @@ -156,6 +169,7 @@ function instrumentAmqplib(): void { subscribePublish(); subscribeConfirmPublish(); subscribeConsume(); + subscribeRegisterConsumer(); subscribeDispatch(); subscribeSettle(); } @@ -199,32 +213,68 @@ function subscribeConfirmPublish(): void { }); } -/** - * Records `consumerTag -> { noAck, queue }` when a consumer is registered, so the per-message - * dispatch hook can name the span after the queue and know when to end it. - */ +/** Stashes the `{ noAck, queue }` each `consume` asks for until the broker assigns it a tag. */ function subscribeConsume(): void { - const channel = diagnosticsChannel.tracingChannel(CHANNELS.AMQPLIB_CONSUME); + const channel = diagnosticsChannel.tracingChannel(CHANNELS.AMQPLIB_CONSUME); - // A `start` subscriber is required for orchestrion to wrap `consume` at all. - channel.start.subscribe(NOOP); - channel.asyncEnd.subscribe(message => { - const data = message as AmqpChannelContext; + channel.start.subscribe(message => { + const data = message as AmqpConsumeContext; const consumerChannel = data.self; - const result = data.result as { consumerTag?: string } | undefined; - const consumerTag = result?.consumerTag; - if (!consumerChannel || !consumerTag) { + if (!consumerChannel) { return; } ensureChannelState(consumerChannel); const queueArg = data.arguments[0]; const queue = typeof queueArg === 'string' ? queueArg : ''; + const callback = data.arguments[1]; const options = data.arguments[2] as { noAck?: boolean } | undefined; - consumerChannel[CHANNEL_CONSUMER_INFO]?.set(consumerTag, { noAck: !!options?.noAck, queue }); + const entry = { callback, info: { noAck: !!options?.noAck, queue } }; + consumerChannel[CHANNEL_PENDING_CONSUMERS]?.push(entry); + data._sentryPendingConsumer = entry; + }); + + // A synchronous throw, such as for invalid `arguments`, leaves the channel open. RPC failures close it. + // A stale entry would label the next consumer that shares this callback. + channel.error.subscribe(message => { + const data = message as AmqpConsumeContext; + if (data.self && data._sentryPendingConsumer) { + removePendingConsumer(data.self, data._sentryPendingConsumer); + } + }); +} + +/** + * Records `consumerTag -> { noAck, queue }` for the dispatch hook. `consume`'s promise resolves too late, + * because amqplib can decode `BasicConsumeOk` and the first delivery in one socket read. amqplib always + * registers a consumer before it dispatches to it. + */ +function subscribeRegisterConsumer(): void { + const channel = diagnosticsChannel.tracingChannel(CHANNELS.AMQPLIB_REGISTER_CONSUMER); + + channel.start.subscribe(message => { + const data = message as AmqpChannelContext; + const consumerChannel = data.self; + const consumerTag = data.arguments[0]; + const callback = data.arguments[1]; + if (!consumerChannel || typeof consumerTag !== 'string') { + return; + } + + const entry = consumerChannel[CHANNEL_PENDING_CONSUMERS]?.find(pending => pending.callback === callback); + if (!entry) { + return; + } + + removePendingConsumer(consumerChannel, entry); + consumerChannel[CHANNEL_CONSUMER_INFO]?.set(consumerTag, entry.info); }); } +function removePendingConsumer(channel: ChannelLike, entry: PendingConsumer): void { + channel[CHANNEL_PENDING_CONSUMERS] = channel[CHANNEL_PENDING_CONSUMERS]?.filter(pending => pending !== entry); +} + /** * Per delivered message (`BaseChannel.prototype.dispatchMessage`): continues the producer's trace, * opens a CONSUMER span, and runs the user callback under it. Manual-ack consumers keep the span open @@ -356,6 +406,7 @@ function ensureChannelState(channel: ChannelLike): void { channel[CHANNEL_SPANS_NOT_ENDED] = []; channel[CHANNEL_CONSUMER_INFO] = new Map(); + channel[CHANNEL_PENDING_CONSUMERS] = []; const timer = setInterval(() => checkConsumeTimeoutOnChannel(channel), CONSUME_TIMEOUT_MS); timer.unref?.(); diff --git a/packages/server-utils/src/orchestrion/config/amqplib.ts b/packages/server-utils/src/orchestrion/config/amqplib.ts index a8f0e4fe61b4..184844a8d799 100644 --- a/packages/server-utils/src/orchestrion/config/amqplib.ts +++ b/packages/server-utils/src/orchestrion/config/amqplib.ts @@ -2,9 +2,11 @@ import type { InstrumentationConfig } from '../apmTypes'; import { getModuleNames } from './module-names'; -// `amqplib` splits its API across three files: -// - `lib/channel_model.js` holds `class Channel` (publish/consume/ack/nack/reject/…) and -// `class ConfirmChannel extends Channel` (a `publish` that takes a broker-confirm callback). +// `amqplib` splits its API across these files: +// - `lib/channel_model.js` (promise API) and `lib/callback_model.js` (callback API) each hold a +// `class Channel` (publish/consume/ack/nack/reject/…) and a `class ConfirmChannel extends Channel` +// (a `publish` that takes a broker-confirm callback). The method signatures match, so both files +// publish to the same channels. // - `lib/channel.js` holds `class BaseChannel` whose `dispatchMessage` invokes the registered // consumer callback once per delivered message — the natural per-message hook for consumer spans // (`Channel.consume` itself only registers the callback, it isn't called per message). @@ -15,26 +17,59 @@ import { getModuleNames } from './module-names'; const module = { name: 'amqplib', versionRange: '>=0.5.5 <3' } as const; export const amqplibConfig = [ - // Producer span + trace-header injection. `sendToQueue` delegates to `publish`, so it's covered. + ...['lib/channel_model.js', 'lib/callback_model.js'].flatMap((filePath): InstrumentationConfig[] => [ + // Producer span + trace-header injection. `sendToQueue` delegates to `publish`, so it's covered. + { + channelName: 'publish', + module: { ...module, filePath }, + functionQuery: { className: 'Channel', methodName: 'publish', kind: 'Sync' }, + }, + // Confirm-channel producer span; the trailing broker-confirm callback ends the span when the + // broker acks/nacks. It internally calls the base `publish`, so the subscriber skips that nested call. + { + channelName: 'confirmPublish', + module: { ...module, filePath }, + functionQuery: { className: 'ConfirmChannel', methodName: 'publish', kind: 'Callback' }, + }, + // `consume` knows the queue and `noAck`, and `registerConsumer` knows the tag. Together they tell the + // dispatch hook how to name the consumer span and when to end it. `Sync`, because the callback API + // returns the channel and only a synchronous throw leaves the channel open. + { + channelName: 'consume', + module: { ...module, filePath }, + functionQuery: { className: 'Channel', methodName: 'consume', kind: 'Sync' }, + }, + // End the consumer span when the user settles the message. + { + channelName: 'ack', + module: { ...module, filePath }, + functionQuery: { className: 'Channel', methodName: 'ack', kind: 'Sync' }, + }, + { + channelName: 'nack', + module: { ...module, filePath }, + functionQuery: { className: 'Channel', methodName: 'nack', kind: 'Sync' }, + }, + { + channelName: 'reject', + module: { ...module, filePath }, + functionQuery: { className: 'Channel', methodName: 'reject', kind: 'Sync' }, + }, + { + channelName: 'ackAll', + module: { ...module, filePath }, + functionQuery: { className: 'Channel', methodName: 'ackAll', kind: 'Sync' }, + }, + { + channelName: 'nackAll', + module: { ...module, filePath }, + functionQuery: { className: 'Channel', methodName: 'nackAll', kind: 'Sync' }, + }, + ]), { - channelName: 'publish', - module: { ...module, filePath: 'lib/channel_model.js' }, - functionQuery: { className: 'Channel', methodName: 'publish', kind: 'Sync' }, - }, - // Confirm-channel producer span; the trailing broker-confirm callback ends the span when the - // broker acks/nacks. It internally calls `super.publish`, so the subscriber guards against the - // base `publish` channel double-instrumenting. - { - channelName: 'confirmPublish', - module: { ...module, filePath: 'lib/channel_model.js' }, - functionQuery: { className: 'ConfirmChannel', methodName: 'publish', kind: 'Callback' }, - }, - // Records `consumerTag -> { noAck, queue }` so the per-message dispatch hook knows how to name and - // when to end the consumer span. - { - channelName: 'consume', - module: { ...module, filePath: 'lib/channel_model.js' }, - functionQuery: { className: 'Channel', methodName: 'consume', kind: 'Async' }, + channelName: 'registerConsumer', + module: { ...module, filePath: 'lib/channel.js' }, + functionQuery: { className: 'BaseChannel', methodName: 'registerConsumer', kind: 'Sync' }, }, // Per delivered message: creates the consumer span and runs the user callback under it. { @@ -42,32 +77,6 @@ export const amqplibConfig = [ module: { ...module, filePath: 'lib/channel.js' }, functionQuery: { className: 'BaseChannel', methodName: 'dispatchMessage', kind: 'Sync' }, }, - // End the consumer span when the user settles the message. - { - channelName: 'ack', - module: { ...module, filePath: 'lib/channel_model.js' }, - functionQuery: { className: 'Channel', methodName: 'ack', kind: 'Sync' }, - }, - { - channelName: 'nack', - module: { ...module, filePath: 'lib/channel_model.js' }, - functionQuery: { className: 'Channel', methodName: 'nack', kind: 'Sync' }, - }, - { - channelName: 'reject', - module: { ...module, filePath: 'lib/channel_model.js' }, - functionQuery: { className: 'Channel', methodName: 'reject', kind: 'Sync' }, - }, - { - channelName: 'ackAll', - module: { ...module, filePath: 'lib/channel_model.js' }, - functionQuery: { className: 'Channel', methodName: 'ackAll', kind: 'Sync' }, - }, - { - channelName: 'nackAll', - module: { ...module, filePath: 'lib/channel_model.js' }, - functionQuery: { className: 'Channel', methodName: 'nackAll', kind: 'Sync' }, - }, // Stashes connection attributes (url/host/port/protocol/server product) on the connection object // for span-time reads via `channel.connection`. { @@ -83,6 +92,7 @@ export const amqplibChannels = { AMQPLIB_PUBLISH: 'orchestrion:amqplib:publish', AMQPLIB_CONFIRM_PUBLISH: 'orchestrion:amqplib:confirmPublish', AMQPLIB_CONSUME: 'orchestrion:amqplib:consume', + AMQPLIB_REGISTER_CONSUMER: 'orchestrion:amqplib:registerConsumer', AMQPLIB_DISPATCH: 'orchestrion:amqplib:dispatch', AMQPLIB_ACK: 'orchestrion:amqplib:ack', AMQPLIB_NACK: 'orchestrion:amqplib:nack', From 9e98e4bdfe6671d0f4532d8365621aa2f72dfb6c Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 9 Oct 2026 10:57:54 +0200 Subject: [PATCH 08/84] test(node): Port incoming HTTP behavior to span streaming (#25194) Port incoming HTTP behavior test suites to span streaming. Part of #24135 --------- Co-authored-by: GPT-6 --- .../node-suites/excludes.ts | 4 +- .../httpIntegration-streamed/instrument.mjs | 1 - .../instrument-overwrite-server-emit.mjs | 1 - .../suites/tracing/httpIntegration/test.ts | 234 ++++++++++-------- .../instrument.mjs | 1 - .../server.mjs | 0 .../test.ts | 2 +- ...rument-with-datacollection-no-userinfo.mjs | 1 - .../instrument-without-request-data.mjs | 1 - .../instrument.mjs | 1 - .../server.mjs | 0 .../test.ts | 2 +- 12 files changed, 128 insertions(+), 120 deletions(-) rename dev-packages/node-integration-tests/suites/tracing/{httpServerSpans-streamed-unrouted => httpServerSpans-unrouted}/instrument.mjs (90%) rename dev-packages/node-integration-tests/suites/tracing/{httpServerSpans-streamed-unrouted => httpServerSpans-unrouted}/server.mjs (100%) rename dev-packages/node-integration-tests/suites/tracing/{httpServerSpans-streamed-unrouted => httpServerSpans-unrouted}/test.ts (96%) rename dev-packages/node-integration-tests/suites/tracing/{requestData-streamed => requestData}/instrument-with-datacollection-no-userinfo.mjs (91%) rename dev-packages/node-integration-tests/suites/tracing/{requestData-streamed => requestData}/instrument-without-request-data.mjs (92%) rename dev-packages/node-integration-tests/suites/tracing/{requestData-streamed => requestData}/instrument.mjs (90%) rename dev-packages/node-integration-tests/suites/tracing/{requestData-streamed => requestData}/server.mjs (100%) rename dev-packages/node-integration-tests/suites/tracing/{requestData-streamed => requestData}/test.ts (99%) diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts index 5b99ffc866ab..427f5cc0c2cd 100644 --- a/dev-packages/bun-integration-tests/node-suites/excludes.ts +++ b/dev-packages/bun-integration-tests/node-suites/excludes.ts @@ -21,11 +21,11 @@ const NO_HTTP_SERVER_SPANS = [ 'suites/tracing/envelope-header/sampleRate-propagation/test.ts', 'suites/tracing/httpIntegration-streamed/test.ts', 'suites/tracing/httpIntegration/test.ts', - 'suites/tracing/httpServerSpans-streamed-unrouted/test.ts', + 'suites/tracing/httpServerSpans-unrouted/test.ts', 'suites/tracing/ignoreSpans-streamed/**', 'suites/tracing/meta-tags-twp-errors/test.ts', 'suites/tracing/meta-tags/test.ts', - 'suites/tracing/requestData-streamed/test.ts', + 'suites/tracing/requestData/test.ts', 'suites/tracing/sample-rand-propagation/test.ts', 'suites/tracing/sample-rate-propagation/**', 'suites/tracing/sampling-static/test.ts', diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration-streamed/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/httpIntegration-streamed/instrument.mjs index 53b9511a21f0..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration-streamed/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration-streamed/instrument.mjs @@ -6,5 +6,4 @@ Sentry.init({ release: '1.0', tracesSampleRate: 1.0, transport: loggingTransport, - traceLifecycle: 'stream', }); diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/instrument-overwrite-server-emit.mjs b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/instrument-overwrite-server-emit.mjs index 680ed3d3c156..94ad8df95439 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/instrument-overwrite-server-emit.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/instrument-overwrite-server-emit.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, 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 3cbe232bb061..1225b5891e8f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts @@ -1,5 +1,6 @@ +import { HTTP_SERVER } from '@sentry/conventions/op'; import { createTestServer } from '@sentry-internal/test-utils'; -import { URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; +import { HTTP_RESPONSE_STATUS_CODE, SENTRY_OP, URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; import { afterAll, describe, expect, test } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests, createRunner } from '../../../utils/runner'; import { RUNTIME } from '../../../utils'; @@ -205,51 +206,54 @@ describe('httpIntegration', () => { test('handles server.emit being overwritten via classic monkey patching', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /test1', - contexts: { - trace: { + span: { + items: [ + { + name: 'GET', + is_segment: true, span_id: expect.stringMatching(/[a-f\d]{16}/), trace_id: expect.stringMatching(/[a-f\d]{32}/), - data: { - 'http.response.status_code': 200, - 'sentry.op': 'http.server', + attributes: { + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [SENTRY_OP]: { type: 'string', value: HTTP_SERVER }, + [URL_PATH]: { type: 'string', value: '/test1' }, }, }, - }, - spans: [], + ], }, }) .expect({ - transaction: { - transaction: 'GET /test2', - contexts: { - trace: { + span: { + items: [ + { + name: 'GET', + is_segment: true, span_id: expect.stringMatching(/[a-f\d]{16}/), trace_id: expect.stringMatching(/[a-f\d]{32}/), - data: { - 'http.response.status_code': 200, - 'sentry.op': 'http.server', + attributes: { + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [SENTRY_OP]: { type: 'string', value: HTTP_SERVER }, + [URL_PATH]: { type: 'string', value: '/test2' }, }, }, - }, - spans: [], + ], }, }) .expect({ - transaction: { - transaction: 'GET /test3', - contexts: { - trace: { + span: { + items: [ + { + name: 'GET', + is_segment: true, span_id: expect.stringMatching(/[a-f\d]{16}/), trace_id: expect.stringMatching(/[a-f\d]{32}/), - data: { - 'http.response.status_code': 200, - 'sentry.op': 'http.server', + attributes: { + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [SENTRY_OP]: { type: 'string', value: HTTP_SERVER }, + [URL_PATH]: { type: 'string', value: '/test3' }, }, }, - }, - spans: [], + ], }, }) .start(); @@ -263,50 +267,54 @@ describe('httpIntegration', () => { test('handles server.emit being overwritten via proxy', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /test1-proxy', - contexts: { - trace: { + span: { + items: [ + { + name: 'GET', + is_segment: true, span_id: expect.stringMatching(/[a-f\d]{16}/), trace_id: expect.stringMatching(/[a-f\d]{32}/), - data: { - 'http.response.status_code': 200, - 'sentry.op': 'http.server', + attributes: { + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [SENTRY_OP]: { type: 'string', value: HTTP_SERVER }, + [URL_PATH]: { type: 'string', value: '/test1-proxy' }, }, }, - }, - spans: [], + ], }, }) .expect({ - transaction: { - transaction: 'GET /test2-proxy', - contexts: { - trace: { + span: { + items: [ + { + name: 'GET', + is_segment: true, span_id: expect.stringMatching(/[a-f\d]{16}/), trace_id: expect.stringMatching(/[a-f\d]{32}/), - data: { - 'http.response.status_code': 200, - 'sentry.op': 'http.server', + attributes: { + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [SENTRY_OP]: { type: 'string', value: HTTP_SERVER }, + [URL_PATH]: { type: 'string', value: '/test2-proxy' }, }, }, - }, - spans: [], + ], }, }) .expect({ - transaction: { - transaction: 'GET /test3-proxy', - contexts: { - trace: { + span: { + items: expect.arrayContaining([ + expect.objectContaining({ + name: 'GET', + is_segment: true, span_id: expect.stringMatching(/[a-f\d]{16}/), trace_id: expect.stringMatching(/[a-f\d]{32}/), - data: { - 'http.response.status_code': 200, - 'sentry.op': 'http.server', - }, - }, - }, + attributes: expect.objectContaining({ + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [SENTRY_OP]: { type: 'string', value: HTTP_SERVER }, + [URL_PATH]: { type: 'string', value: '/test3-proxy' }, + }), + }), + ]), }, }) .start(); @@ -320,51 +328,54 @@ describe('httpIntegration', () => { test('handles server.emit being overwritten via classic monkey patching, using initial server.emit', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /test1-original', - contexts: { - trace: { + span: { + items: [ + { + name: 'GET', + is_segment: true, span_id: expect.stringMatching(/[a-f\d]{16}/), trace_id: expect.stringMatching(/[a-f\d]{32}/), - data: { - 'http.response.status_code': 200, - 'sentry.op': 'http.server', + attributes: { + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [SENTRY_OP]: { type: 'string', value: HTTP_SERVER }, + [URL_PATH]: { type: 'string', value: '/test1-original' }, }, }, - }, - spans: [], + ], }, }) .expect({ - transaction: { - transaction: 'GET /test2-original', - contexts: { - trace: { + span: { + items: [ + { + name: 'GET', + is_segment: true, span_id: expect.stringMatching(/[a-f\d]{16}/), trace_id: expect.stringMatching(/[a-f\d]{32}/), - data: { - 'http.response.status_code': 200, - 'sentry.op': 'http.server', + attributes: { + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [SENTRY_OP]: { type: 'string', value: HTTP_SERVER }, + [URL_PATH]: { type: 'string', value: '/test2-original' }, }, }, - }, - spans: [], + ], }, }) .expect({ - transaction: { - transaction: 'GET /test3-original', - contexts: { - trace: { + span: { + items: [ + { + name: 'GET', + is_segment: true, span_id: expect.stringMatching(/[a-f\d]{16}/), trace_id: expect.stringMatching(/[a-f\d]{32}/), - data: { - 'http.response.status_code': 200, - 'sentry.op': 'http.server', + attributes: { + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [SENTRY_OP]: { type: 'string', value: HTTP_SERVER }, + [URL_PATH]: { type: 'string', value: '/test3-original' }, }, }, - }, - spans: [], + ], }, }) .start(); @@ -378,51 +389,54 @@ describe('httpIntegration', () => { test('handles server.emit being overwritten via proxy, using initial server.emit', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /test1-proxy-original', - contexts: { - trace: { + span: { + items: [ + { + name: 'GET', + is_segment: true, span_id: expect.stringMatching(/[a-f\d]{16}/), trace_id: expect.stringMatching(/[a-f\d]{32}/), - data: { - 'http.response.status_code': 200, - 'sentry.op': 'http.server', + attributes: { + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [SENTRY_OP]: { type: 'string', value: HTTP_SERVER }, + [URL_PATH]: { type: 'string', value: '/test1-proxy-original' }, }, }, - }, - spans: [], + ], }, }) .expect({ - transaction: { - transaction: 'GET /test2-proxy-original', - contexts: { - trace: { + span: { + items: [ + { + name: 'GET', + is_segment: true, span_id: expect.stringMatching(/[a-f\d]{16}/), trace_id: expect.stringMatching(/[a-f\d]{32}/), - data: { - 'http.response.status_code': 200, - 'sentry.op': 'http.server', + attributes: { + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [SENTRY_OP]: { type: 'string', value: HTTP_SERVER }, + [URL_PATH]: { type: 'string', value: '/test2-proxy-original' }, }, }, - }, - spans: [], + ], }, }) .expect({ - transaction: { - transaction: 'GET /test3-proxy-original', - contexts: { - trace: { + span: { + items: [ + { + name: 'GET', + is_segment: true, span_id: expect.stringMatching(/[a-f\d]{16}/), trace_id: expect.stringMatching(/[a-f\d]{32}/), - data: { - 'http.response.status_code': 200, - 'sentry.op': 'http.server', + attributes: { + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [SENTRY_OP]: { type: 'string', value: HTTP_SERVER }, + [URL_PATH]: { type: 'string', value: '/test3-proxy-original' }, }, }, - }, - spans: [], + ], }, }) .start(); diff --git a/dev-packages/node-integration-tests/suites/tracing/httpServerSpans-streamed-unrouted/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/httpServerSpans-unrouted/instrument.mjs similarity index 90% rename from dev-packages/node-integration-tests/suites/tracing/httpServerSpans-streamed-unrouted/instrument.mjs rename to dev-packages/node-integration-tests/suites/tracing/httpServerSpans-unrouted/instrument.mjs index 53b9511a21f0..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpServerSpans-streamed-unrouted/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/httpServerSpans-unrouted/instrument.mjs @@ -6,5 +6,4 @@ Sentry.init({ release: '1.0', tracesSampleRate: 1.0, transport: loggingTransport, - traceLifecycle: 'stream', }); diff --git a/dev-packages/node-integration-tests/suites/tracing/httpServerSpans-streamed-unrouted/server.mjs b/dev-packages/node-integration-tests/suites/tracing/httpServerSpans-unrouted/server.mjs similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing/httpServerSpans-streamed-unrouted/server.mjs rename to dev-packages/node-integration-tests/suites/tracing/httpServerSpans-unrouted/server.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/httpServerSpans-streamed-unrouted/test.ts b/dev-packages/node-integration-tests/suites/tracing/httpServerSpans-unrouted/test.ts similarity index 96% rename from dev-packages/node-integration-tests/suites/tracing/httpServerSpans-streamed-unrouted/test.ts rename to dev-packages/node-integration-tests/suites/tracing/httpServerSpans-unrouted/test.ts index d488bf9bcd5a..b2bbda3797d4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpServerSpans-streamed-unrouted/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/httpServerSpans-unrouted/test.ts @@ -1,7 +1,7 @@ import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; -describe('httpServerSpans-streamed (no route)', () => { +describe('httpServerSpans (no route)', () => { afterAll(() => { cleanupChildProcesses(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/requestData-streamed/instrument-with-datacollection-no-userinfo.mjs b/dev-packages/node-integration-tests/suites/tracing/requestData/instrument-with-datacollection-no-userinfo.mjs similarity index 91% rename from dev-packages/node-integration-tests/suites/tracing/requestData-streamed/instrument-with-datacollection-no-userinfo.mjs rename to dev-packages/node-integration-tests/suites/tracing/requestData/instrument-with-datacollection-no-userinfo.mjs index 0ae53602ff0c..d12edc0ab2fa 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requestData-streamed/instrument-with-datacollection-no-userinfo.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/requestData/instrument-with-datacollection-no-userinfo.mjs @@ -6,7 +6,6 @@ Sentry.init({ release: '1.0', tracesSampleRate: 1.0, transport: loggingTransport, - traceLifecycle: 'stream', dataCollection: { userInfo: false, }, diff --git a/dev-packages/node-integration-tests/suites/tracing/requestData-streamed/instrument-without-request-data.mjs b/dev-packages/node-integration-tests/suites/tracing/requestData/instrument-without-request-data.mjs similarity index 92% rename from dev-packages/node-integration-tests/suites/tracing/requestData-streamed/instrument-without-request-data.mjs rename to dev-packages/node-integration-tests/suites/tracing/requestData/instrument-without-request-data.mjs index c1f011300062..a68ae15e47b1 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requestData-streamed/instrument-without-request-data.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/requestData/instrument-without-request-data.mjs @@ -6,6 +6,5 @@ Sentry.init({ release: '1.0', tracesSampleRate: 1.0, transport: loggingTransport, - traceLifecycle: 'stream', integrations: defaults => defaults.filter(i => i.name !== 'RequestData'), }); diff --git a/dev-packages/node-integration-tests/suites/tracing/requestData-streamed/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/requestData/instrument.mjs similarity index 90% rename from dev-packages/node-integration-tests/suites/tracing/requestData-streamed/instrument.mjs rename to dev-packages/node-integration-tests/suites/tracing/requestData/instrument.mjs index 53b9511a21f0..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requestData-streamed/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/requestData/instrument.mjs @@ -6,5 +6,4 @@ Sentry.init({ release: '1.0', tracesSampleRate: 1.0, transport: loggingTransport, - traceLifecycle: 'stream', }); diff --git a/dev-packages/node-integration-tests/suites/tracing/requestData-streamed/server.mjs b/dev-packages/node-integration-tests/suites/tracing/requestData/server.mjs similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing/requestData-streamed/server.mjs rename to dev-packages/node-integration-tests/suites/tracing/requestData/server.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/requestData-streamed/test.ts b/dev-packages/node-integration-tests/suites/tracing/requestData/test.ts similarity index 99% rename from dev-packages/node-integration-tests/suites/tracing/requestData-streamed/test.ts rename to dev-packages/node-integration-tests/suites/tracing/requestData/test.ts index 5a356e3d91e9..c6028677a738 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requestData-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requestData/test.ts @@ -1,7 +1,7 @@ import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; -describe('requestData-streamed', () => { +describe('requestData', () => { afterAll(() => { cleanupChildProcesses(); }); From 31717a4faffed05c0caea9781da3108e92d5ed32 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 9 Oct 2026 11:23:13 +0200 Subject: [PATCH 09/84] ref(server-utils): Share Orchestrion channel context (#25208) Consolidate Orchestrion payload types into a shared interface used by server-utils, NestJS, and Remix. Integrations extend it only for narrower payload types or local bookkeeping, keeping the common fields consistent without changing instrumentation behavior. Fixes #25173 Co-authored-by: GPT-6 --- .../integrations/orchestrion-subscriber.ts | 35 +++++++-------- .../orchestrion-subscriber.test.ts | 39 ++++++---------- .../server/integrations/tracing-channel.ts | 38 +++++++--------- packages/remix/src/v3/assetServer.ts | 5 +-- packages/remix/src/v3/server/instrument.ts | 44 +++++++++---------- packages/server-utils/src/index.ts | 1 + .../server-utils/src/integrations/amqplib.ts | 20 ++------- .../src/integrations/anthropic.ts | 12 ++--- .../src/integrations/aws-sdk/index.ts | 6 +-- .../src/integrations/dataloader.ts | 10 +---- .../src/integrations/express/types.ts | 8 ++-- .../src/integrations/firebase/functions.ts | 11 +---- .../integrations/firebase/instrumentation.ts | 12 ++--- .../src/integrations/generic-pool.ts | 7 +-- .../src/integrations/google-genai.ts | 15 ++----- .../src/integrations/graphql/index.ts | 14 ++---- .../src/integrations/hapi/index.ts | 6 +-- .../src/integrations/hono/honoIntegration.ts | 11 +++-- .../src/integrations/kafkajs/index.ts | 6 +-- .../server-utils/src/integrations/knex.ts | 16 ++----- .../src/integrations/koa/index.ts | 11 ++--- .../src/integrations/langchain.ts | 23 +++------- .../src/integrations/langgraph.ts | 21 +++------ .../src/integrations/lru-memoizer.ts | 7 +-- .../src/integrations/mastra-classifier.ts | 16 ++----- .../server-utils/src/integrations/mastra.ts | 21 +++------ .../src/integrations/mcp-server.ts | 10 ++--- .../server-utils/src/integrations/mistral.ts | 14 ++---- .../src/integrations/mongodb/index.ts | 4 +- .../src/integrations/mongoose/index.ts | 7 +-- .../server-utils/src/integrations/mysql.ts | 8 +--- .../src/integrations/mysql2/index.ts | 9 +--- .../src/integrations/openai-compatible.ts | 16 ++----- .../server-utils/src/integrations/openai.ts | 17 ++----- .../src/integrations/pi-durable.ts | 20 +++------ .../src/integrations/postgres-js.ts | 11 +++-- .../server-utils/src/integrations/postgres.ts | 8 +--- .../src/integrations/prisma/orchestrion.ts | 7 +-- .../src/integrations/redis/index.ts | 33 +++++++------- .../redis/ioredis-channel-subscriber.ts | 10 ++--- .../server-utils/src/integrations/tedious.ts | 7 ++- .../server-utils/src/integrations/typesafe.ts | 15 +------ .../vercel-ai-orchestrion-subscriber.ts | 18 +++----- .../server-utils/src/orchestrion/types.ts | 7 +++ .../integrations/mastra/error-capture.test.ts | 3 +- .../integrations/postgres/postgres.test.ts | 23 +++++----- 46 files changed, 228 insertions(+), 434 deletions(-) create mode 100644 packages/server-utils/src/orchestrion/types.ts diff --git a/packages/nestjs/src/integrations/orchestrion-subscriber.ts b/packages/nestjs/src/integrations/orchestrion-subscriber.ts index e2ac9e7524f2..53e21dda735f 100644 --- a/packages/nestjs/src/integrations/orchestrion-subscriber.ts +++ b/packages/nestjs/src/integrations/orchestrion-subscriber.ts @@ -1,5 +1,6 @@ import * as diagnosticsChannel from 'node:diagnostics_channel'; import { debug, startInactiveSpan, waitForTracingChannelBinding } from '@sentry/core'; +import type { OrchestrionChannelContext } from '@sentry/server-utils'; import { bindTracingChannelToSpan } from '@sentry/server-utils'; import { nestjsChannels as CHANNELS } from '@sentry/server-utils/orchestrion/config'; import { DEBUG_BUILD } from '../debug-build'; @@ -21,18 +22,6 @@ import { getAppCreationSpanOptions, wrapRequestContextHandler, wrapRouteHandler const NOOP = (): void => {}; -/** - * The orchestrion tracing-channel context. `arguments` is the live call args - * array; `result` is the return value, which an `end` handler may reassign to - * substitute it (`traceSync`/`tracePromise` always return `ctx.result`). - */ -interface ChannelContext { - arguments: unknown[]; - moduleVersion?: string; - result?: unknown; - error?: unknown; -} - /** * Subscribe to a decorator channel (`Injectable`/`Catch`). * @@ -42,7 +31,7 @@ interface ChannelContext { * spans later. */ function subscribeDecoratorChannel(channelName: string, patch: (target: T) => void): void { - diagnosticsChannel.tracingChannel(channelName).subscribe({ + diagnosticsChannel.tracingChannel(channelName).subscribe({ start(data) { const target = data.arguments?.[0] as T | undefined; if (target) { @@ -90,8 +79,11 @@ function makeProcessorDecorator(original: AnyFn, queueName: string | undefined): * whatever `end` leaves there. `wrap` receives the original decorator and the * channel context (for the factory's args, e.g. the BullMQ queue name). */ -function subscribeFactoryDecorator(channelName: string, wrap: (decorator: AnyFn, data: ChannelContext) => AnyFn): void { - diagnosticsChannel.tracingChannel(channelName).subscribe({ +function subscribeFactoryDecorator( + channelName: string, + wrap: (decorator: AnyFn, data: OrchestrionChannelContext) => AnyFn, +): void { + diagnosticsChannel.tracingChannel(channelName).subscribe({ start: NOOP, end(data) { const decorator = data.result; @@ -134,10 +126,13 @@ export function subscribeToNestChannels(): void { // calls below stay synchronous because the decorator channels fire at // module-load time, which a deferred subscription could miss. waitForTracingChannelBinding(() => { - bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(CHANNELS.NESTJS_APP_CREATION), data => { - const moduleCls = data.arguments?.[0] as { name?: string } | undefined; - return startInactiveSpan(getAppCreationSpanOptions(data.moduleVersion, moduleCls?.name)); - }); + bindTracingChannelToSpan( + diagnosticsChannel.tracingChannel(CHANNELS.NESTJS_APP_CREATION), + data => { + const moduleCls = data.arguments?.[0] as { name?: string } | undefined; + return startInactiveSpan(getAppCreationSpanOptions(data.moduleVersion, moduleCls?.name)); + }, + ); }); // request_context + request_handler. `RouterExecutionContext.create` @@ -146,7 +141,7 @@ export function subscribeToNestChannels(): void { // (-> handler span per call) and `end` reassigns `data.result` to // replace the returned handler (-> request_context span per request). const routerMeta = new WeakMap(); - diagnosticsChannel.tracingChannel(CHANNELS.NESTJS_ROUTER_CONTEXT).subscribe({ + diagnosticsChannel.tracingChannel(CHANNELS.NESTJS_ROUTER_CONTEXT).subscribe({ start(data) { const instance = data.arguments?.[0] as { constructor?: { name?: string } } | undefined; const callback = data.arguments?.[1]; diff --git a/packages/nestjs/test/integrations/orchestrion-subscriber.test.ts b/packages/nestjs/test/integrations/orchestrion-subscriber.test.ts index 04d6d5598c63..37409a4312d1 100644 --- a/packages/nestjs/test/integrations/orchestrion-subscriber.test.ts +++ b/packages/nestjs/test/integrations/orchestrion-subscriber.test.ts @@ -15,6 +15,7 @@ import { spanToJSON, } from '@sentry/core'; import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { OrchestrionChannelContext } from '@sentry/server-utils'; import { nestjsChannels as CHANNELS } from '@sentry/server-utils/orchestrion/config'; import { subscribeToNestChannels } from '../../src/integrations/orchestrion-subscriber'; @@ -94,13 +95,6 @@ function installTestAsyncContextStrategy(): void { }); } -interface NestFactoryCreateData { - arguments: unknown[]; - moduleVersion?: string; - result?: unknown; - error?: unknown; -} - describe('NestJS orchestrion subscriber: app_creation', () => { afterEach(() => { setAsyncContextStrategy(undefined); @@ -112,12 +106,12 @@ describe('NestJS orchestrion subscriber: app_creation', () => { // `data._sentrySpan` function captureSpan(): { getSpan: () => Span | undefined } { let span: Span | undefined; - const grab = (data: NestFactoryCreateData): void => { + const grab = (data: OrchestrionChannelContext): void => { span ??= (data as { _sentrySpan?: Span })._sentrySpan; }; // The raw node `tracingChannel` type wants all five handlers; only // `end`/`asyncEnd` carry the bound span by the time it settles. - tracingChannel(CHANNELS.NESTJS_APP_CREATION).subscribe({ + tracingChannel(CHANNELS.NESTJS_APP_CREATION).subscribe({ start: () => undefined, asyncStart: () => undefined, asyncEnd: grab, @@ -133,7 +127,7 @@ describe('NestJS orchestrion subscriber: app_creation', () => { subscribeToNestChannels(); const { getSpan } = captureSpan(); - const channel = tracingChannel(CHANNELS.NESTJS_APP_CREATION); + const channel = tracingChannel(CHANNELS.NESTJS_APP_CREATION); class AppModule {} await channel.tracePromise(async () => ({ app: true }), { arguments: [AppModule], moduleVersion: '10.4.1' }); @@ -160,7 +154,7 @@ describe('NestJS orchestrion subscriber: app_creation', () => { subscribeToNestChannels(); const { getSpan } = captureSpan(); - const channel = tracingChannel(CHANNELS.NESTJS_APP_CREATION); + const channel = tracingChannel(CHANNELS.NESTJS_APP_CREATION); await channel.tracePromise(async () => ({ app: true }), { arguments: [] }); @@ -173,13 +167,6 @@ describe('NestJS orchestrion subscriber: app_creation', () => { type AnyFn = (this: unknown, ...args: unknown[]) => unknown; -interface RouterCreateData { - arguments: unknown[]; - moduleVersion?: string; - result?: unknown; - error?: unknown; -} - describe('NestJS orchestrion subscriber: request_context / request_handler', () => { afterEach(() => { setAsyncContextStrategy(undefined); @@ -195,10 +182,10 @@ describe('NestJS orchestrion subscriber: request_context / request_handler', () instance: object, callback: AnyFn, moduleVersion: string | undefined, - makeHandler: (data: RouterCreateData) => AnyFn, + makeHandler: (data: OrchestrionChannelContext) => AnyFn, ): { effectiveHandler: AnyFn; wrappedCallback: AnyFn } { - const channel = tracingChannel(CHANNELS.NESTJS_ROUTER_CONTEXT); - const data: RouterCreateData = { arguments: [instance, callback], moduleVersion }; + const channel = tracingChannel(CHANNELS.NESTJS_ROUTER_CONTEXT); + const data: OrchestrionChannelContext = { arguments: [instance, callback], moduleVersion }; channel.traceSync(() => makeHandler(data), data); return { effectiveHandler: data.result as AnyFn, wrappedCallback: data.arguments[1] as AnyFn }; } @@ -340,7 +327,7 @@ describe('NestJS orchestrion subscriber: @Injectable (middleware/guard/pipe/inte // Fire the @Injectable channel against `target` (as if its decorator arrow // ran), so the subscriber's `start` patches `target.prototype`. function applyInjectable(target: object): void { - tracingChannel<{ arguments: unknown[] }>(CHANNELS.NESTJS_INJECTABLE).traceSync(() => undefined, { + tracingChannel(CHANNELS.NESTJS_INJECTABLE).traceSync(() => undefined, { arguments: [target], }); } @@ -643,7 +630,7 @@ describe('NestJS orchestrion subscriber: @Catch (exception filter)', () => { }); function applyCatch(target: object): void { - tracingChannel<{ arguments: unknown[] }>(CHANNELS.NESTJS_CATCH).traceSync(() => undefined, { + tracingChannel(CHANNELS.NESTJS_CATCH).traceSync(() => undefined, { arguments: [target], }); } @@ -697,7 +684,7 @@ describe('NestJS orchestrion subscriber: @Catch (exception filter)', () => { // @Catch first. Because the two passes use separate patched-flags, both // must wrap their own methods regardless of which channel fires first. function fireInjectable(target: object): void { - tracingChannel<{ arguments: unknown[] }>(CHANNELS.NESTJS_INJECTABLE).traceSync(() => undefined, { + tracingChannel(CHANNELS.NESTJS_INJECTABLE).traceSync(() => undefined, { arguments: [target], }); } @@ -801,8 +788,8 @@ describe('NestJS orchestrion subscriber: schedule / event / bullmq', () => { // to the factory's return (our `originalDecorator`), then the subscriber's // `end` reassigns `data.result`. Returns effective (wrapped) decorator. function driveFactory(channelName: string, factoryArgs: unknown[], originalDecorator: AnyFn): AnyFn { - const data: { arguments: unknown[]; result?: unknown } = { arguments: factoryArgs }; - tracingChannel<{ arguments: unknown[]; result?: unknown }>(channelName).traceSync(() => originalDecorator, data); + const data: OrchestrionChannelContext = { arguments: factoryArgs }; + tracingChannel(channelName).traceSync(() => originalDecorator, data); return data.result as AnyFn; } diff --git a/packages/remix/src/server/integrations/tracing-channel.ts b/packages/remix/src/server/integrations/tracing-channel.ts index 6ab1645ca503..8e93813a19dd 100644 --- a/packages/remix/src/server/integrations/tracing-channel.ts +++ b/packages/remix/src/server/integrations/tracing-channel.ts @@ -14,6 +14,7 @@ import { waitForTracingChannelBinding, filterCollectedUrl, } from '@sentry/core'; +import type { OrchestrionChannelContext } from '@sentry/server-utils'; import { bindTracingChannelToSpan } from '@sentry/server-utils'; import { SENTRY_SEGMENT_NAME_SOURCE, @@ -41,17 +42,6 @@ const NOOP = (): void => {}; const MATCH_ROUTE_ID = 'match.route.id'; const MATCH_PARAMS = 'match.params'; -/** - * The shape orchestrion's transform attaches to a tracing-channel `context` object. Documented here - * rather than imported because orchestrion's runtime doesn't export it. - */ -interface ChannelContext { - // The live `arguments` of the wrapped call. - arguments: unknown[]; - result?: unknown; - error?: unknown; -} - // `callRouteLoader`/`callRouteAction` receive a single options object as `arguments[0]`. interface RouteCallParams { request?: Request; @@ -61,7 +51,7 @@ interface RouteCallParams { // The in-flight form-data read, started at span start (before the action consumes the body) so it // overlaps the action's execution rather than starting after it settles. -interface ActionChannelContext extends ChannelContext { +interface ActionChannelContext extends OrchestrionChannelContext { _sentryFormData?: Promise; } @@ -140,7 +130,7 @@ function enrichActiveSpanWithRoute(result: unknown): void { } function subscribeRequestHandler(): void { - bindTracingChannelToSpan( + bindTracingChannelToSpan( diagnosticsChannel.tracingChannel(remixChannels.REMIX_REQUEST_HANDLER), data => { const requestAttributes = getRequestAttributes(data.arguments[0]); @@ -178,19 +168,21 @@ function subscribeRequestHandler(): void { function subscribeMatchServerRoutes(): void { // `matchServerRoutes` is synchronous, so only the `end` event carries a result; the rest are // no-ops. `subscribe` types demand a handler for each channel. - diagnosticsChannel.tracingChannel(remixChannels.REMIX_MATCH_SERVER_ROUTES).subscribe({ - start: NOOP, - end(data) { - enrichActiveSpanWithRoute(data.result); - }, - asyncStart: NOOP, - asyncEnd: NOOP, - error: NOOP, - }); + diagnosticsChannel + .tracingChannel(remixChannels.REMIX_MATCH_SERVER_ROUTES) + .subscribe({ + start: NOOP, + end(data) { + enrichActiveSpanWithRoute(data.result); + }, + asyncStart: NOOP, + asyncEnd: NOOP, + error: NOOP, + }); } function subscribeCallRouteLoader(): void { - bindTracingChannelToSpan( + bindTracingChannelToSpan( diagnosticsChannel.tracingChannel(remixChannels.REMIX_CALL_ROUTE_LOADER), data => { const params = (data.arguments[0] ?? {}) as RouteCallParams; diff --git a/packages/remix/src/v3/assetServer.ts b/packages/remix/src/v3/assetServer.ts index 88ce5060662a..2c1c1bd649f2 100644 --- a/packages/remix/src/v3/assetServer.ts +++ b/packages/remix/src/v3/assetServer.ts @@ -1,5 +1,6 @@ import * as diagnosticsChannel from 'node:diagnostics_channel'; import { consoleSandbox } from '@sentry/core'; +import type { OrchestrionChannelContext } from '@sentry/server-utils'; import { remixV3Channels } from '@sentry/server-utils/orchestrion/config'; import { addDebugIdToSourceMap, findDebugId, getDebugId, injectDebugIdSnippet } from './debugId'; import { getShimUrl, isOrchestrionLoader, orchestrionLoader, resolveShimPath } from './orchestrionLoader'; @@ -39,9 +40,7 @@ interface AssetServer { fetch: (request: Request) => Promise; } -interface CreateAssetServerContext { - arguments: unknown[]; - result?: unknown; +interface CreateAssetServerContext extends OrchestrionChannelContext { _sentryHideSourceMaps?: boolean; } diff --git a/packages/remix/src/v3/server/instrument.ts b/packages/remix/src/v3/server/instrument.ts index 2fee25db560b..f348c9ae01b0 100644 --- a/packages/remix/src/v3/server/instrument.ts +++ b/packages/remix/src/v3/server/instrument.ts @@ -1,6 +1,7 @@ import * as diagnosticsChannel from 'node:diagnostics_channel'; import { createMultiMatcher } from 'remix/route-pattern/match'; import { consoleSandbox } from '@sentry/core'; +import type { OrchestrionChannelContext } from '@sentry/server-utils'; import { remixV3Channels } from '@sentry/server-utils/orchestrion/config'; import type { MatcherLike, RequestListenerOptionsLike, RouterOptionsLike } from '../types'; @@ -9,11 +10,6 @@ import { sentryRemixMiddleware } from './middleware'; const NOOP = (): void => {}; -/** The call's arguments, as orchestrion's transform attaches them to a tracing channel context. */ -interface ChannelContext { - arguments: unknown[]; -} - // Marks an options object already injected into, so the same mutation cannot be applied twice. const INJECTED = Symbol.for('SentryRemixV3Injected'); @@ -41,22 +37,24 @@ export function instrumentRemixV3(): void { } function subscribeToCreateRouter(): void { - diagnosticsChannel.tracingChannel(remixV3Channels.REMIX_V3_CREATE_ROUTER).subscribe({ - start(data) { - // Node rethrows anything this handler throws as an uncaught exception, which would kill an app - // that runs fine without Sentry. Frozen options, a non-writable property and a `route-pattern` - // copy that cannot build a matcher all reach here, so the router is left uninstrumented instead. - try { - injectRouterMiddleware(ensureOptions(data.arguments)); - } catch { - // Ignored on purpose. - } - }, - end: NOOP, - asyncStart: NOOP, - asyncEnd: NOOP, - error: NOOP, - }); + diagnosticsChannel + .tracingChannel(remixV3Channels.REMIX_V3_CREATE_ROUTER) + .subscribe({ + start(data) { + // Node rethrows anything this handler throws as an uncaught exception, which would kill an app + // that runs fine without Sentry. Frozen options, a non-writable property and a `route-pattern` + // copy that cannot build a matcher all reach here, so the router is left uninstrumented instead. + try { + injectRouterMiddleware(ensureOptions(data.arguments)); + } catch { + // Ignored on purpose. + } + }, + end: NOOP, + asyncStart: NOOP, + asyncEnd: NOOP, + error: NOOP, + }); } /** @@ -66,7 +64,9 @@ function subscribeToCreateRouter(): void { */ function subscribeToCreateRequestListener(): void { diagnosticsChannel - .tracingChannel(remixV3Channels.REMIX_V3_CREATE_REQUEST_LISTENER) + .tracingChannel( + remixV3Channels.REMIX_V3_CREATE_REQUEST_LISTENER, + ) .subscribe({ start(data) { try { diff --git a/packages/server-utils/src/index.ts b/packages/server-utils/src/index.ts index 446812f8eed5..22b7470fa7b7 100644 --- a/packages/server-utils/src/index.ts +++ b/packages/server-utils/src/index.ts @@ -8,6 +8,7 @@ import { attachHapiErrorHandler as _attachHapiErrorHandler } from './integration export { bindTracingChannelToSpan } from './tracing-channel'; export type { TracingChannelPayloadWithSpan } from './tracing-channel'; export type { InstrumentationConfig } from './orchestrion/apmTypes'; +export type { OrchestrionChannelContext } from './orchestrion/types'; // Runtime target of the bundler-injected module snippet. The snippet imports it // from this entry alongside the module's subscriber factory (see // `orchestrion/bundler/moduleInjectedTransform.ts`); it is a plain runtime diff --git a/packages/server-utils/src/integrations/amqplib.ts b/packages/server-utils/src/integrations/amqplib.ts index 9768cf27a26c..ec1d29db3b66 100644 --- a/packages/server-utils/src/integrations/amqplib.ts +++ b/packages/server-utils/src/integrations/amqplib.ts @@ -28,6 +28,7 @@ import { SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { QUEUE_PROCESS, QUEUE_PUBLISH } from '@sentry/conventions/op'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { amqplibModuleNames } from '../orchestrion/config/amqplib'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; import { CHANNELS } from '../orchestrion/channels'; @@ -125,16 +126,8 @@ interface ChannelLike { [CHANNEL_IS_CONFIRM_PUBLISHING]?: boolean; } -/** - * The shape orchestrion's transform attaches to the tracing-channel `context`. Documented here rather - * than imported because orchestrion's runtime doesn't export it. - */ -interface AmqpChannelContext { - // The live args array passed to the wrapped call. - arguments: unknown[]; +interface AmqpChannelContext extends OrchestrionChannelContext { self?: ChannelLike; - result?: unknown; - error?: unknown; } interface AmqpDispatchContext extends AmqpChannelContext { @@ -148,11 +141,6 @@ interface AmqpConsumeContext extends AmqpChannelContext { _sentryPendingConsumer?: PendingConsumer; } -interface AmqpConnectContext { - arguments?: unknown[]; - result?: unknown; -} - const NOOP = (): void => {}; const _amqplibIntegration = (() => { @@ -352,11 +340,11 @@ function subscribeSettle(): void { /** Captures connection attributes on the connection object for span-time reads via `channel.connection`. */ function subscribeConnect(): void { - const channel = diagnosticsChannel.tracingChannel(CHANNELS.AMQPLIB_CONNECT); + const channel = diagnosticsChannel.tracingChannel(CHANNELS.AMQPLIB_CONNECT); // A `start` subscriber is required for orchestrion to wrap the callback-style `connect` at all. channel.start.subscribe(NOOP); channel.asyncEnd.subscribe(message => { - const data = message as AmqpConnectContext; + const data = message as OrchestrionChannelContext; const conn = data.result as ConnectionLike | undefined; if (!conn || typeof conn !== 'object') { return; diff --git a/packages/server-utils/src/integrations/anthropic.ts b/packages/server-utils/src/integrations/anthropic.ts index d14d3cbb2f6a..a45bbf02496f 100644 --- a/packages/server-utils/src/integrations/anthropic.ts +++ b/packages/server-utils/src/integrations/anthropic.ts @@ -14,6 +14,7 @@ import { wrapApiPromiseResponse } from '../ai/core/apiPromise'; import { addPrivateRequestAttributes, addResponseAttributes, extractRequestAttributes } from '../ai/anthropic-ai'; import { instrumentAsyncIterableStream, instrumentMessageStream } from '../ai/anthropic-ai/streaming'; import type { AnthropicAiOptions, AnthropicAiResponse } from '../ai/anthropic-ai/types'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { bindTracingChannelToSpan } from '../tracing-channel'; import { anthropicAiModuleNames } from '../orchestrion/config/anthropic-ai'; @@ -37,11 +38,6 @@ const INSTRUMENTED_CHANNELS = [ type StreamMode = (typeof INSTRUMENTED_CHANNELS)[number]['stream']; -interface AnthropicChannelContext { - arguments: unknown[]; - result?: unknown; -} - const _anthropicAIIntegration = ((options: AnthropicAiOptions = {}) => { return { name: INTEGRATION_NAME, @@ -54,7 +50,7 @@ const _anthropicAIIntegration = ((options: AnthropicAiOptions = {}) => { function instrumentAnthropic(options: AnthropicAiOptions): void { for (const { channel, operation, stream } of INSTRUMENTED_CHANNELS) { bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(channel), + diagnosticsChannel.tracingChannel(channel), data => createGenAiSpan(data, operation, options, stream), { beforeSpanEnd: (span, data) => { @@ -85,7 +81,7 @@ function instrumentAnthropic(options: AnthropicAiOptions): void { * Returning `undefined` opts the payload out so no span is opened. */ function createGenAiSpan( - data: AnthropicChannelContext, + data: OrchestrionChannelContext, operation: string, options: AnthropicAiOptions, stream: StreamMode, @@ -191,7 +187,7 @@ function isMessageStream(value: unknown): value is MessageStreamEmitter { */ function wrapStreamResult( span: Span, - data: AnthropicChannelContext, + data: OrchestrionChannelContext, stream: StreamMode, options: AnthropicAiOptions, ): boolean { diff --git a/packages/server-utils/src/integrations/aws-sdk/index.ts b/packages/server-utils/src/integrations/aws-sdk/index.ts index 25a75931deb7..95b38b41eece 100644 --- a/packages/server-utils/src/integrations/aws-sdk/index.ts +++ b/packages/server-utils/src/integrations/aws-sdk/index.ts @@ -10,6 +10,7 @@ import { SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { RPC } from '@sentry/conventions/op'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { awsSdkModuleNames } from '../../orchestrion/config/aws-sdk'; import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation'; @@ -26,11 +27,8 @@ const INTEGRATION_NAME = 'Aws' as const; // The context orchestrion's transform attaches to the channel: `arguments` is the live args of the // wrapped `Client.prototype.send` call (`[command, ...]`), `self` the client, `result`/`error` the // settled value. The `_sentry*` fields are stashed by us across the call's lifecycle. -interface AwsSendChannelContext { - arguments: unknown[]; +interface AwsSendChannelContext extends OrchestrionChannelContext { self?: { config?: AwsClientConfig; constructor?: { name?: string } }; - result?: unknown; - error?: unknown; _sentryNormalizedRequest?: NormalizedRequest; _sentryRequestMetadata?: RequestMetadata; _sentryRegion?: { settled: boolean; promise: Promise }; diff --git a/packages/server-utils/src/integrations/dataloader.ts b/packages/server-utils/src/integrations/dataloader.ts index 89decb7338e9..5fb50a577325 100644 --- a/packages/server-utils/src/integrations/dataloader.ts +++ b/packages/server-utils/src/integrations/dataloader.ts @@ -21,6 +21,7 @@ import { waitForTracingChannelBinding, } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import type { ChannelName } from '../orchestrion/channels'; import { CHANNELS } from '../orchestrion/channels'; import type { TracingChannelPayloadWithSpan } from '../tracing-channel'; @@ -63,15 +64,8 @@ interface DataLoaderInstance { _batch?: DataLoaderBatch | null; } -/** - * The shape orchestrion's transform attaches to the tracing-channel `context`. Documented here rather - * than imported because orchestrion's runtime doesn't export it. - */ -interface DataLoaderChannelContext { - arguments: unknown[]; +interface DataLoaderChannelContext extends OrchestrionChannelContext { self?: DataLoaderInstance; - result?: unknown; - error?: unknown; } // Marks a wrapped `batchLoadFn` so a re-used loader (or a double construct) isn't wrapped twice. diff --git a/packages/server-utils/src/integrations/express/types.ts b/packages/server-utils/src/integrations/express/types.ts index 5536f524e849..ea1d979a18af 100644 --- a/packages/server-utils/src/integrations/express/types.ts +++ b/packages/server-utils/src/integrations/express/types.ts @@ -1,3 +1,4 @@ +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import type { Span } from '@sentry/core'; export type ExpressLayerType = 'router' | 'middleware' | 'request_handler'; @@ -40,13 +41,11 @@ export interface ExpressResponse { * for this layer by `bindTracingChannelToSpan`, and `error` is present on the * channel's `error` event. */ -export interface HandleChannelContext { +export interface HandleChannelContext extends OrchestrionChannelContext { self?: ExpressLayer; - arguments?: unknown[]; _sentryCleanup?: () => void; _sentryStoredLayer?: boolean; _sentrySpan?: Span; - error?: unknown; } /** @@ -55,9 +54,8 @@ export interface HandleChannelContext { * the last entry in `stack`) and `arguments` are the registration args (the * first of which is the path pattern). */ -export interface RegistrationChannelContext { +export interface RegistrationChannelContext extends OrchestrionChannelContext { self?: { stack?: ExpressLayer[] }; - arguments?: unknown[]; } /** An Express error carrying an optional HTTP status, in the various shapes middleware use. */ diff --git a/packages/server-utils/src/integrations/firebase/functions.ts b/packages/server-utils/src/integrations/firebase/functions.ts index 75fd439a3cd6..1ad3d8c03f90 100644 --- a/packages/server-utils/src/integrations/firebase/functions.ts +++ b/packages/server-utils/src/integrations/firebase/functions.ts @@ -1,3 +1,4 @@ +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { FAAS_NAME, FAAS_TRIGGER, @@ -26,14 +27,6 @@ const WRAPPED = '__sentryFirebaseWrapped'; type Handler = (this: unknown, ...args: unknown[]) => unknown; -interface FunctionsChannelContext { - // The live args of the `onX(...)` registration call. firebase-functions accepts either - // `onX(handler)` or `onX(documentOrOptions, handler)`, so the handler is `arguments[0]` when it's a - // function, otherwise `arguments[1]`. Mutating the entry here swaps in the wrapped handler. - arguments: unknown[]; - self?: unknown; -} - /** * Rewrap the handler argument of a firebase-functions `onX(...)` registration so the returned cloud * function opens a `SERVER` span (and error boundary) each time it's invoked. Runs as the tracing @@ -42,7 +35,7 @@ interface FunctionsChannelContext { * The registration call itself is trivial and synchronous, so — unlike the firestore path — this does * not bind a span to the channel; it only uses the channel as an injection point. */ -export function wrapFunctionsRegistration(data: FunctionsChannelContext, triggerType: string): void { +export function wrapFunctionsRegistration(data: OrchestrionChannelContext, triggerType: string): void { const args = data.arguments; if (!Array.isArray(args) || args.length === 0) { return; diff --git a/packages/server-utils/src/integrations/firebase/instrumentation.ts b/packages/server-utils/src/integrations/firebase/instrumentation.ts index 5fcadfd2ae45..b10534aea30a 100644 --- a/packages/server-utils/src/integrations/firebase/instrumentation.ts +++ b/packages/server-utils/src/integrations/firebase/instrumentation.ts @@ -1,4 +1,5 @@ import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { bindTracingChannelToSpan, safeChannelCallback } from '../../tracing-channel'; import type { FirestoreReference } from './firestore-types'; @@ -7,13 +8,6 @@ import { wrapFunctionsRegistration } from './functions'; // The context orchestrion's transform attaches to each firestore channel: `arguments` is the live args // of the wrapped `addDoc`/`getDocs`/`setDoc`/`deleteDoc` call, `arguments[0]` the reference. -interface FirestoreChannelContext { - arguments: unknown[]; - self?: unknown; - result?: unknown; - error?: unknown; -} - // The firestore operations, keyed by channel. `useParent` mirrors the OTel integration: `setDoc`/ // `deleteDoc` take a *document* reference but the span is named after its parent *collection*. const FIRESTORE_OPERATIONS: Array<{ channel: string; spanName: string; useParent: boolean }> = [ @@ -43,7 +37,7 @@ const NOOP = (): void => {}; export function instrumentFirebase() { for (const { channel, spanName, useParent } of FIRESTORE_OPERATIONS) { - bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(channel), data => + bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(channel), data => safeChannelCallback(() => { const reference = data.arguments[0] as FirestoreReference | undefined; if (!reference) { @@ -61,7 +55,7 @@ export function instrumentFirebase() { // span inside that wrapper. The other lifecycle events are irrelevant, so no-op them. diagnosticsChannel.tracingChannel(channel).subscribe({ start: data => - safeChannelCallback(() => wrapFunctionsRegistration(data as { arguments: unknown[] }, triggerType)), + safeChannelCallback(() => wrapFunctionsRegistration(data as OrchestrionChannelContext, triggerType)), end: NOOP, asyncStart: NOOP, asyncEnd: NOOP, diff --git a/packages/server-utils/src/integrations/generic-pool.ts b/packages/server-utils/src/integrations/generic-pool.ts index f5236f5a364c..ca27d9bd29ff 100644 --- a/packages/server-utils/src/integrations/generic-pool.ts +++ b/packages/server-utils/src/integrations/generic-pool.ts @@ -3,6 +3,7 @@ import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration, startInactiveSpan } from '@sentry/core'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { genericPoolModuleNames } from '../orchestrion/config/generic-pool'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; @@ -12,10 +13,6 @@ import { bindTracingChannelToSpan } from '../tracing-channel'; // 'GenericPool' integration is omitted from the default set. const INTEGRATION_NAME = 'GenericPool' as const; -interface GenericPoolAcquireContext { - arguments: unknown[]; -} - const _genericPoolIntegration = (() => { return { name: INTEGRATION_NAME, @@ -35,7 +32,7 @@ export const genericPoolIntegration = defineIntegration(_genericPoolIntegration) function instrumentGenericPool(): void { bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(CHANNELS.GENERIC_POOL_ACQUIRE), + diagnosticsChannel.tracingChannel(CHANNELS.GENERIC_POOL_ACQUIRE), () => startInactiveSpan({ name: 'generic-pool.acquire', diff --git a/packages/server-utils/src/integrations/google-genai.ts b/packages/server-utils/src/integrations/google-genai.ts index b191c503df5b..846ba9ae40cf 100644 --- a/packages/server-utils/src/integrations/google-genai.ts +++ b/packages/server-utils/src/integrations/google-genai.ts @@ -19,6 +19,7 @@ import { } from '../ai/google-genai'; import { instrumentStream } from '../ai/google-genai/streaming'; import type { GoogleGenAIOptions, GoogleGenAIResponse } from '../ai/google-genai/types'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { bindTracingChannelToSpan } from '../tracing-channel'; import { googleGenAiModuleNames } from '../orchestrion/config/google-genai'; @@ -37,14 +38,6 @@ const INSTRUMENTED_CHANNELS = [ { channel: CHANNELS.GOOGLE_GENAI_CHAT, operation: 'chat' }, ] as const; -interface GoogleGenAIChannelContext { - arguments: unknown[]; - // The transform stashes the call's `this` here, which chat methods need since the model lives on - // the `Chat` instance (`this.model`/`this.modelVersion`), not in the call arguments. - self?: unknown; - result?: unknown; -} - const _googleGenAIIntegration = ((options: GoogleGenAIOptions = {}) => { return { name: INTEGRATION_NAME, @@ -57,7 +50,7 @@ const _googleGenAIIntegration = ((options: GoogleGenAIOptions = {}) => { function instrumentGoogleGenai(options: GoogleGenAIOptions): void { for (const { channel, operation } of INSTRUMENTED_CHANNELS) { bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(channel), + diagnosticsChannel.tracingChannel(channel), data => createGenAiSpan(data, operation, options), { beforeSpanEnd: (span, data) => { @@ -81,7 +74,7 @@ function instrumentGoogleGenai(options: GoogleGenAIOptions): void { * Returning `undefined` opts the payload out so no span is opened. */ function createGenAiSpan( - data: GoogleGenAIChannelContext, + data: OrchestrionChannelContext, operation: string, options: GoogleGenAIOptions, ): Span | undefined { @@ -143,7 +136,7 @@ function isAsyncIterable(value: unknown): value is AsyncIterableStream { * For a stream we patch `result[Symbol.asyncIterator]` in place so `instrumentStream` ends the * span when iteration finishes. */ -function wrapStreamResult(span: Span, data: GoogleGenAIChannelContext, options: GoogleGenAIOptions): boolean { +function wrapStreamResult(span: Span, data: OrchestrionChannelContext, options: GoogleGenAIOptions): boolean { const result = data.result; if (!isAsyncIterable(result)) { return false; diff --git a/packages/server-utils/src/integrations/graphql/index.ts b/packages/server-utils/src/integrations/graphql/index.ts index e0397b1ac769..b217b84e45f5 100644 --- a/packages/server-utils/src/integrations/graphql/index.ts +++ b/packages/server-utils/src/integrations/graphql/index.ts @@ -2,6 +2,7 @@ import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration, waitForTracingChannelBinding } from '@sentry/core'; import { subscribeGraphqlDiagnosticChannels, type GraphQLOptions } from './graphql-dc-subscriber'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { graphqlModuleNames } from '../../orchestrion/config/graphql'; import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation'; @@ -20,13 +21,6 @@ const INTEGRATION_NAME = 'Graphql' as const; // The context orchestrion's transform attaches to each channel: `arguments` is the live args of the // wrapped call, `result` the settled return value. -interface GraphqlChannelContext { - arguments: unknown[]; - self?: unknown; - result?: unknown; - error?: unknown; -} - function getOptionsWithDefaults(options: GraphQLOptions): GraphqlResolvedConfig { return { ignoreResolveSpans: options.ignoreResolveSpans !== false, @@ -51,18 +45,18 @@ const _graphqlIntegration = ((options: GraphQLOptions = {}) => { }) satisfies IntegrationFn; function instrumentGraphql(config: GraphqlResolvedConfig, getConfig: () => GraphqlResolvedConfig): void { - bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(CHANNELS.GRAPHQL_PARSE), () => + bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(CHANNELS.GRAPHQL_PARSE), () => safeChannelCallback(() => startParseSpan()), ); bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(CHANNELS.GRAPHQL_VALIDATE), + diagnosticsChannel.tracingChannel(CHANNELS.GRAPHQL_VALIDATE), data => safeChannelCallback(() => startValidateSpan(data.arguments[1])), { beforeSpanEnd: (span, data) => safeChannelCallback(() => finalizeValidateSpan(span, data.result)) }, ); bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(CHANNELS.GRAPHQL_EXECUTE), + diagnosticsChannel.tracingChannel(CHANNELS.GRAPHQL_EXECUTE), data => safeChannelCallback(() => startExecuteSpan(data.arguments, data.self, config, getConfig)), { beforeSpanEnd: (span, data) => safeChannelCallback(() => finalizeExecuteSpan(span, data.result)) }, ); diff --git a/packages/server-utils/src/integrations/hapi/index.ts b/packages/server-utils/src/integrations/hapi/index.ts index e7b8af943694..a4e83be91e1d 100644 --- a/packages/server-utils/src/integrations/hapi/index.ts +++ b/packages/server-utils/src/integrations/hapi/index.ts @@ -1,6 +1,7 @@ import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { hapiModuleNames } from '../../orchestrion/config/hapi'; import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation'; @@ -46,8 +47,7 @@ interface HapiIntegrationOptions { * the hapi server instance: the root server has `self.realm.plugin === undefined`, * while a plugin's clone server exposes the registering plugin's name there. */ -interface HapiChannelContext { - arguments: unknown[]; +interface HapiChannelContext extends OrchestrionChannelContext { self?: { realm?: { plugin?: string } }; } @@ -55,7 +55,7 @@ interface HapiChannelContext { * The `start`/`initialize` channel `context` shape: `self` is the live server * we attach the auto-registered error listener to. */ -interface HapiServerContext { +interface HapiServerContext extends OrchestrionChannelContext { self?: HapiServer; } diff --git a/packages/server-utils/src/integrations/hono/honoIntegration.ts b/packages/server-utils/src/integrations/hono/honoIntegration.ts index d62e949125ef..c2b504db2dab 100644 --- a/packages/server-utils/src/integrations/hono/honoIntegration.ts +++ b/packages/server-utils/src/integrations/hono/honoIntegration.ts @@ -6,6 +6,7 @@ import type { IntegrationFn } from '@sentry/core'; import { debug, defineIntegration, startInactiveSpan } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; import type { Env, GetConnInfo, Hono, MiddlewareHandler } from './honoTypes'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { honoModuleNames } from '../../orchestrion/config/hono'; import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation'; @@ -82,8 +83,6 @@ export function honoMiddleware(app: Hono, options: HonoIntegra }); } -type ChannelArgs = { arguments: unknown[] }; - // A Hono `matchResult[0]` entry: `[[handler, routeMeta], paramIndexMap]` — `compose` runs the // handler (`entry[0][0]`) and the `matchedRoutes` getter reads `routeMeta` (`entry[0][1]`). // oxlint-disable-next-line typescript/no-explicit-any @@ -100,7 +99,7 @@ type MatchedHandlerEntry = [[any, any], any]; * Running per request (no module-scope state) is what lets this work on Cloudflare. */ function injectHonoInstrumentation( - message: ChannelArgs, + message: OrchestrionChannelContext, requestMiddleware: MiddlewareHandler, injectedHandlerLists: WeakSet, ): void { @@ -155,7 +154,7 @@ function injectHonoInstrumentation( */ function instrumentInternalRequests(): void { bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(CHANNELS.HONO_REQUEST), + diagnosticsChannel.tracingChannel(CHANNELS.HONO_REQUEST), data => { // Alongside the manual middleware, the instance `app.request` Proxy has already opened this // span and is calling through — returning `undefined` (no span) avoids nesting a duplicate. @@ -202,9 +201,9 @@ function instrumentHono(options: HonoIntegrationOptions): void { // The Context constructor's `end` fires synchronously inside `new Context()`, before `#dispatch` // reads `matchResult[0].length`, so the injected middleware is in place for the same request. - diagnosticsChannel.tracingChannel(CHANNELS.HONO_CONTEXT).end.subscribe(message => { + diagnosticsChannel.tracingChannel(CHANNELS.HONO_CONTEXT).end.subscribe(message => { safeChannelCallback(() => - injectHonoInstrumentation(message as ChannelArgs, requestMiddleware, injectedHandlerLists), + injectHonoInstrumentation(message as OrchestrionChannelContext, requestMiddleware, injectedHandlerLists), ); }); diff --git a/packages/server-utils/src/integrations/kafkajs/index.ts b/packages/server-utils/src/integrations/kafkajs/index.ts index 5160dedf80ca..0bc54f12f892 100644 --- a/packages/server-utils/src/integrations/kafkajs/index.ts +++ b/packages/server-utils/src/integrations/kafkajs/index.ts @@ -2,6 +2,7 @@ import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import type { TracingChannelSubscribers } from 'node:diagnostics_channel'; import type { IntegrationFn, Span } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { kafkajsModuleNames } from '../../orchestrion/config/kafkajs'; import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation'; @@ -14,16 +15,15 @@ import type { ConsumerRunConfig, ProducerBatch } from './types'; const INTEGRATION_NAME = 'Kafka' as const; /** The tracing-channel context the transform attaches around `messageProducer.js`'s `sendBatch`. */ -interface SendBatchChannelContext { +interface SendBatchChannelContext extends OrchestrionChannelContext { // `arguments[0]` is the `{ topicMessages }` batch (kafkajs normalizes `send` into `sendBatch`). arguments: [ProducerBatch?, ...unknown[]]; - error?: unknown; // The producer spans opened at `start`, ended on `asyncEnd` (and marked errored on `error`). _sentrySpans?: Span[]; } /** The tracing-channel context the transform attaches around `consumer/index.js`'s `run`. */ -interface ConsumerRunChannelContext { +interface ConsumerRunChannelContext extends OrchestrionChannelContext { // `arguments[0]` is the `run(config)` config whose `eachMessage`/`eachBatch` we swap in place. arguments: [ConsumerRunConfig?, ...unknown[]]; } diff --git a/packages/server-utils/src/integrations/knex.ts b/packages/server-utils/src/integrations/knex.ts index 781d26aa70c8..bdc3d4dac16f 100644 --- a/packages/server-utils/src/integrations/knex.ts +++ b/packages/server-utils/src/integrations/knex.ts @@ -31,6 +31,7 @@ import { } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import { DEBUG_BUILD } from '../debug-build'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { bindTracingChannelToSpan } from '../tracing-channel'; import { sanitizeSqlQueryWithSummary, toSqlDialect } from '../utils/sql'; @@ -89,21 +90,10 @@ interface KnexRunner { } /** Context orchestrion attaches to the `orchestrion:knex:query` channel (wrapping `Runner.query`). */ -interface KnexQueryChannelContext { +interface KnexQueryChannelContext extends OrchestrionChannelContext { // `arguments[0]` is the query object (`{ sql, method, bindings }`). arguments: [KnexQuery?]; self?: KnexRunner; - moduleVersion?: string; - result?: unknown; - error?: unknown; -} - -/** Context orchestrion attaches to the `Client.queryBuilder`/`schemaBuilder`/`raw` channels. */ -interface KnexBuilderChannelContext { - arguments: unknown[]; - self?: unknown; - // The builder returned by the wrapped method. - result?: KnexBuilder; } const _knexIntegration = (() => { @@ -135,7 +125,7 @@ const _knexIntegration = (() => { */ function subscribeBuilder(channelName: string): void { diagnosticsChannel.tracingChannel(channelName).end.subscribe(message => { - const builder = (message as KnexBuilderChannelContext).result; + const builder = (message as OrchestrionChannelContext).result; if (!builder || typeof builder !== 'object' || parentSpanSymbol in builder) { return; } diff --git a/packages/server-utils/src/integrations/koa/index.ts b/packages/server-utils/src/integrations/koa/index.ts index e3f1c10690c0..d2bba675abfe 100644 --- a/packages/server-utils/src/integrations/koa/index.ts +++ b/packages/server-utils/src/integrations/koa/index.ts @@ -15,6 +15,7 @@ import { import { CODE_FUNCTION_NAME, HTTP_ROUTE, KOA_TYPE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { MIDDLEWARE, ROUTER } from '@sentry/conventions/op'; import { DEBUG_BUILD } from '../../debug-build'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { koaModuleNames } from '../../orchestrion/config/koa'; import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation'; @@ -66,12 +67,8 @@ interface KoaMiddlewareMetadata { name: string; } -interface KoaUseContext { - arguments: unknown[]; -} - /** The `callback` channel `context` shape: `self` is the live app to attach the error listener to. */ -interface KoaCallbackContext { +interface KoaCallbackContext extends OrchestrionChannelContext { self?: KoaApp; } @@ -96,7 +93,7 @@ const _koaIntegration = ((options: KoaIntegrationOptions = {}) => { function instrumentKoa(ignoreLayersType: KoaLayerType[]): void { diagnosticsChannel.tracingChannel(CHANNELS.KOA_USE).subscribe({ start(rawCtx) { - handleUse(rawCtx as KoaUseContext, ignoreLayersType); + handleUse(rawCtx as OrchestrionChannelContext, ignoreLayersType); }, end() {}, asyncStart() {}, @@ -123,7 +120,7 @@ function instrumentKoa(ignoreLayersType: KoaLayerType[]): void { }); } -function handleUse(ctx: KoaUseContext, ignoreLayersType: KoaLayerType[]): void { +function handleUse(ctx: OrchestrionChannelContext, ignoreLayersType: KoaLayerType[]): void { const middleware = ctx.arguments[0]; if (typeof middleware === 'function') { ctx.arguments[0] = patchUse(middleware as KoaMiddleware, ignoreLayersType); diff --git a/packages/server-utils/src/integrations/langchain.ts b/packages/server-utils/src/integrations/langchain.ts index c10b9c79b6ae..54492e5f2b4f 100644 --- a/packages/server-utils/src/integrations/langchain.ts +++ b/packages/server-utils/src/integrations/langchain.ts @@ -16,6 +16,7 @@ import { import { MISTRAL_INTEGRATION_NAME } from '../ai/mistral/constants'; import { OPENAI_INTEGRATION_NAME } from '../ai/openai/constants'; import { GROQ_INTEGRATION_NAME } from './groq'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { langchainEmbeddingsChannels } from '../orchestrion/config/langchain'; import { bindTracingChannelToSpan } from '../tracing-channel'; @@ -38,17 +39,7 @@ const SKIPPED_PROVIDERS = [ ]; // The chat-model channels carry the live args array of `invoke(input, options)` / `_streamIterator(input, options)`. -interface RunnableChannelContext { - arguments: unknown[]; - self?: unknown; -} - // The embeddings channels carry the instance (`self`) and the `embedQuery(text)` / `embedDocuments(texts)` args. -interface EmbeddingsChannelContext { - self?: unknown; - arguments: unknown[]; -} - // Registered lazily on the first LangChain call (not at `setupOnce`) so a direct provider call made // before any LangChain call still gets its own span — matches the OTel patch-on-import timing. It // also stops the underlying SDK from double-instrumenting embeddings, whose `embedQuery`/ @@ -83,7 +74,7 @@ function instrumentChatModels(options: LangChainOptions): void { // callback dispatch then creates the spans, exactly as in the OTel path, so no span is opened // here — a `start` subscriber (which also makes orchestrion wrap the function) is enough. const injectHandler = (message: unknown): void => { - const args = (message as RunnableChannelContext).arguments; + const args = (message as OrchestrionChannelContext).arguments; if (!Array.isArray(args)) { return; } @@ -107,7 +98,7 @@ function instrumentChatModels(options: LangChainOptions): void { }; for (const channelName of [CHANNELS.LANGCHAIN_CHAT_MODEL_INVOKE, CHANNELS.LANGCHAIN_CHAT_MODEL_STREAM]) { - diagnosticsChannel.tracingChannel(channelName).start.subscribe(message => { + diagnosticsChannel.tracingChannel(channelName).start.subscribe(message => { markProvidersSkipped(); injectHandler(message); }); @@ -115,10 +106,10 @@ function instrumentChatModels(options: LangChainOptions): void { // `TypeSafeClassifier` calls Jev with `fetch`, not through a provider SDK, so nothing needs skipping. diagnosticsChannel - .tracingChannel(CHANNELS.LANGCHAIN_TYPESAFE_CLASSIFIER_INVOKE) + .tracingChannel(CHANNELS.LANGCHAIN_TYPESAFE_CLASSIFIER_INVOKE) .start.subscribe(message => { injectHandler(message); - const { self, arguments: args } = message as RunnableChannelContext; + const { self, arguments: args } = message as OrchestrionChannelContext; recordTypeSafeClassifierState(self, args?.[0]); }); } @@ -128,13 +119,13 @@ function instrumentChatModels(options: LangChainOptions): void { // still marks failed on error) and do not capture them. function instrumentEmbeddings(options: LangChainOptions): void { for (const channelName of langchainEmbeddingsChannels) { - bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(channelName), data => + bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(channelName), data => createEmbeddingsSpan(data, options), ); } } -function createEmbeddingsSpan(data: EmbeddingsChannelContext, options: LangChainOptions): Span { +function createEmbeddingsSpan(data: OrchestrionChannelContext, options: LangChainOptions): Span { // `embedQuery`/`embedDocuments` call the provider SDK internally, so skip that SDK's own // instrumentation before its channel fires (the producer runs at the embeddings channel's `start`). markProvidersSkipped(); diff --git a/packages/server-utils/src/integrations/langgraph.ts b/packages/server-utils/src/integrations/langgraph.ts index 5a41a8296aa3..d9d5bd863144 100644 --- a/packages/server-utils/src/integrations/langgraph.ts +++ b/packages/server-utils/src/integrations/langgraph.ts @@ -8,6 +8,7 @@ import { LANGGRAPH_INTEGRATION_NAME } from '../ai/langgraph/constants'; import type { CompiledGraph, LangGraphOptions } from '../ai/langgraph/types'; import { extractAgentNameFromParams, extractLLMFromParams, wrapToolsWithSpans } from '../ai/langgraph/utils'; import { DEBUG_BUILD } from '../debug-build'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { langgraphModuleNames } from '../orchestrion/config/langgraph'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; @@ -16,16 +17,6 @@ import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation // deduplicated out of the default set. const INTEGRATION_NAME = LANGGRAPH_INTEGRATION_NAME; -interface CompileChannelContext { - arguments: unknown[]; - result?: unknown; -} - -interface CreateReactAgentChannelContext { - arguments: unknown[]; - result?: unknown; -} - // `createReactAgent` compiles a `StateGraph` internally. When set, the compile subscriber skips that // nested graph so its `invoke` is wrapped once (by the createReactAgent handler), not twice. let insideCreateReactAgent = false; @@ -49,18 +40,18 @@ function instrumentLanggraph(options: LangGraphOptions): void { // StateGraph.compile returns synchronously; wrap the returned graph's `invoke` at `end`. diagnosticsChannel - .tracingChannel(CHANNELS.LANGGRAPH_STATE_GRAPH_COMPILE) + .tracingChannel(CHANNELS.LANGGRAPH_STATE_GRAPH_COMPILE) .end.subscribe(message => { if (insideCreateReactAgent) { return; } - const { arguments: args, result } = message as CompileChannelContext; + const { arguments: args, result } = message as OrchestrionChannelContext; wrapCompiledGraphInvoke(result, getFirstArgObject(args) ?? {}, resolvedOptions, null, sentryHandler); }); // createReactAgent only wraps tools and the returned graph's `invoke`. Tools are wrapped at // `start` (before the agent runs), invoke at `end`. - const reactAgentChannel = diagnosticsChannel.tracingChannel( + const reactAgentChannel = diagnosticsChannel.tracingChannel( CHANNELS.LANGGRAPH_CREATE_REACT_AGENT, ); reactAgentChannel.start.subscribe(message => { @@ -70,7 +61,7 @@ function instrumentLanggraph(options: LangGraphOptions): void { // stay off during this call's nested compile. Tool wrapping is guarded for the same reason. insideCreateReactAgent = true; try { - const { arguments: args } = message as CreateReactAgentChannelContext; + const { arguments: args } = message as OrchestrionChannelContext; const params = getFirstArgObject(args); if (params && Array.isArray(params.tools) && params.tools.length > 0) { wrapToolsWithSpans(params.tools, resolvedOptions, extractAgentNameFromParams(args) ?? undefined); @@ -81,7 +72,7 @@ function instrumentLanggraph(options: LangGraphOptions): void { }); reactAgentChannel.end.subscribe(message => { insideCreateReactAgent = false; - const { arguments: args, result } = message as CreateReactAgentChannelContext; + const { arguments: args, result } = message as OrchestrionChannelContext; const agentName = extractAgentNameFromParams(args) ?? undefined; const compileOptions = agentName ? { name: agentName } : {}; wrapCompiledGraphInvoke(result, compileOptions, resolvedOptions, extractLLMFromParams(args), sentryHandler); diff --git a/packages/server-utils/src/integrations/lru-memoizer.ts b/packages/server-utils/src/integrations/lru-memoizer.ts index 6bb2b8acbd53..8b4ec5c0391b 100644 --- a/packages/server-utils/src/integrations/lru-memoizer.ts +++ b/packages/server-utils/src/integrations/lru-memoizer.ts @@ -1,6 +1,7 @@ import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { bindTracingChannelToSpan } from '../tracing-channel'; import { lruMemoizerModuleNames } from '../orchestrion/config/lru-memoizer'; @@ -10,10 +11,6 @@ import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation // 'LruMemoizer' integration is omitted from the default set. const INTEGRATION_NAME = 'LruMemoizer' as const; -interface LruMemoizerLoadContext { - arguments: unknown[]; -} - const _lruMemoizerIntegration = (() => { return { name: INTEGRATION_NAME, @@ -25,7 +22,7 @@ const _lruMemoizerIntegration = (() => { function instrumentLruMemoizer(): void { bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(CHANNELS.LRU_MEMOIZER_LOAD), + diagnosticsChannel.tracingChannel(CHANNELS.LRU_MEMOIZER_LOAD), // We only want the helper's caller-context restore for the callback lru-memoizer fires from a detached `setImmediate`. () => undefined, ); diff --git a/packages/server-utils/src/integrations/mastra-classifier.ts b/packages/server-utils/src/integrations/mastra-classifier.ts index d25434bb2c9d..5db546336779 100644 --- a/packages/server-utils/src/integrations/mastra-classifier.ts +++ b/packages/server-utils/src/integrations/mastra-classifier.ts @@ -3,25 +3,17 @@ import { isObjectLike } from '@sentry/core'; import type { ClassifierEvaluationCall } from '../ai/mastra/classifier-evaluation'; import { processClassifierEvaluationData, setStartingClassifierEvaluation } from '../ai/mastra/classifier-evaluation'; import { getEvaluationInputMessages, getEvaluationOutputMessages } from '../ai/core/utils'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { safeChannelCallback } from '../tracing-channel'; import * as diagnosticsChannel from '../utils/diagnosticsChannel'; -interface ClassifierEvaluateChannelContext { - // `evaluate({ state, questions? })`; `self` is the `Classifier`. - arguments: unknown[]; - self?: unknown; - result?: unknown; -} - /** * Add the evaluated state, questions and answers of a `Classifier.evaluate()` call to the exporter's * `classifier_evaluation` span, which Mastra leaves without input or output. */ export function recordClassifierEvaluations(): void { - const channel = diagnosticsChannel.tracingChannel( - CHANNELS.MASTRA_CLASSIFIER_EVALUATE, - ); + const channel = diagnosticsChannel.tracingChannel(CHANNELS.MASTRA_CLASSIFIER_EVALUATE); const calls = new WeakMap(); channel.start.subscribe(message => { @@ -39,13 +31,13 @@ export function recordClassifierEvaluations(): void { safeChannelCallback(() => { const call = calls.get(message as object); if (call) { - finishClassifierEvaluation(call, message as ClassifierEvaluateChannelContext); + finishClassifierEvaluation(call, message as OrchestrionChannelContext); } }); }); } -function finishClassifierEvaluation(call: ClassifierEvaluationCall, message: ClassifierEvaluateChannelContext): void { +function finishClassifierEvaluation(call: ClassifierEvaluationCall, message: OrchestrionChannelContext): void { call.settled = true; const { span, recordInputs, recordOutputs } = call; if (!span) { diff --git a/packages/server-utils/src/integrations/mastra.ts b/packages/server-utils/src/integrations/mastra.ts index 1f541fe83303..126afd052d15 100644 --- a/packages/server-utils/src/integrations/mastra.ts +++ b/packages/server-utils/src/integrations/mastra.ts @@ -22,6 +22,7 @@ import type { MastraExporterOptions } from '../ai/mastra'; import { getSentrySpanForMastraId } from '../ai/mastra/span-registry'; import type { MastraObservabilityExporter } from '../ai/mastra/types'; import { DEBUG_BUILD } from '../debug-build'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { mastraModuleNames } from '../orchestrion/config/mastra'; import { recordClassifierEvaluations } from './mastra-classifier'; @@ -45,16 +46,6 @@ interface MastraInstance { observability?: { getDefaultInstance?: () => MastraObservabilityInstance | undefined }; } -interface ConstructorChannelContext { - arguments: unknown[]; - self?: unknown; -} - -interface ExecuteWithContextChannelContext { - // `executeWithContext({ span, fn })` — the first arg carries the Mastra AISpan. - arguments: unknown[]; -} - /** Mastra AISpan → the id the exporter keys its Sentry span on. */ function mastraSpanId(span: unknown): string | undefined { if (!isObjectLike(span)) { @@ -89,9 +80,9 @@ const _mastraIntegration = ((options: MastraOptions = {}) => { }) satisfies IntegrationFn; function instrumentExporter(options: MastraOptions): void { - diagnosticsChannel.tracingChannel(CHANNELS.MASTRA_CONSTRUCTOR).end.subscribe(message => { + diagnosticsChannel.tracingChannel(CHANNELS.MASTRA_CONSTRUCTOR).end.subscribe(message => { safeChannelCallback(() => { - const { self } = message as ConstructorChannelContext; + const { self } = message as OrchestrionChannelContext; attachExporter(self, options); }); }); @@ -109,10 +100,10 @@ function instrumentExporter(options: MastraOptions): void { */ function captureExecuteWithContextErrors(): void { diagnosticsChannel - .tracingChannel(CHANNELS.MASTRA_EXECUTE_WITH_CONTEXT) + .tracingChannel(CHANNELS.MASTRA_EXECUTE_WITH_CONTEXT) .error.subscribe(message => { safeChannelCallback(() => { - const data = message as ExecuteWithContextChannelContext & { error: unknown }; + const data = message as OrchestrionChannelContext; captureMastraError(data.error, (data.arguments as unknown[] | undefined)?.[0]); }); }); @@ -168,7 +159,7 @@ function captureMastraError(error: unknown, params: unknown): void { */ function instrumentExecuteWithContext(): void { bindSpanToChannelStore( - diagnosticsChannel.tracingChannel(CHANNELS.MASTRA_EXECUTE_WITH_CONTEXT), + diagnosticsChannel.tracingChannel(CHANNELS.MASTRA_EXECUTE_WITH_CONTEXT), data => { const params = (data.arguments as unknown[] | undefined)?.[0]; const id = isObjectLike(params) ? mastraSpanId(params.span) : undefined; diff --git a/packages/server-utils/src/integrations/mcp-server.ts b/packages/server-utils/src/integrations/mcp-server.ts index 8f3a75f1a0f1..d3a4823cf389 100644 --- a/packages/server-utils/src/integrations/mcp-server.ts +++ b/packages/server-utils/src/integrations/mcp-server.ts @@ -2,6 +2,7 @@ import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; import { wrapMcpServerWithSentry } from '@sentry/core/server'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { mcpServerModuleNames } from '../orchestrion/config/mcp-server'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; @@ -9,11 +10,6 @@ import { safeChannelCallback } from '../tracing-channel'; const INTEGRATION_NAME = 'McpServer'; -interface ConstructorChannelContext { - arguments: unknown[]; - self?: unknown; -} - const _mcpServerIntegration = (() => { return { name: INTEGRATION_NAME, @@ -34,9 +30,9 @@ function subscribe(): void { // wrap's own `WeakSet` guard makes a later manual call on the same instance a no-op, so the two // paths coexist safely. for (const channel of [CHANNELS.MCP_SERVER_V2_CONSTRUCTOR, CHANNELS.MCP_SERVER_V1_CONSTRUCTOR]) { - diagnosticsChannel.tracingChannel(channel).end.subscribe(message => { + diagnosticsChannel.tracingChannel(channel).end.subscribe(message => { safeChannelCallback(() => { - const { self } = message as ConstructorChannelContext; + const { self } = message as OrchestrionChannelContext; if (self) { wrapMcpServerWithSentry(self); } diff --git a/packages/server-utils/src/integrations/mistral.ts b/packages/server-utils/src/integrations/mistral.ts index f70d026cf358..f916321bd31c 100644 --- a/packages/server-utils/src/integrations/mistral.ts +++ b/packages/server-utils/src/integrations/mistral.ts @@ -8,6 +8,7 @@ import { MISTRAL_INTEGRATION_NAME, MISTRAL_ORIGIN } from '../ai/mistral/constant import { instrumentEventStream } from '../ai/mistral/streaming'; import type { MistralOptions } from '../ai/mistral/types'; import { addResponseAttributes, getSpanName } from '../ai/mistral/utils'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { mistralModuleNames } from '../orchestrion/config/mistral'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; @@ -25,15 +26,6 @@ const INSTRUMENTED_CHANNELS = [ { channel: CHANNELS.MISTRAL_AGENTS_STREAM, operation: 'invoke_agent', streaming: true }, ] as const; -/** - * The context orchestrion shares across the tracing-channel lifecycle hooks: `arguments` is the live - * args array passed to the SDK method, and Node's `tracingChannel` attaches `result` when it settles. - */ -interface MistralChannelContext { - arguments: unknown[]; - result?: unknown; -} - const _mistralAIIntegration = ((options: MistralOptions = {}) => { return { name: INTEGRATION_NAME, @@ -46,7 +38,7 @@ const _mistralAIIntegration = ((options: MistralOptions = {}) => { function instrumentMistral(options: MistralOptions): void { for (const { channel, operation, streaming } of INSTRUMENTED_CHANNELS) { bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(channel), + diagnosticsChannel.tracingChannel(channel), data => createGenAiSpan(data, operation, streaming, options), { beforeSpanEnd: (span, data) => { @@ -65,7 +57,7 @@ function instrumentMistral(options: MistralOptions): void { * Returning `undefined` opts the payload out so no span is opened. */ function createGenAiSpan( - data: MistralChannelContext, + data: OrchestrionChannelContext, operation: string, streaming: boolean, options: MistralOptions, diff --git a/packages/server-utils/src/integrations/mongodb/index.ts b/packages/server-utils/src/integrations/mongodb/index.ts index 6f492a575a6b..593638025c6f 100644 --- a/packages/server-utils/src/integrations/mongodb/index.ts +++ b/packages/server-utils/src/integrations/mongodb/index.ts @@ -3,6 +3,7 @@ import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; import type { MongodbNamespace, MongoV3Topology } from './mongodb-span'; import { getV3CommandOperation, getV3SpanAttributes, getV4SpanAttributes, startMongoSpan } from './mongodb-span'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { bindTracingChannelToSpan } from '../../tracing-channel'; import { mongodbModuleNames } from '../../orchestrion/config/mongodb'; @@ -16,9 +17,8 @@ const ORIGIN = 'auto.db.mongo'; * what orchestrion's transform attaches to a channel context: * `self` is the `this`, plus args. */ -interface MongoChannelContext { +interface MongoChannelContext extends OrchestrionChannelContext { self?: { address?: string }; - arguments?: unknown[]; } // Details extracted from a v3 wireprotocol call's arguments to build its span. diff --git a/packages/server-utils/src/integrations/mongoose/index.ts b/packages/server-utils/src/integrations/mongoose/index.ts index 4e00635939c1..62b7ead8b720 100644 --- a/packages/server-utils/src/integrations/mongoose/index.ts +++ b/packages/server-utils/src/integrations/mongoose/index.ts @@ -4,6 +4,7 @@ import { defineIntegration, getActiveSpan, waitForTracingChannelBinding } from ' import { subscribeMongooseDiagnosticChannels } from './mongoose-dc-subscriber'; import type { MongooseLegacyCollection } from './mongoose-legacy-span'; import { startMongooseLegacySpan } from './mongoose-legacy-span'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { MONGOOSE_CONTEXT_CAPTURE_CHANNELS } from '../../orchestrion/config/mongoose'; import type { SentryTracingChannel } from '../../tracing-channel'; @@ -38,12 +39,8 @@ interface MongooseDocument { * The shape orchestrion's transform attaches to the tracing-channel context * object. `self` is the `this` of the traced method */ -interface MongooseChannelContext { +interface MongooseChannelContext extends OrchestrionChannelContext { self?: object; - arguments?: unknown[]; - result?: unknown; - error?: unknown; - moduleVersion?: string; } // The active span captured when a query/aggregate was *built*, keyed by the diff --git a/packages/server-utils/src/integrations/mysql.ts b/packages/server-utils/src/integrations/mysql.ts index e4049b65d30a..d3e8010cb1ba 100644 --- a/packages/server-utils/src/integrations/mysql.ts +++ b/packages/server-utils/src/integrations/mysql.ts @@ -23,6 +23,7 @@ import { startInactiveSpan, } from '@sentry/core'; import { sanitizeSqlQueryWithSummary } from '../utils/sql'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { bindTracingChannelToSpan } from '../tracing-channel'; import { mysqlModuleNames } from '../orchestrion/config/mysql'; @@ -42,13 +43,8 @@ const DB_SYSTEM_NAME_VALUE_MYSQL = 'mysql' as const; * The shape orchestrion's transform attaches to the tracing-channel `context` object. Documented here * rather than imported because orchestrion's runtime doesn't export it. */ -interface MysqlQueryChannelContext { - // The live args array passed to the wrapped `connection.query` call; `arguments[0]` is the SQL. - arguments: unknown[]; +interface MysqlQueryChannelContext extends OrchestrionChannelContext { self?: MysqlConnection; - moduleVersion?: string; - result?: unknown; - error?: unknown; // The caller's scope, captured at `start` and replayed onto the streamed `Query` emitter (see below). _sentryCallerScope?: Scope; } diff --git a/packages/server-utils/src/integrations/mysql2/index.ts b/packages/server-utils/src/integrations/mysql2/index.ts index b8f6501a26ba..5f337d21a41d 100644 --- a/packages/server-utils/src/integrations/mysql2/index.ts +++ b/packages/server-utils/src/integrations/mysql2/index.ts @@ -10,6 +10,7 @@ import { } from '@sentry/core'; import { sanitizeSqlQueryWithSummary } from '../../utils/sql'; import { subscribeMysql2DiagnosticChannels } from './mysql2-dc-subscriber'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import type { ChannelName } from '../../orchestrion/channels'; import { CHANNELS } from '../../orchestrion/channels'; import { bindTracingChannelToSpan } from '../../tracing-channel'; @@ -38,14 +39,8 @@ const DB_SYSTEM_VALUE_MYSQL = 'mysql'; * object. Documented here rather than imported because orchestrion's runtime * doesn't export it. */ -interface Mysql2QueryChannelContext { - // The live args array passed to the wrapped `query`/`execute` call: - // `arguments[0]` is the SQL (a string, `Query`, or `{ sql, values }`), - // `arguments[1]` is the values array or a callback. - arguments: unknown[]; +interface Mysql2QueryChannelContext extends OrchestrionChannelContext { self?: Mysql2Connection; - result?: unknown; - error?: unknown; } interface Mysql2ConnectionConfig { diff --git a/packages/server-utils/src/integrations/openai-compatible.ts b/packages/server-utils/src/integrations/openai-compatible.ts index 2c23c711b60f..48bacae5bcf8 100644 --- a/packages/server-utils/src/integrations/openai-compatible.ts +++ b/packages/server-utils/src/integrations/openai-compatible.ts @@ -13,6 +13,7 @@ import { addRequestAttributes, extractRequestAttributes } from '../ai/openai'; import { instrumentStream } from '../ai/openai/streaming'; import type { OpenAiOptions } from '../ai/openai/types'; import { addResponseAttributes } from '../ai/openai/utils'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; import { bindTracingChannelToSpan } from '../tracing-channel'; @@ -33,15 +34,6 @@ export interface OpenAiCompatibleProvider { channels: { chat: string; embeddings: string }; } -/** - * The context orchestrion shares across the tracing-channel lifecycle hooks: `arguments` is the live args - * array passed to `Completions.create(body, options)`, and `result` holds its returned `APIPromise`. - */ -interface OpenAiCompatibleChannelContext { - arguments: unknown[]; - result?: unknown; -} - /** * Builds a diagnostics-channel integration for an OpenAI-compatible provider SDK. It subscribes to the * `orchestrion::{chat,embeddings}` channels injected into the SDK's `create` methods, so it @@ -60,7 +52,7 @@ export function createOpenAiCompatibleIntegration(channel), + diagnosticsChannel.tracingChannel(channel), data => createGenAiSpan(data, operation, provider, options), { beforeSpanEnd: (span, data) => { @@ -97,7 +89,7 @@ export function createOpenAiCompatibleIntegration { return { name: INTEGRATION_NAME, @@ -53,7 +44,7 @@ const _openAIIntegration = ((options: OpenAiOptions = {}) => { function instrumentOpenai(options: OpenAiOptions): void { for (const { channel, operation } of INSTRUMENTED_CHANNELS) { bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(channel), + diagnosticsChannel.tracingChannel(channel), data => createGenAiSpan(data, operation, options), { beforeSpanEnd: (span, data) => { @@ -80,7 +71,7 @@ function instrumentOpenai(options: OpenAiOptions): void { * Build the span for an instrumented `create` call. * Returning `undefined` opts the payload out so no span is opened. */ -function createGenAiSpan(data: OpenAiChatChannelContext, operation: string, options: OpenAiOptions): Span | undefined { +function createGenAiSpan(data: OrchestrionChannelContext, operation: string, options: OpenAiOptions): Span | undefined { // When another provider (e.g. LangChain) is driving the SDK, it records the spans itself and marks this // provider as skipped; skip here to avoid double spans. if (_INTERNAL_shouldSkipAiProviderWrapping(INTEGRATION_NAME)) { @@ -126,7 +117,7 @@ function isAsyncIterable(value: unknown): value is AsyncIterableStream { * to hand span-ending ownership to `instrumentStream`; `false` for non-streaming/errored results, which end * via the normal `beforeSpanEnd` path. */ -function wrapStreamResult(span: Span, data: OpenAiChatChannelContext, options: OpenAiOptions): boolean { +function wrapStreamResult(span: Span, data: OrchestrionChannelContext, options: OpenAiOptions): boolean { const result = data.result; if (!isAsyncIterable(result)) { return false; diff --git a/packages/server-utils/src/integrations/pi-durable.ts b/packages/server-utils/src/integrations/pi-durable.ts index b5b115ec6c7b..a9bd9e1b6a99 100644 --- a/packages/server-utils/src/integrations/pi-durable.ts +++ b/packages/server-utils/src/integrations/pi-durable.ts @@ -7,19 +7,11 @@ import { PI_DURABLE_INTEGRATION_NAME } from '../ai/pi-durable/constants'; import { markBuiltInTool } from '../ai/pi-durable/tools'; import type { PiHarnessOptions } from '../ai/pi-durable/types'; import { DEBUG_BUILD } from '../debug-build'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { piDurableModuleNames } from '../orchestrion/config/pi-durable'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; -interface HarnessOpenChannelContext { - arguments: unknown[]; - result?: unknown; -} - -interface CodingToolChannelContext { - result?: unknown; -} - const _piDurableIntegration = ((options: PiDurableOptions = {}) => { return { name: PI_DURABLE_INTEGRATION_NAME, @@ -33,10 +25,10 @@ const _piDurableIntegration = ((options: PiDurableOptions = {}) => { }) satisfies IntegrationFn; function instrumentPiDurable(options: PiDurableOptions): void { - const harnessOpen = diagnosticsChannel.tracingChannel(CHANNELS.PI_DURABLE_HARNESS_OPEN); + const harnessOpen = diagnosticsChannel.tracingChannel(CHANNELS.PI_DURABLE_HARNESS_OPEN); harnessOpen.start.subscribe(message => { - const args = (message as HarnessOpenChannelContext).arguments; + const args = (message as OrchestrionChannelContext).arguments; try { if (args && isObjectLike(args[1])) { args[1] = instrumentPiDurableHarnessOptions(args[1] as PiHarnessOptions, options); @@ -47,7 +39,7 @@ function instrumentPiDurable(options: PiDurableOptions): void { }); harnessOpen.asyncEnd.subscribe(message => { - const { arguments: args, result } = message as HarnessOpenChannelContext; + const { arguments: args, result } = message as OrchestrionChannelContext; try { if (args && isObjectLike(args[1])) { endRunsOnClose(args[1], result); @@ -58,8 +50,8 @@ function instrumentPiDurable(options: PiDurableOptions): void { }); diagnosticsChannel - .tracingChannel(CHANNELS.PI_DURABLE_CODING_TOOL) - .end.subscribe(message => markBuiltInTool((message as CodingToolChannelContext).result)); + .tracingChannel(CHANNELS.PI_DURABLE_CODING_TOOL) + .end.subscribe(message => markBuiltInTool((message as OrchestrionChannelContext).result)); } /** diff --git a/packages/server-utils/src/integrations/postgres-js.ts b/packages/server-utils/src/integrations/postgres-js.ts index 5d3b865cd20d..0ac51daa8f54 100644 --- a/packages/server-utils/src/integrations/postgres-js.ts +++ b/packages/server-utils/src/integrations/postgres-js.ts @@ -28,6 +28,7 @@ import { _reconstructQuery, type PostgresConnectionContext, } from './postgresjs'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { bindTracingChannelToSpan } from '../tracing-channel'; import { postgresJsModuleNames } from '../orchestrion/config/postgres'; @@ -81,11 +82,9 @@ interface PostgresQuery { reject?: (...args: unknown[]) => unknown; } -interface PostgresJsQueryContext { - arguments?: unknown[]; +interface PostgresJsQueryContext extends OrchestrionChannelContext { self?: PostgresQuery; - result?: unknown; - error?: unknown; + [SPAN_ENDED]?: boolean; } // A connection object -> its resolved context, populated on the `connection` @@ -178,7 +177,7 @@ function wrapQuerySettlement(data: PostgresJsQueryContext, span: Span, sanitized // Claim ownership of ending the span up front, so `deferSpanEnd` defers to the // wrapper even if `span.end()` below throws. const markEnded = (): void => { - (data as Record)[SPAN_ENDED] = true; + data[SPAN_ENDED] = true; }; const originalResolve = query.resolve; @@ -332,7 +331,7 @@ function instrumentPostgresJs(options: PostgresJsIntegrationOptions): void { deferSpanEnd({ data }) { // `handle` is async: its promise settles on dispatch (asyncEnd), long // before the query does. The resolve/reject wrappers own the ending. - if ((data as Record)[SPAN_ENDED]) { + if (data[SPAN_ENDED]) { return true; // wrappers already ended it } if ('error' in data) { diff --git a/packages/server-utils/src/integrations/postgres.ts b/packages/server-utils/src/integrations/postgres.ts index 5d93ac17769f..c1a2980943db 100644 --- a/packages/server-utils/src/integrations/postgres.ts +++ b/packages/server-utils/src/integrations/postgres.ts @@ -23,6 +23,7 @@ import { startInactiveSpan, } from '@sentry/core'; import { sanitizeSqlQueryWithSummary } from '../utils/sql'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { bindTracingChannelToSpan } from '../tracing-channel'; import { pgModuleNames } from '../orchestrion/config/pg'; @@ -59,12 +60,7 @@ const SPAN_POOL_CONNECT = 'pg-pool.connect'; * The shape orchestrion's transform attaches to the tracing-channel `context`. Documented here rather * than imported because orchestrion's runtime doesn't export it. */ -interface PgChannelContext { - // The live args array passed to the wrapped `query`/`connect` call. - arguments: unknown[]; - self?: unknown; - result?: unknown; - error?: unknown; +interface PgChannelContext extends OrchestrionChannelContext { // The caller's scope, captured at `start` and replayed onto a streamed `Submittable` emitter (see below). _sentryCallerScope?: Scope; } diff --git a/packages/server-utils/src/integrations/prisma/orchestrion.ts b/packages/server-utils/src/integrations/prisma/orchestrion.ts index 656b7cf8fc75..de209f694d71 100644 --- a/packages/server-utils/src/integrations/prisma/orchestrion.ts +++ b/packages/server-utils/src/integrations/prisma/orchestrion.ts @@ -3,6 +3,7 @@ import type { Span } from '@sentry/core'; import { getActiveSpan, getRootSpan, spanIsIgnored, startInactiveSpan, withActiveSpan } from '@sentry/core'; import { DB_COLLECTION_NAME, DB_OPERATION_NAME, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { prismaChannels, PRISMA_LAZY_TERMINALS } from '../../orchestrion/config/prisma'; import type { TracingChannelLifeCycleOptions } from '../../tracing-channel'; import { bindTracingChannelToSpan, safeChannelCallback } from '../../tracing-channel'; @@ -20,12 +21,8 @@ interface PrismaCollection { tableName?: unknown; } -interface PrismaTerminalChannelContext { - arguments: unknown[]; +interface PrismaTerminalChannelContext extends OrchestrionChannelContext { self?: PrismaCollection; - moduleVersion?: string; - result?: unknown; - error?: unknown; } // Prisma's `AsyncIterableResult`: `await`, `.then()` and `.first()` all funnel through `toArray()`; only diff --git a/packages/server-utils/src/integrations/redis/index.ts b/packages/server-utils/src/integrations/redis/index.ts index 4140156cee57..7176a14b57ec 100644 --- a/packages/server-utils/src/integrations/redis/index.ts +++ b/packages/server-utils/src/integrations/redis/index.ts @@ -21,6 +21,7 @@ import { withActiveSpan, waitForTracingChannelBinding, } from '@sentry/core'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { getRedisQueryNaming } from './redis-span-name'; import { defaultDbStatementSerializer } from './redis-statement-serializer'; @@ -67,13 +68,6 @@ interface NodeRedisClient { options?: NodeRedisClientOptions; } -interface CommandContext { - arguments?: unknown[]; - self?: unknown; - result?: unknown; - error?: unknown; -} - function endSpan(span: Span, err: unknown): void { if (err) { span.setStatus({ code: SPAN_STATUS_ERROR, message: err instanceof Error ? err.message : String(err) }); @@ -140,7 +134,7 @@ function startCommandSpan( // Settles via `command_obj.callback`, not the sync return — so instead of // `bindTracingChannelToSpan` we open the span in `start`, wrap the callback to end it, and end on `error` for sync throws. function subscribeLegacyRedisCommand(cacheOptions: RedisCacheOptions): void { - const channel = diagnosticsChannel.tracingChannel(CHANNELS.REDIS_COMMAND); + const channel = diagnosticsChannel.tracingChannel(CHANNELS.REDIS_COMMAND); const noop = (): void => {}; channel.subscribe({ end: noop, @@ -170,7 +164,7 @@ function subscribeLegacyRedisCommand(cacheOptions: RedisCacheOptions): void { attributes[SERVER_PORT] = client.connection_options.port; } const span = startCommandSpan(command.command, command.args ?? [], attributes, cacheOptions); - (data as CommandContext & { _sentrySpan?: Span })._sentrySpan = span; + (data as OrchestrionChannelContext & { _sentrySpan?: Span })._sentrySpan = span; const parentSpan = getActiveSpan(); command.callback = function (this: unknown, err: Error | null | undefined, reply: unknown) { @@ -185,7 +179,7 @@ function subscribeLegacyRedisCommand(cacheOptions: RedisCacheOptions): void { }, error(data) { // Synchronous throw: the wrapped callback never fires, so end here instead. - const span = (data as CommandContext & { _sentrySpan?: Span })._sentrySpan; + const span = (data as OrchestrionChannelContext & { _sentrySpan?: Span })._sentrySpan; if (span) { endSpan(span, data.error); } @@ -197,10 +191,10 @@ function subscribeLegacyRedisCommand(cacheOptions: RedisCacheOptions): void { function bindNodeRedisCommandChannel( channelName: string, - getWireArgs: (data: CommandContext) => Array | undefined, + getWireArgs: (data: OrchestrionChannelContext) => Array | undefined, cacheOptions: RedisCacheOptions, ): void { - const channel = diagnosticsChannel.tracingChannel(channelName); + const channel = diagnosticsChannel.tracingChannel(channelName); bindTracingChannelToSpan( channel, data => { @@ -223,14 +217,14 @@ function bindNodeRedisCommandChannel( } // `sendCommand(args, options)` — `args` are already the wire arguments. -function getSendCommandArgs(data: CommandContext): Array | undefined { +function getSendCommandArgs(data: OrchestrionChannelContext): Array | undefined { const args = data.arguments?.[0]; return Array.isArray(args) ? (args as Array) : undefined; } // `commandsExecutor(command, jsArgs)` — derive the wire arguments the same way // `@redis/client` does internally, via `command.transformArguments`. -function getExecutorArgs(data: CommandContext): Array | undefined { +function getExecutorArgs(data: OrchestrionChannelContext): Array | undefined { const command = data.arguments?.[0] as RedisCommandDefinition | undefined; const jsArgs = data.arguments?.[1]; if (typeof command?.transformArguments !== 'function' || !Array.isArray(jsArgs)) { @@ -244,7 +238,9 @@ function getExecutorArgs(data: CommandContext): Array | undefin } function bindNodeRedisConnectChannel(): void { - const channel = diagnosticsChannel.tracingChannel(CHANNELS.NODE_REDIS_CONNECT); + const channel = diagnosticsChannel.tracingChannel( + CHANNELS.NODE_REDIS_CONNECT, + ); bindTracingChannelToSpan(channel, data => { const options = (data.self as NodeRedisClient | undefined)?.options; return startInactiveSpan({ @@ -261,8 +257,11 @@ function bindNodeRedisConnectChannel(): void { // Batch (multi/pipeline): one span per `exec`. Batched commands bypass `sendCommand`, so // the executor's `ctx.arguments[0]` (the queued commands) gives the batch size. Span shape // mirrors the native `node-redis:batch` span (see `redis-dc-subscriber.ts`). -function bindNodeRedisBatchChannel(channelName: string, getOperation: (data: CommandContext) => string): void { - const channel = diagnosticsChannel.tracingChannel(channelName); +function bindNodeRedisBatchChannel( + channelName: string, + getOperation: (data: OrchestrionChannelContext) => string, +): void { + const channel = diagnosticsChannel.tracingChannel(channelName); bindTracingChannelToSpan(channel, data => { const commands = data.arguments?.[0]; const size = Array.isArray(commands) ? commands.length : undefined; diff --git a/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts b/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts index 0e91d59c03bb..d918d080f749 100644 --- a/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts +++ b/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts @@ -12,6 +12,7 @@ import { import { DB_QUERY, DB } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; import { startInactiveSpan } from '@sentry/core'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { bindTracingChannelToSpan } from '../../tracing-channel'; import type { RedisCacheOptions } from './redis-cache'; @@ -33,15 +34,10 @@ interface RedisClientLike { options?: { host?: string; port?: number }; } -interface IORedisCommandContext { - arguments?: unknown[]; +interface IORedisCommandContext extends OrchestrionChannelContext { self?: RedisClientLike; - result?: unknown; - error?: unknown; } -type IORedisConnectContext = Omit; - function getConnectionOptions(self: RedisClientLike | undefined): { host?: string; port?: number } { return { host: self?.options?.host, port: self?.options?.port }; } @@ -111,7 +107,7 @@ export function instrumentIoredis(options: RedisCacheOptions): void { const commandChannel = diagnosticsChannel.tracingChannel( CHANNELS.IOREDIS_COMMAND, ); - const connectChannel = diagnosticsChannel.tracingChannel( + const connectChannel = diagnosticsChannel.tracingChannel( CHANNELS.IOREDIS_CONNECT, ); diff --git a/packages/server-utils/src/integrations/tedious.ts b/packages/server-utils/src/integrations/tedious.ts index 21ff279dd13f..0878dc5e3118 100644 --- a/packages/server-utils/src/integrations/tedious.ts +++ b/packages/server-utils/src/integrations/tedious.ts @@ -25,6 +25,7 @@ import { SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { tediousModuleNames } from '../orchestrion/config/tedious'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; @@ -66,16 +67,14 @@ interface TediousRequest extends EventEmitter { } /** Context orchestrion attaches to the query channels (wrapping the `Connection` request methods). */ -interface TediousQueryChannelContext { +interface TediousQueryChannelContext extends OrchestrionChannelContext { // `arguments[0]` is the `Request` (or `BulkLoad` for `execBulkLoad`), both `EventEmitter`s. arguments: [TediousRequest?, ...unknown[]]; self?: TediousConnection; - moduleVersion?: string; } /** Context orchestrion attaches to the `Connection.connect` channel. */ -interface TediousConnectChannelContext { - arguments: unknown[]; +interface TediousConnectChannelContext extends OrchestrionChannelContext { self?: TediousConnection; } diff --git a/packages/server-utils/src/integrations/typesafe.ts b/packages/server-utils/src/integrations/typesafe.ts index 594f9df7a068..9575e1206435 100644 --- a/packages/server-utils/src/integrations/typesafe.ts +++ b/packages/server-utils/src/integrations/typesafe.ts @@ -5,23 +5,12 @@ import type { GenAiOptions } from '../ai/core/utils'; import { resolveAIRecordingOptions } from '../ai/core/utils'; import { addResponseAttributes, onSystemOneResponse, startEvaluateSpan } from '../ai/typesafe'; import { TYPESAFE_INTEGRATION_NAME } from '../ai/typesafe/constants'; +import type { OrchestrionChannelContext } from '../orchestrion/types'; import { CHANNELS } from '../orchestrion/channels'; import { typesafeModuleNames } from '../orchestrion/config/typesafe'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; import { bindTracingChannelToSpan } from '../tracing-channel'; -/** - * The context orchestrion shares across the tracing-channel lifecycle hooks: `arguments` is the live - * args array passed to `systemOne(request, options)`, `self` the `TypeSafeClient`, and `result` the - * returned `APIPromise` (replaced by the parsed response body once it arrives). `Sync` tracing keeps - * orchestrion from calling `.then()` on it, see `orchestrion/config/typesafe.ts`. - */ -interface TypeSafeChannelContext { - arguments: unknown[]; - self?: unknown; - result?: unknown; -} - const _typesafeIntegration = ((options: GenAiOptions = {}) => { return { name: TYPESAFE_INTEGRATION_NAME, @@ -33,7 +22,7 @@ const _typesafeIntegration = ((options: GenAiOptions = {}) => { function instrumentTypeSafe(options: GenAiOptions): void { bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(CHANNELS.TYPESAFE_SYSTEM_ONE), + diagnosticsChannel.tracingChannel(CHANNELS.TYPESAFE_SYSTEM_ONE), data => startEvaluateSpan(data.arguments?.[0], data.self, resolveAIRecordingOptions(options).recordInputs), { beforeSpanEnd: (span, data) => { diff --git a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts index daf4ee24584c..2a59de79b4f8 100644 --- a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts +++ b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts @@ -9,6 +9,7 @@ import { withActiveSpan, } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; +import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { bindTracingChannelToSpan, type TracingChannelPayloadWithSpan } from '../../tracing-channel'; import { @@ -56,13 +57,6 @@ import { asString, isReadableStream, tapModelCallStream } from './util'; * double-counts tool spans. */ -/** Shape orchestrion's transform attaches to the tracing-channel context. */ -interface OrchestrionContext { - arguments: unknown[]; - result?: unknown; - error?: unknown; -} - /** Builds the normalized message for a channel from the wrapped call's first-arg options. */ type MessageBuilder = (options: Record, telemetry: Record) => VercelAiChannelMessage; @@ -227,13 +221,13 @@ function bindOperation( build: MessageBuilder, options: VercelAiChannelOptions, ): void { - const channel = tracingChannel(channelName); + const channel = tracingChannel(channelName); // Build the operation span from the wrapped call's first argument and track it (so a model call can // resolve it as its parent). `bindTracingChannelToSpan` calls this once at channel `start` and makes // the returned span the active async context for the operation's duration — that active span is what // `resolveModelCallParent` reads. It also sets `data._sentrySpan`, so we don't here. - const buildOperationSpan = (data: TracingChannelPayloadWithSpan): Span | undefined => { + const buildOperationSpan = (data: TracingChannelPayloadWithSpan): Span | undefined => { const callOptions = isObjectLike(data.arguments[0]) ? data.arguments[0] : {}; const telemetry = isObjectLike(callOptions.experimental_telemetry) ? callOptions.experimental_telemetry : {}; // `isEnabled === false` means the user opted out — emit no span. But `isEnabled` is also `false` on @@ -288,7 +282,7 @@ function bindOperation( bindTracingChannelToSpan( channel, - (data: TracingChannelPayloadWithSpan) => buildOperationSpan(data), + (data: TracingChannelPayloadWithSpan) => buildOperationSpan(data), { beforeSpanEnd: (span, data) => { const message = messages.get(data); @@ -334,7 +328,7 @@ function bindOperation( * anything that isn't a streamed `streamText` result, so the helper ends the span as usual. */ function deferStreamTextOperationEnd( - data: TracingChannelPayloadWithSpan, + data: TracingChannelPayloadWithSpan, end: (error?: unknown) => void, ): boolean { if (messages.get(data)?.type !== 'streamText' || 'error' in data || !isStreamingResult(data.result)) { @@ -404,7 +398,7 @@ function subscribeResolveLanguageModel( channelName: string, options: VercelAiChannelOptions, ): void { - tracingChannel(channelName).subscribe({ + tracingChannel(channelName).subscribe({ end(rawCtx) { const ctx = rawCtx; if (!isObjectLike(ctx.result)) { diff --git a/packages/server-utils/src/orchestrion/types.ts b/packages/server-utils/src/orchestrion/types.ts new file mode 100644 index 000000000000..b30376cb44fb --- /dev/null +++ b/packages/server-utils/src/orchestrion/types.ts @@ -0,0 +1,7 @@ +export interface OrchestrionChannelContext { + arguments: unknown[]; + self?: unknown; + result?: unknown; + error?: unknown; + moduleVersion?: string; +} diff --git a/packages/server-utils/test/integrations/mastra/error-capture.test.ts b/packages/server-utils/test/integrations/mastra/error-capture.test.ts index d0bd26dd7335..2ee0f7e368c3 100644 --- a/packages/server-utils/test/integrations/mastra/error-capture.test.ts +++ b/packages/server-utils/test/integrations/mastra/error-capture.test.ts @@ -1,3 +1,4 @@ +import type { OrchestrionChannelContext } from '../../../src/orchestrion/types'; import { tracingChannel } from 'node:diagnostics_channel'; import { GLOBAL_OBJ, setCurrentClient } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -5,7 +6,7 @@ import { mastraIntegration } from '../../../src/integrations/mastra'; import { CHANNELS } from '../../../src/orchestrion/channels'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; -const channel = tracingChannel<{ error: unknown; arguments: unknown[] }>(CHANNELS.MASTRA_EXECUTE_WITH_CONTEXT); +const channel = tracingChannel(CHANNELS.MASTRA_EXECUTE_WITH_CONTEXT); describe('mastraIntegration error capture', () => { let client: TestClient; diff --git a/packages/server-utils/test/integrations/postgres/postgres.test.ts b/packages/server-utils/test/integrations/postgres/postgres.test.ts index 376edf60c883..db64ef9e137c 100644 --- a/packages/server-utils/test/integrations/postgres/postgres.test.ts +++ b/packages/server-utils/test/integrations/postgres/postgres.test.ts @@ -1,3 +1,4 @@ +import type { OrchestrionChannelContext } from '../../../src/orchestrion/types'; import { AsyncLocalStorage } from 'node:async_hooks'; import { tracingChannel } from 'node:diagnostics_channel'; import type { Client, Scope, Span } from '@sentry/core'; @@ -73,11 +74,6 @@ function makeSpan(): Span { return { end: vi.fn(), setStatus: vi.fn(), setAttributes: vi.fn() } as unknown as Span; } -interface ChannelContext { - arguments: unknown[]; - self?: unknown; -} - describe('postgresIntegration', () => { let startInactiveSpanSpy: MockInstance; let getActiveSpanSpy: MockInstance; @@ -113,7 +109,10 @@ describe('postgresIntegration', () => { const CONNECTION = { database: 'tests', host: 'localhost', port: 5432, user: 'tim' }; it('query: builds a `pg.query` span with db attributes and the orchestrion origin', async () => { - const ctx: ChannelContext = { arguments: ['SELECT * FROM "User"'], self: { connectionParameters: CONNECTION } }; + const ctx: OrchestrionChannelContext = { + arguments: ['SELECT * FROM "User"'], + self: { connectionParameters: CONNECTION }, + }; await tracingChannel(CHANNELS.PG_QUERY).tracePromise(async () => ({ rows: [] }), ctx); @@ -138,7 +137,7 @@ describe('postgresIntegration', () => { }); it('query: records the prepared-statement name as `db.postgresql.plan`', async () => { - const ctx: ChannelContext = { + const ctx: OrchestrionChannelContext = { arguments: [{ name: 'select-user-by-email', text: 'SELECT * FROM "User" WHERE "email" = $1', values: ['x'] }], self: { connectionParameters: CONNECTION }, }; @@ -159,7 +158,7 @@ describe('postgresIntegration', () => { }); it('query: sets error status and ends the span when the query rejects', async () => { - const ctx: ChannelContext = { arguments: ['SELECT 1'], self: { connectionParameters: CONNECTION } }; + const ctx: OrchestrionChannelContext = { arguments: ['SELECT 1'], self: { connectionParameters: CONNECTION } }; await expect( tracingChannel(CHANNELS.PG_QUERY).tracePromise(async () => { @@ -172,7 +171,7 @@ describe('postgresIntegration', () => { }); it('connect: builds a `pg.connect` span with no origin (defaults to manual)', async () => { - const ctx: ChannelContext = { arguments: [], self: { connectionParameters: CONNECTION } }; + const ctx: OrchestrionChannelContext = { arguments: [], self: { connectionParameters: CONNECTION } }; await tracingChannel(CHANNELS.PG_CONNECT).tracePromise(async () => undefined, ctx); @@ -193,7 +192,7 @@ describe('postgresIntegration', () => { }); it('pool connect: builds a `pg-pool.connect` span with masked connection string + pool attributes', async () => { - const ctx: ChannelContext = { + const ctx: OrchestrionChannelContext = { arguments: [], self: { options: { @@ -229,7 +228,7 @@ describe('postgresIntegration', () => { }); it('pool connect: falls back to the explicit `port` when the connection string omits it (OTel parity)', async () => { - const ctx: ChannelContext = { + const ctx: OrchestrionChannelContext = { arguments: [], self: { options: { @@ -254,7 +253,7 @@ describe('postgresIntegration', () => { it('requireParentSpan: does not create a span when there is no active span', async () => { getActiveSpanSpy.mockReturnValue(undefined); - const ctx: ChannelContext = { arguments: ['SELECT 1'], self: { connectionParameters: CONNECTION } }; + const ctx: OrchestrionChannelContext = { arguments: ['SELECT 1'], self: { connectionParameters: CONNECTION } }; await tracingChannel(CHANNELS.PG_QUERY).tracePromise(async () => ({ rows: [] }), ctx); From 84d63780e31fb258d9f5bcd7c0677bb7e39ff803 Mon Sep 17 00:00:00 2001 From: Sigrid <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:26:50 +0200 Subject: [PATCH 10/84] fix(profiling-node): Stop the trace lifecycle profiler when spans are unsampled (#25210) With `profileLifecycle: 'trace'`, the profiler counted every `spanStart` and `spanEnd`. Unsampled child spans emit `spanStart` but never `spanEnd`, so the count never got back to 0. The profiler kept running, and Node never exited. - Trace mode now tracks only sampled spans, by span ID. - A stop just before the next 60s chunk started did not prevent that chunk, so profiling started again (manual and trace mode). A stop now cancels the scheduled chunk. - New debug logs show skipped unsampled traces and cancelled chunks. A fake-timer test stops profiling in the gap between two chunks and checks that no new chunk starts. Fixes #25174 --- packages/profiling-node/src/integration.ts | 78 +++++-- .../profiling-node/test/integration.test.ts | 190 ++++++++++++++++++ 2 files changed, 253 insertions(+), 15 deletions(-) diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index a158019ee036..0832bd379c11 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -1,6 +1,15 @@ /* eslint-disable max-lines */ import type { Event, IntegrationFn, ProfileChunk, ProfilingIntegration } from '@sentry/core'; -import { consoleSandbox, debug, defineIntegration, getCurrentScope, getGlobalScope, uuid4 } from '@sentry/core'; +import { + consoleSandbox, + debug, + defineIntegration, + getCurrentScope, + getGlobalScope, + getRootSpan, + spanIsSampled, + uuid4, +} from '@sentry/core'; import type { NodeClient } from '@sentry/node'; import { CpuProfilerBindings, ProfileFormat } from '@sentry/node-cpu-profiler'; import { isMainThread } from 'worker_threads'; @@ -24,6 +33,7 @@ class ContinuousProfiler { private _profilerId: string | undefined; private _client: NodeClient | undefined = undefined; private _chunkData: ChunkData | undefined = undefined; + private _pendingChunkRestart: NodeJS.Immediate | undefined = undefined; private _profileLifecycle: 'manual' | 'trace' | undefined = undefined; private _sampled: boolean | undefined = undefined; private _sessionSamplingRate: number | undefined = undefined; @@ -95,7 +105,7 @@ class ContinuousProfiler { } private _startProfiler(): void { - if (this._chunkData !== undefined) { + if (this._isSessionRunning()) { DEBUG_BUILD && debug.log('[Profiling] Profile session already running, no-op.'); return; } @@ -125,7 +135,7 @@ class ContinuousProfiler { return; } - if (!this._chunkData) { + if (!this._isSessionRunning()) { DEBUG_BUILD && debug.log('[Profiling] No profile session running, no-op.'); return; } @@ -151,19 +161,30 @@ class ContinuousProfiler { return; } - let activeSpanCounter = 0; - this._client.on('spanStart', _span => { - if (activeSpanCounter === 0) { + // Unsampled child spans emit `spanStart` but never `spanEnd`, so only sampled spans are tracked. + const activeSpanIds = new Set(); + this._client.on('spanStart', span => { + if (!spanIsSampled(span)) { + if (DEBUG_BUILD && span === getRootSpan(span)) { + debug.log('[Profiling] Not profiling trace because its root span was not sampled.'); + } + return; + } + if (activeSpanIds.size === 0) { this._startChunkProfiling(); } - activeSpanCounter++; + activeSpanIds.add(span.spanContext().spanId); }); - this._client.on('spanEnd', _span => { - if (activeSpanCounter === 1) { + this._client.on('spanEnd', span => { + const spanId = span.spanContext().spanId; + if (!activeSpanIds.has(spanId)) { + return; + } + activeSpanIds.delete(spanId); + if (activeSpanIds.size === 0) { this._stopChunkProfiling(); } - activeSpanCounter--; }); } @@ -194,6 +215,10 @@ class ContinuousProfiler { * Stops profiling of the current chunks and flushes the profile to Sentry */ private _stopChunkProfiling(): void { + // The chunk timer stops the current chunk before it schedules the next one, so a stop that lands + // in between must also cancel that restart, or profiling continues with nothing left to profile. + this._cancelPendingChunkRestart(); + if (!this._chunkData) { DEBUG_BUILD && debug.log('[Profiling] No chunk data found, no-op.'); return; @@ -257,10 +282,6 @@ class ContinuousProfiler { } this._flush(chunk); - // Depending on the profile and stack sizes, stopping the profile and converting - // the format may negatively impact the performance of the application. To avoid - // blocking for too long, enqueue the next chunk start inside the next macrotask. - // clear current chunk this._resetChunkData(); } @@ -305,13 +326,40 @@ class ContinuousProfiler { DEBUG_BUILD && debug.log(`[Profiling] Stopping profiling chunk: ${chunk.id}`); this._stopChunkProfiling(); DEBUG_BUILD && debug.log('[Profiling] Starting new profiling chunk.'); - setImmediate(this._restartChunkProfiling.bind(this)); + // Depending on the profile and stack sizes, stopping the profile and converting + // the format may negatively impact the performance of the application. To avoid + // blocking for too long, enqueue the next chunk start inside the next macrotask. + this._pendingChunkRestart = setImmediate(() => { + this._pendingChunkRestart = undefined; + this._restartChunkProfiling(); + }); }, CHUNK_INTERVAL_MS); // Unref timeout so it doesn't keep the process alive. chunk.timer.unref(); } + /** + * A session is also running in the gap after the chunk timer stopped one chunk and before the next one starts. + */ + private _isSessionRunning(): boolean { + return this._chunkData !== undefined || this._pendingChunkRestart !== undefined; + } + + /** + * Cancels the next chunk if the chunk timer scheduled one that has not started yet. + */ + private _cancelPendingChunkRestart(): void { + if (!this._pendingChunkRestart) { + return; + } + + clearImmediate(this._pendingChunkRestart); + this._pendingChunkRestart = undefined; + DEBUG_BUILD && + debug.log('[Profiling] Profiling stopped before the next chunk started, cancelled the scheduled chunk.'); + } + /** * Attaches profiling information to spans that were started * during a profiling session. diff --git a/packages/profiling-node/test/integration.test.ts b/packages/profiling-node/test/integration.test.ts index b7c24525419d..cff99664dc78 100644 --- a/packages/profiling-node/test/integration.test.ts +++ b/packages/profiling-node/test/integration.test.ts @@ -1,4 +1,5 @@ import type { Transport } from '@sentry/core'; +import { debug } from '@sentry/core'; import * as Sentry from '@sentry/node'; import type { NodeClientOptions } from '@sentry/node/build/types/types'; import { CpuProfilerBindings } from '@sentry/node-cpu-profiler'; @@ -54,6 +55,21 @@ describe('ProfilingIntegration', () => { expect(stopProfilingSpy).toHaveBeenCalled(); }); + it('logs a no-op when stopping without a running profile session', () => { + const [client] = makeSpanProfilingClient({ + profileLifecycle: 'manual', + profileSessionSampleRate: 1, + }); + Sentry.setCurrentClient(client); + client.init(); + + const debugLogSpy = vi.spyOn(debug, 'log'); + + Sentry.profiler.stopProfiler(); + + expect(debugLogSpy).toHaveBeenCalledWith('[Profiling] No profile session running, no-op.'); + }); + it('calling start and stop while profile session is running does nothing', () => { const [client] = makeSpanProfilingClient({ profileLifecycle: 'manual', @@ -305,6 +321,90 @@ describe('ProfilingIntegration', () => { expect(stopProfilingSpy).toHaveBeenCalled(); }); + it('logs when it skips a trace because the root span is not sampled', () => { + const [client] = makeSpanProfilingClient({ + profileLifecycle: 'trace', + profileSessionSampleRate: 1, + tracesSampleRate: 0, + }); + + Sentry.setCurrentClient(client); + client.init(); + + const debugLogSpy = vi.spyOn(debug, 'log'); + + Sentry.startInactiveSpan({ name: 'GET /users/:id' }); + + expect(debugLogSpy).toHaveBeenCalledWith( + '[Profiling] Not profiling trace because its root span was not sampled.', + ); + }); + + it('does not log skipped unsampled child spans', () => { + const [client] = makeSpanProfilingClient({ + profileLifecycle: 'trace', + profileSessionSampleRate: 1, + tracesSampleRate: 0, + }); + + Sentry.setCurrentClient(client); + client.init(); + + const rootSpan = Sentry.startInactiveSpan({ name: 'GET /users/:id' }); + const debugLogSpy = vi.spyOn(debug, 'log'); + + Sentry.startInactiveSpan({ name: 'SELECT * FROM users', parentSpan: rootSpan }); + + expect(debugLogSpy).not.toHaveBeenCalledWith( + '[Profiling] Not profiling trace because its root span was not sampled.', + ); + }); + + it('does not start the profiler for unsampled spans', () => { + const [client] = makeSpanProfilingClient({ + profileLifecycle: 'trace', + profileSessionSampleRate: 1, + tracesSampleRate: 0, + }); + + Sentry.setCurrentClient(client); + client.init(); + + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + + Sentry.startSpan({ name: 'root' }, () => { + Sentry.startSpan({ name: 'child' }, () => {}); + }); + + expect(startProfilingSpy).not.toHaveBeenCalled(); + }); + + it('stops the profiler after the last sampled span ends, ignoring unsampled spans', () => { + const [client] = makeSpanProfilingClient({ + profileLifecycle: 'trace', + profileSessionSampleRate: 1, + tracesSampler: ({ name }) => (name === 'sampled' ? 1 : 0), + }); + + Sentry.setCurrentClient(client); + client.init(); + + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + + Sentry.startSpan({ name: 'unsampled' }, () => { + Sentry.startSpan({ name: 'unsampled-child' }, () => {}); + }); + + Sentry.startSpan({ name: 'sampled' }, () => { + Sentry.startSpan({ name: 'sampled-child' }, () => {}); + expect(startProfilingSpy).toHaveBeenCalledTimes(1); + expect(stopProfilingSpy).not.toHaveBeenCalled(); + }); + + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + }); + describe('envelope', () => { beforeEach(() => { vi.useRealTimers(); @@ -345,6 +445,96 @@ describe('ProfilingIntegration', () => { }); }); }); + + describe('chunk restart', () => { + const lifecycles = [ + { + profileLifecycle: 'manual', + startProfiling: () => { + Sentry.profiler.startProfiler(); + return () => Sentry.profiler.stopProfiler(); + }, + }, + { + profileLifecycle: 'trace', + startProfiling: () => { + const span = Sentry.startInactiveSpan({ name: 'GET /users/:id' }); + return () => span.end(); + }, + }, + ] as const; + + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it.each(lifecycles)( + 'does not start a new chunk when $profileLifecycle profiling stops before the scheduled restart', + ({ profileLifecycle, startProfiling }) => { + const [client] = makeSpanProfilingClient({ profileLifecycle, profileSessionSampleRate: 1 }); + Sentry.setCurrentClient(client); + client.init(); + + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + const debugLogSpy = vi.spyOn(debug, 'log'); + const stopProfiling = startProfiling(); + vi.advanceTimersToNextTimer(); + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + + stopProfiling(); + vi.runOnlyPendingTimers(); + + expect(startProfilingSpy).toHaveBeenCalledTimes(1); + expect(debugLogSpy).toHaveBeenCalledWith( + '[Profiling] Profiling stopped before the next chunk started, cancelled the scheduled chunk.', + ); + }, + ); + + it('does not start a second chunk when manual profiling starts between two chunks', () => { + const [client] = makeSpanProfilingClient({ profileLifecycle: 'manual', profileSessionSampleRate: 1 }); + Sentry.setCurrentClient(client); + client.init(); + + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + Sentry.profiler.startProfiler(); + vi.advanceTimersToNextTimer(); + + Sentry.profiler.startProfiler(); + vi.runOnlyPendingTimers(); + + expect(startProfilingSpy).toHaveBeenCalledTimes(2); + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + }); + + it.each(lifecycles)( + 'does not report a cancelled chunk when $profileLifecycle profiling stops after the restart', + ({ profileLifecycle, startProfiling }) => { + const [client] = makeSpanProfilingClient({ profileLifecycle, profileSessionSampleRate: 1 }); + Sentry.setCurrentClient(client); + client.init(); + + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const debugLogSpy = vi.spyOn(debug, 'log'); + const stopProfiling = startProfiling(); + vi.advanceTimersToNextTimer(); + vi.runOnlyPendingTimers(); + expect(startProfilingSpy).toHaveBeenCalledTimes(2); + + stopProfiling(); + + expect(debugLogSpy).not.toHaveBeenCalledWith( + '[Profiling] Profiling stopped before the next chunk started, cancelled the scheduled chunk.', + ); + }, + ); + }); }); describe('NODE_VERSION', () => { From 83e3b70dd456fa65f6a90bec231299e24a527d59 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 9 Oct 2026 11:29:36 +0200 Subject: [PATCH 11/84] test(node): Port HTTP options and hooks to span streaming (#25195) Exercise HTTP hooks, ignored requests, and static-asset handling with streamed spans. Part of #24135 --------- Co-authored-by: GPT-6 --- .../httpIntegration/instrument-options.mjs | 11 +- .../server-ignoreIncomingRequests.js | 9 +- .../server-ignoreOutgoingRequests.js | 3 +- .../server-ignoreStaticAssets.js | 12 +- .../httpIntegration/server-outgoingHooks.js | 1 - .../server-traceStaticAssets.js | 1 - .../suites/tracing/httpIntegration/test.ts | 168 ++++++++++-------- 7 files changed, 117 insertions(+), 88 deletions(-) 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(); From 668a513a62f6cd1722dc91bf1a35e3297324fce7 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 9 Oct 2026 11:37:41 +0200 Subject: [PATCH 12/84] ci: Install Playwright in the background during E2E app builds (#25207) E2E jobs installed Playwright after the app build. This uses the new [parallel steps](https://github.blog/changelog/2026-06-25-actions-steps-can-now-be-run-in-parallel/) to install the root workspace's Playwright version in the background while the app builds. Apps on a different version still get theirs installed after the build. This saves about 16-19s per E2E job. The background install starts after the artifact downloads on purpose. Starting it next to `download-artifact` made about 5% of jobs fail with "Unexpected end of JSON input". Refs #25134 --------- Co-authored-by: Claude Opus 5.5 --- .github/actions/install-playwright/action.yml | 12 ++++++++ .github/workflows/build.yml | 30 +++++++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/.github/actions/install-playwright/action.yml b/.github/actions/install-playwright/action.yml index d2f1fd723b1a..6c8ea05d3c91 100644 --- a/.github/actions/install-playwright/action.yml +++ b/.github/actions/install-playwright/action.yml @@ -7,6 +7,15 @@ inputs: cwd: description: 'The working directory to run Playwright in.' default: '.' + installed-version: + description: + 'A Playwright version that an earlier run of this action already installed. Skips the install if it matches.' + default: '' + +outputs: + version: + description: 'The resolved Playwright version, empty if Playwright is not installed or the install was skipped.' + value: ${{ steps.playwright-version.outputs.version }} runs: using: 'composite' @@ -18,8 +27,11 @@ runs: id: playwright-version run: | version=$(node -e "let v = ''; try { v = require('@playwright/test/package.json').version; } catch {} process.stdout.write(String(v));") + if [ "$version" = "$INSTALLED_VERSION" ]; then version=''; fi echo "version=$version" >> "$GITHUB_OUTPUT" shell: bash + env: + INSTALLED_VERSION: ${{ inputs.installed-version }} working-directory: ${{ inputs.cwd }} - name: Restore cached playwright binaries diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9a4147ac902e..658fd862749c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1092,6 +1092,16 @@ jobs: name: build-layer-output path: ${{ env.BUILD_LAYER_PATH }} + # Installs the root workspace's Playwright version while the app builds. Most apps use the same version. + # The app's own version is only known after its build, so it gets installed then if it differs. + # Started after the artifact downloads: starting next to them made `download-artifact` fail to read the event file. + - name: Install Playwright + id: install-playwright + background: true + uses: ./.github/actions/install-playwright + with: + browsers: chromium + - name: Prepare e2e tests run: yarn test:prepare working-directory: dev-packages/e2e-tests @@ -1117,11 +1127,14 @@ jobs: env: SENTRY_E2E_WORKSPACE_ROOT: ${{ github.workspace }} - - name: Install Playwright + - wait: [install-playwright] + + - name: Install Playwright for the app's version uses: ./.github/actions/install-playwright with: browsers: chromium cwd: ${{ runner.temp }}/test-application + installed-version: ${{ steps.install-playwright.outputs.version }} - name: Run E2E test working-directory: ${{ runner.temp }}/test-application @@ -1221,6 +1234,16 @@ jobs: name: build-tarball-output path: ${{ env.TARBALL_ARTIFACT_DOWNLOAD_PATH }} + # Installs the root workspace's Playwright version while the app builds. Most apps use the same version. + # The app's own version is only known after its build, so it gets installed then if it differs. + # Started after the artifact downloads: starting next to them made `download-artifact` fail to read the event file. + - name: Install Playwright + id: install-playwright + background: true + uses: ./.github/actions/install-playwright + with: + browsers: chromium + - name: Prepare E2E tests run: yarn test:prepare working-directory: dev-packages/e2e-tests @@ -1244,11 +1267,14 @@ jobs: timeout-minutes: 7 run: ${{ matrix.build-command || 'pnpm test:build' }} - - name: Install Playwright + - wait: [install-playwright] + + - name: Install Playwright for the app's version uses: ./.github/actions/install-playwright with: browsers: chromium cwd: ${{ runner.temp }}/test-application + installed-version: ${{ steps.install-playwright.outputs.version }} - name: Run E2E test working-directory: ${{ runner.temp }}/test-application From 4422cef113bcaae1cde729daa68043564d8b5b51 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 9 Oct 2026 11:41:05 +0200 Subject: [PATCH 13/84] test(node): Port outgoing HTTP client spans to streaming (#25192) Exercise outgoing request hooks, header collection, query handling, and fetch errors through streamed spans. Preserve the existing basic HTTP and fetch transaction suites as static coverage, along with the request and correlation checks in the migrated cases. Part of #24135 --------- Co-authored-by: GPT-6 --- .../node-suites/excludes.ts | 2 +- .../node-suites/excludes.ts | 2 +- .../instrument.mjs | 1 - .../scenario-fetch.mjs | 0 .../test.ts | 3 +- .../fetch-basic-streamed/instrument.mjs | 1 - .../fetch-error/instrument.mjs | 1 - .../http-client-spans/fetch-error/test.ts | 27 +++++--- .../fetch-forward-request-hook/instrument.mjs | 1 - .../fetch-forward-request-hook/test.ts | 46 +++++++------ .../instrument.mjs | 1 - .../fetch-headers-to-span-attributes/test.ts | 46 ++++++++----- .../fetch-strip-query/instrument.mjs | 1 - .../fetch-strip-query/test.ts | 67 ++++++++++++------- .../http-strip-query/instrument.mjs | 1 - .../http-strip-query/test.ts | 62 ++++++++++------- 16 files changed, 155 insertions(+), 107 deletions(-) rename dev-packages/node-integration-tests/suites/tracing/{http-client-span-streamed => http-client-span}/instrument.mjs (91%) rename dev-packages/node-integration-tests/suites/tracing/{http-client-span-streamed => http-client-span}/scenario-fetch.mjs (100%) rename dev-packages/node-integration-tests/suites/tracing/{http-client-span-streamed => http-client-span}/test.ts (90%) diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts index 427f5cc0c2cd..495ca29dec35 100644 --- a/dev-packages/bun-integration-tests/node-suites/excludes.ts +++ b/dev-packages/bun-integration-tests/node-suites/excludes.ts @@ -39,7 +39,7 @@ const NO_HTTP_SERVER_SPANS = [ // project runs these suites, except the ones in `SENTRY_BUN_EXCLUDE`. const NO_FETCH_INSTRUMENTATION = [ 'suites/tracing/double-baggage/**', - 'suites/tracing/http-client-span-streamed/test.ts', + 'suites/tracing/http-client-span/test.ts', 'suites/tracing/http-client-spans/fetch-basic-streamed/test.ts', 'suites/tracing/http-client-spans/fetch-basic/test.ts', 'suites/tracing/http-client-spans/fetch-error/test.ts', diff --git a/dev-packages/deno-integration-tests/node-suites/excludes.ts b/dev-packages/deno-integration-tests/node-suites/excludes.ts index 36098da333df..e5ec281a7a52 100644 --- a/dev-packages/deno-integration-tests/node-suites/excludes.ts +++ b/dev-packages/deno-integration-tests/node-suites/excludes.ts @@ -9,7 +9,7 @@ const NODE_ONLY = ['suites/anr/test.ts', 'suites/breadcrumbs/**', 'suites/thread // spans, breadcrumbs or headers of outgoing `fetch` requests. const NO_FETCH_INSTRUMENTATION = [ 'suites/tracing/double-baggage/**', - 'suites/tracing/http-client-span-streamed/test.ts', + 'suites/tracing/http-client-span/test.ts', 'suites/tracing/http-client-spans/fetch-basic-streamed/test.ts', 'suites/tracing/http-client-spans/fetch-basic/test.ts', 'suites/tracing/http-client-spans/fetch-error/test.ts', diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-span-streamed/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/http-client-span/instrument.mjs similarity index 91% rename from dev-packages/node-integration-tests/suites/tracing/http-client-span-streamed/instrument.mjs rename to dev-packages/node-integration-tests/suites/tracing/http-client-span/instrument.mjs index b56505ef5e2d..3a69e61ceb90 100644 --- a/dev-packages/node-integration-tests/suites/tracing/http-client-span-streamed/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-span/instrument.mjs @@ -6,6 +6,5 @@ Sentry.init({ release: '1.0', tracesSampleRate: 1.0, transport: loggingTransport, - traceLifecycle: 'stream', clientReportFlushInterval: 1_000, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-span-streamed/scenario-fetch.mjs b/dev-packages/node-integration-tests/suites/tracing/http-client-span/scenario-fetch.mjs similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing/http-client-span-streamed/scenario-fetch.mjs rename to dev-packages/node-integration-tests/suites/tracing/http-client-span/scenario-fetch.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-span-streamed/test.ts b/dev-packages/node-integration-tests/suites/tracing/http-client-span/test.ts similarity index 90% rename from dev-packages/node-integration-tests/suites/tracing/http-client-span-streamed/test.ts rename to dev-packages/node-integration-tests/suites/tracing/http-client-span/test.ts index d5187a062b08..11e3e07fd557 100644 --- a/dev-packages/node-integration-tests/suites/tracing/http-client-span-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-span/test.ts @@ -1,7 +1,7 @@ import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; -describe('http.client span with streaming enabled', () => { +describe('http.client span without a local parent', () => { afterAll(() => { cleanupChildProcesses(); }); @@ -18,6 +18,7 @@ describe('http.client span with streaming enabled', () => { ); expect(httpClientSpan).toBeDefined(); + expect(httpClientSpan?.is_segment).toBe(true); // The URL path is high cardinality, so a streamed span name keeps only the domain. expect(httpClientSpan?.name).toBe('GET localhost'); expect(httpClientSpan?.attributes['url.domain']?.value).toBe('localhost'); diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-basic-streamed/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-basic-streamed/instrument.mjs index b52ee5c5fdf1..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-basic-streamed/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-basic-streamed/instrument.mjs @@ -5,6 +5,5 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, - traceLifecycle: 'stream', transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-error/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-error/instrument.mjs index 170ad6f6a702..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-error/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-error/instrument.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, diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-error/test.ts b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-error/test.ts index dcd890b1d766..27a9a0ba941e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-error/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-error/test.ts @@ -1,3 +1,5 @@ +import { HTTP_CLIENT } from '@sentry/conventions/op'; +import { SENTRY_OP, SENTRY_ORIGIN, URL_FULL } from '@sentry/conventions/attributes'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createCjsTests } from '../../../../utils/runner'; @@ -10,16 +12,21 @@ describe('outgoing fetch spans - error', () => { test('captures an errored span for a failed outgoing fetch request', async () => { await createRunner() .expect({ - transaction: { - transaction: 'test_transaction', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: expect.stringMatching(/GET http:\/\/localhost:\d+\//), - op: 'http.client', - origin: 'auto.http.node_fetch', - status: 'internal_error', - }), - ]), + span: container => { + expect(container.items.find(span => span.is_segment)?.name).toBe('test_transaction'); + expect(container.items.filter(span => !span.is_segment)).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'GET localhost', + status: 'error', + attributes: expect.objectContaining({ + [URL_FULL]: { type: 'string', value: expect.stringMatching(/http:\/\/localhost:\d+\//) }, + [SENTRY_OP]: { type: 'string', value: HTTP_CLIENT }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.http.node_fetch' }, + }), + }), + ]), + ); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-forward-request-hook/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-forward-request-hook/instrument.mjs index 04a31f91c150..8cb369af50dc 100644 --- a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-forward-request-hook/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-forward-request-hook/instrument.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, diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-forward-request-hook/test.ts b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-forward-request-hook/test.ts index 84c79873d8dd..9e533959e3e7 100644 --- a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-forward-request-hook/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-forward-request-hook/test.ts @@ -1,3 +1,5 @@ +import { HTTP_CLIENT } from '@sentry/conventions/op'; +import { HTTP_RESPONSE_STATUS_CODE, SENTRY_OP, SENTRY_ORIGIN, URL_FULL } from '@sentry/conventions/attributes'; import { createTestServer } from '@sentry-internal/test-utils'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createCjsTests } from '../../../../utils/runner'; @@ -9,7 +11,7 @@ describe('outgoing fetch spans - request/response hooks', () => { createCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { test('adds requestHook and responseHook attributes to spans of outgoing fetch requests', async () => { - expect.assertions(3); + expect.assertions(4); const [SERVER_URL, closeTestServer] = await createTestServer() .get('/api/v0', () => { @@ -29,33 +31,35 @@ describe('outgoing fetch spans - request/response hooks', () => { await createRunner() .withEnv({ SERVER_URL }) .expect({ - transaction: { - transaction: 'test_transaction', - spans: [ + span: container => { + expect(container.items.find(span => span.is_segment)?.name).toBe('test_transaction'); + expect(container.items.filter(span => !span.is_segment)).toEqual([ expect.objectContaining({ - description: expect.stringMatching(/GET .*\/api\/v0/), - op: 'http.client', - origin: 'auto.http.node_fetch', + name: 'GET localhost', status: 'ok', - data: expect.objectContaining({ - 'sentry.request.hook': '/api/v0', - 'sentry.response.hook.path': '/api/v0', - 'sentry.response.hook.status_code': 200, + attributes: expect.objectContaining({ + [URL_FULL]: { type: 'string', value: expect.stringMatching(/.*\/api\/v0/) }, + [SENTRY_OP]: { type: 'string', value: HTTP_CLIENT }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.http.node_fetch' }, + 'sentry.request.hook': { type: 'string', value: '/api/v0' }, + 'sentry.response.hook.path': { type: 'string', value: '/api/v0' }, + 'sentry.response.hook.status_code': { type: 'integer', value: 200 }, }), }), expect.objectContaining({ - description: expect.stringMatching(/GET .*\/api\/v1/), - op: 'http.client', - origin: 'auto.http.node_fetch', - status: 'not_found', - data: expect.objectContaining({ - 'sentry.request.hook': '/api/v1', - 'sentry.response.hook.path': '/api/v1', - 'sentry.response.hook.status_code': 404, - 'http.response.status_code': 404, + name: 'GET localhost', + status: 'error', + attributes: expect.objectContaining({ + [URL_FULL]: { type: 'string', value: expect.stringMatching(/.*\/api\/v1/) }, + [SENTRY_OP]: { type: 'string', value: HTTP_CLIENT }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.http.node_fetch' }, + 'sentry.request.hook': { type: 'string', value: '/api/v1' }, + 'sentry.response.hook.path': { type: 'string', value: '/api/v1' }, + 'sentry.response.hook.status_code': { type: 'integer', value: 404 }, + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 404 }, }), }), - ], + ]); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-headers-to-span-attributes/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-headers-to-span-attributes/instrument.mjs index 11ff9c8eb451..cf8fec6e7e1d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-headers-to-span-attributes/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-headers-to-span-attributes/instrument.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, diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-headers-to-span-attributes/test.ts b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-headers-to-span-attributes/test.ts index 6bfb18909525..d0bd81d8d5ea 100644 --- a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-headers-to-span-attributes/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-headers-to-span-attributes/test.ts @@ -1,3 +1,10 @@ +import { HTTP_CLIENT } from '@sentry/conventions/op'; +import { + HTTP_RESPONSE_HEADER_CONTENT_LENGTH, + SENTRY_OP, + SENTRY_ORIGIN, + URL_FULL, +} from '@sentry/conventions/attributes'; import { createTestServer } from '@sentry-internal/test-utils'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createCjsTests } from '../../../../utils/runner'; @@ -9,7 +16,7 @@ describe('outgoing fetch spans - headers to span attributes', () => { createCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { test('maps configured request & response headers to span attributes', async () => { - expect.assertions(2); + expect.assertions(3); const [SERVER_URL, closeTestServer] = await createTestServer() .get('/api/v0', headers => { @@ -20,24 +27,27 @@ describe('outgoing fetch spans - headers to span attributes', () => { await createRunner() .withEnv({ SERVER_URL }) .expect({ - transaction: { - transaction: 'test_transaction', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: expect.stringMatching(/GET .*\/api\/v0/), - op: 'http.client', - origin: 'auto.http.node_fetch', - data: expect.objectContaining({ - 'http.request.header.x-test-header': ['test-value'], - // Listed in `headersToSpanAttributes`, but the built-in denylist still wins. - 'http.request.header.authorization': ['[Filtered]'], - // Listed in `headersToSpanAttributes`, but denied via `dataCollection.httpHeaders`. - 'http.request.header.x-tenant-id': ['[Filtered]'], - 'http.response.header.x-powered-by': ['Express'], - 'http.response.header.content-length': ['[Filtered]'], + span: container => { + expect(container.items.find(span => span.is_segment)?.name).toBe('test_transaction'); + expect(container.items.filter(span => !span.is_segment)).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'GET localhost', + attributes: expect.objectContaining({ + [URL_FULL]: { type: 'string', value: expect.stringMatching(/.*\/api\/v0/) }, + [SENTRY_OP]: { type: 'string', value: HTTP_CLIENT }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.http.node_fetch' }, + 'http.request.header.x-test-header': { type: 'array', value: ['test-value'] }, + // Listed in `headersToSpanAttributes`, but the built-in denylist still wins. + 'http.request.header.authorization': { type: 'array', value: ['[Filtered]'] }, + // Listed in `headersToSpanAttributes`, but denied via `dataCollection.httpHeaders`. + 'http.request.header.x-tenant-id': { type: 'array', value: ['[Filtered]'] }, + 'http.response.header.x-powered-by': { type: 'array', value: ['Express'] }, + [HTTP_RESPONSE_HEADER_CONTENT_LENGTH]: { type: 'array', value: ['[Filtered]'] }, + }), }), - }), - ]), + ]), + ); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-strip-query/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-strip-query/instrument.mjs index 170ad6f6a702..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-strip-query/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-strip-query/instrument.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, diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-strip-query/test.ts b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-strip-query/test.ts index 8d2a20e79d9e..99b485b76141 100644 --- a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-strip-query/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-strip-query/test.ts @@ -1,3 +1,20 @@ +import { HTTP_CLIENT } from '@sentry/conventions/op'; +import { + HTTP_REQUEST_METHOD, + HTTP_RESPONSE_STATUS_CODE, + NETWORK_PEER_ADDRESS, + NETWORK_PEER_PORT, + SENTRY_KIND, + SENTRY_OP, + SENTRY_ORIGIN, + SERVER_ADDRESS, + SERVER_PORT, + URL_FULL, + URL_PATH, + URL_QUERY, + URL_SCHEME, + USER_AGENT_ORIGINAL, +} from '@sentry/conventions/attributes'; import { createTestServer } from '@sentry-internal/test-utils'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createCjsTests } from '../../../../utils/runner'; @@ -21,34 +38,34 @@ describe('outgoing fetch spans - strip query', () => { await createRunner() .withEnv({ SERVER_URL }) .expect({ - transaction: txn => { - expect(txn.transaction).toEqual('test_transaction'); - expect(txn.spans).toHaveLength(1); - expect(txn.spans?.[0]).toMatchObject({ - data: { - 'url.full': `${SERVER_URL}/api/v0/users?id=1`, - 'url.path': '/api/v0/users', - 'url.query': 'id=1', - 'url.scheme': 'http', - 'http.request.method': 'GET', - 'http.request.method_original': 'GET', - 'http.response.status_code': 200, - 'network.peer.address': '::1', - 'network.peer.port': expect.any(Number), - 'sentry.kind': 'client', - 'server.port': expect.any(Number), - 'user_agent.original': 'node', - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.node_fetch', - 'server.address': 'localhost', + span: container => { + const segment = container.items.find(span => span.is_segment); + const spans = container.items.filter(span => !span.is_segment); + expect(segment?.name).toEqual('test_transaction'); + expect(spans).toHaveLength(1); + expect(spans[0]).toMatchObject({ + attributes: { + [URL_FULL]: { type: 'string', value: `${SERVER_URL}/api/v0/users?id=1` }, + [URL_PATH]: { type: 'string', value: '/api/v0/users' }, + [URL_QUERY]: { type: 'string', value: 'id=1' }, + [URL_SCHEME]: { type: 'string', value: 'http' }, + [HTTP_REQUEST_METHOD]: { type: 'string', value: 'GET' }, + 'http.request.method_original': { type: 'string', value: 'GET' }, + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [NETWORK_PEER_ADDRESS]: { type: 'string', value: '::1' }, + [NETWORK_PEER_PORT]: { type: 'integer', value: expect.any(Number) }, + [SENTRY_KIND]: { type: 'string', value: 'client' }, + [SERVER_PORT]: { type: 'integer', value: expect.any(Number) }, + [USER_AGENT_ORIGINAL]: { type: 'string', value: 'node' }, + [SENTRY_OP]: { type: 'string', value: HTTP_CLIENT }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.http.node_fetch' }, + [SERVER_ADDRESS]: { type: 'string', value: 'localhost' }, }, - description: `GET ${SERVER_URL}/api/v0/users`, - op: 'http.client', - origin: 'auto.http.node_fetch', + name: 'GET localhost', status: 'ok', - parent_span_id: txn.contexts?.trace?.span_id, + parent_span_id: segment?.span_id, span_id: expect.stringMatching(/[a-f\d]{16}/), - trace_id: txn.contexts?.trace?.trace_id, + trace_id: segment?.trace_id, }); }, }) diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-strip-query/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-strip-query/instrument.mjs index 170ad6f6a702..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-strip-query/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-strip-query/instrument.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, diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-strip-query/test.ts b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-strip-query/test.ts index 115b2517e81f..e018a905ba28 100644 --- a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-strip-query/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-strip-query/test.ts @@ -1,3 +1,19 @@ +import { HTTP_CLIENT } from '@sentry/conventions/op'; +import { + HTTP_REQUEST_METHOD, + HTTP_RESPONSE_BODY_SIZE, + HTTP_RESPONSE_STATUS_CODE, + HTTP_RESPONSE_STATUS_TEXT, + NETWORK_PEER_ADDRESS, + NETWORK_PEER_PORT, + NETWORK_TRANSPORT, + SENTRY_KIND, + SENTRY_OP, + SENTRY_ORIGIN, + SERVER_ADDRESS, + URL_FULL, + URL_QUERY, +} from '@sentry/conventions/attributes'; import { createTestServer } from '@sentry-internal/test-utils'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createCjsTests } from '../../../../utils/runner'; @@ -21,32 +37,32 @@ describe('outgoing http spans - strip query', () => { await createRunner() .withEnv({ SERVER_URL }) .expect({ - transaction: txn => { - expect(txn.transaction).toEqual('test_transaction'); - expect(txn.spans).toHaveLength(1); - expect(txn.spans?.[0]).toMatchObject({ - data: { - 'url.full': `${SERVER_URL}/api/v0/users?id=1`, - 'http.request.method': 'GET', - 'url.query': 'id=1', - 'http.response.status_code': 200, - 'http.response.body.size': 0, - 'http.response.status_text': 'OK', - 'network.peer.address': '::1', - 'server.address': 'localhost', - 'network.peer.port': expect.any(Number), - 'network.transport': 'tcp', - 'sentry.kind': 'client', - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.client', + span: container => { + const segment = container.items.find(span => span.is_segment); + const spans = container.items.filter(span => !span.is_segment); + expect(segment?.name).toEqual('test_transaction'); + expect(spans).toHaveLength(1); + expect(spans[0]).toMatchObject({ + attributes: { + [URL_FULL]: { type: 'string', value: `${SERVER_URL}/api/v0/users?id=1` }, + [HTTP_REQUEST_METHOD]: { type: 'string', value: 'GET' }, + [URL_QUERY]: { type: 'string', value: 'id=1' }, + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [HTTP_RESPONSE_BODY_SIZE]: { type: 'integer', value: 0 }, + [HTTP_RESPONSE_STATUS_TEXT]: { type: 'string', value: 'OK' }, + [NETWORK_PEER_ADDRESS]: { type: 'string', value: '::1' }, + [SERVER_ADDRESS]: { type: 'string', value: 'localhost' }, + [NETWORK_PEER_PORT]: { type: 'integer', value: expect.any(Number) }, + [NETWORK_TRANSPORT]: { type: 'string', value: 'tcp' }, + [SENTRY_KIND]: { type: 'string', value: 'client' }, + [SENTRY_OP]: { type: 'string', value: HTTP_CLIENT }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.http.client' }, }, - description: `GET ${SERVER_URL}/api/v0/users`, - op: 'http.client', - origin: 'auto.http.client', + name: 'GET localhost', status: 'ok', - parent_span_id: txn.contexts?.trace?.span_id, + parent_span_id: segment?.span_id, span_id: expect.stringMatching(/[a-f\d]{16}/), - trace_id: txn.contexts?.trace?.trace_id, + trace_id: segment?.trace_id, }); }, }) From ee6df73b4a100167bbfc7e45ddd23339b59ed9f4 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 9 Oct 2026 11:41:21 +0200 Subject: [PATCH 14/84] test(node): Port HTTP request suites to span streaming (#25191) Run the HTTP request suites with the default span-streaming lifecycle. Part of #24135 --------- Co-authored-by: GPT-6 Co-authored-by: Lukas Stracke --- .../requests/http-breadcrumbs/instrument.mjs | 1 - .../requests/http-maxed-out-sockets/instrument.mjs | 1 - .../requests/http-maxed-out-sockets/test.ts | 4 ++-- .../http-no-trace-propagation/instrument.mjs | 1 - .../http-no-tracing-no-spans/instrument.mjs | 1 - .../requests/http-no-tracing/instrument.mjs | 1 - .../http-sampled-no-active-span/instrument.mjs | 1 - .../tracing/requests/http-sampled/instrument.mjs | 1 - .../suites/tracing/requests/http-sampled/test.ts | 4 ++-- .../tracing/requests/http-unsampled/instrument.mjs | 1 - .../tracing/requests/traceparent/instrument.mjs | 1 - .../suites/tracing/requests/traceparent/test.ts | 14 ++++++++------ 12 files changed, 12 insertions(+), 19 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-breadcrumbs/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/http-breadcrumbs/instrument.mjs index 3e95fc68c6bc..dfe7b95bfeb7 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/http-breadcrumbs/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-breadcrumbs/instrument.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', tracePropagationTargets: [/\/v0/, 'v1'], diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-maxed-out-sockets/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/http-maxed-out-sockets/instrument.mjs index 170ad6f6a702..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/http-maxed-out-sockets/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-maxed-out-sockets/instrument.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, diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-maxed-out-sockets/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/http-maxed-out-sockets/test.ts index 0dbe7867110f..1f3f507c699f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/http-maxed-out-sockets/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-maxed-out-sockets/test.ts @@ -21,8 +21,8 @@ describe('outgoing http with maxed-out agent sockets', () => { await createRunner() .withEnv({ SERVER_URL }) .expect({ - transaction: { - // we're not too concerned with the actual transaction here since this is tested elsewhere + span: { + // Span contents are tested elsewhere. }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-no-trace-propagation/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/http-no-trace-propagation/instrument.mjs index 8d1cf5e660fb..3eacba8fd6c8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/http-no-trace-propagation/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-no-trace-propagation/instrument.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', integrations: [Sentry.httpIntegration({ tracePropagation: false, spans: false })], diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/instrument.mjs index 5a2c1a2ab1fd..9f713557b30a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/instrument.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', tracePropagationTargets: [/\/v0/, 'v1'], diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-no-tracing/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/http-no-tracing/instrument.mjs index 3e95fc68c6bc..dfe7b95bfeb7 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/http-no-tracing/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-no-tracing/instrument.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', tracePropagationTargets: [/\/v0/, 'v1'], diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-no-active-span/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-no-active-span/instrument.mjs index 459b690e0801..518e3f83de83 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-no-active-span/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-no-active-span/instrument.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, diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled/instrument.mjs index 459b690e0801..518e3f83de83 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled/instrument.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, diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled/test.ts index ebd6198bfd4e..fe3621236b80 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled/test.ts @@ -31,8 +31,8 @@ describe('outgoing http', () => { await createRunner() .withEnv({ SERVER_URL }) .expect({ - transaction: { - // we're not too concerned with the actual transaction here since this is tested elsewhere + span: { + // Span contents are tested elsewhere, we just want to know that a span envelope was sent }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-unsampled/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/http-unsampled/instrument.mjs index 0aa2b791f9cd..9063352bc6d9 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/http-unsampled/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-unsampled/instrument.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', tracePropagationTargets: [/\/v0/, 'v1'], diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/instrument.mjs index dbbe03ce5e0c..886377ef59e7 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/instrument.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, diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/test.ts index 1367c3ed3a07..32c7204baeb2 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/test.ts @@ -1,3 +1,5 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { HTTP_CLIENT } from '@sentry/conventions/op'; import { createTestServer } from '@sentry-internal/test-utils'; import { describe, expect } from 'vitest'; import { RUNTIME } from '../../../../utils'; @@ -23,14 +25,14 @@ describe('outgoing traceparent', () => { await createRunner() .withEnv({ SERVER_URL }) .expect({ - // The propagated `sentry-trace` must reference the `http.client` span, not the surrounding transaction span. - transaction: event => { + // The propagated `sentry-trace` must reference the `http.client` span, not the surrounding segment span. + span: container => { const propagatedSpanId = outgoingSentryTrace?.split('-')[1]; - const httpClientSpan = event.spans?.find(span => span.op === 'http.client'); + const httpClientSpan = container.items.find(span => span.attributes[SENTRY_OP]?.value === HTTP_CLIENT); expect(httpClientSpan).toBeDefined(); expect(propagatedSpanId).toBe(httpClientSpan?.span_id); - expect(propagatedSpanId).not.toBe(event.contexts?.trace?.span_id); + expect(propagatedSpanId).not.toBe(container.items.find(span => span.is_segment)?.span_id); }, }) .start() @@ -57,8 +59,8 @@ describe('outgoing traceparent', () => { await createRunner() .withEnv({ SERVER_URL }) .expect({ - transaction: { - // we're not too concerned with the actual transaction here since this is tested elsewhere + span: { + // Span contents are tested elsewhere. }, }) .start() From 861710dea2c580a65eff03879f06457f34335be9 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 9 Oct 2026 11:41:39 +0200 Subject: [PATCH 15/84] test(node): Port fetch request suites to span streaming (#25189) Run the fetch request suites with the default span-streaming lifecycle. Part of #24135 --------- Co-authored-by: GPT-6 --- .../requests/fetch-breadcrumbs/instrument.mjs | 1 - .../fetch-no-trace-propagation/instrument.mjs | 1 - .../fetch-no-tracing-no-spans/instrument.mjs | 1 - .../requests/fetch-no-tracing/instrument.mjs | 1 - .../instrument.mjs | 1 - .../fetch-sampled-no-active-span/test.ts | 30 +++++++++++++++---- .../requests/fetch-unsampled/instrument.mjs | 1 - 7 files changed, 25 insertions(+), 11 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-breadcrumbs/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-breadcrumbs/instrument.mjs index de0f2b480962..76fc52f6a863 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-breadcrumbs/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-breadcrumbs/instrument.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', tracePropagationTargets: [/\/v0/, 'v1'], diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/instrument.mjs index c5aa203f5359..1330c5c2baed 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-trace-propagation/instrument.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', integrations: [Sentry.nativeNodeFetchIntegration({ tracePropagation: false })], diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/instrument.mjs index 50d1d8e5c30d..c3c5e4fdb3de 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/instrument.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', tracePropagationTargets: [/\/v0/, 'v1'], diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-tracing/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-tracing/instrument.mjs index da94a1c511b3..0a2184dacd2e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-tracing/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-no-tracing/instrument.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', tracePropagationTargets: [/\/v0/, 'v1'], diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/instrument.mjs index 1a3c6c3e401f..99792f59545a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/instrument.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', tracePropagationTargets: [/\/v0/, 'v1'], diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts index cd7f91dd97f2..ec155b8c5c38 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts @@ -1,3 +1,5 @@ +import { SENTRY_OP, URL_FULL } from '@sentry/conventions/attributes'; +import { HTTP_CLIENT } from '@sentry/conventions/op'; import { createTestServer } from '@sentry-internal/test-utils'; import { describe, expect } from 'vitest'; import { createEsmAndCjsTests } from '../../../../utils/runner'; @@ -5,18 +7,18 @@ import { createEsmAndCjsTests } from '../../../../utils/runner'; describe('outgoing fetch', () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { test('outgoing sampled fetch requests without active span are correctly instrumented', async () => { - expect.assertions(11); + expect.assertions(21); const [SERVER_URL, closeTestServer] = await createTestServer() .get('/api/v0', headers => { expect(headers['baggage']).toEqual(expect.any(String)); - expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-0$/)); - expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-1'); }) .get('/api/v1', headers => { expect(headers['baggage']).toEqual(expect.any(String)); - expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-0$/)); - expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-1'); }) .get('/api/v2', headers => { expect(headers['baggage']).toBeUndefined(); @@ -30,6 +32,7 @@ describe('outgoing fetch', () => { await createRunner() .withEnv({ SERVER_URL }) + .unordered() .expect({ event: { exception: { @@ -42,6 +45,23 @@ describe('outgoing fetch', () => { }, }, }) + .expect({ + span: container => { + const spans = container.items; + expect(spans).toHaveLength(4); + expect(spans.map(span => span.attributes[URL_FULL]?.value).sort()).toEqual([ + `${SERVER_URL}/api/v0`, + `${SERVER_URL}/api/v1`, + `${SERVER_URL}/api/v2`, + `${SERVER_URL}/api/v3`, + ]); + + for (const span of spans) { + expect(span.attributes[SENTRY_OP]?.value).toBe(HTTP_CLIENT); + expect(span.is_segment).toBe(true); + } + }, + }) .start() .completed(); closeTestServer(); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/instrument.mjs index 0aa2b791f9cd..9063352bc6d9 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/instrument.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', tracePropagationTargets: [/\/v0/, 'v1'], From 148960a0e3c33f48723e0d45dffb7e2b41f5c2ec Mon Sep 17 00:00:00 2001 From: "javascript-sdk-gitflow[bot]" <255134079+javascript-sdk-gitflow[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:56:48 +0200 Subject: [PATCH 16/84] chore(size-limit): weekly auto-bump (#25213) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Size limit auto-bump | Entry | Old limit | New limit | Δ | | --- | --- | --- | --- | | @sentry/browser (incl. Tracing, Replay with Canvas) | 101 KB | 102 KB | +1 KB | | @sentry/browser (incl. Tracing, Replay, Feedback) | 114 KB | 115 KB | +1 KB | | @sentry/browser (incl. Logs) | 36 KB | 37 KB | +1 KB | | @sentry/react (incl. Tracing) | 59 KB | 60 KB | +1 KB | | @sentry/remix (Remix 3 client bundle) | 61 KB | 62 KB | +1 KB | | CDN Bundle (incl. Tracing) | 57 KB | 58 KB | +1 KB | | CDN Bundle (incl. Tracing, Logs, Metrics) | 59 KB | 60 KB | +1 KB | | CDN Bundle (incl. Tracing, Replay) | 95 KB | 96 KB | +1 KB | | CDN Bundle (incl. Tracing, Replay, Feedback) | 101 KB | 102 KB | +1 KB | | CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) | 103 KB | 104 KB | +1 KB | | CDN Bundle (incl. Tracing) - uncompressed | 160 KB | 161 KB | +1 KB | | CDN Bundle (incl. Logs, Metrics) - uncompressed | 104 KB | 105 KB | +1 KB | | CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed | 166 KB | 167 KB | +1 KB | | CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed | 234 KB | 235 KB | +1 KB | | CDN Bundle (incl. Tracing, Replay) - uncompressed | 280 KB | 281 KB | +1 KB | | CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed | 286 KB | 287 KB | +1 KB | | CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed | 293 KB | 295 KB | +2 KB | | CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed | 299 KB | 301 KB | +2 KB | | @sentry/sveltekit (client) | 57 KB | 58 KB | +1 KB | | @sentry/node | 151 KB | 156 KB | +5 KB | | @sentry/node - without tracing | 99 KB | 100 KB | +1 KB | | @sentry/node - without channel injection | 130 KB | 135 KB | +5 KB | | @sentry/aws-serverless | 107 KB | 108 KB | +1 KB | | @sentry/cloudflare (withSentry) - minified | 209 KiB | 210 KiB | +1 KiB | | @sentry/cloudflare (withSentry) | 512 KiB | 514 KiB | +2 KiB | | @sentry/nextjs/cloudflare (withSentry) - minified | 223 KiB | 228 KiB | +5 KiB | Co-authored-by: chargome <20254395+chargome@users.noreply.github.com> --- .size-limit.js | 52 +++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 6e085d4f6e52..b276522b06e3 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -121,7 +121,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/prod/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'), gzip: true, - limit: '101 KB', + limit: '102 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -129,7 +129,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/prod/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'feedbackIntegration'), gzip: true, - limit: '114 KB', + limit: '115 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -169,7 +169,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/prod/index.js', import: createImport('init', 'logger'), gzip: true, - limit: '36 KB', + limit: '37 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -196,7 +196,7 @@ module.exports = [ import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'), ignore: ['react/jsx-runtime'], gzip: true, - limit: '59 KB', + limit: '60 KB', disablePlugins: ['@size-limit/esbuild'], }, // Vue SDK (ESM) @@ -235,7 +235,7 @@ module.exports = [ path: 'packages/remix/build/esm/v3/client-bundle.js', import: '*', gzip: true, - limit: '61 KB', + limit: '62 KB', disablePlugins: ['@size-limit/esbuild'], }, // Browser CDN bundles @@ -250,7 +250,7 @@ module.exports = [ name: 'CDN Bundle (incl. Tracing)', path: createCDNPath('bundle.tracing.min.js'), gzip: true, - limit: '57 KB', + limit: '58 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -264,7 +264,7 @@ module.exports = [ name: 'CDN Bundle (incl. Tracing, Logs, Metrics)', path: createCDNPath('bundle.tracing.logs.metrics.min.js'), gzip: true, - limit: '59 KB', + limit: '60 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -278,7 +278,7 @@ module.exports = [ name: 'CDN Bundle (incl. Tracing, Replay)', path: createCDNPath('bundle.tracing.replay.min.js'), gzip: true, - limit: '95 KB', + limit: '96 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -292,14 +292,14 @@ module.exports = [ name: 'CDN Bundle (incl. Tracing, Replay, Feedback)', path: createCDNPath('bundle.tracing.replay.feedback.min.js'), gzip: true, - limit: '101 KB', + limit: '102 KB', disablePlugins: ['@size-limit/esbuild'], }, { name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)', path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'), gzip: true, - limit: '103 KB', + limit: '104 KB', disablePlugins: ['@size-limit/esbuild'], }, // browser CDN bundles (non-gzipped) @@ -316,7 +316,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.min.js'), gzip: false, brotli: false, - limit: '160 KB', + limit: '161 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -324,7 +324,7 @@ module.exports = [ path: createCDNPath('bundle.logs.metrics.min.js'), gzip: false, brotli: false, - limit: '104 KB', + limit: '105 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -332,7 +332,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.logs.metrics.min.js'), gzip: false, brotli: false, - limit: '166 KB', + limit: '167 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -340,7 +340,7 @@ module.exports = [ path: createCDNPath('bundle.replay.logs.metrics.min.js'), gzip: false, brotli: false, - limit: '234 KB', + limit: '235 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -348,7 +348,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.replay.min.js'), gzip: false, brotli: false, - limit: '280 KB', + limit: '281 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -356,7 +356,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.replay.logs.metrics.min.js'), gzip: false, brotli: false, - limit: '286 KB', + limit: '287 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -364,7 +364,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.replay.feedback.min.js'), gzip: false, brotli: false, - limit: '293 KB', + limit: '295 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -372,7 +372,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'), gzip: false, brotli: false, - limit: '299 KB', + limit: '301 KB', disablePlugins: ['@size-limit/esbuild'], }, // Next.js SDK (ESM) @@ -392,7 +392,7 @@ module.exports = [ import: createImport('init'), ignore: ['$app/stores'], gzip: true, - limit: '57 KB', + limit: '58 KB', disablePlugins: ['@size-limit/esbuild'], }, // Core SDK subpath entry points (ESM) @@ -419,7 +419,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '151 KB', + limit: '156 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -443,7 +443,7 @@ module.exports = [ path: 'packages/node/build/esm/index.js', import: createImport('initWithoutDefaultIntegrations', 'getDefaultIntegrationsWithoutPerformance'), gzip: true, - limit: '99 KB', + limit: '100 KB', disablePlugins: ['@size-limit/esbuild'], ignore: [...builtinModules, ...nodePrefixedBuiltinModules], modifyWebpackConfig: function (config) { @@ -465,7 +465,7 @@ module.exports = [ path: 'packages/node/build/esm/index.js', import: createImport('init'), gzip: true, - limit: '130 KB', + limit: '135 KB', disablePlugins: ['@size-limit/esbuild'], ignore: [...builtinModules, ...nodePrefixedBuiltinModules], modifyWebpackConfig: function (config) { @@ -486,7 +486,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '107 KB', + limit: '108 KB', disablePlugins: ['@size-limit/esbuild'], }, // Cloudflare SDK (ESM) - compressed, minified to match `wrangler deploy --dry-run --minify` output @@ -497,7 +497,7 @@ module.exports = [ ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: false, brotli: false, - limit: '209 KiB', + limit: '210 KiB', disablePlugins: ['@size-limit/webpack'], webpack: false, modifyEsbuildConfig: function (config) { @@ -517,7 +517,7 @@ module.exports = [ ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: false, brotli: false, - limit: '512 KiB', + limit: '514 KiB', disablePlugins: ['@size-limit/webpack'], webpack: false, modifyEsbuildConfig: function (config) { @@ -542,7 +542,7 @@ module.exports = [ ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: false, brotli: false, - limit: '223 KiB', + limit: '228 KiB', disablePlugins: ['@size-limit/webpack'], webpack: false, modifyEsbuildConfig: function (config) { From cb34995af083147991d37712d162b8c1674b8c4b Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 9 Oct 2026 12:15:31 +0200 Subject: [PATCH 17/84] test(node): Run sampling propagation tests with default streaming (#25221) Run sample-rate and sample-rand propagation scenarios with the default streaming lifecycle. Part of #24140 Co-authored-by: GPT-6 --- .../suites/tracing/sample-rand-propagation/server.js | 1 - .../tracing/sample-rate-propagation/no-tracing-enabled/server.js | 1 - .../tracing/sample-rate-propagation/tracesSampleRate-0/server.js | 1 - .../tracing/sample-rate-propagation/tracesSampleRate/server.js | 1 - .../tracing/sample-rate-propagation/tracesSampler/server.js | 1 - 5 files changed, 5 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rand-propagation/server.js b/dev-packages/node-integration-tests/suites/tracing/sample-rand-propagation/server.js index 26699a3d642d..9203829df912 100644 --- a/dev-packages/node-integration-tests/suites/tracing/sample-rand-propagation/server.js +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rand-propagation/server.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', transport: loggingTransport, tracesSampleRate: 0.00000001, // It's important that this is not 1, so that we also check logic for NonRecordingSpans, which is usually the edge-case diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/server.js b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/server.js index 828606a12514..683b646104f4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/server.js +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/server.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', transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/server.js b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/server.js index 453a43dd48f5..c94638c4491b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/server.js +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/server.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', transport: loggingTransport, tracesSampleRate: 0, diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/server.js b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/server.js index 54a61482e577..96f9b84e4ffd 100644 --- a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/server.js +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/server.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', transport: loggingTransport, tracesSampleRate: 0.69, diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/server.js b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/server.js index e7c89eda341b..f057a870d0df 100644 --- a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/server.js +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/server.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', transport: loggingTransport, tracesSampler: ({ inheritOrSampleWith }) => { From e144978ba8f5e175096cdb3a290df4625bf63129 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 9 Oct 2026 12:17:34 +0200 Subject: [PATCH 18/84] test(core): Fix flaky span end timestamp test (#25211) Since #24903 a span's end time is its start time plus the `performance.now()` duration, which can differ from a `timestampInSeconds()` value taken mid-span by fractions of a microsecond in either direction. Basically because a double can't reliably hold the very minimal time difference here. This PR relaxes the test assertion a bit while still asserting that the span ended at the current time. --------- Co-authored-by: Claude Opus 5.5 (1M context) --- packages/core/test/lib/tracing/sentrySpan.test.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/core/test/lib/tracing/sentrySpan.test.ts b/packages/core/test/lib/tracing/sentrySpan.test.ts index 521d30b63ae8..15ebe588d4c1 100644 --- a/packages/core/test/lib/tracing/sentrySpan.test.ts +++ b/packages/core/test/lib/tracing/sentrySpan.test.ts @@ -12,7 +12,13 @@ import { markSpanAsTracerProviderSpan } from '../../../src/tracing/utils'; import { withStaticSpan } from '../../../src/tracing/spans/beforeSendSpan'; import type { Envelope } from '../../../src/types/envelope'; import type { Span, SpanJSON } from '../../../src/types/span'; -import { getRootSpan, spanToStaticSpanJSON, TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED } from '../../../src/utils/spanUtils'; +import { + getRootSpan, + spanToJSON, + spanToStaticSpanJSON, + TRACE_FLAG_NONE, + TRACE_FLAG_SAMPLED, +} from '../../../src/utils/spanUtils'; import { timestampInSeconds } from '../../../src/utils/time'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; @@ -588,7 +594,9 @@ describe('SentrySpan', () => { const now = timestampInSeconds(); span.end(); - expect(spanToStaticSpanJSON(span).timestamp).toBeGreaterThanOrEqual(now); + const { start_timestamp, end_timestamp } = spanToJSON(span); + expect(end_timestamp).toBeGreaterThanOrEqual(start_timestamp); + expect(end_timestamp).toBeCloseTo(now, 2); }); it('works with endTimestamp in seconds', () => { From 9ef8a0ec61cc08e76909ce4a5e973b89cc565ec7 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 9 Oct 2026 12:41:00 +0200 Subject: [PATCH 19/84] ref(server-utils): Export the pg query span helpers (#25163) Exports the pg query-channel binding, the connection attribute builder and `PgConnectionParams` so a driver that bundles its own copy of pg can emit the same span shape. The Neon integration in #25164 is the first consumer. The deprecated `db.connection_string` attribute moves behind an opt-in. Only the pg integration sets it, for parity with its OTel predecessor, so new callers describe the connection with `server.address` and `server.port` alone. No behavior change for pg. Refs #25144 Co-authored-by: Claude Fable 5.1 --- .../server-utils/src/integrations/postgres.ts | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/packages/server-utils/src/integrations/postgres.ts b/packages/server-utils/src/integrations/postgres.ts index c1a2980943db..9dc4e62bf01e 100644 --- a/packages/server-utils/src/integrations/postgres.ts +++ b/packages/server-utils/src/integrations/postgres.ts @@ -65,7 +65,7 @@ interface PgChannelContext extends OrchestrionChannelContext { _sentryCallerScope?: Scope; } -interface PgConnectionParams { +export interface PgConnectionParams { database?: string; host?: string; port?: number; @@ -92,9 +92,7 @@ const _postgresIntegration = ((options: { ignoreConnectSpans?: boolean } = {}) = }) satisfies IntegrationFn; function instrumentPostgres(options: { ignoreConnectSpans?: boolean }): void { - // Query spans: `pg`/native `Client.prototype.query`. Only this channel can return a streamable - // `Submittable` result, so it's the only one that defers span-ending to the emitter (see below). - subscribeQueryLikeChannel(CHANNELS.PG_QUERY, querySpanOptions, { deferStreamedResult: true }); + bindPgQueryChannel(CHANNELS.PG_QUERY, { origin: ORIGIN, connectionString: true }); // Connect spans, gated by `ignoreConnectSpans` (same as OTel pg). // `Client.prototype.connect` (pg + native) @@ -105,6 +103,22 @@ function instrumentPostgres(options: { ignoreConnectSpans?: boolean }): void { } } +export interface PgQuerySpanOptions { + origin: string; + // `db.connection_string` is deprecated in favor of `server.address`/`server.port`. Only the pg + // integration keeps emitting it, for parity with its OTel predecessor. + connectionString?: boolean; +} + +/** + * Query spans for a channel carrying `pg` `Client.prototype.query` calls, from `pg` itself or from + * a copy bundled into another driver (`@neondatabase/serverless`). Only this call can return a + * streamable `Submittable` result, so it's the only one that defers span-ending to the emitter. + */ +export function bindPgQueryChannel(channelName: string, options: PgQuerySpanOptions): void { + subscribeQueryLikeChannel(channelName, ctx => querySpanOptions(ctx, options), { deferStreamedResult: true }); +} + /** * Subscribe to a pg tracing-channel and manage a span across its lifecycle. * Shared by the query/connect/pool-connect channels. They differ only in how @@ -172,7 +186,10 @@ function subscribeQueryLikeChannel( ); } -function querySpanOptions(ctx: PgChannelContext): { name: string; attributes: SpanAttributes } { +function querySpanOptions( + ctx: PgChannelContext, + { origin, connectionString }: PgQuerySpanOptions, +): { name: string; attributes: SpanAttributes } { const params = (ctx.self as { connectionParameters?: PgConnectionParams } | undefined)?.connectionParameters ?? {}; const queryConfig = extractQueryConfig(ctx.arguments); const client = getClient(); @@ -186,8 +203,8 @@ function querySpanOptions(ctx: PgChannelContext): { name: string; attributes: Sp name, attributes: { [SENTRY_OP]: DB, - ...getConnectionAttributes(params), - [SENTRY_ORIGIN]: ORIGIN, + ...(connectionString ? getPgConnectionAttributes(params) : getConnectionAttributes(params)), + [SENTRY_ORIGIN]: origin, [DB_QUERY_TEXT]: queryText || undefined, [DB_QUERY_SUMMARY]: querySummary, [ATTR_PG_PLAN]: typeof queryConfig?.name === 'string' ? queryConfig.name : undefined, @@ -198,7 +215,7 @@ function querySpanOptions(ctx: PgChannelContext): { name: string; attributes: Sp function connectSpanOptions(ctx: PgChannelContext): { name: string; attributes: SpanAttributes } { const params = (ctx.self as { connectionParameters?: PgConnectionParams } | undefined)?.connectionParameters ?? {}; // No origin set -> defaults to 'manual' - return { name: SPAN_CONNECT, attributes: { [SENTRY_OP]: DB, ...getConnectionAttributes(params) } }; + return { name: SPAN_CONNECT, attributes: { [SENTRY_OP]: DB, ...getPgConnectionAttributes(params) } }; } function poolConnectSpanOptions(ctx: PgChannelContext): { name: string; attributes: SpanAttributes } { @@ -225,10 +242,10 @@ function extractQueryConfig(args: unknown[]): { text: string; name?: unknown } | return undefined; } -function getConnectionAttributes(params: PgConnectionParams): SpanAttributes { +/** The `db.*`/`server.*` attributes for a pg-style set of connection parameters. */ +export function getConnectionAttributes(params: PgConnectionParams): SpanAttributes { return { [DB_SYSTEM_NAME]: DB_SYSTEM_POSTGRESQL, - [ATTR_DB_CONNECTION_STRING]: getConnectionString(params), [DB_NAMESPACE]: params.database, [DB_USER]: params.user, [SERVER_ADDRESS]: params.host, @@ -236,6 +253,10 @@ function getConnectionAttributes(params: PgConnectionParams): SpanAttributes { }; } +function getPgConnectionAttributes(params: PgConnectionParams): SpanAttributes { + return { ...getConnectionAttributes(params), [ATTR_DB_CONNECTION_STRING]: getConnectionString(params) }; +} + function getPoolConnectionAttributes(opts: PgPoolOptions): SpanAttributes { let url: URL | undefined; try { From 541c8cf4c75d380b7abc73fb115d5f1719f3e4bc Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 9 Oct 2026 12:41:00 +0200 Subject: [PATCH 20/84] feat(server-utils): Add the orchestrion config for @neondatabase/serverless (#25176) Adds the orchestrion config for `@neondatabase/serverless`, so the bundler plugins and the runtime hook can inject channels into it. The integration that subscribes to those channels follows in #25164 and registers the config there, which keeps every layer's CI green on its own. The package ships two minified esbuild bundles with mangled identifiers that differ between CJS and ESM, so the name-based matchers find nothing. The selectors key on property names and string literals that survive minification: pg's `_pulseQueryQueue` sibling method for `Client.prototype.query`, the `Neon-Connection-String` header literal for the HTTP executor, and the `resolvedURL` return shape for the connection resolver. `@neondatabase/serverless` becomes a dev dependency of server-utils for a test that transforms and executes the published bundles, so a Neon release that reshapes them fails CI. The version range is capped at `<2`. Stacked on #25163. Refs #25144 Co-authored-by: Claude Fable 5.1 --- packages/server-utils/package.json | 1 + .../src/orchestrion/config/neon.ts | 51 ++++++++ .../test/orchestrion/neon-bundle.test.ts | 121 ++++++++++++++++++ yarn.lock | 5 + 4 files changed, 178 insertions(+) create mode 100644 packages/server-utils/src/orchestrion/config/neon.ts create mode 100644 packages/server-utils/test/orchestrion/neon-bundle.test.ts diff --git a/packages/server-utils/package.json b/packages/server-utils/package.json index a247fd8ae307..92ccae7b686b 100644 --- a/packages/server-utils/package.json +++ b/packages/server-utils/package.json @@ -102,6 +102,7 @@ }, "devDependencies": { "@apm-js-collab/code-transformer-bundler-plugins": "^0.7.4", + "@neondatabase/serverless": "1.2.0", "@types/node": "^18.19.1", "meriyah": "^6.1.4", "vite": "^8.3.1" diff --git a/packages/server-utils/src/orchestrion/config/neon.ts b/packages/server-utils/src/orchestrion/config/neon.ts new file mode 100644 index 000000000000..011e94deacc7 --- /dev/null +++ b/packages/server-utils/src/orchestrion/config/neon.ts @@ -0,0 +1,51 @@ +import type { InstrumentationConfig } from '../apmTypes'; + +import { getModuleNames } from './module-names'; + +// `@neondatabase/serverless` ships as two minified esbuild bundles (`index.js` for CJS, +// `index.mjs` for ESM) that inline their own copy of `pg`. Class and function identifiers are +// mangled and differ between the two builds, so the name-based `functionQuery` matchers can't +// find anything. Every selector below keys on property names and string literals, which survive +// minification and are identical in both bundles. +const module = { name: '@neondatabase/serverless', versionRange: '>=1.0.0 <2', filePath: /^index\.m?js$/ }; + +export const neonConfig = [ + // The bundled pg `Client.prototype.query` (WebSocket driver). `_pulseQueryQueue` is a pg + // internal that only `Client` defines, which keeps the pool and wire-protocol `query` methods + // out. `methodName` is ignored for matching (`astQuery` wins) but makes the transformer emit a + // method wrapper that exposes `self`, which carries `connectionParameters`. + { + channelName: 'query', + module, + astQuery: + 'ClassBody:has(MethodDefinition[key.name="_pulseQueryQueue"]) > MethodDefinition[key.name="query"] > FunctionExpression', + functionQuery: { methodName: 'query', kind: 'Auto' }, + }, + // The HTTP executor nested inside `neon()`. Every HTTP query funnels through it: lazy + // `NeonQueryPromise`s call it from `then`/`catch`/`finally`, and `transaction()` calls it once + // with the whole batch, so wrapping it gives one span per request with no double counting. + { + channelName: 'http-query', + module, + astQuery: 'FunctionDeclaration[async=true]:has(Literal[value="Neon-Connection-String"])', + functionQuery: { kind: 'Async' }, + }, + // `resolveConnectionParams`, awaited by the HTTP executor. The connection string lives in the + // `neon()` closure and never reaches the executor's channel context, so the subscriber reads + // `server.address`/`db.namespace` from this call's result instead, inside the query span. + { + channelName: 'resolve-connection', + module, + astQuery: + 'FunctionDeclaration[async=true]:has(ReturnStatement > ObjectExpression > Property[key.name="resolvedURL"])', + functionQuery: { kind: 'Async' }, + }, +] satisfies InstrumentationConfig[]; + +export const neonModuleNames = getModuleNames(neonConfig); + +export const neonChannels = { + NEON_QUERY: 'orchestrion:@neondatabase/serverless:query', + NEON_HTTP_QUERY: 'orchestrion:@neondatabase/serverless:http-query', + NEON_RESOLVE_CONNECTION: 'orchestrion:@neondatabase/serverless:resolve-connection', +} as const; diff --git a/packages/server-utils/test/orchestrion/neon-bundle.test.ts b/packages/server-utils/test/orchestrion/neon-bundle.test.ts new file mode 100644 index 000000000000..e84a1d35ebe9 --- /dev/null +++ b/packages/server-utils/test/orchestrion/neon-bundle.test.ts @@ -0,0 +1,121 @@ +import { createCodeTransformer } from '@apm-js-collab/code-transformer-bundler-plugins/core'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { tracingChannel } from 'node:diagnostics_channel'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { neonChannels, neonConfig } from '../../src/orchestrion/config/neon'; + +// The selectors key on internals of Neon's minified bundles, so this runs the bundler plugins' +// code transformer over the real published files and executes the result: a release that +// reshapes the bundle fails here rather than silently dropping spans. +const nodeRequire = createRequire(import.meta.url); +const packageDir = dirname(nodeRequire.resolve('@neondatabase/serverless')); + +interface NeonModule { + neon: (connectionString: string) => { + (strings: TemplateStringsArray, ...values: unknown[]): Promise; + query: (text: string, params?: unknown[]) => Promise; + transaction: (queries: Promise[]) => Promise; + }; + neonConfig: { fetchFunction: unknown }; + Client: new (connectionString: string) => { + query: (text: string) => Promise; + end: () => Promise; + }; +} + +function neonResponse(body: unknown): () => Promise { + return () => Promise.resolve(new Response(JSON.stringify(body), { status: 200 })); +} + +const EMPTY_RESULT = { rows: [], fields: [], command: 'SELECT', rowCount: 0 }; + +// Every wrapper opens with a `hasSubscribers` guard on its channel, one per instrumented site. +function countWrappedSites(code: string, channelVariable: string): number { + return code.split(`tr_ch_apm_hasSubscribers(${channelVariable})`).length - 1; +} + +describe('neon orchestrion config on the published bundles', () => { + let outDir: string; + const events: string[] = []; + + beforeAll(() => { + outDir = mkdtempSync(join(tmpdir(), 'neon-bundle-')); + for (const name of ['query', 'http-query', 'resolve-connection']) { + tracingChannel(`orchestrion:@neondatabase/serverless:${name}`).subscribe({ + start: data => { + const ctx = data as { arguments: unknown[]; self?: { connectionParameters?: { database?: string } } }; + events.push(`${name}:start:${ctx.self?.connectionParameters?.database ?? JSON.stringify(ctx.arguments[0])}`); + }, + end: () => {}, + asyncStart: () => {}, + asyncEnd: data => { + const ctx = data as { result?: { resolvedURL?: URL } }; + events.push(`${name}:asyncEnd:${ctx.result?.resolvedURL?.hostname ?? ''}`); + }, + error: () => {}, + }); + } + }); + + afterAll(() => rmSync(outDir, { recursive: true, force: true })); + + it.each([ + ['index.js', 'cjs'], + ['index.mjs', 'esm'], + ])('instruments both drivers in %s', async (file, format) => { + const transformer = createCodeTransformer({ instrumentations: neonConfig }); + const source = join(packageDir, file); + const result = transformer.transform(readFileSync(source, 'utf8'), source); + expect(result).not.toBeNull(); + + for (const channel of Object.values(neonChannels)) { + expect(result!.code).toContain(channel); + } + // Only pg's `Client.prototype.query` gets the channel, not the pool or wire-protocol `query`. + expect(countWrappedSites(result!.code, 'tr_ch_apm$query')).toBe(1); + expect(countWrappedSites(result!.code, 'tr_ch_apm$http_query')).toBe(1); + expect(countWrappedSites(result!.code, 'tr_ch_apm$resolve_connection')).toBe(1); + // The query wrapper exposes `self`, which carries pg's `connectionParameters`. + const querySite = result!.code.indexOf('Client was passed a null or undefined query'); + expect(result!.code.slice(result!.code.lastIndexOf('__apm$ctx = {', querySite), querySite)).toContain('self: this'); + + const outFile = join(outDir, file); + writeFileSync(outFile, result!.code); + const mod: NeonModule = + format === 'cjs' ? nodeRequire(outFile) : ((await import(pathToFileURL(outFile).href)) as NeonModule); + + events.length = 0; + mod.neonConfig.fetchFunction = neonResponse(EMPTY_RESULT); + const sql = mod.neon('postgres://user:pass@ep-x.aws.neon.tech/neondb'); + await sql.query('SELECT $1', [1]); + await sql`SELECT 2`; + mod.neonConfig.fetchFunction = neonResponse({ results: [EMPTY_RESULT, EMPTY_RESULT] }); + await sql.transaction([sql`SELECT 3`, sql`SELECT 4`]); + + const client = new mod.Client('postgres://user:pass@ep-x.aws.neon.tech/neondb'); + // Never connects: the channel fires at the call, before the socket is needed. + client.query('SELECT 5').catch(() => {}); + await client.end().catch(() => {}); + + const resolve = [ + 'resolve-connection:start:"postgres://user:pass@ep-x.aws.neon.tech/neondb"', + 'resolve-connection:asyncEnd:ep-x.aws.neon.tech', + ]; + expect(events).toEqual([ + 'http-query:start:{"query":"SELECT $1","params":[1]}', + ...resolve, + 'http-query:asyncEnd:', + 'http-query:start:{"strings":["SELECT 2"],"values":[]}', + ...resolve, + 'http-query:asyncEnd:', + 'http-query:start:[{"strings":["SELECT 3"],"values":[]},{"strings":["SELECT 4"],"values":[]}]', + ...resolve, + 'http-query:asyncEnd:', + 'query:start:neondb', + ]); + }); +}); diff --git a/yarn.lock b/yarn.lock index 0feac03098c9..190af4e5ae9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5664,6 +5664,11 @@ "@emnapi/runtime" "^1.1.0" "@tybys/wasm-util" "^0.9.0" +"@neondatabase/serverless@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@neondatabase/serverless/-/serverless-1.2.0.tgz#d58553f0804eb9ddacb09662d607756b7dc74b77" + integrity sha512-tnaqgixUengZQNd9ZrS3ytQfamJzLKY8QxgqWQTNHHtB+KrFQWk77JTbbpg4RD+J3qE4eO2lVytvGDX5Kj6FdQ== + "@nestjs/common@^10.0.0": version "10.4.15" resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.4.15.tgz#27c291466d9100eb86fdbe6f7bbb4d1a6ad55f70" From 9f8f8485f6aa9b00fe2f01cbc063655c1a41e6ca Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 9 Oct 2026 12:41:01 +0200 Subject: [PATCH 21/84] feat(node): Add neonIntegration for @neondatabase/serverless (#25164) Adds `neonIntegration` for `@neondatabase/serverless`. Both drivers emit `db` spans with the pg span shape: the WebSocket `Client`/`Pool` and the HTTP `neon()` function, including Drizzle's `neon-http` and `neon-serverless` adapters. Subscribes to the channels from #25176 and registers the config, so build-time bundler plugins get the same coverage as the runtime hook. - The HTTP span wraps the executor inside `neon()`, not the lazy query promise, so `transaction()` batches get one span instead of one per statement plus one for the batch. - Connection attributes for HTTP come from the connection resolver, which runs inside the query span. The executor itself never sees the connection string. - `db.connection_string` is deprecated and not emitted. End-to-end tests follow in #25177. Stacked on #25176. Fixes #25144 Co-authored-by: Claude Fable 5.1 --- packages/astro/src/index.server.ts | 1 + packages/aws-serverless/src/index.ts | 1 + packages/bun/src/index.ts | 1 + packages/deno/src/index.ts | 1 + packages/elysia/src/index.ts | 1 + packages/google-cloud-serverless/src/index.ts | 1 + packages/node/src/index.ts | 1 + packages/server-utils/src/index.ts | 1 + .../server-utils/src/integrations/index.ts | 2 + .../server-utils/src/integrations/neon.ts | 158 ++++++++++ .../server-utils/src/orchestrion/channels.ts | 2 + .../config/channel-integration-definitions.ts | 1 + .../src/orchestrion/config/index.ts | 2 + .../test/integrations/neon.test.ts | 286 ++++++++++++++++++ .../test/orchestrion/neon-bundle.test.ts | 16 +- packages/solidstart/src/server/index.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + 17 files changed, 475 insertions(+), 2 deletions(-) create mode 100644 packages/server-utils/src/integrations/neon.ts create mode 100644 packages/server-utils/test/integrations/neon.test.ts diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 7c997a3de32f..dc77f7d5f6b9 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -109,6 +109,7 @@ export { pinoIntegration, postgresIntegration, postgresJsIntegration, + neonIntegration, prismaIntegration, openTelemetryIntegration, getOtlpTracesEndpoint, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index d5aabe372bd0..cd302d5523f0 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -129,6 +129,7 @@ export { pinoIntegration, postgresIntegration, postgresJsIntegration, + neonIntegration, processSessionIntegration, prismaIntegration, openTelemetryIntegration, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index aa7b2cfc74bf..c0cc191e2850 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -150,6 +150,7 @@ export { tediousIntegration, postgresIntegration, postgresJsIntegration, + neonIntegration, prismaIntegration, openTelemetryIntegration, getOtlpTracesEndpoint, diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index dd23caea66cc..c47dc1628c74 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -156,6 +156,7 @@ export { typesafeIntegration, postgresIntegration, postgresJsIntegration, + neonIntegration, tediousIntegration, eveConversationHook, eveIntegration, diff --git a/packages/elysia/src/index.ts b/packages/elysia/src/index.ts index 659cb35cf870..865ace1fc978 100644 --- a/packages/elysia/src/index.ts +++ b/packages/elysia/src/index.ts @@ -129,6 +129,7 @@ export { tediousIntegration, postgresIntegration, postgresJsIntegration, + neonIntegration, prismaIntegration, processSessionIntegration, hapiIntegration, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 828f77501a48..5bbf486e2342 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -129,6 +129,7 @@ export { pinoIntegration, postgresIntegration, postgresJsIntegration, + neonIntegration, prismaIntegration, openTelemetryIntegration, getOtlpTracesEndpoint, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 0dbf9dc692b9..df5d309ee9e6 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -38,6 +38,7 @@ export { typesafeIntegration, postgresIntegration, postgresJsIntegration, + neonIntegration, redisIntegration, tediousIntegration, vercelAIIntegration, diff --git a/packages/server-utils/src/index.ts b/packages/server-utils/src/index.ts index 22b7470fa7b7..41eb3845cf1a 100644 --- a/packages/server-utils/src/index.ts +++ b/packages/server-utils/src/index.ts @@ -68,6 +68,7 @@ export { mysql2Integration } from './integrations/mysql2'; export { openAIIntegration } from './integrations/openai'; export { postgresIntegration } from './integrations/postgres'; export { postgresJsIntegration } from './integrations/postgres-js'; +export { neonIntegration } from './integrations/neon'; export { tediousIntegration } from './integrations/tedious'; export { vercelAIIntegration } from './integrations/vercel-ai'; export { expressIntegration } from './integrations/express'; diff --git a/packages/server-utils/src/integrations/index.ts b/packages/server-utils/src/integrations/index.ts index a09d590e1121..5b044dd29430 100644 --- a/packages/server-utils/src/integrations/index.ts +++ b/packages/server-utils/src/integrations/index.ts @@ -27,6 +27,7 @@ import { groqIntegration } from './groq'; import { togetherAIIntegration } from './together-ai'; import { typesafeIntegration } from './typesafe'; import { postgresJsIntegration } from './postgres-js'; +import { neonIntegration } from './neon'; import { firebaseIntegration } from './firebase'; import { expressIntegration } from './express'; import { fastifyIntegration } from './fastify'; @@ -70,6 +71,7 @@ export function getTracingIntegrations(): Integration[] { togetherAIIntegration(), typesafeIntegration(), postgresJsIntegration(), + neonIntegration(), firebaseIntegration(), mcpServerIntegration(), ]; diff --git a/packages/server-utils/src/integrations/neon.ts b/packages/server-utils/src/integrations/neon.ts new file mode 100644 index 000000000000..f7e66ff953a2 --- /dev/null +++ b/packages/server-utils/src/integrations/neon.ts @@ -0,0 +1,158 @@ +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; +import { + DB_OPERATION_BATCH_SIZE, + DB_QUERY_SUMMARY, + DB_QUERY_TEXT, + DB_SYSTEM_NAME, + SENTRY_KIND, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; +import { DB } from '@sentry/conventions/op'; +import type { IntegrationFn, Span } from '@sentry/core'; +import { defineIntegration, getActiveSpan, startInactiveSpan } from '@sentry/core'; +import { sanitizeSqlQueryWithSummary } from '../utils/sql'; +import { CHANNELS } from '../orchestrion/channels'; +import { neonModuleNames } from '../orchestrion/config/neon'; +import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; +import { bindTracingChannelToSpan } from '../tracing-channel'; +import { bindPgQueryChannel, getConnectionAttributes } from './postgres'; + +const INTEGRATION_NAME = 'Neon' as const; +const ORIGIN = 'auto.db.neon'; +const DB_SYSTEM_POSTGRESQL = 'postgresql'; +const DEFAULT_PORT = 5432; +const NOOP = (): void => {}; + +// `sql.query(text, params)` builds the first shape, a tagged-template call the second. +type NeonQueryData = { query: string; params?: unknown[] } | NeonSqlTemplate; + +interface NeonSqlTemplate { + strings: readonly string[]; + values: unknown[]; + toParameterizedQuery?: () => { query: string }; +} + +interface NeonHttpQueryContext { + // `[queryData, opts]` for a single query, `[queryData[], opts[]]` for a `transaction()` batch. + arguments: unknown[]; +} + +interface NeonResolveConnectionContext { + result?: { resolvedURL?: URL }; +} + +// The HTTP query spans this integration started, so the connection-resolver subscriber only +// enriches those and never a user's own active span. +const httpQuerySpans = new WeakSet(); + +const _neonIntegration = (() => { + return { + name: INTEGRATION_NAME, + setup(client) { + invokeOrchestrionInstrumentation(client, neonModuleNames, instrumentNeon, []); + }, + }; +}) satisfies IntegrationFn; + +function instrumentNeon(): void { + // The WebSocket driver is pg's `Client`, bundled into the package, so its query channel gets + // the pg span shape. + bindPgQueryChannel(CHANNELS.NEON_QUERY, { origin: ORIGIN }); + + bindTracingChannelToSpan( + diagnosticsChannel.tracingChannel(CHANNELS.NEON_HTTP_QUERY), + data => { + const span = startInactiveSpan(httpQuerySpanOptions(data.arguments)); + httpQuerySpans.add(span); + return span; + }, + { requiresParentSpan: true }, + ); + + // The connection string is resolved inside the HTTP executor, so the query span is active + // when this settles. The executor's own context never sees the string. + diagnosticsChannel.tracingChannel(CHANNELS.NEON_RESOLVE_CONNECTION).subscribe({ + start: NOOP, + end: NOOP, + asyncStart: NOOP, + error: NOOP, + asyncEnd(data) { + const span = getActiveSpan(); + const url = data.result?.resolvedURL; + if (!span || !httpQuerySpans.has(span) || !url) { + return; + } + span.setAttributes( + getConnectionAttributes({ + host: url.hostname, + port: Number(url.port) || DEFAULT_PORT, + database: url.pathname.slice(1), + user: url.username || undefined, + }), + ); + }, + }); +} + +function httpQuerySpanOptions(args: unknown[]): Parameters[0] { + const queryData = args[0] as NeonQueryData | NeonQueryData[] | undefined; + const batchSize = Array.isArray(queryData) ? queryData.length : undefined; + // A batch gets one span whose text and summary list every statement, in order. + const statements = (Array.isArray(queryData) ? queryData : [queryData]) + .map(getQueryText) + .filter((text): text is string => !!text) + .map(text => sanitizeSqlQueryWithSummary(text)); + const queryText = joinDefined(statements.map(s => s.queryText)); + const querySummary = joinDefined(statements.map(s => s.querySummary)); + const name = querySummary || DB_SYSTEM_POSTGRESQL; + + return { + name, + attributes: { + [SENTRY_OP]: DB, + [SENTRY_ORIGIN]: ORIGIN, + [SENTRY_KIND]: 'client', + [DB_SYSTEM_NAME]: DB_SYSTEM_POSTGRESQL, + [DB_QUERY_TEXT]: queryText || undefined, + [DB_QUERY_SUMMARY]: querySummary, + [DB_OPERATION_BATCH_SIZE]: batchSize, + }, + }; +} + +function joinDefined(parts: (string | undefined)[]): string | undefined { + const defined = parts.filter((part): part is string => !!part); + return defined.length ? defined.join('; ') : undefined; +} + +function getQueryText(data: NeonQueryData | undefined): string | undefined { + if (!data || typeof data !== 'object') { + return undefined; + } + if ('query' in data && typeof data.query === 'string') { + return data.query; + } + if (!('strings' in data) || !Array.isArray(data.strings)) { + return undefined; + } + if (typeof data.toParameterizedQuery === 'function') { + try { + // Neon's own rendering also inlines `sql.unsafe()` fragments and nested templates. + return data.toParameterizedQuery().query; + } catch { + // fall through to the plain placeholder rendering + } + } + return data.strings.reduce((text, part, i) => `${text}$${i}${part}`); +} + +/** + * Diagnostics-channel-based `@neondatabase/serverless` integration. + * + * Subscribes to the channels Sentry's code transform injects into the package's bundled pg + * `Client.prototype.query` (WebSocket driver) and into the HTTP executor behind `neon()`, so both + * drivers emit `db` spans with the same shape as the `pg` integration. Requires the Sentry runtime + * hook or bundler plugin to be active. + */ +export const neonIntegration = defineIntegration(_neonIntegration); diff --git a/packages/server-utils/src/orchestrion/channels.ts b/packages/server-utils/src/orchestrion/channels.ts index 4211c8c88d5c..b5919b9eb071 100644 --- a/packages/server-utils/src/orchestrion/channels.ts +++ b/packages/server-utils/src/orchestrion/channels.ts @@ -24,6 +24,7 @@ import { mongodbChannels } from './config/mongodb'; import { mongooseChannels } from './config/mongoose'; import { mysql2Channels } from './config/mysql2'; import { mysqlChannels } from './config/mysql'; +import { neonChannels } from './config/neon'; import { nestjsChannels } from './config/nestjs'; import { openaiChannels } from './config/openai'; import { pgChannels } from './config/pg'; @@ -80,6 +81,7 @@ export const CHANNELS = { ...mongooseChannels, ...mysql2Channels, ...mysqlChannels, + ...neonChannels, ...nestjsChannels, ...openaiChannels, ...pgChannels, diff --git a/packages/server-utils/src/orchestrion/config/channel-integration-definitions.ts b/packages/server-utils/src/orchestrion/config/channel-integration-definitions.ts index 683370c0fe61..d3583a3977ec 100644 --- a/packages/server-utils/src/orchestrion/config/channel-integration-definitions.ts +++ b/packages/server-utils/src/orchestrion/config/channel-integration-definitions.ts @@ -18,6 +18,7 @@ export const CHANNEL_INTEGRATION_DEFINITIONS = [ { exportName: 'postgresIntegration', modules: ['pg', 'pg-pool'] }, { exportName: 'postgresJsIntegration', modules: ['postgres'] }, + { exportName: 'neonIntegration', modules: ['@neondatabase/serverless'] }, { exportName: 'prismaIntegration', modules: ['@prisma/orm-family-sql'] }, { exportName: 'mysqlIntegration', modules: ['mysql'] }, { exportName: 'mysql2Integration', modules: ['mysql2'] }, diff --git a/packages/server-utils/src/orchestrion/config/index.ts b/packages/server-utils/src/orchestrion/config/index.ts index 2c6a7835db1a..0cf0e363728f 100644 --- a/packages/server-utils/src/orchestrion/config/index.ts +++ b/packages/server-utils/src/orchestrion/config/index.ts @@ -30,6 +30,7 @@ import { mongodbConfig } from './mongodb'; import { mongooseConfig } from './mongoose'; import { mysql2Config } from './mysql2'; import { mysqlConfig } from './mysql'; +import { neonConfig } from './neon'; import { nestjsConfig } from './nestjs'; import { openaiConfig } from './openai'; import { pgConfig } from './pg'; @@ -86,6 +87,7 @@ export const SENTRY_INSTRUMENTATIONS: InstrumentationConfig[] = [ ...mongooseConfig, ...mysql2Config, ...mysqlConfig, + ...neonConfig, ...nestjsConfig, ...openaiConfig, ...pgConfig, diff --git a/packages/server-utils/test/integrations/neon.test.ts b/packages/server-utils/test/integrations/neon.test.ts new file mode 100644 index 000000000000..661a65cc7389 --- /dev/null +++ b/packages/server-utils/test/integrations/neon.test.ts @@ -0,0 +1,286 @@ +import { AsyncLocalStorage } from 'node:async_hooks'; +import { tracingChannel } from 'node:diagnostics_channel'; +import { + DB_NAMESPACE, + DB_OPERATION_BATCH_SIZE, + DB_QUERY_SUMMARY, + DB_QUERY_TEXT, + DB_SYSTEM_NAME, + DB_USER, + SENTRY_KIND, + SENTRY_OP, + SENTRY_ORIGIN, + SERVER_ADDRESS, + SERVER_PORT, +} from '@sentry/conventions/attributes'; +import { DB } from '@sentry/conventions/op'; +import type { Client, Scope, Span } from '@sentry/core'; +import * as SentryCore from '@sentry/core'; +import { + _INTERNAL_setSpanForScope, + getDefaultCurrentScope, + getDefaultIsolationScope, + GLOBAL_OBJ, + setAsyncContextStrategy, +} from '@sentry/core'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, type MockInstance, vi } from 'vitest'; +import { neonIntegration } from '../../src/integrations/neon'; +import { CHANNELS } from '../../src/orchestrion/channels'; + +interface TestStore { + scope: Scope; + isolationScope: Scope; +} + +// Same minimal async-context strategy as the pg tests: `bindTracingChannelToSpan` only binds +// when a `getTracingChannelBinding` exists. +function installTestAsyncContextStrategy(): void { + const asyncStorage = new AsyncLocalStorage(); + + function getScopes(): TestStore { + return asyncStorage.getStore() || { scope: getDefaultCurrentScope(), isolationScope: getDefaultIsolationScope() }; + } + + setAsyncContextStrategy({ + withScope: callback => { + const scope = getScopes().scope.clone(); + const isolationScope = getScopes().isolationScope; + return asyncStorage.run({ scope, isolationScope }, () => callback(scope)); + }, + withSetScope: (scope, callback) => { + const isolationScope = getScopes().isolationScope; + return asyncStorage.run({ scope, isolationScope }, () => callback(scope)); + }, + withIsolationScope: callback => { + const scope = getScopes().scope; + const isolationScope = getScopes().isolationScope.clone(); + return asyncStorage.run({ scope, isolationScope }, () => callback(isolationScope)); + }, + withSetIsolationScope: (isolationScope, callback) => { + const scope = getScopes().scope; + return asyncStorage.run({ scope, isolationScope }, () => callback(isolationScope)); + }, + getCurrentScope: () => getScopes().scope, + getIsolationScope: () => getScopes().isolationScope, + getTracingChannelBinding: () => ({ + asyncLocalStorage: asyncStorage, + getStoreWithActiveSpan: span => { + const scope = getScopes().scope.clone(); + const isolationScope = getScopes().isolationScope; + _INTERNAL_setSpanForScope(scope, span); + return { scope, isolationScope }; + }, + }), + }); +} + +function makeSpan(): Span { + return { end: vi.fn(), setStatus: vi.fn(), setAttributes: vi.fn() } as unknown as Span; +} + +interface ChannelContext { + arguments: unknown[]; + self?: unknown; + result?: unknown; +} + +const CONNECTION = { database: 'neondb', host: 'ep-x.aws.neon.tech', port: 5432, user: 'tim' }; + +// Shape of `@neondatabase/serverless`'s `SqlTemplate`, the query data a tagged-template call builds. +function sqlTemplate(strings: string[], values: unknown[]): unknown { + return { + strings, + values, + toParameterizedQuery() { + return { query: strings.reduce((acc, s, i) => `${acc}$${i}${s}`), params: values }; + }, + }; +} + +describe('neonIntegration', () => { + let startInactiveSpanSpy: MockInstance; + let getActiveSpanSpy: MockInstance; + let span: Span; + + beforeAll(() => { + installTestAsyncContextStrategy(); + GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { runtime: ['@neondatabase/serverless'] }; + neonIntegration().setup?.({ on: () => () => undefined } as unknown as Client); + }); + + afterAll(() => { + setAsyncContextStrategy(undefined); + delete GLOBAL_OBJ.__SENTRY_ORCHESTRION__; + }); + + beforeEach(() => { + span = makeSpan(); + startInactiveSpanSpy = vi.spyOn(SentryCore, 'startInactiveSpan').mockReturnValue(span); + getActiveSpanSpy = vi.spyOn(SentryCore, 'getActiveSpan').mockReturnValue({} as Span); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('websocket driver (`Client.prototype.query`)', () => { + it('builds a db span with the pg connection attributes and the neon origin', async () => { + const ctx: ChannelContext = { arguments: ['SELECT * FROM "User"'], self: { connectionParameters: CONNECTION } }; + + await tracingChannel(CHANNELS.NEON_QUERY).tracePromise(async () => ({ rows: [] }), ctx); + + expect(startInactiveSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'SELECT * FROM "User"', + attributes: expect.objectContaining({ + [SENTRY_OP]: DB, + [SENTRY_KIND]: 'client', + [DB_SYSTEM_NAME]: 'postgresql', + [DB_NAMESPACE]: 'neondb', + [DB_USER]: 'tim', + [SERVER_ADDRESS]: 'ep-x.aws.neon.tech', + [SERVER_PORT]: 5432, + [DB_QUERY_TEXT]: 'SELECT * FROM "User"', + [SENTRY_ORIGIN]: 'auto.db.neon', + }), + }), + ); + // The deprecated `db.connection_string` is left to the pg integration. + expect(startInactiveSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + attributes: expect.not.objectContaining({ 'db.connection_string': expect.anything() }), + }), + ); + expect(span.end).toHaveBeenCalledTimes(1); + }); + + it('does not start a span without a parent span', async () => { + getActiveSpanSpy.mockReturnValue(undefined); + const ctx: ChannelContext = { arguments: ['SELECT 1'], self: { connectionParameters: CONNECTION } }; + + await tracingChannel(CHANNELS.NEON_QUERY).tracePromise(async () => ({ rows: [] }), ctx); + + expect(startInactiveSpanSpy).not.toHaveBeenCalled(); + }); + }); + + describe('http driver (`neon()`)', () => { + it('builds a db span for `sql.query(text, params)`', async () => { + const ctx: ChannelContext = { + arguments: [{ query: 'SELECT * FROM "User" WHERE id = $1', params: [1] }, undefined], + }; + + await tracingChannel(CHANNELS.NEON_HTTP_QUERY).tracePromise(async () => [], ctx); + + expect(startInactiveSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'SELECT "User"', + attributes: expect.objectContaining({ + [SENTRY_OP]: DB, + [SENTRY_KIND]: 'client', + [DB_SYSTEM_NAME]: 'postgresql', + [DB_QUERY_TEXT]: 'SELECT * FROM "User" WHERE id = $1', + [DB_QUERY_SUMMARY]: 'SELECT "User"', + [SENTRY_ORIGIN]: 'auto.db.neon', + [DB_OPERATION_BATCH_SIZE]: undefined, + }), + }), + ); + expect(span.end).toHaveBeenCalledTimes(1); + }); + + it('renders a tagged-template query with positional placeholders', async () => { + const ctx: ChannelContext = { + arguments: [sqlTemplate(['SELECT * FROM "User" WHERE id = ', ' AND name = ', ''], [1, 'tim'])], + }; + + await tracingChannel(CHANNELS.NEON_HTTP_QUERY).tracePromise(async () => [], ctx); + + expect(startInactiveSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'SELECT "User"', + attributes: expect.objectContaining({ + [DB_QUERY_TEXT]: 'SELECT * FROM "User" WHERE id = $1 AND name = $2', + }), + }), + ); + }); + + it('builds one span for a `transaction()` batch with the batch size', async () => { + const ctx: ChannelContext = { + arguments: [ + [ + { query: 'INSERT INTO "User" ("name") VALUES ($1)', params: ['tim'] }, + sqlTemplate(['SELECT * FROM "User"'], []), + ], + [{}, {}], + ], + }; + + await tracingChannel(CHANNELS.NEON_HTTP_QUERY).tracePromise(async () => [[], []], ctx); + + expect(startInactiveSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'INSERT "User"; SELECT "User"', + attributes: expect.objectContaining({ + [DB_QUERY_TEXT]: 'INSERT INTO "User" ("name") VALUES ($1); SELECT * FROM "User"', + [DB_QUERY_SUMMARY]: 'INSERT "User"; SELECT "User"', + [DB_OPERATION_BATCH_SIZE]: 2, + [SENTRY_ORIGIN]: 'auto.db.neon', + }), + }), + ); + expect(span.end).toHaveBeenCalledTimes(1); + }); + + it('sets error status and ends the span when the request rejects', async () => { + const ctx: ChannelContext = { arguments: [{ query: 'SELECT 1', params: [] }] }; + + await expect( + tracingChannel(CHANNELS.NEON_HTTP_QUERY).tracePromise(async () => { + throw new Error('boom'); + }, ctx), + ).rejects.toThrow('boom'); + + expect(span.setStatus).toHaveBeenCalledWith({ code: expect.anything(), message: 'boom' }); + expect(span.end).toHaveBeenCalledTimes(1); + }); + + it('adds the connection attributes once the connection string resolves inside the query', async () => { + const ctx: ChannelContext = { arguments: [{ query: 'SELECT 1', params: [] }] }; + + await tracingChannel(CHANNELS.NEON_HTTP_QUERY).tracePromise(async () => { + // The resolver runs inside the query span's async context. + getActiveSpanSpy.mockReturnValue(span); + await tracingChannel(CHANNELS.NEON_RESOLVE_CONNECTION).tracePromise( + async () => ({ + resolvedConnectionString: 'postgresql://tim:secret@ep-x.aws.neon.tech/neondb?application_name=x', + resolvedURL: new URL('postgresql://tim:secret@ep-x.aws.neon.tech/neondb?application_name=x'), + }), + { arguments: ['postgresql://tim:secret@ep-x.aws.neon.tech/neondb'] }, + ); + }, ctx); + + // No port in the URL means the Postgres default, like pg's own connection parameters. + expect(span.setAttributes).toHaveBeenCalledWith({ + [DB_SYSTEM_NAME]: 'postgresql', + [DB_NAMESPACE]: 'neondb', + [DB_USER]: 'tim', + [SERVER_ADDRESS]: 'ep-x.aws.neon.tech', + [SERVER_PORT]: 5432, + }); + }); + + it('leaves spans it did not start alone when the connection string resolves', async () => { + const foreignSpan = makeSpan(); + getActiveSpanSpy.mockReturnValue(foreignSpan); + + await tracingChannel(CHANNELS.NEON_RESOLVE_CONNECTION).tracePromise( + async () => ({ resolvedURL: new URL('postgresql://tim:secret@ep-x.aws.neon.tech/neondb') }), + { arguments: [] }, + ); + + expect(foreignSpan.setAttributes).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/server-utils/test/orchestrion/neon-bundle.test.ts b/packages/server-utils/test/orchestrion/neon-bundle.test.ts index e84a1d35ebe9..79d9907467e2 100644 --- a/packages/server-utils/test/orchestrion/neon-bundle.test.ts +++ b/packages/server-utils/test/orchestrion/neon-bundle.test.ts @@ -6,7 +6,9 @@ import { dirname, join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { tracingChannel } from 'node:diagnostics_channel'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -import { neonChannels, neonConfig } from '../../src/orchestrion/config/neon'; +import { SENTRY_INSTRUMENTATIONS } from '../../src/orchestrion/config'; +import { neonChannels } from '../../src/orchestrion/config/neon'; +import { orchestrionTransformOptions } from '../../src/orchestrion/bundler/options'; // The selectors key on internals of Neon's minified bundles, so this runs the bundler plugins' // code transformer over the real published files and executes the result: a release that @@ -63,11 +65,21 @@ describe('neon orchestrion config on the published bundles', () => { afterAll(() => rmSync(outDir, { recursive: true, force: true })); + it('injects the subscriber snippet for the neon integration', () => { + const transformer = createCodeTransformer(orchestrionTransformOptions({})); + const result = transformer.transform( + readFileSync(join(packageDir, 'index.js'), 'utf8'), + join(packageDir, 'index.js'), + ); + + expect(result?.code).toContain('orchestrionModuleInjected("@neondatabase/serverless", () => neonIntegration())'); + }); + it.each([ ['index.js', 'cjs'], ['index.mjs', 'esm'], ])('instruments both drivers in %s', async (file, format) => { - const transformer = createCodeTransformer({ instrumentations: neonConfig }); + const transformer = createCodeTransformer({ instrumentations: SENTRY_INSTRUMENTATIONS }); const source = join(packageDir, file); const result = transformer.transform(readFileSync(source, 'utf8'), source); expect(result).not.toBeNull(); diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index 8c8576aeb7ae..fc4f29d21181 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -82,6 +82,7 @@ export { parameterize, postgresIntegration, postgresJsIntegration, + neonIntegration, prismaIntegration, redisIntegration, requestDataIntegration, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index cb0730b4c513..d01ea0d03fc2 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -81,6 +81,7 @@ export { parameterize, postgresIntegration, postgresJsIntegration, + neonIntegration, prismaIntegration, redisIntegration, requestDataIntegration, From cb88771e9936caef5fd20ac390d12159605c17bd Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 9 Oct 2026 12:41:01 +0200 Subject: [PATCH 22/84] test(node): Add integration tests for the Neon serverless driver (#25177) Runs both Neon drivers end to end against Neon's own proxy (`ghcr.io/timowilhelm/local-neon-http-proxy`) in front of a local Postgres, so the HTTP `/sql` and WebSocket `/v2` wire protocols are the real ones. Covers `sql.query()`, tagged templates, `transaction()` batches, a failing statement and pool queries, in ESM and CJS under the default span lifecycle. The suite is excluded on Bun like the other database suites, since `bun run` cannot inject the diagnostics channels; the Bun plugin build does run it. The pool test is Node only, because on Bun and Deno the pool's connect callback runs outside the segment's async context. `ws` is added because Node 20 has no global `WebSocket`. Stacked on #25164. Refs #25144 Co-authored-by: Claude Fable 5.1 --- .../node-suites/excludes.ts | 1 + .../node-integration-tests/package.json | 2 + .../suites/tracing/neon/docker-compose.yml | 43 +++++ .../suites/tracing/neon/instrument.mjs | 9 + .../suites/tracing/neon/scenario-http.mjs | 33 ++++ .../suites/tracing/neon/scenario-pool.mjs | 35 ++++ .../suites/tracing/neon/scenario-ws.mjs | 44 +++++ .../suites/tracing/neon/test.ts | 165 ++++++++++++++++++ .../node-integration-tests/utils/index.ts | 8 + 9 files changed, 340 insertions(+) create mode 100644 dev-packages/node-integration-tests/suites/tracing/neon/docker-compose.yml create mode 100644 dev-packages/node-integration-tests/suites/tracing/neon/instrument.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/neon/scenario-http.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/neon/scenario-pool.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/neon/scenario-ws.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/neon/test.ts diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts index 495ca29dec35..36e00ca5f35c 100644 --- a/dev-packages/bun-integration-tests/node-suites/excludes.ts +++ b/dev-packages/bun-integration-tests/node-suites/excludes.ts @@ -120,6 +120,7 @@ export const NO_AUTO_INSTRUMENTATION = [ 'suites/tracing/mysql/test.ts', 'suites/tracing/mysql2-tracing-channel/test.ts', 'suites/tracing/mysql2/test.ts', + 'suites/tracing/neon/test.ts', 'suites/tracing/openai/test.ts', 'suites/tracing/openai/v6/test.ts', 'suites/tracing/openai/v7/test.ts', diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index fa0e94ba095d..d5d4dd9d8f83 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -104,9 +104,11 @@ "rxjs": "^7.8.2", "tedious": "^19.2.1", "winston": "^3.17.0", + "ws": "^8.20.1", "yargs": "^16.2.0" }, "devDependencies": { + "@neondatabase/serverless": "1.2.0", "@opentelemetry/instrumentation-http": "0.220.0", "@sentry/conventions": "0.26.0", "@sentry-internal/test-utils": "11.6.0", diff --git a/dev-packages/node-integration-tests/suites/tracing/neon/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/neon/docker-compose.yml new file mode 100644 index 000000000000..49ce9976e3e2 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/neon/docker-compose.yml @@ -0,0 +1,43 @@ +version: '3.9' + +services: + db: + image: postgres:16 + restart: always + ports: + - '5496:5432' + environment: + POSTGRES_USER: test + POSTGRES_PASSWORD: test + POSTGRES_DB: tests + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U test -d tests'] + interval: 2s + timeout: 3s + retries: 30 + start_period: 5s + + # Neon's own proxy binary in front of the local Postgres. It serves the HTTP driver on `/sql` + # and the WebSocket driver on `/v2`, both on port 4444, so the scenarios run the real + # `@neondatabase/serverless` wire protocols against a throwaway database. + neon-proxy: + image: ghcr.io/timowilhelm/local-neon-http-proxy:main + restart: always + ports: + - '4494:4444' + environment: + PG_CONNECTION_STRING: postgres://test:test@db:5432/tests + depends_on: + db: + condition: service_healthy + healthcheck: + test: + [ + 'CMD-SHELL', + 'curl -sf -X POST http://localhost:4444/sql -H "Neon-Connection-String: + postgres://test:test@db.localtest.me:5432/tests" -d ''{"query":"SELECT 1","params":[]}''', + ] + interval: 2s + timeout: 3s + retries: 30 + start_period: 5s diff --git a/dev-packages/node-integration-tests/suites/tracing/neon/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/neon/instrument.mjs new file mode 100644 index 000000000000..46a27dd03b74 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/neon/instrument.mjs @@ -0,0 +1,9 @@ +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, + transport: loggingTransport, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/neon/scenario-http.mjs b/dev-packages/node-integration-tests/suites/tracing/neon/scenario-http.mjs new file mode 100644 index 000000000000..80469395a28e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/neon/scenario-http.mjs @@ -0,0 +1,33 @@ +import * as Sentry from '@sentry/node'; +import { waitForConnection } from '@sentry-internal/node-integration-tests'; +import { neon, neonConfig } from '@neondatabase/serverless'; + +neonConfig.fetchEndpoint = 'http://localhost:4494/sql'; + +// The connection string is never dialed by this process: the drivers only talk to the proxy, which +// connects to Postgres itself. Host and port here are what the spans report as the server. +const sql = neon('postgres://test:test@db.localtest.me:5432/tests'); + +async function run() { + await waitForConnection(() => sql`SELECT 1`); + + await Sentry.startSpan({ name: 'Test Span', op: 'test' }, async () => { + try { + await sql`CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))`; + + await sql.query('INSERT INTO "User" ("email", "name") VALUES ($1, $2)', ['tim@domain.com', 'tim']); + await sql`SELECT * FROM "User" WHERE "name" = ${'tim'}`; + + await sql.transaction([sql`SELECT "email" FROM "User"`, sql`SELECT "name" FROM "User"`]); + + // A failing query should still produce an errored span + await sql`SELECT * FROM "does_not_exist_table"`.catch(() => { + // swallow: we only care about the span it produces + }); + } finally { + await sql`DROP TABLE "User"`; + } + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/neon/scenario-pool.mjs b/dev-packages/node-integration-tests/suites/tracing/neon/scenario-pool.mjs new file mode 100644 index 000000000000..0562030fe3ce --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/neon/scenario-pool.mjs @@ -0,0 +1,35 @@ +import * as Sentry from '@sentry/node'; +import { waitForConnection } from '@sentry-internal/node-integration-tests'; +import { Client, Pool, neonConfig } from '@neondatabase/serverless'; +import ws from 'ws'; + +neonConfig.webSocketConstructor = ws; +neonConfig.wsProxy = () => 'localhost:4494/v2'; +neonConfig.useSecureWebSocket = false; +neonConfig.pipelineConnect = false; + +// The connection string is never dialed by this process: the drivers only talk to the proxy, which +// connects to Postgres itself. Host and port here are what the spans report as the server. +const connectionString = 'postgres://test:test@db.localtest.me:5432/tests'; + +async function run() { + await waitForConnection(async () => { + const probe = new Client(connectionString); + await probe.connect(); + await probe.end(); + }); + + const pool = new Pool({ connectionString }); + await Sentry.startSpan({ name: 'Test Span', op: 'test' }, async () => { + try { + await pool.query('SELECT "email", "name" FROM pg_catalog.pg_user LIMIT 1').catch(() => { + // swallow: pg_user has no "email" column, the span is what matters + }); + await pool.query('SELECT "usename" FROM pg_catalog.pg_user'); + } finally { + await pool.end(); + } + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/neon/scenario-ws.mjs b/dev-packages/node-integration-tests/suites/tracing/neon/scenario-ws.mjs new file mode 100644 index 000000000000..d4f314de0bc8 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/neon/scenario-ws.mjs @@ -0,0 +1,44 @@ +import * as Sentry from '@sentry/node'; +import { waitForConnection } from '@sentry-internal/node-integration-tests'; +import { Client, neonConfig } from '@neondatabase/serverless'; +import ws from 'ws'; + +neonConfig.webSocketConstructor = ws; +neonConfig.wsProxy = () => 'localhost:4494/v2'; +neonConfig.useSecureWebSocket = false; +neonConfig.pipelineConnect = false; + +// The connection string is never dialed by this process: the drivers only talk to the proxy, which +// connects to Postgres itself. Host and port here are what the spans report as the server. +const connectionString = 'postgres://test:test@db.localtest.me:5432/tests'; +const client = new Client(connectionString); + +async function run() { + await waitForConnection(async () => { + const probe = new Client(connectionString); + await probe.connect(); + await probe.end(); + }); + + await Sentry.startSpan({ name: 'Test Span', op: 'test' }, async () => { + try { + await client.connect(); + + await client.query( + 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))', + ); + await client.query('INSERT INTO "User" ("email", "name") VALUES ($1, $2)', ['tim@domain.com', 'tim']); + await client.query('SELECT * FROM "User"'); + + // A failing query should still produce an errored span + await client.query('SELECT * FROM "does_not_exist_table"').catch(() => { + // swallow: we only care about the span it produces + }); + } finally { + await client.query('DROP TABLE "User"'); + await client.end(); + } + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/neon/test.ts b/dev-packages/node-integration-tests/suites/tracing/neon/test.ts new file mode 100644 index 000000000000..e1310a6ff703 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/neon/test.ts @@ -0,0 +1,165 @@ +import type { SerializedStreamedSpan } from '@sentry/core'; +import { + DB_NAMESPACE, + DB_OPERATION_BATCH_SIZE, + DB_QUERY_SUMMARY, + DB_QUERY_TEXT, + DB_SYSTEM_NAME, + DB_USER, + SENTRY_KIND, + SENTRY_OP, + SENTRY_ORIGIN, + SERVER_ADDRESS, + SERVER_PORT, +} from '@sentry/conventions/attributes'; +import { DB } from '@sentry/conventions/op'; +import { afterAll, describe, expect } from 'vitest'; +import { RUNTIME, streamedAttribute as attr } from '../../../utils'; +import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; + +const ORIGIN = 'auto.db.neon'; + +const CREATE_USER_TABLE_STATEMENT = + 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))'; + +// Both drivers report the same connection attributes: the WebSocket driver from pg's +// `connectionParameters`, the HTTP driver from the resolved connection URL. +const CONNECTION_ATTRIBUTES = { + [DB_SYSTEM_NAME]: attr('postgresql'), + [DB_NAMESPACE]: attr('tests'), + [DB_USER]: attr('test'), + [SERVER_ADDRESS]: attr('db.localtest.me'), + [SERVER_PORT]: attr(5432, 'integer'), +}; + +function expectedDbSpan({ + name, + statement, + status = 'ok', + batchSize, +}: { + name: string; + statement: string; + status?: 'ok' | 'error'; + batchSize?: number; +}): unknown { + return expect.objectContaining({ + attributes: expect.objectContaining({ + ...CONNECTION_ATTRIBUTES, + [SENTRY_OP]: attr(DB), + [SENTRY_KIND]: attr('client'), + [SENTRY_ORIGIN]: attr(ORIGIN), + [DB_QUERY_TEXT]: attr(statement), + [DB_QUERY_SUMMARY]: attr(name), + ...(batchSize ? { [DB_OPERATION_BATCH_SIZE]: attr(batchSize, 'integer') } : {}), + }), + is_segment: false, + name, + parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), + status, + }); +} + +function getNeonSpans(spans: SerializedStreamedSpan[]): SerializedStreamedSpan[] { + const neonSpans = spans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === ORIGIN); + // `server.address`/`server.port` describe the connection; the deprecated string is not emitted. + expect(neonSpans.some(span => 'db.connection_string' in span.attributes)).toBe(false); + return neonSpans; +} + +describeWithDockerCompose('neon auto instrumentation', { workingDirectory: [__dirname] }, () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + describe('http driver', () => { + createEsmAndCjsTests(__dirname, 'scenario-http.mjs', 'instrument.mjs', (createTestRunner, test) => { + test('instruments `neon()` queries, templates and transactions', { timeout: 90_000 }, async () => { + const runner = createTestRunner(); + const spansPromise = runner.collectStreamedSpansUntilSegment('Test Span'); + + await runner.start().completed(); + + const spans = await spansPromise; + const segment = spans.find(span => span.is_segment && span.name === 'Test Span'); + expect(segment).toBeDefined(); + + const neonSpans = getNeonSpans(spans); + expect(neonSpans.every(span => span.parent_span_id === segment!.span_id)).toBe(true); + expect(neonSpans).toEqual([ + expectedDbSpan({ name: 'CREATE TABLE "User"', statement: CREATE_USER_TABLE_STATEMENT }), + expectedDbSpan({ name: 'INSERT "User"', statement: 'INSERT INTO "User" ("email", "name") VALUES ($1, $2)' }), + expectedDbSpan({ name: 'SELECT "User"', statement: 'SELECT * FROM "User" WHERE "name" = $1' }), + expectedDbSpan({ + name: 'SELECT "User"; SELECT "User"', + statement: 'SELECT "email" FROM "User"; SELECT "name" FROM "User"', + batchSize: 2, + }), + expectedDbSpan({ + name: 'SELECT "does_not_exist_table"', + statement: 'SELECT * FROM "does_not_exist_table"', + status: 'error', + }), + expectedDbSpan({ name: 'DROP TABLE "User"', statement: 'DROP TABLE "User"' }), + ]); + }); + }); + }); + + describe('websocket driver', () => { + createEsmAndCjsTests(__dirname, 'scenario-ws.mjs', 'instrument.mjs', (createTestRunner, test) => { + test('instruments `Client` queries', { timeout: 90_000 }, async () => { + const runner = createTestRunner(); + const spansPromise = runner.collectStreamedSpansUntilSegment('Test Span'); + + await runner.start().completed(); + + const spans = await spansPromise; + const segment = spans.find(span => span.is_segment && span.name === 'Test Span'); + expect(segment).toBeDefined(); + + const neonSpans = getNeonSpans(spans); + expect(neonSpans.every(span => span.parent_span_id === segment!.span_id)).toBe(true); + expect(neonSpans).toEqual([ + expectedDbSpan({ name: 'CREATE TABLE "User"', statement: CREATE_USER_TABLE_STATEMENT }), + expectedDbSpan({ name: 'INSERT "User"', statement: 'INSERT INTO "User" ("email", "name") VALUES ($1, $2)' }), + expectedDbSpan({ name: 'SELECT "User"', statement: 'SELECT * FROM "User"' }), + expectedDbSpan({ + name: 'SELECT "does_not_exist_table"', + statement: 'SELECT * FROM "does_not_exist_table"', + status: 'error', + }), + expectedDbSpan({ name: 'DROP TABLE "User"', statement: 'DROP TABLE "User"' }), + ]); + }); + }); + }); + + describe('websocket pool', () => { + createEsmAndCjsTests(__dirname, 'scenario-pool.mjs', 'instrument.mjs', (createTestRunner, test) => { + // On Bun and Deno the pool's connect callback runs outside the segment's async context, so + // the queries it issues have no parent span. + test.skipIf(RUNTIME !== 'node')('instruments `Pool` queries', { timeout: 90_000 }, async () => { + const runner = createTestRunner(); + const spansPromise = runner.collectStreamedSpansUntilSegment('Test Span'); + + await runner.start().completed(); + + const spans = await spansPromise; + const segment = spans.find(span => span.is_segment && span.name === 'Test Span'); + expect(segment).toBeDefined(); + + const neonSpans = getNeonSpans(spans); + expect(neonSpans.every(span => span.parent_span_id === segment!.span_id)).toBe(true); + expect(neonSpans).toEqual([ + expectedDbSpan({ + name: 'SELECT pg_catalog.pg_user', + statement: 'SELECT "email", "name" FROM pg_catalog.pg_user LIMIT ?', + status: 'error', + }), + expectedDbSpan({ name: 'SELECT pg_catalog.pg_user', statement: 'SELECT "usename" FROM pg_catalog.pg_user' }), + ]); + }); + }); + }); +}); diff --git a/dev-packages/node-integration-tests/utils/index.ts b/dev-packages/node-integration-tests/utils/index.ts index 21fdff8fe8a8..0849c4464bfc 100644 --- a/dev-packages/node-integration-tests/utils/index.ts +++ b/dev-packages/node-integration-tests/utils/index.ts @@ -98,3 +98,11 @@ export const parseEnvelope = (body: string): Array> => { export function getStringAttributeValue(value: unknown): string | undefined { return typeof value === 'string' ? value : undefined; } + +/** + * A serialized streamed-span attribute (`{ type, value }`), for strict `toEqual` assertions on + * `attributes` of a streamed span. Defaults to a string attribute. + */ +export function streamedAttribute(value: unknown, type = 'string'): { type: string; value: unknown } { + return { type, value }; +} From 4132a3491ee9b40c13264458f42ccf24d4c0c410 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 9 Oct 2026 12:45:41 +0200 Subject: [PATCH 23/84] fix(nuxt): Remove @opentelemetry/resources dev alias (#25216) In dev mode the Nuxt module aliased `@opentelemetry/resources` to `@opentelemetry/resources/build/src/index.js`. Nuxt writes every alias into the `paths` of the generated `.nuxt/tsconfig.*.json` files. Those configs have no `baseUrl`, so with `typescript: { typeCheck: true }` vue-tsc fails with TS5090 ("Non-relative paths are not allowed when 'baseUrl' is not set"). The alias was a workaround for incomplete ESM imports in `@opentelemetry/resources` (#15204). In v11 nothing in the Nuxt SDK's dependency tree uses that package anymore, so the alias can be removed instead of rewritten. Fixes #25188 --------- Co-authored-by: Claude Opus 5.5 --- packages/nuxt/src/module.ts | 4 +- packages/nuxt/src/vite/utils.ts | 24 --------- packages/nuxt/test/vite/utils.test.ts | 70 --------------------------- 3 files changed, 1 insertion(+), 97 deletions(-) diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index f1cd456b84c6..10f22891ac2d 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -7,7 +7,7 @@ import type { SentryNuxtModuleOptions } from './common/types'; import { setupNitroServer } from './vite/nitroServer'; import { setupOrchestrion } from './vite/orchestrion'; import { setupSourceMaps } from './vite/sourceMaps'; -import { addOTelCommonJSImportAlias, findDefaultSdkInitFile, resolveServerApi } from './vite/utils'; +import { findDefaultSdkInitFile, resolveServerApi } from './vite/utils'; export type ModuleOptions = SentryNuxtModuleOptions; type NuxtPageSubset = { file?: string; path: string }; @@ -97,8 +97,6 @@ export default defineNuxtModule({ setupSourceMaps(moduleOptions, nuxt, addVitePlugin); } - addOTelCommonJSImportAlias(nuxt, serverApi === 'nitro3'); - let pagesData: NuxtPageSubset[] = []; nuxt.hooks.hook('pages:extend', pages => { diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index cab39bc95f19..fc393242b682 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -224,27 +224,3 @@ export function toResolvablePath(source: string): { path: string; wasFileUrl: bo return undefined; } } - -/** - * Sets up alias to work around OpenTelemetry's incomplete ESM imports. - * https://github.com/getsentry/sentry-javascript/issues/15204 - * - * OpenTelemetry's @opentelemetry/resources package has incomplete imports missing - * the .js file extensions (like execAsync for machine-id detection). This causes module resolution - * errors in certain Nuxt configurations, particularly when local Nuxt modules in Nuxt 4 are present. - * - * @see https://nuxt.com/docs/guide/concepts/esm#aliasing-libraries - */ -export function addOTelCommonJSImportAlias(nuxt: Nuxt, isNitroV3 = false): void { - if (!nuxt.options.dev || isNitroV3) { - return; - } - - if (!nuxt.options.alias) { - nuxt.options.alias = {}; - } - - if (!nuxt.options.alias['@opentelemetry/resources']) { - nuxt.options.alias['@opentelemetry/resources'] = '@opentelemetry/resources/build/src/index.js'; - } -} diff --git a/packages/nuxt/test/vite/utils.test.ts b/packages/nuxt/test/vite/utils.test.ts index 605bf368ca9c..7d1213fdc959 100644 --- a/packages/nuxt/test/vite/utils.test.ts +++ b/packages/nuxt/test/vite/utils.test.ts @@ -3,7 +3,6 @@ import * as fs from 'fs'; import * as path from 'path'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { - addOTelCommonJSImportAlias, constructFunctionReExport, constructWrappedFunctionExportQuery, extractFunctionReexportQueryParameters, @@ -428,72 +427,3 @@ export { foo_sentryWrapped as foo }; expect(result).toBe(''); }); }); - -describe('addOTelCommonJSImportAlias', () => { - it('adds alias for @opentelemetry/resources when options.alias does not exist', () => { - const nuxtMock: Nuxt = { - options: { dev: true }, - } as unknown as Nuxt; - - addOTelCommonJSImportAlias(nuxtMock); - - expect(nuxtMock.options.alias).toEqual({ - '@opentelemetry/resources': '@opentelemetry/resources/build/src/index.js', - }); - }); - - it('adds alias for @opentelemetry/resources when options.alias already exists', () => { - const nuxtMock: Nuxt = { - options: { - dev: true, - alias: { - 'existing-alias': 'some-path', - }, - }, - } as unknown as Nuxt; - - addOTelCommonJSImportAlias(nuxtMock); - - expect(nuxtMock.options.alias).toEqual({ - 'existing-alias': 'some-path', - '@opentelemetry/resources': '@opentelemetry/resources/build/src/index.js', - }); - }); - - it('does not override existing alias for @opentelemetry/resources', () => { - const nuxtMock: Nuxt = { - options: { - dev: true, - alias: { - '@opentelemetry/resources': 'some-other-path', - }, - }, - } as unknown as Nuxt; - - addOTelCommonJSImportAlias(nuxtMock); - - expect(nuxtMock.options.alias).toEqual({ - '@opentelemetry/resources': 'some-other-path', - }); - }); - - it('does not add alias when not development mode', () => { - const nuxtMock: Nuxt = { - options: {}, - } as unknown as Nuxt; - - addOTelCommonJSImportAlias(nuxtMock); - - expect(nuxtMock.options.alias).toBeUndefined(); - }); - - it('does not add alias when in Nitro v3+ (Rolldown incompatibility)', () => { - const nuxtMock: Nuxt = { - options: { dev: true }, - } as unknown as Nuxt; - - addOTelCommonJSImportAlias(nuxtMock, true); - - expect(nuxtMock.options.alias).toBeUndefined(); - }); -}); From 19185a133e35907178be26ee4a6be94fd961a1f4 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 9 Oct 2026 12:50:58 +0200 Subject: [PATCH 24/84] fix(core): Report actual item count in client reports of envelope send failures (#25185) When sending an envelope fails (network error, 413, or a full buffer), the transport recorded every item with quantity 1. This undercounts span, log and metric discard quantities. Same for transactions that [should also](https://develop.sentry.dev/sdk/telemetry/client-reports/#span-outcomes) report `span` counts. Containers now report their `item_count`, and transactions also report `spans.length + 1` `span` outcomes, matching how the client counts dropped transactions in `beforeSend`. For transactions, we also record `span` outcomes on envelope send failures, analogously to what https://github.com/getsentry/sentry-javascript/pull/25006 adds to other discard reasons --------- Co-authored-by: Claude Opus 5.5 (1M context) --- packages/core/src/transports/base.ts | 21 ++++- .../core/test/lib/transports/base.test.ts | 91 +++++++++++++++---- 2 files changed, 87 insertions(+), 25 deletions(-) diff --git a/packages/core/src/transports/base.ts b/packages/core/src/transports/base.ts index 45553115fa64..86c52037d6f7 100644 --- a/packages/core/src/transports/base.ts +++ b/packages/core/src/transports/base.ts @@ -1,6 +1,7 @@ import { DEBUG_BUILD } from '../debug-build'; import type { EventDropReason } from '../types/clientreport'; -import type { Envelope, EnvelopeItem } from '../types/envelope'; +import type { Envelope, EnvelopeItem, EnvelopeItemType } from '../types/envelope'; +import type { Event } from '../types/event'; import type { InternalBaseTransportOptions, Transport, @@ -36,14 +37,24 @@ export function createTransport( let rateLimits: RateLimits = {}; const flush = (timeout?: number): PromiseLike => buffer.drain(timeout); + function recordItemLoss(reason: EventDropReason, [headers, payload]: EnvelopeItem, type: EnvelopeItemType): void { + const dataCategory = envelopeItemTypeToDataCategory(type); + if (type === 'transaction') { + options.recordDroppedEvent(reason, dataCategory); + // The transaction itself counts as a span, too + options.recordDroppedEvent(reason, 'span', ((payload as Event).spans?.length ?? 0) + 1); + } else { + options.recordDroppedEvent(reason, dataCategory, typeof headers.item_count === 'number' ? headers.item_count : 1); + } + } + function send(envelope: Envelope): PromiseLike { const filteredEnvelopeItems: EnvelopeItem[] = []; // Drop rate limited items from envelope forEachEnvelopeItem(envelope, (item, type) => { - const dataCategory = envelopeItemTypeToDataCategory(type); - if (isRateLimited(rateLimits, dataCategory)) { - options.recordDroppedEvent('ratelimit_backoff', dataCategory); + if (isRateLimited(rateLimits, envelopeItemTypeToDataCategory(type))) { + recordItemLoss('ratelimit_backoff', item, type); } else { filteredEnvelopeItems.push(item); } @@ -64,7 +75,7 @@ export function createTransport( return; } forEachEnvelopeItem(filteredEnvelope, (item, type) => { - options.recordDroppedEvent(reason, envelopeItemTypeToDataCategory(type)); + recordItemLoss(reason, item, type); }); }; diff --git a/packages/core/test/lib/transports/base.test.ts b/packages/core/test/lib/transports/base.test.ts index 9a2d53582686..9a2a0ef6275b 100644 --- a/packages/core/test/lib/transports/base.test.ts +++ b/packages/core/test/lib/transports/base.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; import { createTransport } from '../../../src/transports/base'; import type { ClientReport } from '../../../src/types/clientreport'; -import type { AttachmentItem, EventEnvelope, EventItem } from '../../../src/types/envelope'; +import type { AttachmentItem, Envelope, EventEnvelope, EventItem } from '../../../src/types/envelope'; import type { TransportMakeRequestResponse } from '../../../src/types/transport'; import { createClientReportEnvelope } from '../../../src/utils/clientreport'; import { createEnvelope, serializeEnvelope } from '../../../src/utils/envelope'; @@ -157,7 +157,7 @@ describe('createTransport', () => { await transport.send(ERROR_ENVELOPE); expect(requestExecutor).not.toHaveBeenCalled(); - expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'error'); + expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'error', 1); requestExecutor.mockClear(); recordDroppedEventCallback.mockClear(); @@ -199,7 +199,7 @@ describe('createTransport', () => { await transport.send(ERROR_ENVELOPE); // Error envelope should not be sent because of pending rate limit expect(requestExecutor).not.toHaveBeenCalled(); - expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'error'); + expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'error', 1); requestExecutor.mockClear(); recordDroppedEventCallback.mockClear(); @@ -242,18 +242,19 @@ describe('createTransport', () => { await transport.send(TRANSACTION_ENVELOPE); // Transaction envelope should not be sent because of pending rate limit expect(requestExecutor).not.toHaveBeenCalled(); expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'transaction'); + expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'span', 1); requestExecutor.mockClear(); recordDroppedEventCallback.mockClear(); await transport.send(ERROR_ENVELOPE); // Error envelope should not be sent because of pending rate limit expect(requestExecutor).not.toHaveBeenCalled(); - expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'error'); + expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'error', 1); requestExecutor.mockClear(); recordDroppedEventCallback.mockClear(); await transport.send(ATTACHMENT_ENVELOPE); // Attachment envelope should not be sent because of pending rate limit expect(requestExecutor).not.toHaveBeenCalled(); - expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'attachment'); + expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'attachment', 1); requestExecutor.mockClear(); recordDroppedEventCallback.mockClear(); @@ -302,12 +303,13 @@ describe('createTransport', () => { await transport.send(TRANSACTION_ENVELOPE); // Transaction envelope should not be sent because of pending rate limit expect(requestExecutor).not.toHaveBeenCalled(); expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'transaction'); + expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'span', 1); requestExecutor.mockClear(); recordDroppedEventCallback.mockClear(); await transport.send(ERROR_ENVELOPE); // Error envelope should not be sent because of pending rate limit expect(requestExecutor).not.toHaveBeenCalled(); - expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'error'); + expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'error', 1); requestExecutor.mockClear(); recordDroppedEventCallback.mockClear(); @@ -324,10 +326,35 @@ describe('createTransport', () => { expect(requestExecutor).toHaveBeenCalledTimes(1); expect(recordDroppedEventCallback).not.toHaveBeenCalled(); }); + + it('records the item count of rate limited container items', async () => { + const { retryAfterSeconds, beforeLimit, withinLimit } = setRateLimitTimes(); + const [transport, , requestExecutor, recordDroppedEventCallback] = createTestTransport({ + headers: { + 'x-sentry-rate-limits': `${retryAfterSeconds}:log_item:scope`, + 'retry-after': null, + }, + }); + + const dateNowSpy = vi.spyOn(Date, 'now').mockImplementation(() => beforeLimit); + + await transport.send(ERROR_ENVELOPE); + requestExecutor.mockClear(); + + dateNowSpy.mockImplementation(() => withinLimit); + + await transport.send( + createEnvelope({ sent_at: '123' }, [ + [{ type: 'log', item_count: 3, content_type: 'application/vnd.sentry.items.log+json' }, { items: [] }], + ] as Envelope[1]), + ); + expect(requestExecutor).not.toHaveBeenCalled(); + expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'log_item', 3); + }); }); describe('Client Reports', () => { - it('should not record outcomes when client reports fail to send', async () => { + it('does not record outcomes when client reports fail to send', async () => { expect.assertions(2); const mockRecordDroppedEventCallback = vi.fn(); @@ -347,7 +374,7 @@ describe('createTransport', () => { expect(mockRecordDroppedEventCallback).not.toHaveBeenCalled(); }); - it('should not record outcomes when client reports fail due to buffer overflow', async () => { + it('does not record outcomes when client reports fail due to buffer overflow', async () => { expect.assertions(2); const mockRecordDroppedEventCallback = vi.fn(); @@ -371,7 +398,7 @@ describe('createTransport', () => { expect(mockRecordDroppedEventCallback).not.toHaveBeenCalled(); }); - it('should record outcomes when regular events fail to send', async () => { + it('records outcomes when regular events fail to send', async () => { expect.assertions(2); const mockRecordDroppedEventCallback = vi.fn(); @@ -388,12 +415,12 @@ describe('createTransport', () => { } // recordDroppedEvent SHOULD be called for regular events - expect(mockRecordDroppedEventCallback).toHaveBeenCalledWith('network_error', 'error'); + expect(mockRecordDroppedEventCallback).toHaveBeenCalledWith('network_error', 'error', 1); }); }); describe('HTTP 413 Content Too Large', () => { - it('should record send_error outcome when receiving 413 response', async () => { + it('records send_error outcome when receiving 413 response', async () => { const mockRecordDroppedEventCallback = vi.fn(); const transport = createTransport({ recordDroppedEvent: mockRecordDroppedEventCallback }, () => @@ -405,17 +432,17 @@ describe('createTransport', () => { // Should resolve without throwing expect(result).toEqual({ statusCode: 413 }); // recordDroppedEvent SHOULD be called with send_error reason - expect(mockRecordDroppedEventCallback).toHaveBeenCalledWith('send_error', 'error'); + expect(mockRecordDroppedEventCallback).toHaveBeenCalledWith('send_error', 'error', 1); }); - it('should record send_error for each item in envelope when receiving 413', async () => { + it('records send_error for each item in envelope when receiving 413', async () => { const mockRecordDroppedEventCallback = vi.fn(); const multiItemEnvelope = createEnvelope( { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, - [{ type: 'transaction' }, { event_id: 'bb3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, + [{ type: 'transaction' }, { event_id: 'bb3ff046696b4bc6b609ce6d28fde9e2', spans: [{}, {}] }] as EventItem, ], ); @@ -425,13 +452,37 @@ describe('createTransport', () => { await transport.send(multiItemEnvelope); - // recordDroppedEvent SHOULD be called for each item - expect(mockRecordDroppedEventCallback).toHaveBeenCalledTimes(2); - expect(mockRecordDroppedEventCallback).toHaveBeenCalledWith('send_error', 'error'); + expect(mockRecordDroppedEventCallback).toHaveBeenCalledTimes(3); + expect(mockRecordDroppedEventCallback).toHaveBeenCalledWith('send_error', 'error', 1); expect(mockRecordDroppedEventCallback).toHaveBeenCalledWith('send_error', 'transaction'); + expect(mockRecordDroppedEventCallback).toHaveBeenCalledWith('send_error', 'span', 3); + }); + + it('records send_error with the item count of container items when receiving 413', async () => { + const mockRecordDroppedEventCallback = vi.fn(); + + const containerEnvelope = createEnvelope({ sent_at: '123' }, [ + [{ type: 'span', item_count: 5, content_type: 'application/vnd.sentry.items.span.v2+json' }, { items: [] }], + [{ type: 'log', item_count: 3, content_type: 'application/vnd.sentry.items.log+json' }, { items: [] }], + [ + { type: 'trace_metric', item_count: 2, content_type: 'application/vnd.sentry.items.trace-metric+json' }, + { items: [] }, + ], + ] as Envelope[1]); + + const transport = createTransport({ recordDroppedEvent: mockRecordDroppedEventCallback }, () => + resolvedSyncPromise({ statusCode: 413 }), + ); + + await transport.send(containerEnvelope); + + expect(mockRecordDroppedEventCallback).toHaveBeenCalledTimes(3); + expect(mockRecordDroppedEventCallback).toHaveBeenCalledWith('send_error', 'span', 5); + expect(mockRecordDroppedEventCallback).toHaveBeenCalledWith('send_error', 'log_item', 3); + expect(mockRecordDroppedEventCallback).toHaveBeenCalledWith('send_error', 'metric', 2); }); - it('should not record outcomes for client reports when receiving 413', async () => { + it('does not record outcomes for client reports when receiving 413', async () => { const mockRecordDroppedEventCallback = vi.fn(); const transport = createTransport({ recordDroppedEvent: mockRecordDroppedEventCallback }, () => @@ -446,7 +497,7 @@ describe('createTransport', () => { expect(mockRecordDroppedEventCallback).not.toHaveBeenCalled(); }); - it('should not apply rate limits after receiving 413', async () => { + it('does not apply rate limits after receiving 413', async () => { const mockRecordDroppedEventCallback = vi.fn(); const mockRequestExecutor = vi.fn(() => resolvedSyncPromise({ statusCode: 413 })); @@ -455,7 +506,7 @@ describe('createTransport', () => { // First request gets 413 await transport.send(ERROR_ENVELOPE); expect(mockRequestExecutor).toHaveBeenCalledTimes(1); - expect(mockRecordDroppedEventCallback).toHaveBeenCalledWith('send_error', 'error'); + expect(mockRecordDroppedEventCallback).toHaveBeenCalledWith('send_error', 'error', 1); mockRequestExecutor.mockClear(); mockRecordDroppedEventCallback.mockClear(); From cae4c95f5beac502b38aab12435276195658d6f2 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 9 Oct 2026 12:53:21 +0200 Subject: [PATCH 25/84] test(node): Use default streaming in scope and span suites (#25222) Exercise scope and span suites with the default streaming lifecycle. Part of #24140 Co-authored-by: GPT-6 --- .../node-suites/excludes.ts | 16 ++++++++-------- .../node-suites/excludes.ts | 6 +++--- .../{context-streamed => context}/scenario.ts | 1 - .../scope-contexts/scenario.ts | 1 - .../scope-contexts/test.ts | 0 .../suites/{context-streamed => context}/test.ts | 0 .../scenario.ts | 0 .../test.ts | 0 .../public-api/scopes/initialScopes/scenario.ts | 1 - .../public-api/scopes/isolationScope/scenario.ts | 1 - .../attributes/instrument.mjs | 1 - .../attributes/server.mjs | 0 .../attributes/test.ts | 2 +- .../children/instrument.mjs | 1 - .../children/server.mjs | 0 .../children/test.ts | 2 +- .../continued-trace-child/instrument.mjs | 1 - .../continued-trace-child/server.mjs | 0 .../continued-trace-child/test.ts | 2 +- .../continued-trace-http-client/instrument.mjs | 1 - .../continued-trace-http-client/server.mjs | 0 .../continued-trace-http-client/test.ts | 2 +- .../continued-trace-segment/instrument.mjs | 1 - .../continued-trace-segment/server.mjs | 0 .../continued-trace-segment/test.ts | 2 +- .../segments/instrument.mjs | 1 - .../segments/server.mjs | 0 .../segments/test.ts | 2 +- .../instrument.mjs | 1 - .../{sampling-streamed => sampling}/server.mjs | 0 .../{sampling-streamed => sampling}/test.ts | 2 +- 31 files changed, 18 insertions(+), 29 deletions(-) rename dev-packages/node-integration-tests/suites/{context-streamed => context}/scenario.ts (92%) rename dev-packages/node-integration-tests/suites/{context-streamed => context}/scope-contexts/scenario.ts (96%) rename dev-packages/node-integration-tests/suites/{context-streamed => context}/scope-contexts/test.ts (100%) rename dev-packages/node-integration-tests/suites/{context-streamed => context}/test.ts (100%) rename dev-packages/node-integration-tests/suites/public-api/{beforeSendSpan-streamed => beforeSendSpan}/scenario.ts (100%) rename dev-packages/node-integration-tests/suites/public-api/{beforeSendSpan-streamed => beforeSendSpan}/test.ts (100%) rename dev-packages/node-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/attributes/instrument.mjs (92%) rename dev-packages/node-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/attributes/server.mjs (100%) rename dev-packages/node-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/attributes/test.ts (94%) rename dev-packages/node-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/children/instrument.mjs (92%) rename dev-packages/node-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/children/server.mjs (100%) rename dev-packages/node-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/children/test.ts (97%) rename dev-packages/node-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/continued-trace-child/instrument.mjs (92%) rename dev-packages/node-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/continued-trace-child/server.mjs (100%) rename dev-packages/node-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/continued-trace-child/test.ts (96%) rename dev-packages/node-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/continued-trace-http-client/instrument.mjs (93%) rename dev-packages/node-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/continued-trace-http-client/server.mjs (100%) rename dev-packages/node-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/continued-trace-http-client/test.ts (98%) rename dev-packages/node-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/continued-trace-segment/instrument.mjs (92%) rename dev-packages/node-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/continued-trace-segment/server.mjs (100%) rename dev-packages/node-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/continued-trace-segment/test.ts (96%) rename dev-packages/node-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/segments/instrument.mjs (92%) rename dev-packages/node-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/segments/server.mjs (100%) rename dev-packages/node-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/segments/test.ts (95%) rename dev-packages/node-integration-tests/suites/tracing/{sampling-streamed => sampling}/instrument.mjs (95%) rename dev-packages/node-integration-tests/suites/tracing/{sampling-streamed => sampling}/server.mjs (100%) rename dev-packages/node-integration-tests/suites/tracing/{sampling-streamed => sampling}/test.ts (97%) diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts index 36e00ca5f35c..7eea10e62c6f 100644 --- a/dev-packages/bun-integration-tests/node-suites/excludes.ts +++ b/dev-packages/bun-integration-tests/node-suites/excludes.ts @@ -22,14 +22,14 @@ const NO_HTTP_SERVER_SPANS = [ 'suites/tracing/httpIntegration-streamed/test.ts', 'suites/tracing/httpIntegration/test.ts', 'suites/tracing/httpServerSpans-unrouted/test.ts', - 'suites/tracing/ignoreSpans-streamed/**', + 'suites/tracing/ignoreSpans/**', 'suites/tracing/meta-tags-twp-errors/test.ts', 'suites/tracing/meta-tags/test.ts', 'suites/tracing/requestData/test.ts', 'suites/tracing/sample-rand-propagation/test.ts', 'suites/tracing/sample-rate-propagation/**', 'suites/tracing/sampling-static/test.ts', - 'suites/tracing/sampling-streamed/test.ts', + 'suites/tracing/sampling/test.ts', 'suites/tracing/traceid-recycling-with-spans/test.ts', 'suites/tracing/traceid-recycling/test.ts', ]; @@ -171,7 +171,7 @@ const FETCH_INTEGRATION_DIFFERS = [ 'suites/tracing/http-client-spans/fetch-basic/test.ts', 'suites/tracing/http-client-spans/fetch-error/test.ts', 'suites/tracing/http-client-spans/fetch-strip-query/test.ts', - 'suites/tracing/ignoreSpans-streamed/continued-trace-http-client/test.ts', + 'suites/tracing/ignoreSpans/continued-trace-http-client/test.ts', 'suites/tracing/requests/fetch-breadcrumbs/test.ts', 'suites/tracing/requests/fetch-sampled-no-active-span/test.ts', ]; @@ -195,11 +195,11 @@ const NO_EXPRESS_INSTRUMENTATION = [ 'suites/sessions/crashed-session-aggregate/test.ts', 'suites/sessions/errored-session-aggregate/test.ts', 'suites/tracing/httpIntegration-streamed/test.ts', - 'suites/tracing/ignoreSpans-streamed/attributes/test.ts', - 'suites/tracing/ignoreSpans-streamed/children/test.ts', - 'suites/tracing/ignoreSpans-streamed/continued-trace-child/test.ts', - 'suites/tracing/ignoreSpans-streamed/segments/test.ts', - 'suites/tracing/sampling-streamed/test.ts', + 'suites/tracing/ignoreSpans/attributes/test.ts', + 'suites/tracing/ignoreSpans/children/test.ts', + 'suites/tracing/ignoreSpans/continued-trace-child/test.ts', + 'suites/tracing/ignoreSpans/segments/test.ts', + 'suites/tracing/sampling/test.ts', 'suites/tracing/traceid-recycling-with-spans/test.ts', ]; diff --git a/dev-packages/deno-integration-tests/node-suites/excludes.ts b/dev-packages/deno-integration-tests/node-suites/excludes.ts index e5ec281a7a52..14205893d332 100644 --- a/dev-packages/deno-integration-tests/node-suites/excludes.ts +++ b/dev-packages/deno-integration-tests/node-suites/excludes.ts @@ -16,9 +16,9 @@ const NO_FETCH_INSTRUMENTATION = [ 'suites/tracing/http-client-spans/fetch-forward-request-hook/test.ts', 'suites/tracing/http-client-spans/fetch-headers-to-span-attributes/test.ts', 'suites/tracing/http-client-spans/fetch-strip-query/test.ts', - 'suites/tracing/ignoreSpans-streamed/continued-trace-child/test.ts', - 'suites/tracing/ignoreSpans-streamed/continued-trace-http-client/test.ts', - 'suites/tracing/ignoreSpans-streamed/continued-trace-segment/test.ts', + 'suites/tracing/ignoreSpans/continued-trace-child/test.ts', + 'suites/tracing/ignoreSpans/continued-trace-http-client/test.ts', + 'suites/tracing/ignoreSpans/continued-trace-segment/test.ts', 'suites/tracing/no-parent-span-client-report/test.ts', 'suites/tracing/requests/fetch-breadcrumbs/test.ts', 'suites/tracing/requests/fetch-no-trace-propagation/test.ts', diff --git a/dev-packages/node-integration-tests/suites/context-streamed/scenario.ts b/dev-packages/node-integration-tests/suites/context/scenario.ts similarity index 92% rename from dev-packages/node-integration-tests/suites/context-streamed/scenario.ts rename to dev-packages/node-integration-tests/suites/context/scenario.ts index 7f1b5ddd053f..4eb6adf001a0 100644 --- a/dev-packages/node-integration-tests/suites/context-streamed/scenario.ts +++ b/dev-packages/node-integration-tests/suites/context/scenario.ts @@ -4,7 +4,6 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1.0, - traceLifecycle: 'stream', transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/context-streamed/scope-contexts/scenario.ts b/dev-packages/node-integration-tests/suites/context/scope-contexts/scenario.ts similarity index 96% rename from dev-packages/node-integration-tests/suites/context-streamed/scope-contexts/scenario.ts rename to dev-packages/node-integration-tests/suites/context/scope-contexts/scenario.ts index f29857444e4d..6e2e0c7cd341 100644 --- a/dev-packages/node-integration-tests/suites/context-streamed/scope-contexts/scenario.ts +++ b/dev-packages/node-integration-tests/suites/context/scope-contexts/scenario.ts @@ -4,7 +4,6 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1.0, - traceLifecycle: 'stream', transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/context-streamed/scope-contexts/test.ts b/dev-packages/node-integration-tests/suites/context/scope-contexts/test.ts similarity index 100% rename from dev-packages/node-integration-tests/suites/context-streamed/scope-contexts/test.ts rename to dev-packages/node-integration-tests/suites/context/scope-contexts/test.ts diff --git a/dev-packages/node-integration-tests/suites/context-streamed/test.ts b/dev-packages/node-integration-tests/suites/context/test.ts similarity index 100% rename from dev-packages/node-integration-tests/suites/context-streamed/test.ts rename to dev-packages/node-integration-tests/suites/context/test.ts diff --git a/dev-packages/node-integration-tests/suites/public-api/beforeSendSpan-streamed/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/beforeSendSpan/scenario.ts similarity index 100% rename from dev-packages/node-integration-tests/suites/public-api/beforeSendSpan-streamed/scenario.ts rename to dev-packages/node-integration-tests/suites/public-api/beforeSendSpan/scenario.ts diff --git a/dev-packages/node-integration-tests/suites/public-api/beforeSendSpan-streamed/test.ts b/dev-packages/node-integration-tests/suites/public-api/beforeSendSpan/test.ts similarity index 100% rename from dev-packages/node-integration-tests/suites/public-api/beforeSendSpan-streamed/test.ts rename to dev-packages/node-integration-tests/suites/public-api/beforeSendSpan/test.ts diff --git a/dev-packages/node-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts index 4a4327c15b3a..f9245b584256 100644 --- a/dev-packages/node-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts @@ -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', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts index 1e0e81cf58cb..b527d14699cc 100644 --- a/dev-packages/node-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts @@ -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', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/attributes/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/attributes/instrument.mjs similarity index 92% rename from dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/attributes/instrument.mjs rename to dev-packages/node-integration-tests/suites/tracing/ignoreSpans/attributes/instrument.mjs index 31da3f1dacfb..0db17515dbba 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/attributes/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/attributes/instrument.mjs @@ -6,7 +6,6 @@ Sentry.init({ release: '1.0', tracesSampleRate: 1.0, transport: loggingTransport, - traceLifecycle: 'stream', ignoreSpans: [{ attributes: { 'http.request.method': 'POST' } }], clientReportFlushInterval: 1_000, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/attributes/server.mjs b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/attributes/server.mjs similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/attributes/server.mjs rename to dev-packages/node-integration-tests/suites/tracing/ignoreSpans/attributes/server.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/attributes/test.ts b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/attributes/test.ts similarity index 94% rename from dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/attributes/test.ts rename to dev-packages/node-integration-tests/suites/tracing/ignoreSpans/attributes/test.ts index 22eac608d616..dbc23242a06b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/attributes/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/attributes/test.ts @@ -1,7 +1,7 @@ import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; -describe('filtering segment spans by attribute with ignoreSpans (streaming)', () => { +describe('filtering segment spans by attribute with ignoreSpans', () => { afterAll(() => { cleanupChildProcesses(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/children/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/children/instrument.mjs similarity index 92% rename from dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/children/instrument.mjs rename to dev-packages/node-integration-tests/suites/tracing/ignoreSpans/children/instrument.mjs index 266ffdc4f52c..4e6a4d0677c9 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/children/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/children/instrument.mjs @@ -6,7 +6,6 @@ Sentry.init({ release: '1.0', tracesSampleRate: 1.0, transport: loggingTransport, - traceLifecycle: 'stream', ignoreSpans: ['expressInit', /custom-to-drop/, { op: 'ignored-op' }], clientReportFlushInterval: 1_000, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/children/server.mjs b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/children/server.mjs similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/children/server.mjs rename to dev-packages/node-integration-tests/suites/tracing/ignoreSpans/children/server.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/children/test.ts b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/children/test.ts similarity index 97% rename from dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/children/test.ts rename to dev-packages/node-integration-tests/suites/tracing/ignoreSpans/children/test.ts index fefa45353102..03bac0660313 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/children/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/children/test.ts @@ -2,7 +2,7 @@ import { SENTRY_OP } from '@sentry/conventions/attributes'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; -describe('filtering child spans with ignoreSpans (streaming)', () => { +describe('filtering child spans with ignoreSpans', () => { afterAll(() => { cleanupChildProcesses(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-child/instrument.mjs similarity index 92% rename from dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/instrument.mjs rename to dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-child/instrument.mjs index 75d3d12bc730..ca7f173be631 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-child/instrument.mjs @@ -5,7 +5,6 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 0, transport: loggingTransport, - traceLifecycle: 'stream', ignoreSpans: ['ignoredMiddleware'], tracePropagationTargets: [process.env.SERVER_URL], clientReportFlushInterval: 1_000, diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/server.mjs b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-child/server.mjs similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/server.mjs rename to dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-child/server.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/test.ts b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-child/test.ts similarity index 96% rename from dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/test.ts rename to dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-child/test.ts index a9c5574810d5..2f04092502b5 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-child/test.ts @@ -3,7 +3,7 @@ import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; import { SENTRY_OP } from '@sentry/conventions/attributes'; -describe('ignoring a child of a continued server segment (streaming)', () => { +describe('ignoring a child of a continued server segment', () => { afterAll(() => { cleanupChildProcesses(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-http-client/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-http-client/instrument.mjs similarity index 93% rename from dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-http-client/instrument.mjs rename to dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-http-client/instrument.mjs index 2d534bda43f2..8d6990f70f60 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-http-client/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-http-client/instrument.mjs @@ -5,7 +5,6 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 0, transport: loggingTransport, - traceLifecycle: 'stream', ignoreSpans: [{ attributes: { 'url.path': '/outgoing' } }], tracePropagationTargets: [process.env.SERVER_URL], clientReportFlushInterval: 1_000, diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-http-client/server.mjs b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-http-client/server.mjs similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-http-client/server.mjs rename to dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-http-client/server.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-http-client/test.ts b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-http-client/test.ts similarity index 98% rename from dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-http-client/test.ts rename to dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-http-client/test.ts index 79e727254710..da9f1e5a7d9f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-http-client/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-http-client/test.ts @@ -3,7 +3,7 @@ import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; import { SENTRY_OP } from '@sentry/conventions/attributes'; -describe('ignoring an HTTP client child of a continued server segment (streaming)', () => { +describe('ignoring an HTTP client child of a continued server segment', () => { afterAll(() => { cleanupChildProcesses(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-segment/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-segment/instrument.mjs similarity index 92% rename from dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-segment/instrument.mjs rename to dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-segment/instrument.mjs index d6c2931b331f..950501539e15 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-segment/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-segment/instrument.mjs @@ -5,7 +5,6 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 0, transport: loggingTransport, - traceLifecycle: 'stream', ignoreSpans: [{ op: 'http.server' }], tracePropagationTargets: [process.env.SERVER_URL], }); diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-segment/server.mjs b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-segment/server.mjs similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-segment/server.mjs rename to dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-segment/server.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-segment/test.ts b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-segment/test.ts similarity index 96% rename from dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-segment/test.ts rename to dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-segment/test.ts index 5aa058ff4d7c..afc8eb2158ba 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-segment/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/continued-trace-segment/test.ts @@ -3,7 +3,7 @@ import { afterAll, describe, expect } from 'vitest'; import { RUNTIME } from '../../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; -describe('ignoring a continued server segment (streaming)', () => { +describe('ignoring a continued server segment', () => { afterAll(() => { cleanupChildProcesses(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/segments/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/segments/instrument.mjs similarity index 92% rename from dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/segments/instrument.mjs rename to dev-packages/node-integration-tests/suites/tracing/ignoreSpans/segments/instrument.mjs index 812ba9fb86a8..e86e9f19984b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/segments/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/segments/instrument.mjs @@ -6,7 +6,6 @@ Sentry.init({ release: '1.0', tracesSampleRate: 1.0, transport: loggingTransport, - traceLifecycle: 'stream', ignoreSpans: [{ attributes: { 'url.path': '/health' } }], clientReportFlushInterval: 1_000, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/segments/server.mjs b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/segments/server.mjs similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/segments/server.mjs rename to dev-packages/node-integration-tests/suites/tracing/ignoreSpans/segments/server.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/segments/test.ts b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/segments/test.ts similarity index 95% rename from dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/segments/test.ts rename to dev-packages/node-integration-tests/suites/tracing/ignoreSpans/segments/test.ts index 538c9c77a532..ebbd956d8239 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/segments/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans/segments/test.ts @@ -1,7 +1,7 @@ import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; -describe('filtering segment spans with ignoreSpans (streaming)', () => { +describe('filtering segment spans with ignoreSpans', () => { afterAll(() => { cleanupChildProcesses(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/sampling-streamed/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/sampling/instrument.mjs similarity index 95% rename from dev-packages/node-integration-tests/suites/tracing/sampling-streamed/instrument.mjs rename to dev-packages/node-integration-tests/suites/tracing/sampling/instrument.mjs index 0ce3c59cf745..4fd072ae569a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/sampling-streamed/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/sampling/instrument.mjs @@ -12,6 +12,5 @@ Sentry.init({ return inheritOrSampleWith(1); }, transport: loggingTransport, - traceLifecycle: 'stream', clientReportFlushInterval: 1_000, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/sampling-streamed/server.mjs b/dev-packages/node-integration-tests/suites/tracing/sampling/server.mjs similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing/sampling-streamed/server.mjs rename to dev-packages/node-integration-tests/suites/tracing/sampling/server.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/sampling-streamed/test.ts b/dev-packages/node-integration-tests/suites/tracing/sampling/test.ts similarity index 97% rename from dev-packages/node-integration-tests/suites/tracing/sampling-streamed/test.ts rename to dev-packages/node-integration-tests/suites/tracing/sampling/test.ts index aa28c3c7723b..fbcf06a52eec 100644 --- a/dev-packages/node-integration-tests/suites/tracing/sampling-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/sampling/test.ts @@ -1,7 +1,7 @@ import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; -describe('negative sampling (streaming)', () => { +describe('negative sampling', () => { afterAll(() => { cleanupChildProcesses(); }); From 40e027529fc474b3195ccb56936b7704667762e3 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Fri, 9 Oct 2026 13:02:34 +0200 Subject: [PATCH 26/84] test(browser): migrate trace lifecycle and span API tests to streaming (#24881) Run the browser trace lifecycle and public span API tests with default span streaming, asserting across envelopes as roots and children are sent independently. Retain the existing transaction counterparts as explicitly pinned `*-static` suites so both lifecycles keep regression coverage for sampling, propagation, linking, and span APIs. Cover circular attributes in both lifecycles: streamed spans drop unsupported objects while preserving primitive attributes, and static transactions retain circular-reference normalization. Filter feedback waits by envelope type so unrelated span envelopes cannot satisfy them. Keep `public-api/startSpan/setMeasurement` on the static lifecycle because the legacy measurement API has no span v2 representation. Fixes #24144 --------- Co-authored-by: GPT-6 --- .../integrations/httpclient/fetch/init.js | 1 - .../fetch/withAbortController/init.js | 1 - .../fetch/withAbortController/test.ts | 13 +- .../fetch/withDataCollection/init.js | 1 - .../fetch/withDisabledDataCollection/init.js | 1 - .../withHeadersOnlyDataCollection/init.js | 1 - .../suites/integrations/httpclient/init.js | 1 - .../httpclient/xhr/withDataCollection/init.js | 1 - .../init.js | 0 .../test.ts | 3 +- .../init.js | 0 .../test.ts | 0 .../simple_message_no_stacktrace/test.ts | 5 +- .../public-api/startSpan/attributes/test.ts | 20 +- .../public-api/startSpan/basic-static/init.js | 10 + .../startSpan/basic-static/subject.js | 14 + .../public-api/startSpan/basic-static/test.ts | 52 ++ .../startSpan/{streamed => basic}/init.js | 1 - .../public-api/startSpan/basic/subject.js | 25 +- .../suites/public-api/startSpan/basic/test.ts | 335 ++++++++++-- .../startSpan/circular_data-static/init.js | 10 + .../startSpan/circular_data-static/subject.js | 7 + .../startSpan/circular_data-static/test.ts | 28 + .../startSpan/circular_data/subject.js | 4 +- .../startSpan/circular_data/test.ts | 44 +- .../startSpan/error-async-reject/test.ts | 15 +- .../error-async-throw-not-awaited/test.ts | 15 +- .../startSpan/error-async-throw/test.ts | 15 +- .../public-api/startSpan/error-sync/test.ts | 27 +- .../suites/public-api/startSpan/init.js | 1 - .../parallel-root-spans-in-scope/test.ts | 20 +- .../test.ts | 20 +- .../startSpan/parallel-root-spans/test.ts | 20 +- .../startSpan/setMeasurement/init.js | 11 + .../startSpan/setMeasurement/test.ts | 29 +- .../public-api/startSpan/streamed/subject.js | 15 - .../public-api/startSpan/streamed/test.ts | 295 ----------- .../thenable-with-extra-methods/test.ts | 11 +- .../suites/tracing/bindScopeToEmitter/init.js | 1 - .../suites/tracing/bindScopeToEmitter/test.ts | 18 +- .../browserTracingIntegrationShim/init.js | 1 - .../continueTrace/full-tracing/init.js | 1 - .../continueTrace/full-tracing/test.ts | 67 ++- .../tracesSampleRate-zero/init.js | 1 - .../tracesSampleRate-zero/test.ts | 22 +- .../tracing-without-performance/init.js | 1 - .../tracing-without-performance/test.ts | 2 +- .../tracing/dsc-txn-name-update/init.js | 1 - .../tracing/dsc-txn-name-update/test.ts | 28 +- .../envelope-header-transaction-name/init.js | 1 - .../envelope-header-transaction-name/test.ts | 16 +- .../suites/tracing/envelope-header/init.js | 1 - .../suites/tracing/envelope-header/test.ts | 16 +- .../init.js | 5 +- .../subject.js | 0 .../test.ts | 11 +- .../suites/tracing/ignoreSpans-static/init.js | 50 ++ .../suites/tracing/ignoreSpans-static/test.ts | 29 ++ .../attributes/init.js | 2 +- .../attributes/subject.js | 0 .../attributes/test.ts | 0 .../child/init.js | 2 +- .../child/subject.js | 0 .../child/test.ts | 0 .../continued-trace-child/init.js | 2 +- .../continued-trace-child/subject.js | 0 .../continued-trace-child/template.html | 0 .../continued-trace-child/test.ts | 0 .../continued-trace-http-client/init.js | 2 +- .../continued-trace-http-client/subject.js | 0 .../continued-trace-http-client/template.html | 0 .../continued-trace-http-client/test.ts | 0 .../continued-trace-segment/init.js | 2 +- .../continued-trace-segment/subject.js | 0 .../continued-trace-segment/template.html | 0 .../continued-trace-segment/test.ts | 0 .../suites/tracing/ignoreSpans/init.js | 1 - .../segment/init.js | 2 +- .../segment/subject.js | 0 .../segment/test.ts | 0 .../suites/tracing/ignoreSpans/test.ts | 16 +- .../nested => linking-addLink-static}/init.js | 2 +- .../subject.js | 0 .../tracing/linking-addLink-static/test.ts | 77 +++ .../tracing/linking-addLink-streamed/test.ts | 66 --- .../suites/tracing/linking-addLink/init.js | 1 - .../suites/tracing/linking-addLink/test.ts | 94 ++-- .../suites/tracing/linking-addLinks/init.js | 1 - .../suites/tracing/linking-addLinks/test.ts | 74 +-- .../tracing/linking-spanOptions/init.js | 1 - .../tracing/linking-spanOptions/test.ts | 43 +- .../suites/tracing/maxSpans-static/init.js | 16 + .../suites/tracing/maxSpans-static/test.ts | 16 + .../suites/tracing/maxSpans/init.js | 1 - .../suites/tracing/maxSpans/test.ts | 26 +- .../microfrontend-span-attribution/init.js | 1 - .../microfrontend-span-attribution/test.ts | 42 +- .../init.js | 0 .../subject.js | 0 .../test.ts | 0 .../default}/init.js | 2 +- .../default/subject.js | 0 .../default/test.ts | 22 +- .../nested-parentAlwaysRoot/init.js | 2 +- .../nested-parentAlwaysRoot/subject.js | 0 .../nested-parentAlwaysRoot/test.ts | 35 +- .../nested}/init.js | 2 +- .../nested/subject.js | 0 .../setSpanActive-static/nested/test.ts | 52 ++ .../setSpanActive-streamed/nested/test.ts | 53 -- .../tracing/setSpanActive/default/init.js | 2 +- .../tracing/setSpanActive/default/test.ts | 24 +- .../nested-parentAlwaysRoot/init.js | 2 +- .../nested-parentAlwaysRoot/test.ts | 36 +- .../tracing/setSpanActive/nested/init.js | 2 +- .../tracing/setSpanActive/nested/test.ts | 36 +- .../suites/tracing/stringSampleRate/init.js | 1 - .../suites/tracing/stringSampleRate/test.ts | 9 +- .../suites/tracing/trace-lifetime/init.js | 1 - .../init.js | 3 +- .../navigation-static/subject.js | 23 + .../navigation-static/template.html | 12 + .../trace-lifetime/navigation-static/test.ts | 363 +++++++++++++ .../navigation-streamed/test.ts | 317 ------------ .../{pageload-streamed => navigation}/init.js | 2 +- .../tracing/trace-lifetime/navigation/test.ts | 288 +++++------ .../trace-lifetime/pageload-headers/test.ts | 44 +- .../trace-lifetime/pageload-meta/test.ts | 127 +++-- .../trace-lifetime/pageload-static/init.js | 13 + .../trace-lifetime/pageload-static/subject.js | 23 + .../pageload-static/template.html | 12 + .../trace-lifetime/pageload-static/test.ts | 484 ++++++++++++++++++ .../trace-lifetime/pageload-streamed/test.ts | 237 --------- .../tracing/trace-lifetime/pageload/init.js | 12 + .../tracing/trace-lifetime/pageload/test.ts | 409 +++------------ .../startNewTrace-multiple-spans/test.ts | 42 +- .../startNewTrace-static/init.js | 13 + .../subject.js | 0 .../template.html | 0 .../startNewTrace-static/test.ts | 109 ++++ .../startNewTrace-streamed/test.ts | 44 -- .../init.js | 2 +- .../trace-lifetime/startNewTrace/test.ts | 99 +--- .../startNewTraceSampling/init.js | 1 - .../startNewTraceSampling/test.ts | 62 +-- .../init.js | 1 - .../test.ts | 9 +- .../init.js | 1 - .../tracing-without-performance/init.js | 1 - .../tracing-without-performance/test.ts | 13 +- .../tracesSampler/op-attribute/init.js | 1 - .../tracesSampler/op-attribute/test.ts | 16 +- .../utils/helpers.ts | 6 +- .../utils/spanUtils.ts | 41 +- 154 files changed, 2596 insertions(+), 2349 deletions(-) rename dev-packages/browser-integration-tests/suites/public-api/{beforeSendSpan-streamed => beforeSendSpan}/init.js (100%) rename dev-packages/browser-integration-tests/suites/public-api/{beforeSendSpan-streamed => beforeSendSpan}/test.ts (89%) rename dev-packages/browser-integration-tests/suites/public-api/{beforeSendTransaction => beforeSendTransaction-static}/init.js (100%) rename dev-packages/browser-integration-tests/suites/public-api/{beforeSendTransaction => beforeSendTransaction-static}/test.ts (100%) create mode 100644 dev-packages/browser-integration-tests/suites/public-api/startSpan/basic-static/init.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/startSpan/basic-static/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/startSpan/basic-static/test.ts rename dev-packages/browser-integration-tests/suites/public-api/startSpan/{streamed => basic}/init.js (77%) create mode 100644 dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data-static/init.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data-static/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data-static/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/public-api/startSpan/setMeasurement/init.js delete mode 100644 dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/subject.js delete mode 100644 dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts rename dev-packages/browser-integration-tests/suites/tracing/{http-client-span-streamed => http-client-span}/init.js (54%) rename dev-packages/browser-integration-tests/suites/tracing/{http-client-span-streamed => http-client-span}/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/{http-client-span-streamed => http-client-span}/test.ts (63%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-static/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-static/test.ts rename dev-packages/browser-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/attributes/init.js (82%) rename dev-packages/browser-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/attributes/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/attributes/test.ts (100%) rename dev-packages/browser-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/child/init.js (82%) rename dev-packages/browser-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/child/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/child/test.ts (100%) rename dev-packages/browser-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/continued-trace-child/init.js (76%) rename dev-packages/browser-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/continued-trace-child/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/continued-trace-child/template.html (100%) rename dev-packages/browser-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/continued-trace-child/test.ts (100%) rename dev-packages/browser-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/continued-trace-http-client/init.js (76%) rename dev-packages/browser-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/continued-trace-http-client/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/continued-trace-http-client/template.html (100%) rename dev-packages/browser-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/continued-trace-http-client/test.ts (100%) rename dev-packages/browser-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/continued-trace-segment/init.js (74%) rename dev-packages/browser-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/continued-trace-segment/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/continued-trace-segment/template.html (100%) rename dev-packages/browser-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/continued-trace-segment/test.ts (100%) rename dev-packages/browser-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/segment/init.js (79%) rename dev-packages/browser-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/segment/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/{ignoreSpans-streamed => ignoreSpans}/segment/test.ts (100%) rename dev-packages/browser-integration-tests/suites/tracing/{setSpanActive-streamed/nested => linking-addLink-static}/init.js (75%) rename dev-packages/browser-integration-tests/suites/tracing/{linking-addLink-streamed => linking-addLink-static}/subject.js (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/linking-addLink-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/linking-addLink-streamed/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/maxSpans-static/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/maxSpans-static/test.ts rename dev-packages/browser-integration-tests/suites/tracing/{no-parent-span-client-report => no-parent-span-client-report-static}/init.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/{no-parent-span-client-report => no-parent-span-client-report-static}/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/{no-parent-span-client-report => no-parent-span-client-report-static}/test.ts (100%) rename dev-packages/browser-integration-tests/suites/tracing/{linking-addLink-streamed => setSpanActive-static/default}/init.js (75%) rename dev-packages/browser-integration-tests/suites/tracing/{setSpanActive-streamed => setSpanActive-static}/default/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/{setSpanActive-streamed => setSpanActive-static}/default/test.ts (52%) rename dev-packages/browser-integration-tests/suites/tracing/{setSpanActive-streamed => setSpanActive-static}/nested-parentAlwaysRoot/init.js (78%) rename dev-packages/browser-integration-tests/suites/tracing/{setSpanActive-streamed => setSpanActive-static}/nested-parentAlwaysRoot/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/{setSpanActive-streamed => setSpanActive-static}/nested-parentAlwaysRoot/test.ts (53%) rename dev-packages/browser-integration-tests/suites/tracing/{setSpanActive-streamed/default => setSpanActive-static/nested}/init.js (75%) rename dev-packages/browser-integration-tests/suites/tracing/{setSpanActive-streamed => setSpanActive-static}/nested/subject.js (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/nested/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested/test.ts rename dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/{navigation-streamed => navigation-static}/init.js (89%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-static/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-static/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-streamed/test.ts rename dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/{pageload-streamed => navigation}/init.js (89%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-static/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-static/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-static/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-streamed/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-static/init.js rename dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/{startNewTrace-streamed => startNewTrace-static}/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/{startNewTrace-streamed => startNewTrace-static}/template.html (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-streamed/test.ts rename dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/{startNewTrace-streamed => startNewTrace}/init.js (71%) diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/init.js b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/init.js index 43248f09b63d..f44669962912 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/init.js @@ -4,7 +4,6 @@ import { httpClientIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [httpClientIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/init.js b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/init.js index bd0421b32761..7c200c542c56 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/test.ts b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/test.ts index fbd30098773a..85b7663dabc0 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import type { Event as SentryEvent } from '@sentry/core'; import { sentryTest } from '../../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../../utils/helpers'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForErrorRequest } from '../../../../../utils/helpers'; +import { waitForStreamedSpan } from '../../../../../utils/spanUtils'; sentryTest('should handle aborted fetch calls', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { @@ -14,7 +14,7 @@ sentryTest('should handle aborted fetch calls', async ({ getLocalTestUrl, page } // never fulfil this route because we abort the request as part of the test }); - const transactionEventPromise = getFirstSentryEnvelopeRequest(page); + const spanPromise = waitForStreamedSpan(page, span => span.name === 'with-abort-controller'); const hasAbortedFetchPromise = new Promise(resolve => { page.on('console', msg => { @@ -29,10 +29,13 @@ sentryTest('should handle aborted fetch calls', async ({ getLocalTestUrl, page } await page.locator('[data-test-id=start-button]').click(); await page.locator('[data-test-id=abort-button]').click(); - const transactionEvent = await transactionEventPromise; + await spanPromise; + const eventPromise = waitForErrorRequest(page); + await page.evaluate(() => (window as any).Sentry.captureMessage('After aborted fetch')); + const event = envelopeRequestParser(await eventPromise); // assert that fetch calls do not return undefined - const fetchBreadcrumbs = transactionEvent.breadcrumbs?.filter( + const fetchBreadcrumbs = event.breadcrumbs?.filter( ({ category, data }) => category === 'fetch' && data === undefined, ); expect(fetchBreadcrumbs).toHaveLength(0); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withDataCollection/init.js b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withDataCollection/init.js index fd831d3f7c14..8584933da0fa 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withDataCollection/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withDataCollection/init.js @@ -4,7 +4,6 @@ import { httpClientIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [httpClientIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withDisabledDataCollection/init.js b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withDisabledDataCollection/init.js index 7d53e07a4c0c..a20cf0664d13 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withDisabledDataCollection/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withDisabledDataCollection/init.js @@ -4,7 +4,6 @@ import { httpClientIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [httpClientIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withHeadersOnlyDataCollection/init.js b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withHeadersOnlyDataCollection/init.js index 2739a4aeea64..5eafca47503b 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withHeadersOnlyDataCollection/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withHeadersOnlyDataCollection/init.js @@ -4,7 +4,6 @@ import { httpClientIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [httpClientIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/init.js b/dev-packages/browser-integration-tests/suites/integrations/httpclient/init.js index 43248f09b63d..f44669962912 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/httpclient/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/init.js @@ -4,7 +4,6 @@ import { httpClientIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [httpClientIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/xhr/withDataCollection/init.js b/dev-packages/browser-integration-tests/suites/integrations/httpclient/xhr/withDataCollection/init.js index fd831d3f7c14..8584933da0fa 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/httpclient/xhr/withDataCollection/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/xhr/withDataCollection/init.js @@ -4,7 +4,6 @@ import { httpClientIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [httpClientIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/public-api/beforeSendSpan-streamed/init.js b/dev-packages/browser-integration-tests/suites/public-api/beforeSendSpan/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/public-api/beforeSendSpan-streamed/init.js rename to dev-packages/browser-integration-tests/suites/public-api/beforeSendSpan/init.js diff --git a/dev-packages/browser-integration-tests/suites/public-api/beforeSendSpan-streamed/test.ts b/dev-packages/browser-integration-tests/suites/public-api/beforeSendSpan/test.ts similarity index 89% rename from dev-packages/browser-integration-tests/suites/public-api/beforeSendSpan-streamed/test.ts rename to dev-packages/browser-integration-tests/suites/public-api/beforeSendSpan/test.ts index c3218f27e98a..9491bc7feab6 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/beforeSendSpan-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/beforeSendSpan/test.ts @@ -1,3 +1,4 @@ +import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../utils/helpers'; @@ -22,7 +23,7 @@ sentryTest('beforeSendSpan applies changes to streamed span', async ({ getLocalT spanId: '456', }, attributes: { - 'sentry.link.type': { type: 'string', value: 'custom_link' }, + [SENTRY_LINK_TYPE]: { type: 'string', value: 'custom_link' }, }, }, ]); diff --git a/dev-packages/browser-integration-tests/suites/public-api/beforeSendTransaction/init.js b/dev-packages/browser-integration-tests/suites/public-api/beforeSendTransaction-static/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/public-api/beforeSendTransaction/init.js rename to dev-packages/browser-integration-tests/suites/public-api/beforeSendTransaction-static/init.js diff --git a/dev-packages/browser-integration-tests/suites/public-api/beforeSendTransaction/test.ts b/dev-packages/browser-integration-tests/suites/public-api/beforeSendTransaction-static/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/public-api/beforeSendTransaction/test.ts rename to dev-packages/browser-integration-tests/suites/public-api/beforeSendTransaction-static/test.ts diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/test.ts index ae9db5f3064d..2c42ced57737 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/test.ts @@ -1,12 +1,11 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequestOnUrl } from '../../../../utils/helpers'; sentryTest('does not capture a stack trace if `attachStackTrace` is `false`', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const eventData = envelopeRequestParser(await waitForErrorRequestOnUrl(page, url)); expect(eventData.message).toBe('foo'); expect(eventData.level).toBe('info'); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/attributes/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/attributes/test.ts index 4d6d78f0eea1..7820d0108fc2 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/attributes/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/attributes/test.ts @@ -1,20 +1,14 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { - envelopeRequestParser, - shouldSkipTracingTest, - waitForTransactionRequestOnUrl, -} from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest('sends an empty string attribute', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - const req = await waitForTransactionRequestOnUrl(page, url); - const transaction = envelopeRequestParser(req); + const spanPromise = waitForStreamedSpan(page, span => span.name === 'child_span'); + await page.goto(url); - const childSpan = transaction.spans?.[0]; - expect(childSpan?.data?.someAttribute).toBe(''); + const span = await spanPromise; + expect(span.attributes.someAttribute).toEqual({ type: 'string', value: '' }); }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic-static/init.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic-static/init.js new file mode 100644 index 000000000000..527fad0d6f15 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic-static/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1.0, + normalizeDepth: 10, +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic-static/subject.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic-static/subject.js new file mode 100644 index 000000000000..35d41b55e69d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic-static/subject.js @@ -0,0 +1,14 @@ +async function run() { + Sentry.startSpan({ name: 'parent_span' }, () => { + Sentry.startSpan({ name: 'child_span' }, () => { + // whatever a user would do here + }); + + // unfinished spans are filtered out + Sentry.startInactiveSpan({ name: 'span_4' }); + }); +} + +(async () => { + await run(); +})(); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic-static/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic-static/test.ts new file mode 100644 index 000000000000..d3819b797a11 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic-static/test.ts @@ -0,0 +1,52 @@ +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { expect } from '@playwright/test'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; +import { sentryTest } from '../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipTracingTest, + waitForTransactionRequestOnUrl, +} from '../../../../utils/helpers'; + +sentryTest( + 'sends a transaction in an envelope with manual origin and custom source', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + const req = await waitForTransactionRequestOnUrl(page, url); + const transaction = envelopeRequestParser(req); + + const attributes = transaction.contexts?.trace?.data; + expect(attributes).toEqual({ + [SENTRY_ORIGIN]: 'manual', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', + }); + + expect(transaction.transaction_info?.source).toBe('custom'); + + expect(transaction.transaction).toBe('parent_span'); + expect(transaction.spans).toBeDefined(); + }, +); + +sentryTest('should report finished spans as children of the root transaction', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + const req = await waitForTransactionRequestOnUrl(page, url); + const transaction = envelopeRequestParser(req); + + expect(transaction.spans).toHaveLength(1); + + const span_1 = transaction.spans?.[0]; + expect(span_1?.description).toBe('child_span'); + expect(span_1?.parent_span_id).toEqual(transaction?.contexts?.trace?.span_id); + expect(span_1?.origin).toEqual('manual'); + expect(span_1?.data?.[SENTRY_ORIGIN]).toEqual('manual'); +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/init.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/init.js similarity index 77% rename from dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/init.js rename to dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/init.js index aaafd3396f14..e546c7cc6a94 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/init.js @@ -4,7 +4,6 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.spanStreamingIntegration()], tracesSampleRate: 1.0, debug: true, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/subject.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/subject.js index 35d41b55e69d..afe2527d8c7e 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/subject.js +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/subject.js @@ -1,14 +1,15 @@ -async function run() { - Sentry.startSpan({ name: 'parent_span' }, () => { - Sentry.startSpan({ name: 'child_span' }, () => { - // whatever a user would do here - }); - - // unfinished spans are filtered out - Sentry.startInactiveSpan({ name: 'span_4' }); +Sentry.startSpan({ name: 'test-span', op: 'test' }, () => { + Sentry.startSpan({ name: 'test-child-span', op: 'test-child' }, () => { + // noop }); -} -(async () => { - await run(); -})(); + const inactiveSpan = Sentry.startInactiveSpan({ name: 'test-inactive-span' }); + inactiveSpan.end(); + + Sentry.startSpanManual({ name: 'test-manual-span' }, span => { + // 2 = SPAN_STATUS_ERROR. The message must be preserved as the `sentry.status.message` + // attribute on the streamed span, since v2 statuses are reduced to ok/error. + span.setStatus({ code: 2, message: 'Connection Refused' }); + span.end(); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts index 824220e5133f..ba2404e747b5 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts @@ -1,52 +1,297 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { + SENTRY_IS_LOCALHOST, + CULTURE_CALENDAR, + CULTURE_LOCALE, + CULTURE_TIMEZONE, + URL_FULL, + SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_SEGMENT_ID, + SENTRY_SEGMENT_NAME, + SENTRY_SDK_NAME, + SENTRY_SDK_VERSION, + SENTRY_TRACE_LIFECYCLE, + USER_AGENT_ORIGINAL, + SENTRY_ENVIRONMENT, + SENTRY_SDK_INTEGRATIONS, + SENTRY_STATUS_MESSAGE, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { - envelopeRequestParser, - shouldSkipTracingTest, - waitForTransactionRequestOnUrl, -} from '../../../../utils/helpers'; - -sentryTest( - 'sends a transaction in an envelope with manual origin and custom source', - async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - const url = await getLocalTestUrl({ testDir: __dirname }); - const req = await waitForTransactionRequestOnUrl(page, url); - const transaction = envelopeRequestParser(req); - - const attributes = transaction.contexts?.trace?.data; - expect(attributes).toEqual({ - [SENTRY_ORIGIN]: 'manual', - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, - [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', - }); - - expect(transaction.transaction_info?.source).toBe('custom'); - - expect(transaction.transaction).toBe('parent_span'); - expect(transaction.spans).toBeDefined(); - }, -); - -sentryTest('should report finished spans as children of the root transaction', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; + +sentryTest('sends a streamed span envelope by default', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); + + const spanEnvelopePromise = waitForStreamedSpanEnvelope(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const req = await waitForTransactionRequestOnUrl(page, url); - const transaction = envelopeRequestParser(req); + await page.goto(url); + + const spanEnvelope = await spanEnvelopePromise; + + const envelopeHeader = spanEnvelope[0]; + const envelopeItem = spanEnvelope[1]; + const spans = envelopeItem[0][1].items; + + expect(envelopeHeader).toEqual({ + sdk: { + name: 'sentry.javascript.browser', + version: SDK_VERSION, + }, + sent_at: expect.any(String), + trace: { + environment: 'production', + public_key: 'public', + sample_rand: expect.any(String), + sample_rate: '1', + sampled: 'true', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + transaction: 'test-span', + }, + }); + + const numericSampleRand = parseFloat(envelopeHeader.trace!.sample_rand!); + const traceId = envelopeHeader.trace!.trace_id; + + expect(Number.isNaN(numericSampleRand)).toBe(false); + + expect(envelopeItem).toEqual([ + [ + { content_type: 'application/vnd.sentry.items.span.v2+json', item_count: 4, type: 'span' }, + { + version: 2, + ingest_settings: { infer_ip: 'auto', infer_user_agent: 'auto' }, + items: expect.any(Array), + }, + ], + ]); - expect(transaction.spans).toHaveLength(1); + const segmentSpanId = spans.find(s => !!s.is_segment)?.span_id; + expect(segmentSpanId).toBeDefined(); - const span_1 = transaction.spans?.[0]; - expect(span_1?.description).toBe('child_span'); - expect(span_1?.parent_span_id).toEqual(transaction?.contexts?.trace?.span_id); - expect(span_1?.origin).toEqual('manual'); - expect(span_1?.data?.['sentry.origin']).toEqual('manual'); + expect(spans).toEqual([ + { + attributes: { + [SENTRY_IS_LOCALHOST]: { value: false, type: 'boolean' }, + [SENTRY_OP]: { + type: 'string', + value: 'test-child', + }, + [SENTRY_ORIGIN]: { + type: 'string', + value: 'manual', + }, + [SENTRY_SDK_NAME]: { + type: 'string', + value: 'sentry.javascript.browser', + }, + [SENTRY_SDK_VERSION]: { + type: 'string', + value: SDK_VERSION, + }, + [SENTRY_SEGMENT_ID]: { + type: 'string', + value: segmentSpanId, + }, + [SENTRY_SEGMENT_NAME]: { + type: 'string', + value: 'test-span', + }, + [SENTRY_ENVIRONMENT]: { + type: 'string', + value: 'production', + }, + [SENTRY_TRACE_LIFECYCLE]: { + type: 'string', + value: 'stream', + }, + [USER_AGENT_ORIGINAL]: { + type: 'string', + value: expect.any(String), + }, + }, + end_timestamp: expect.any(Number), + is_segment: false, + name: 'test-child-span', + parent_span_id: segmentSpanId, + span_id: expect.stringMatching(/^[\da-f]{16}$/), + start_timestamp: expect.any(Number), + status: 'ok', + trace_id: traceId, + }, + { + attributes: { + [SENTRY_IS_LOCALHOST]: { value: false, type: 'boolean' }, + [SENTRY_ORIGIN]: { + type: 'string', + value: 'manual', + }, + [SENTRY_SDK_NAME]: { + type: 'string', + value: 'sentry.javascript.browser', + }, + [SENTRY_SDK_VERSION]: { + type: 'string', + value: SDK_VERSION, + }, + [SENTRY_SEGMENT_ID]: { + type: 'string', + value: segmentSpanId, + }, + [SENTRY_SEGMENT_NAME]: { + type: 'string', + value: 'test-span', + }, + [SENTRY_ENVIRONMENT]: { + type: 'string', + value: 'production', + }, + [SENTRY_TRACE_LIFECYCLE]: { + type: 'string', + value: 'stream', + }, + [USER_AGENT_ORIGINAL]: { + type: 'string', + value: expect.any(String), + }, + }, + end_timestamp: expect.any(Number), + is_segment: false, + name: 'test-inactive-span', + parent_span_id: segmentSpanId, + span_id: expect.stringMatching(/^[\da-f]{16}$/), + start_timestamp: expect.any(Number), + status: 'ok', + trace_id: traceId, + }, + { + attributes: { + [SENTRY_IS_LOCALHOST]: { value: false, type: 'boolean' }, + [SENTRY_ORIGIN]: { + type: 'string', + value: 'manual', + }, + [SENTRY_SDK_NAME]: { + type: 'string', + value: 'sentry.javascript.browser', + }, + [SENTRY_SDK_VERSION]: { + type: 'string', + value: SDK_VERSION, + }, + [SENTRY_SEGMENT_ID]: { + type: 'string', + value: segmentSpanId, + }, + [SENTRY_SEGMENT_NAME]: { + type: 'string', + value: 'test-span', + }, + [SENTRY_ENVIRONMENT]: { + type: 'string', + value: 'production', + }, + [SENTRY_STATUS_MESSAGE]: { + type: 'string', + value: 'Connection Refused', + }, + [SENTRY_TRACE_LIFECYCLE]: { + type: 'string', + value: 'stream', + }, + [USER_AGENT_ORIGINAL]: { + type: 'string', + value: expect.any(String), + }, + }, + end_timestamp: expect.any(Number), + is_segment: false, + name: 'test-manual-span', + parent_span_id: segmentSpanId, + span_id: expect.stringMatching(/^[\da-f]{16}$/), + start_timestamp: expect.any(Number), + status: 'error', + trace_id: traceId, + }, + { + attributes: { + [SENTRY_IS_LOCALHOST]: { value: false, type: 'boolean' }, + [CULTURE_CALENDAR]: { + type: 'string', + value: expect.any(String), + }, + [CULTURE_LOCALE]: { + type: 'string', + value: expect.any(String), + }, + [CULTURE_TIMEZONE]: { + type: 'string', + value: expect.any(String), + }, + [USER_AGENT_ORIGINAL]: { + type: 'string', + value: expect.any(String), + }, + [URL_FULL]: { + type: 'string', + value: expect.any(String), + }, + [SENTRY_OP]: { + type: 'string', + value: 'test', + }, + [SENTRY_ORIGIN]: { + type: 'string', + value: 'manual', + }, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { + type: 'integer', + value: 1, + }, + [SENTRY_SDK_NAME]: { + type: 'string', + value: 'sentry.javascript.browser', + }, + [SENTRY_SDK_VERSION]: { + type: 'string', + value: SDK_VERSION, + }, + [SENTRY_SDK_INTEGRATIONS]: { + type: 'array', + value: expect.arrayContaining(['SpanStreaming']), + }, + [SENTRY_SEGMENT_ID]: { + type: 'string', + value: segmentSpanId, + }, + [SENTRY_SEGMENT_NAME]: { + type: 'string', + value: 'test-span', + }, + [SENTRY_SEGMENT_NAME_SOURCE]: { + type: 'string', + value: 'custom', + }, + [SENTRY_ENVIRONMENT]: { + type: 'string', + value: 'production', + }, + [SENTRY_TRACE_LIFECYCLE]: { + type: 'string', + value: 'stream', + }, + }, + end_timestamp: expect.any(Number), + is_segment: true, + name: 'test-span', + span_id: segmentSpanId, + start_timestamp: expect.any(Number), + status: 'ok', + trace_id: traceId, + }, + ]); }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data-static/init.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data-static/init.js new file mode 100644 index 000000000000..527fad0d6f15 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data-static/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1.0, + normalizeDepth: 10, +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data-static/subject.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data-static/subject.js new file mode 100644 index 000000000000..c7aa1cd72dfe --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data-static/subject.js @@ -0,0 +1,7 @@ +const chicken = {}; +const egg = { contains: chicken }; +chicken.lays = egg; + +Sentry.startSpan({ name: 'circular_object_test_transaction', attributes: { chicken, count: 42 } }, () => { + Sentry.startSpan({ op: 'circular_object_test_span', attributes: { chicken, count: 42 } }, () => undefined); +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data-static/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data-static/test.ts new file mode 100644 index 000000000000..d794f101112d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data-static/test.ts @@ -0,0 +1,28 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipTracingTest, + waitForTransactionRequestOnUrl, +} from '../../../../utils/helpers'; + +sentryTest( + 'normalizes circular attributes on root and child spans with static tracing', + async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); + const url = await getLocalTestUrl({ testDir: __dirname }); + const event = envelopeRequestParser(await waitForTransactionRequestOnUrl(page, url)); + + expect(event.transaction).toBe('circular_object_test_transaction'); + expect(event.contexts?.trace?.data).toMatchObject({ + chicken: { lays: { contains: '[Circular ~]' } }, + count: 42, + }); + expect(event.spans).toHaveLength(1); + expect(event.spans?.[0]).toMatchObject({ + op: 'circular_object_test_span', + parent_span_id: event.contexts?.trace?.span_id, + data: { chicken: { lays: { contains: '[Circular ~]' } }, count: 42 }, + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data/subject.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data/subject.js index b5ee8624b124..c7aa1cd72dfe 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data/subject.js +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data/subject.js @@ -2,6 +2,6 @@ const chicken = {}; const egg = { contains: chicken }; chicken.lays = egg; -Sentry.startSpan({ name: 'circular_object_test_transaction', attributes: { chicken } }, () => { - Sentry.startSpan({ op: 'circular_object_test_span', attributes: { chicken } }, () => undefined); +Sentry.startSpan({ name: 'circular_object_test_transaction', attributes: { chicken, count: 42 } }, () => { + Sentry.startSpan({ op: 'circular_object_test_span', attributes: { chicken, count: 42 } }, () => undefined); }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data/test.ts index 41d49b6351ea..7860439b7707 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/circular_data/test.ts @@ -1,29 +1,23 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; -sentryTest('should be able to handle circular data', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +sentryTest( + 'drops circular object attributes while preserving supported attributes on root and child spans', + async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); + const url = await getLocalTestUrl({ testDir: __dirname }); + const rootPromise = waitForStreamedSpan(page, span => span.name === 'circular_object_test_transaction'); + const childPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'circular_object_test_span'); + await page.goto(url); - const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(eventData.type).toBe('transaction'); - expect(eventData.transaction).toBe('circular_object_test_transaction'); - - expect(eventData.contexts).toMatchObject({ - trace: { - data: { chicken: { lays: { contains: '[Circular ~]' } } }, - }, - }); - - expect(eventData?.spans?.[0]).toMatchObject({ - data: { chicken: { lays: { contains: '[Circular ~]' } } }, - op: 'circular_object_test_span', - }); - - await new Promise(resolve => setTimeout(resolve, 2000)); -}); + const [rootSpan, childSpan] = await Promise.all([rootPromise, childPromise]); + expect(rootSpan.is_segment).toBe(true); + expect(childSpan.parent_span_id).toBe(rootSpan.span_id); + expect(rootSpan.attributes.count).toEqual({ type: 'integer', value: 42 }); + expect(childSpan.attributes.count).toEqual({ type: 'integer', value: 42 }); + expect(rootSpan.attributes.chicken).toBeUndefined(); + expect(childSpan.attributes.chicken).toBeUndefined(); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/error-async-reject/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/error-async-reject/test.ts index d8cecd5f09c7..c70b05e99fea 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/error-async-reject/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/error-async-reject/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest( 'should capture a promise rejection within an async startSpan callback', @@ -11,17 +11,18 @@ sentryTest( } const url = await getLocalTestUrl({ testDir: __dirname }); - const envelopePromise = getMultipleSentryEnvelopeRequests(page, 2); + const spanPromise = waitForStreamedSpan(page, span => span.name === 'parent_span'); + const errorPromise = waitForErrorRequest(page); await page.goto(url); const clickPromise = page.getByText('Button 1').click(); - const [, events] = await Promise.all([clickPromise, envelopePromise]); - const txn = events.find(event => event.type === 'transaction'); - const err = events.find(event => !event.type); + const [, span, errorRequest] = await Promise.all([clickPromise, spanPromise, errorPromise]); + const err = envelopeRequestParser(errorRequest); - expect(txn).toMatchObject({ transaction: 'parent_span' }); + expect(span.name).toBe('parent_span'); + expect(span.status).toBe('ok'); expect(err?.exception?.values?.[0]?.value).toBe( 'Non-Error promise rejection captured with value: Async Promise Rejection', diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw-not-awaited/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw-not-awaited/test.ts index 79ecf0f6e9e8..42c87027807f 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw-not-awaited/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw-not-awaited/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest( "should capture a thrown error within an async startSpan callback that's not awaited properly", @@ -9,7 +9,8 @@ sentryTest( if (shouldSkipTracingTest()) { sentryTest.skip(); } - const envelopePromise = getMultipleSentryEnvelopeRequests(page, 2); + const spanPromise = waitForStreamedSpan(page, span => span.name === 'parent_span'); + const errorPromise = waitForErrorRequest(page); const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); @@ -17,11 +18,11 @@ sentryTest( const clickPromise = page.getByText('Button 1').click(); // awaiting both events simultaneously to avoid race conditions - const [, events] = await Promise.all([clickPromise, envelopePromise]); - const txn = events.find(event => event.type === 'transaction'); - const err = events.find(event => !event.type); + const [, span, errorRequest] = await Promise.all([clickPromise, spanPromise, errorPromise]); + const err = envelopeRequestParser(errorRequest); - expect(txn).toMatchObject({ transaction: 'parent_span' }); + expect(span.name).toBe('parent_span'); + expect(span.status).toBe('error'); expect(err?.exception?.values?.[0]?.value).toBe('Async Thrown Error'); }, ); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw/test.ts index 8f527b4e7238..ce66c78553bc 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw/test.ts @@ -1,13 +1,14 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest('should capture a thrown error within an async startSpan callback', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); } - const envelopePromise = getMultipleSentryEnvelopeRequests(page, 2); + const spanPromise = waitForStreamedSpan(page, span => span.name === 'parent_span'); + const errorPromise = waitForErrorRequest(page); const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); @@ -15,10 +16,10 @@ sentryTest('should capture a thrown error within an async startSpan callback', a const clickPromise = page.getByText('Button 1').click(); // awaiting both events simultaneously to avoid race conditions - const [, events] = await Promise.all([clickPromise, envelopePromise]); - const txn = events.find(event => event.type === 'transaction'); - const err = events.find(event => !event.type); + const [, span, errorRequest] = await Promise.all([clickPromise, spanPromise, errorPromise]); + const err = envelopeRequestParser(errorRequest); - expect(txn).toMatchObject({ transaction: 'parent_span' }); + expect(span.name).toBe('parent_span'); + expect(span.status).toBe('error'); expect(err?.exception?.values?.[0]?.value).toBe('Async Thrown Error'); }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/error-sync/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/error-sync/test.ts index 9880598b9294..2a1dc368e98f 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/error-sync/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/error-sync/test.ts @@ -1,11 +1,13 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; +import type * as SentryBrowser from '@sentry/browser'; import { sentryTest } from '../../../../utils/fixtures'; import { - getMultipleSentryEnvelopeRequests, + envelopeRequestParser, + waitForErrorRequest, runScriptInSandbox, shouldSkipTracingTest, } from '../../../../utils/helpers'; +import { waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest( 'should capture an error within a sync startSpan callback', @@ -21,10 +23,20 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); - const errorEventsPromise = getMultipleSentryEnvelopeRequests(page, 2); + const spanPromise = waitForStreamedSpan(page, span => span.name === 'parent_span'); + const errorPromise = waitForErrorRequest(page); await page.goto(url); + // Force a span envelope to arrive before the error so the error waiter must ignore it. + const precedingSpanPromise = waitForStreamedSpan(page, span => span.name === 'preceding_span'); + await page.evaluate(async () => { + const Sentry = (window as typeof window & { Sentry: typeof SentryBrowser }).Sentry; + Sentry.startSpan({ name: 'preceding_span' }, () => undefined); + await Sentry.flush(); + }); + await precedingSpanPromise; + await runScriptInSandbox(page, { content: ` function run() { @@ -37,12 +49,11 @@ sentryTest( `, }); - const events = await errorEventsPromise; - - const txn = events.find(event => event.type === 'transaction'); - const err = events.find(event => !event.type); + const [span, errorRequest] = await Promise.all([spanPromise, errorPromise]); + const err = envelopeRequestParser(errorRequest); - expect(txn).toMatchObject({ transaction: 'parent_span' }); + expect(span.name).toBe('parent_span'); + expect(span.status).toBe('error'); expect(err?.exception?.values?.[0]?.value).toBe('Sync Error'); }, ); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/init.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/init.js index 527fad0d6f15..3364667c960c 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1.0, normalizeDepth: 10, diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-in-scope/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-in-scope/test.ts index b3edbfac7dcf..b233cae07f97 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-in-scope/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-in-scope/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import type { TransactionEvent } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest( 'should send manually started parallel root spans outside of root context', @@ -12,21 +12,21 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); - const transaction1ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_1'); - const transaction2ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_2'); + const transaction1ReqPromise = waitForStreamedSpan(page, event => event.name === 'test_span_1'); + const transaction2ReqPromise = waitForStreamedSpan(page, event => event.name === 'test_span_2'); await page.goto(url); const [transaction1Req, transaction2Req] = await Promise.all([transaction1ReqPromise, transaction2ReqPromise]); - const transaction1 = envelopeRequestParser(transaction1Req); - const transaction2 = envelopeRequestParser(transaction2Req); + const transaction1 = transaction1Req; + const transaction2 = transaction2Req; expect(transaction1).toBeDefined(); expect(transaction2).toBeDefined(); - const trace1Id = transaction1.contexts?.trace?.trace_id; - const trace2Id = transaction2.contexts?.trace?.trace_id; + const trace1Id = transaction1.trace_id; + const trace2Id = transaction2.trace_id; expect(trace1Id).toBeDefined(); expect(trace2Id).toBeDefined(); @@ -34,7 +34,7 @@ sentryTest( // We use the same traceID from the root propagation context here expect(trace1Id).toBe(trace2Id); - expect(transaction1.contexts?.trace?.parent_span_id).toBeUndefined(); - expect(transaction2.contexts?.trace?.parent_span_id).toBeUndefined(); + expect(transaction1.parent_span_id).toBeUndefined(); + expect(transaction2.parent_span_id).toBeUndefined(); }, ); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/test.ts index 62cdeb9bf7c9..49d52dce3c61 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import type { TransactionEvent } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest( 'should send manually started parallel root spans in root context with parentSpanId', @@ -12,26 +12,26 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); - const transaction1ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_1'); - const transaction2ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_2'); + const transaction1ReqPromise = waitForStreamedSpan(page, event => event.name === 'test_span_1'); + const transaction2ReqPromise = waitForStreamedSpan(page, event => event.name === 'test_span_2'); await page.goto(url); const [transaction1Req, transaction2Req] = await Promise.all([transaction1ReqPromise, transaction2ReqPromise]); - const transaction1 = envelopeRequestParser(transaction1Req); - const transaction2 = envelopeRequestParser(transaction2Req); + const transaction1 = transaction1Req; + const transaction2 = transaction2Req; expect(transaction1).toBeDefined(); expect(transaction2).toBeDefined(); - const trace1Id = transaction1.contexts?.trace?.trace_id; - const trace2Id = transaction2.contexts?.trace?.trace_id; + const trace1Id = transaction1.trace_id; + const trace2Id = transaction2.trace_id; expect(trace1Id).toBe('12345678901234567890123456789012'); expect(trace2Id).toBe('12345678901234567890123456789012'); - expect(transaction1.contexts?.trace?.parent_span_id).toBe('1234567890123456'); - expect(transaction2.contexts?.trace?.parent_span_id).toBe('1234567890123456'); + expect(transaction1.parent_span_id).toBe('1234567890123456'); + expect(transaction2.parent_span_id).toBe('1234567890123456'); }, ); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts index 084b7100fa9b..1c1af189cc77 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import type { TransactionEvent } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest('should send manually started parallel root spans in root context', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { @@ -10,21 +10,21 @@ sentryTest('should send manually started parallel root spans in root context', a const url = await getLocalTestUrl({ testDir: __dirname }); - const transaction1ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_1'); - const transaction2ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_2'); + const transaction1ReqPromise = waitForStreamedSpan(page, event => event.name === 'test_span_1'); + const transaction2ReqPromise = waitForStreamedSpan(page, event => event.name === 'test_span_2'); await page.goto(url); const [transaction1Req, transaction2Req] = await Promise.all([transaction1ReqPromise, transaction2ReqPromise]); - const transaction1 = envelopeRequestParser(transaction1Req); - const transaction2 = envelopeRequestParser(transaction2Req); + const transaction1 = transaction1Req; + const transaction2 = transaction2Req; expect(transaction1).toBeDefined(); expect(transaction2).toBeDefined(); - const trace1Id = transaction1.contexts?.trace?.trace_id; - const trace2Id = transaction2.contexts?.trace?.trace_id; + const trace1Id = transaction1.trace_id; + const trace2Id = transaction2.trace_id; expect(trace1Id).toBeDefined(); expect(trace2Id).toBeDefined(); @@ -32,6 +32,6 @@ sentryTest('should send manually started parallel root spans in root context', a // We use the same traceID from the root propagation context here expect(trace1Id).toBe(trace2Id); - expect(transaction1.contexts?.trace?.parent_span_id).toBeUndefined(); - expect(transaction2.contexts?.trace?.parent_span_id).toBeUndefined(); + expect(transaction1.parent_span_id).toBeUndefined(); + expect(transaction2.parent_span_id).toBeUndefined(); }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/setMeasurement/init.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/setMeasurement/init.js new file mode 100644 index 000000000000..5f6b23836f8e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/setMeasurement/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + // setMeasurement is a legacy transaction API without a span v2 representation. + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1.0, + normalizeDepth: 10, +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/setMeasurement/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/setMeasurement/test.ts index 2604470d0a50..8c257a2645cc 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/setMeasurement/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/setMeasurement/test.ts @@ -3,19 +3,22 @@ import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; -sentryTest('should attach measurement to transaction', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +sentryTest( + 'static tracing compatibility: should attach measurement to transaction', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } - const url = await getLocalTestUrl({ testDir: __dirname }); - const event = await getFirstSentryEnvelopeRequest(page, url); + const url = await getLocalTestUrl({ testDir: __dirname }); + const event = await getFirstSentryEnvelopeRequest(page, url); - expect(event.measurements?.['metric.foo'].value).toBe(42); - expect(event.measurements?.['metric.bar'].value).toBe(1337); - expect(event.measurements?.['metric.baz'].value).toBe(1); + expect(event.measurements?.['metric.foo'].value).toBe(42); + expect(event.measurements?.['metric.bar'].value).toBe(1337); + expect(event.measurements?.['metric.baz'].value).toBe(1); - expect(event.measurements?.['metric.foo'].unit).toBe('ms'); - expect(event.measurements?.['metric.bar'].unit).toBe('nanoseconds'); - expect(event.measurements?.['metric.baz'].unit).toBe(''); -}); + expect(event.measurements?.['metric.foo'].unit).toBe('ms'); + expect(event.measurements?.['metric.bar'].unit).toBe('nanoseconds'); + expect(event.measurements?.['metric.baz'].unit).toBe(''); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/subject.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/subject.js deleted file mode 100644 index afe2527d8c7e..000000000000 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/subject.js +++ /dev/null @@ -1,15 +0,0 @@ -Sentry.startSpan({ name: 'test-span', op: 'test' }, () => { - Sentry.startSpan({ name: 'test-child-span', op: 'test-child' }, () => { - // noop - }); - - const inactiveSpan = Sentry.startInactiveSpan({ name: 'test-inactive-span' }); - inactiveSpan.end(); - - Sentry.startSpanManual({ name: 'test-manual-span' }, span => { - // 2 = SPAN_STATUS_ERROR. The message must be preserved as the `sentry.status.message` - // attribute on the streamed span, since v2 statuses are reduced to ok/error. - span.setStatus({ code: 2, message: 'Connection Refused' }); - span.end(); - }); -}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts deleted file mode 100644 index 5cc3bd34f817..000000000000 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts +++ /dev/null @@ -1,295 +0,0 @@ -import { expect } from '@playwright/test'; -import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; -import { - SENTRY_SEGMENT_NAME_SOURCE, - SENTRY_SEGMENT_ID, - SENTRY_SEGMENT_NAME, - SENTRY_SDK_NAME, - SENTRY_SDK_VERSION, - SENTRY_TRACE_LIFECYCLE, - USER_AGENT_ORIGINAL, - SENTRY_STATUS_MESSAGE, - SENTRY_ENVIRONMENT, - SENTRY_SDK_INTEGRATIONS, - SENTRY_OP, - SENTRY_ORIGIN, -} from '@sentry/conventions/attributes'; - -sentryTest( - 'sends a streamed span envelope if spanStreamingIntegration is enabled', - async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const spanEnvelopePromise = waitForStreamedSpanEnvelope(page); - - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - - const spanEnvelope = await spanEnvelopePromise; - - const envelopeHeader = spanEnvelope[0]; - const envelopeItem = spanEnvelope[1]; - const spans = envelopeItem[0][1].items; - - expect(envelopeHeader).toEqual({ - sdk: { - name: 'sentry.javascript.browser', - version: SDK_VERSION, - }, - sent_at: expect.any(String), - trace: { - environment: 'production', - public_key: 'public', - sample_rand: expect.any(String), - sample_rate: '1', - sampled: 'true', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - transaction: 'test-span', - }, - }); - - const numericSampleRand = parseFloat(envelopeHeader.trace!.sample_rand!); - const traceId = envelopeHeader.trace!.trace_id; - - expect(Number.isNaN(numericSampleRand)).toBe(false); - - expect(envelopeItem).toEqual([ - [ - { content_type: 'application/vnd.sentry.items.span.v2+json', item_count: 4, type: 'span' }, - { - version: 2, - ingest_settings: { infer_ip: 'auto', infer_user_agent: 'auto' }, - items: expect.any(Array), - }, - ], - ]); - - const segmentSpanId = spans.find(s => !!s.is_segment)?.span_id; - expect(segmentSpanId).toBeDefined(); - - expect(spans).toEqual([ - { - attributes: { - 'sentry.is_localhost': { value: false, type: 'boolean' }, - [SENTRY_OP]: { - type: 'string', - value: 'test-child', - }, - [SENTRY_ORIGIN]: { - type: 'string', - value: 'manual', - }, - [SENTRY_SDK_NAME]: { - type: 'string', - value: 'sentry.javascript.browser', - }, - [SENTRY_SDK_VERSION]: { - type: 'string', - value: SDK_VERSION, - }, - [SENTRY_SEGMENT_ID]: { - type: 'string', - value: segmentSpanId, - }, - [SENTRY_SEGMENT_NAME]: { - type: 'string', - value: 'test-span', - }, - [SENTRY_ENVIRONMENT]: { - type: 'string', - value: 'production', - }, - [SENTRY_TRACE_LIFECYCLE]: { - type: 'string', - value: 'stream', - }, - [USER_AGENT_ORIGINAL]: { - type: 'string', - value: expect.any(String), - }, - }, - end_timestamp: expect.any(Number), - is_segment: false, - name: 'test-child-span', - parent_span_id: segmentSpanId, - span_id: expect.stringMatching(/^[\da-f]{16}$/), - start_timestamp: expect.any(Number), - status: 'ok', - trace_id: traceId, - }, - { - attributes: { - 'sentry.is_localhost': { value: false, type: 'boolean' }, - [SENTRY_ORIGIN]: { - type: 'string', - value: 'manual', - }, - [SENTRY_SDK_NAME]: { - type: 'string', - value: 'sentry.javascript.browser', - }, - [SENTRY_SDK_VERSION]: { - type: 'string', - value: SDK_VERSION, - }, - [SENTRY_SEGMENT_ID]: { - type: 'string', - value: segmentSpanId, - }, - [SENTRY_SEGMENT_NAME]: { - type: 'string', - value: 'test-span', - }, - [SENTRY_ENVIRONMENT]: { - type: 'string', - value: 'production', - }, - [SENTRY_TRACE_LIFECYCLE]: { - type: 'string', - value: 'stream', - }, - [USER_AGENT_ORIGINAL]: { - type: 'string', - value: expect.any(String), - }, - }, - end_timestamp: expect.any(Number), - is_segment: false, - name: 'test-inactive-span', - parent_span_id: segmentSpanId, - span_id: expect.stringMatching(/^[\da-f]{16}$/), - start_timestamp: expect.any(Number), - status: 'ok', - trace_id: traceId, - }, - { - attributes: { - 'sentry.is_localhost': { value: false, type: 'boolean' }, - [SENTRY_ORIGIN]: { - type: 'string', - value: 'manual', - }, - [SENTRY_SDK_NAME]: { - type: 'string', - value: 'sentry.javascript.browser', - }, - [SENTRY_SDK_VERSION]: { - type: 'string', - value: SDK_VERSION, - }, - [SENTRY_SEGMENT_ID]: { - type: 'string', - value: segmentSpanId, - }, - [SENTRY_SEGMENT_NAME]: { - type: 'string', - value: 'test-span', - }, - [SENTRY_ENVIRONMENT]: { - type: 'string', - value: 'production', - }, - [SENTRY_STATUS_MESSAGE]: { - type: 'string', - value: 'Connection Refused', - }, - [SENTRY_TRACE_LIFECYCLE]: { - type: 'string', - value: 'stream', - }, - [USER_AGENT_ORIGINAL]: { - type: 'string', - value: expect.any(String), - }, - }, - end_timestamp: expect.any(Number), - is_segment: false, - name: 'test-manual-span', - parent_span_id: segmentSpanId, - span_id: expect.stringMatching(/^[\da-f]{16}$/), - start_timestamp: expect.any(Number), - status: 'error', - trace_id: traceId, - }, - { - attributes: { - 'sentry.is_localhost': { value: false, type: 'boolean' }, - 'culture.calendar': { - type: 'string', - value: expect.any(String), - }, - 'culture.locale': { - type: 'string', - value: expect.any(String), - }, - 'culture.timezone': { - type: 'string', - value: expect.any(String), - }, - [USER_AGENT_ORIGINAL]: { - type: 'string', - value: expect.any(String), - }, - 'url.full': { - type: 'string', - value: expect.any(String), - }, - [SENTRY_OP]: { - type: 'string', - value: 'test', - }, - [SENTRY_ORIGIN]: { - type: 'string', - value: 'manual', - }, - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { - type: 'integer', - value: 1, - }, - [SENTRY_SDK_NAME]: { - type: 'string', - value: 'sentry.javascript.browser', - }, - [SENTRY_SDK_VERSION]: { - type: 'string', - value: SDK_VERSION, - }, - [SENTRY_SDK_INTEGRATIONS]: { - type: 'array', - value: expect.arrayContaining(['SpanStreaming']), - }, - [SENTRY_SEGMENT_ID]: { - type: 'string', - value: segmentSpanId, - }, - [SENTRY_SEGMENT_NAME]: { - type: 'string', - value: 'test-span', - }, - [SENTRY_SEGMENT_NAME_SOURCE]: { - type: 'string', - value: 'custom', - }, - [SENTRY_ENVIRONMENT]: { - type: 'string', - value: 'production', - }, - [SENTRY_TRACE_LIFECYCLE]: { - type: 'string', - value: 'stream', - }, - }, - end_timestamp: expect.any(Number), - is_segment: true, - name: 'test-span', - span_id: segmentSpanId, - start_timestamp: expect.any(Number), - status: 'ok', - trace_id: traceId, - }, - ]); - }, -); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/thenable-with-extra-methods/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/thenable-with-extra-methods/test.ts index 44008a93a3e9..ed6ab723310c 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/thenable-with-extra-methods/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/thenable-with-extra-methods/test.ts @@ -1,6 +1,7 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest('preserves extra methods on real jQuery jqXHR objects', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { @@ -8,7 +9,7 @@ sentryTest('preserves extra methods on real jQuery jqXHR objects', async ({ getL } const url = await getLocalTestUrl({ testDir: __dirname }); - const transactionPromise = waitForTransactionRequest(page); + const spanPromise = waitForStreamedSpan(page, span => span.name === 'test-jqxhr'); await page.goto(url); @@ -27,9 +28,9 @@ sentryTest('preserves extra methods on real jQuery jqXHR objects', async ({ getL const testError = await page.evaluate(() => (window as any).jqXHRTestError); expect(testError).toBeNull(); - const transaction = envelopeRequestParser(await transactionPromise); - expect(transaction.transaction).toBe('test-jqxhr'); - expect(transaction.spans).toBeDefined(); + const span = await spanPromise; + expect(span.name).toBe('test-jqxhr'); + expect(span.is_segment).toBe(true); }); sentryTest('aborted request rejects promise correctly', async ({ getLocalTestUrl, page }) => { diff --git a/dev-packages/browser-integration-tests/suites/tracing/bindScopeToEmitter/init.js b/dev-packages/browser-integration-tests/suites/tracing/bindScopeToEmitter/init.js index bd0421b32761..7c200c542c56 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/bindScopeToEmitter/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/bindScopeToEmitter/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/bindScopeToEmitter/test.ts b/dev-packages/browser-integration-tests/suites/tracing/bindScopeToEmitter/test.ts index f95829febe1d..3b269806fc84 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/bindScopeToEmitter/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/bindScopeToEmitter/test.ts @@ -1,11 +1,7 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../utils/fixtures'; -import { - envelopeRequestParser, - shouldSkipCdnBundleTest, - shouldSkipTracingTest, - waitForTransactionRequest, -} from '../../../utils/helpers'; +import { shouldSkipCdnBundleTest, shouldSkipTracingTest } from '../../../utils/helpers'; +import { waitForStreamedSpan } from '../../../utils/spanUtils'; sentryTest('bindScopeToEmitter runs listeners with the bound scope active', async ({ getLocalTestUrl, page }) => { // `bindScopeToEmitter` is not exported from the CDN bundles, only from npm. @@ -13,19 +9,19 @@ sentryTest('bindScopeToEmitter runs listeners with the bound scope active', asyn sentryTest.skip(); } - const req = waitForTransactionRequest(page, e => e.transaction === 'parent'); + const rootSpanPromise = waitForStreamedSpan(page, span => span.name === 'parent'); + const childSpanPromise = waitForStreamedSpan(page, span => span.name === 'child-bound'); const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const parentEvent = envelopeRequestParser(await req); - const parentSpanId = parentEvent.contexts?.trace?.span_id; - const parentTraceId = parentEvent.contexts?.trace?.trace_id; + const [parentEvent, childBound] = await Promise.all([rootSpanPromise, childSpanPromise]); + const parentSpanId = parentEvent.span_id; + const parentTraceId = parentEvent.trace_id; expect(parentSpanId).toMatch(/[a-f\d]{16}/); // The listener fired while no span was active, yet `child-bound` is nested under `parent` // because the parent scope was bound to the emitter. - const childBound = parentEvent.spans?.find(s => s.description === 'child-bound'); expect(childBound).toBeDefined(); expect(childBound?.parent_span_id).toBe(parentSpanId); expect(childBound?.trace_id).toBe(parentTraceId); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/init.js index fdca84ba5c42..3595ea724749 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, integrations: [Sentry.browserTracingIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/tracing/continueTrace/full-tracing/init.js b/dev-packages/browser-integration-tests/suites/tracing/continueTrace/full-tracing/init.js index 65cfbab96ee2..b2280b70e307 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/continueTrace/full-tracing/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/continueTrace/full-tracing/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/continueTrace/full-tracing/test.ts b/dev-packages/browser-integration-tests/suites/tracing/continueTrace/full-tracing/test.ts index f53e4ef0615f..ecca9689b964 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/continueTrace/full-tracing/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/continueTrace/full-tracing/test.ts @@ -1,13 +1,15 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import type { EventAndTraceHeader } from '../../../../utils/helpers'; import { eventAndTraceHeaderRequestParser, - getFirstSentryEnvelopeRequest, shouldSkipTracingTest, waitForErrorRequest, - waitForTransactionRequest, } from '../../../../utils/helpers'; +import { + collectStreamedSpans, + waitForStreamedSpanAndTraceHeader, + waitForStreamedSpanAndTraceHeaderOnUrl, +} from '../../../../utils/spanUtils'; const SAMPLED_TRACE_ID = '12345678901234567890123456789012'; const SAMPLED_SPAN_ID = '1234567890123456'; @@ -28,26 +30,24 @@ sentryTest( return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) }); }); - // Discard the initial pageload transaction. - await getFirstSentryEnvelopeRequest(page, url, eventAndTraceHeaderRequestParser); + await waitForStreamedSpanAndTraceHeaderOnUrl(page, url); - const transactionPromise = waitForTransactionRequest( + const spanPromise = waitForStreamedSpanAndTraceHeader( page, - event => event.contexts?.trace?.trace_id === SAMPLED_TRACE_ID, + span => span.is_segment && span.name === 'continued-sampled', ); await page.locator('#sampled').click(); - const req = await transactionPromise; - const transaction = eventAndTraceHeaderRequestParser(req); - const traceContext = transaction[0].contexts?.trace; + const span = await spanPromise; + const traceContext = span[0]; - expect(traceContext?.trace_id).toBe(SAMPLED_TRACE_ID); - expect(traceContext?.parent_span_id).toBe(SAMPLED_SPAN_ID); - expect(transaction[0].transaction).toBe('continued-sampled'); + expect(traceContext.trace_id).toBe(SAMPLED_TRACE_ID); + expect(traceContext.parent_span_id).toBe(SAMPLED_SPAN_ID); + expect(span[0].name).toBe('continued-sampled'); // The incoming positive sampling decision is honored regardless of local config. - expect(transaction[1]?.sampled).toBe('true'); + expect(span[1]?.sampled).toBe('true'); // Outgoing request carries the continued trace. const outgoingRequest = await outgoingRequestPromise; @@ -58,7 +58,7 @@ sentryTest( ); sentryTest( - 'continueTrace continues an unsampled incoming trace without emitting a transaction', + 'continueTrace continues an unsampled incoming trace without emitting a span', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); @@ -71,11 +71,11 @@ sentryTest( return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) }); }); - await getFirstSentryEnvelopeRequest(page, url, eventAndTraceHeaderRequestParser); + await waitForStreamedSpanAndTraceHeaderOnUrl(page, url); - // The captured error carries the continued (unsampled) trace even though no transaction is sent. const errorPromise = waitForErrorRequest(page); + const spans = collectStreamedSpans(page); await page.locator('#unsampled').click(); const [errorEvent] = eventAndTraceHeaderRequestParser(await errorPromise); @@ -85,6 +85,8 @@ sentryTest( const outgoingRequest = await outgoingRequestPromise; const headers = await outgoingRequest.allHeaders(); expect(headers['sentry-trace']).toMatch(new RegExp(`^${UNSAMPLED_TRACE_ID}-[a-f0-9]{16}-0$`)); + await page.evaluate(() => (window as any).Sentry.flush()); + expect(spans.some(span => span.trace_id === UNSAMPLED_TRACE_ID)).toBe(false); }, ); @@ -101,20 +103,18 @@ sentryTest( return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) }); }); - await getFirstSentryEnvelopeRequest(page, url, eventAndTraceHeaderRequestParser); + await waitForStreamedSpanAndTraceHeaderOnUrl(page, url); - // With tracesSampleRate=1 the deferred decision resolves to sampled, so a transaction is emitted - // on the continued trace id. - const transactionPromise = waitForTransactionRequest( + const spanPromise = waitForStreamedSpanAndTraceHeader( page, - event => event.contexts?.trace?.trace_id === DEFERRED_TRACE_ID, + span => span.is_segment && span.name === 'continued-deferred', ); await page.locator('#deferred').click(); - const transaction = eventAndTraceHeaderRequestParser(await transactionPromise); - expect(transaction[0].contexts?.trace?.trace_id).toBe(DEFERRED_TRACE_ID); - expect(transaction[0].transaction).toBe('continued-deferred'); + const span = await spanPromise; + expect(span[0].trace_id).toBe(DEFERRED_TRACE_ID); + expect(span[0].name).toBe('continued-deferred'); }, ); @@ -129,21 +129,20 @@ sentryTest('continueTrace with no incoming trace starts a fresh trace', async ({ return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) }); }); - const [pageloadEvent] = await getFirstSentryEnvelopeRequest( + const [pageloadEvent] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url); + const pageloadTraceId = pageloadEvent.trace_id; + + const spanPromise = waitForStreamedSpanAndTraceHeader( page, - url, - eventAndTraceHeaderRequestParser, + span => span.is_segment && span.name === 'continued-noTrace', ); - const pageloadTraceId = pageloadEvent.contexts?.trace?.trace_id; - - const transactionPromise = waitForTransactionRequest(page, event => event.transaction === 'continued-noTrace'); await page.locator('#noTrace').click(); - const transaction = eventAndTraceHeaderRequestParser(await transactionPromise); - const traceId = transaction[0].contexts?.trace?.trace_id; + const span = await spanPromise; + const traceId = span[0].trace_id; expect(traceId).toMatch(/^[a-f0-9]{32}$/); expect(traceId).not.toBe(pageloadTraceId); - expect(transaction[0].contexts?.trace).not.toHaveProperty('parent_span_id'); + expect(span[0]).not.toHaveProperty('parent_span_id'); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/continueTrace/tracesSampleRate-zero/init.js b/dev-packages/browser-integration-tests/suites/tracing/continueTrace/tracesSampleRate-zero/init.js index 06a40a8e55ec..5e9c86105ae7 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/continueTrace/tracesSampleRate-zero/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/continueTrace/tracesSampleRate-zero/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/continueTrace/tracesSampleRate-zero/test.ts b/dev-packages/browser-integration-tests/suites/tracing/continueTrace/tracesSampleRate-zero/test.ts index 77956fb134bf..e7321abb38ee 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/continueTrace/tracesSampleRate-zero/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/continueTrace/tracesSampleRate-zero/test.ts @@ -4,8 +4,8 @@ import { eventAndTraceHeaderRequestParser, shouldSkipTracingTest, waitForErrorRequest, - waitForTransactionRequest, } from '../../../../utils/helpers'; +import { collectStreamedSpans, waitForStreamedSpanAndTraceHeader } from '../../../../utils/spanUtils'; const SAMPLED_TRACE_ID = '12345678901234567890123456789012'; const SAMPLED_SPAN_ID = '1234567890123456'; @@ -26,18 +26,17 @@ sentryTest( return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) }); }); - // With tracesSampleRate=0 there is no pageload transaction, so we only wait for the continued one. - const transactionPromise = waitForTransactionRequest( + const spanPromise = waitForStreamedSpanAndTraceHeader( page, - event => event.contexts?.trace?.trace_id === SAMPLED_TRACE_ID, + span => span.is_segment && span.name === 'continued-sampled', ); await page.goto(url); await page.locator('#sampled').click(); - const transaction = eventAndTraceHeaderRequestParser(await transactionPromise); - expect(transaction[0].contexts?.trace?.trace_id).toBe(SAMPLED_TRACE_ID); - expect(transaction[0].contexts?.trace?.parent_span_id).toBe(SAMPLED_SPAN_ID); + const span = await spanPromise; + expect(span[0].trace_id).toBe(SAMPLED_TRACE_ID); + expect(span[0].parent_span_id).toBe(SAMPLED_SPAN_ID); const outgoingRequest = await outgoingRequestPromise; const headers = await outgoingRequest.allHeaders(); @@ -46,7 +45,7 @@ sentryTest( ); sentryTest( - 'continueTrace does not emit a transaction for a deferred decision with tracesSampleRate=0', + 'continueTrace does not emit a span for a deferred decision with tracesSampleRate=0', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); @@ -61,9 +60,9 @@ sentryTest( await page.goto(url); - // The captured error carries the continued trace even though no transaction is sent. const errorPromise = waitForErrorRequest(page); + const spans = collectStreamedSpans(page); await page.locator('#deferred').click(); const [errorEvent] = eventAndTraceHeaderRequestParser(await errorPromise); @@ -73,6 +72,8 @@ sentryTest( const outgoingRequest = await outgoingRequestPromise; const headers = await outgoingRequest.allHeaders(); expect(headers['sentry-trace']).toMatch(new RegExp(`^${DEFERRED_TRACE_ID}-[a-f0-9]{16}-0$`)); + await page.evaluate(() => (window as any).Sentry.flush()); + expect(spans.some(span => span.trace_id === DEFERRED_TRACE_ID)).toBe(false); }, ); @@ -94,6 +95,7 @@ sentryTest( const errorPromise = waitForErrorRequest(page); + const spans = collectStreamedSpans(page); await page.locator('#unsampled').click(); const [errorEvent] = eventAndTraceHeaderRequestParser(await errorPromise); @@ -102,5 +104,7 @@ sentryTest( const outgoingRequest = await outgoingRequestPromise; const headers = await outgoingRequest.allHeaders(); expect(headers['sentry-trace']).toMatch(new RegExp(`^${UNSAMPLED_TRACE_ID}-[a-f0-9]{16}-0$`)); + await page.evaluate(() => (window as any).Sentry.flush()); + expect(spans.some(span => span.trace_id === UNSAMPLED_TRACE_ID)).toBe(false); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/continueTrace/tracing-without-performance/init.js b/dev-packages/browser-integration-tests/suites/tracing/continueTrace/tracing-without-performance/init.js index 9eb615a288dd..79d41d4ce43c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/continueTrace/tracing-without-performance/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/continueTrace/tracing-without-performance/init.js @@ -5,7 +5,6 @@ window.Sentry = Sentry; // In the browser, "tracing without performance" (TwP) means enabling `browserTracingIntegration` // but not setting `tracesSampleRate`. Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/continueTrace/tracing-without-performance/test.ts b/dev-packages/browser-integration-tests/suites/tracing/continueTrace/tracing-without-performance/test.ts index e9ebd9dcec57..bbf746fdc4df 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/continueTrace/tracing-without-performance/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/continueTrace/tracing-without-performance/test.ts @@ -10,7 +10,7 @@ const SAMPLED_TRACE_ID = '12345678901234567890123456789012'; const UNSAMPLED_TRACE_ID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; const DEFERRED_TRACE_ID = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; -// In TwP mode no transactions are emitted, so each variant is observed via the captured error event +// In TwP mode no spans are emitted, so each variant is observed via the captured error event // (which carries the continued trace on its `contexts.trace`) and the outgoing request headers. const VARIANTS = [ { button: 'sampled', traceId: SAMPLED_TRACE_ID, sampledFlag: '-1' }, 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 index 01c1dc121daf..b6c2bad12033 100644 --- 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 @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false })], tracesSampleRate: 1, 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 index 7d33497a988b..23ec0ce9edda 100644 --- 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 @@ -8,6 +8,7 @@ import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest, } from '../../../utils/helpers'; +import { waitForStreamedSpanAndTraceHeader } from '../../../utils/spanUtils'; sentryTest('updates the DSC when the txn name is updated and high-quality', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { @@ -38,7 +39,7 @@ sentryTest('updates the DSC when the txn name is updated and high-quality', asyn 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 + 11. Make another request and check that it starts its own segment in the same trace 12. Capture an error and check that envelope trace header has no span information */ @@ -133,16 +134,9 @@ sentryTest('updates the DSC when the txn name is updated and high-quality', asyn }); // 10 - const txnEventPromise = getMultipleSentryEnvelopeRequests( - page, - 1, - { envelopeType: 'transaction' }, - eventAndTraceHeaderRequestParser, - ); - + const spanPromise = waitForStreamedSpanAndTraceHeader(page, span => span.name === 'updated-root-span-2'); await page.locator('#btnEndSpan').click(); - - const [txnEvent, txnEnvelopeTraceHeader] = (await txnEventPromise)[0]; + const [span, txnEnvelopeTraceHeader] = await spanPromise; expect(txnEnvelopeTraceHeader).toEqual({ environment: 'production', public_key: 'public', @@ -154,16 +148,28 @@ sentryTest('updates the DSC when the txn name is updated and high-quality', asyn sample_rand: expect.any(String), }); - expect(txnEvent.transaction).toEqual('updated-root-span-2'); + expect(span.name).toEqual('updated-root-span-2'); // 11 + const requestSpanPromise = waitForStreamedSpanAndTraceHeader( + page, + span => span.name === 'GET sentry-test-site.example', + ); const baggageItemsAfterEnd = await makeRequestAndGetBaggageItems(page); expect(baggageItemsAfterEnd).toEqual([ 'sentry-environment=production', 'sentry-public_key=public', 'sentry-release=1.1.1', + expect.stringMatching(/sentry-sample_rand=0\.\d+/), + 'sentry-sample_rate=1', + 'sentry-sampled=true', `sentry-trace_id=${traceId}`, + 'sentry-transaction=GET%20sentry-test-site.example', ]); + const [requestSpan] = await requestSpanPromise; + expect(requestSpan.is_segment).toBe(true); + expect(requestSpan.trace_id).toBe(traceId); + expect(requestSpan).not.toHaveProperty('parent_span_id'); // 12 const errorEnvelopeTraceHeaderAfterEnd = await captureErrorAndGetEnvelopeTraceHeader(page); diff --git a/dev-packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js b/dev-packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js index 66fe57da4d6f..376c14f7b559 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js @@ -4,7 +4,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], environment: 'production', diff --git a/dev-packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/test.ts b/dev-packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/test.ts index 23043ebc770d..9e146d46bfac 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/test.ts @@ -1,11 +1,7 @@ import { expect } from '@playwright/test'; -import type { EventEnvelopeHeaders } from '@sentry/core'; import { sentryTest } from '../../../utils/fixtures'; -import { - envelopeHeaderRequestParser, - getFirstSentryEnvelopeRequest, - shouldSkipTracingTest, -} from '../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../utils/helpers'; +import { waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../utils/spanUtils'; sentryTest( 'should only include transaction name if source is better than an unparameterized URL', @@ -16,13 +12,13 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); - const envHeader = await getFirstSentryEnvelopeRequest(page, url, envelopeHeaderRequestParser); + const [, traceHeader] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url); - expect(envHeader.trace).toBeDefined(); - expect(envHeader.trace).toEqual({ + expect(traceHeader).toBeDefined(); + expect(traceHeader).toEqual({ environment: 'production', sample_rate: '1', - transaction: expect.stringContaining('/index.html'), + transaction: 'Pageload', trace_id: expect.stringMatching(/[a-f\d]{32}/), public_key: 'public', sampled: 'true', diff --git a/dev-packages/browser-integration-tests/suites/tracing/envelope-header/init.js b/dev-packages/browser-integration-tests/suites/tracing/envelope-header/init.js index b90217ce3c39..79b4b0c89555 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/envelope-header/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/envelope-header/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracePropagationTargets: [/.*/], diff --git a/dev-packages/browser-integration-tests/suites/tracing/envelope-header/test.ts b/dev-packages/browser-integration-tests/suites/tracing/envelope-header/test.ts index 7617b498efb2..242bf9ff462d 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/envelope-header/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/envelope-header/test.ts @@ -1,14 +1,10 @@ import { expect } from '@playwright/test'; -import type { EventEnvelopeHeaders } from '@sentry/core'; import { sentryTest } from '../../../utils/fixtures'; -import { - envelopeHeaderRequestParser, - getFirstSentryEnvelopeRequest, - shouldSkipTracingTest, -} from '../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../utils/helpers'; +import { waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../utils/spanUtils'; sentryTest( - 'should send dynamic sampling context data in trace envelope header of a transaction envelope', + 'should send dynamic sampling context data in trace envelope header of a streamed span envelope', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); @@ -16,14 +12,14 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); - const envHeader = await getFirstSentryEnvelopeRequest(page, url, envelopeHeaderRequestParser); + const [, traceHeader] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url); // In this test, we don't expect trace.transaction to be present because without a custom routing instrumentation // we for now don't have parameterization. This might change in the future but for now the only way of having // transaction in DSC with the default browserTracingIntegration is when the transaction name is set manually. // This scenario is covered in another integration test (envelope-header-transaction-name). - expect(envHeader.trace).toBeDefined(); - expect(envHeader.trace).toEqual({ + expect(traceHeader).toBeDefined(); + expect(traceHeader).toEqual({ environment: 'production', sample_rate: '1', trace_id: expect.stringMatching(/[a-f\d]{32}/), diff --git a/dev-packages/browser-integration-tests/suites/tracing/http-client-span-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/http-client-span/init.js similarity index 54% rename from dev-packages/browser-integration-tests/suites/tracing/http-client-span-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/http-client-span/init.js index 94db849f5fde..10e5ac1b84eb 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/http-client-span-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/http-client-span/init.js @@ -4,10 +4,7 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [ - Sentry.browserTracingIntegration({ instrumentPageLoad: false, instrumentNavigation: false }), - Sentry.spanStreamingIntegration(), - ], + integrations: [Sentry.browserTracingIntegration({ instrumentPageLoad: false, instrumentNavigation: false })], tracesSampleRate: 1, sendClientReports: true, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/http-client-span-streamed/subject.js b/dev-packages/browser-integration-tests/suites/tracing/http-client-span/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/http-client-span-streamed/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/http-client-span/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/http-client-span-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/http-client-span/test.ts similarity index 63% rename from dev-packages/browser-integration-tests/suites/tracing/http-client-span-streamed/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/http-client-span/test.ts index 81d41ada7ccc..634068358415 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/http-client-span-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/http-client-span/test.ts @@ -1,10 +1,11 @@ +import { SENTRY_ORIGIN, SENTRY_OP, URL_DOMAIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../utils/helpers'; import { getSpanOp, waitForStreamedSpan } from '../../../utils/spanUtils'; sentryTest( - 'sends http.client span for fetch requests without an active span when span streaming is enabled', + 'sends http.client span for fetch requests without an active span with default span streaming', async ({ getLocalTestUrl, page }) => { sentryTest.skip(shouldSkipTracingTest()); @@ -24,9 +25,11 @@ sentryTest( const span = await spanPromise; + expect(span.is_segment).toBe(true); + expect(span).not.toHaveProperty('parent_span_id'); expect(span.name).toBe('GET sentry-test-site.example'); - expect(span.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.browser' }); - expect(span.attributes['sentry.op']).toEqual({ type: 'string', value: 'http.client' }); - expect(span.attributes['url.domain']).toEqual({ type: 'string', value: 'sentry-test-site.example' }); + expect(span.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.http.browser' }); + expect(span.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'http.client' }); + expect(span.attributes[URL_DOMAIN]).toEqual({ type: 'string', value: 'sentry-test-site.example' }); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-static/init.js b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-static/init.js new file mode 100644 index 000000000000..9e5c6fa2f2e9 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-static/init.js @@ -0,0 +1,50 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + idleTimeout: 3000, + finalTimeout: 3000, + childSpanTimeout: 3000, + }), + ], + ignoreSpans: [/ignore/], + tracesSampleRate: 1, + debug: true, +}); + +const waitFor = time => new Promise(resolve => setTimeout(resolve, time)); + +Sentry.startSpanManual( + { + name: 'take-me', + }, + async span => { + await waitFor(500); + span.end(); + }, +); + +Sentry.startSpanManual( + { + name: 'ignore-me', + }, + async span => { + await waitFor(1500); + span.end(); + }, +); + +Sentry.startSpanManual( + { + name: 'ignore-me-too', + }, + async span => { + await waitFor(2500); + span.end(); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-static/test.ts new file mode 100644 index 000000000000..bc752c9cdf41 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-static/test.ts @@ -0,0 +1,29 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers'; + +sentryTest( + 'adjusts the end timestamp of the root idle span if child spans are ignored', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const eventData = envelopeRequestParser(await pageloadRequestPromise); + + const { start_timestamp: startTimestamp, timestamp: endTimestamp } = eventData; + const durationSeconds = endTimestamp! - startTimestamp!; + + const spans = eventData.spans || []; + + expect(durationSeconds).toBeGreaterThan(0); + expect(durationSeconds).toBeLessThan(1.5); + + expect(spans.some(span => span.description === 'take-me')).toBe(true); + expect(spans.some(span => span.description?.includes('ignore-me'))).toBe(false); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/attributes/init.js b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/attributes/init.js similarity index 82% rename from dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/attributes/init.js rename to dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/attributes/init.js index f57cb5bfaa57..d4000b23ae24 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/attributes/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/attributes/init.js @@ -4,7 +4,7 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.spanStreamingIntegration()], + integrations: [], ignoreSpans: [{ attributes: { 'http.response.status_code': 200 } }], tracesSampleRate: 1, debug: true, diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/attributes/subject.js b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/attributes/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/attributes/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/attributes/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/attributes/test.ts b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/attributes/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/attributes/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/attributes/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/child/init.js b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/child/init.js similarity index 82% rename from dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/child/init.js rename to dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/child/init.js index 086383ccdd54..4346f1d8f878 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/child/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/child/init.js @@ -4,7 +4,7 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.spanStreamingIntegration()], + integrations: [], ignoreSpans: [/ignore/, { op: 'ignored-op' }], parentSpanIsAlwaysRootSpan: false, tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/child/subject.js b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/child/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/child/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/child/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/child/test.ts b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/child/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/child/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/child/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/init.js b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-child/init.js similarity index 76% rename from dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/init.js rename to dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-child/init.js index 588769fbea94..df2361332e75 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-child/init.js @@ -4,7 +4,7 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()], + integrations: [Sentry.browserTracingIntegration()], ignoreSpans: [/ignored-click-listener/], parentSpanIsAlwaysRootSpan: false, tracePropagationTargets: ['sentry-test-external.io'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/subject.js b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-child/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-child/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/template.html b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-child/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/template.html rename to dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-child/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/test.ts b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-child/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-child/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-http-client/init.js b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-http-client/init.js similarity index 76% rename from dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-http-client/init.js rename to dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-http-client/init.js index 788e05781740..2bd0d43829a3 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-http-client/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-http-client/init.js @@ -4,7 +4,7 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()], + integrations: [Sentry.browserTracingIntegration()], ignoreSpans: [{ attributes: { 'sentry.op': 'http.client' } }], tracePropagationTargets: ['sentry-test-external.io'], tracesSampleRate: 0, diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-http-client/subject.js b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-http-client/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-http-client/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-http-client/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-http-client/template.html b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-http-client/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-http-client/template.html rename to dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-http-client/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-http-client/test.ts b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-http-client/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-http-client/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-http-client/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-segment/init.js b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-segment/init.js similarity index 74% rename from dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-segment/init.js rename to dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-segment/init.js index 616ae3d942f6..8233baa577e3 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-segment/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-segment/init.js @@ -4,7 +4,7 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()], + integrations: [Sentry.browserTracingIntegration()], ignoreSpans: [{ op: 'pageload' }], tracePropagationTargets: ['sentry-test-external.io'], tracesSampleRate: 0, diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-segment/subject.js b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-segment/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-segment/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-segment/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-segment/template.html b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-segment/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-segment/template.html rename to dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-segment/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-segment/test.ts b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-segment/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-segment/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/continued-trace-segment/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/init.js b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/init.js index 9e5c6fa2f2e9..385a1cdf1df5 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/segment/init.js b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/segment/init.js similarity index 79% rename from dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/segment/init.js rename to dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/segment/init.js index 3a710324f0e1..6f5aa2608b79 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/segment/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/segment/init.js @@ -4,7 +4,7 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.spanStreamingIntegration()], + integrations: [], ignoreSpans: [/ignore/], tracesSampleRate: 1, debug: true, diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/segment/subject.js b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/segment/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/segment/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/segment/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/segment/test.ts b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/segment/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/segment/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/segment/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/test.ts index bc752c9cdf41..aca9af0055e6 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans/test.ts @@ -1,6 +1,7 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../utils/spanUtils'; sentryTest( 'adjusts the end timestamp of the root idle span if child spans are ignored', @@ -9,21 +10,22 @@ sentryTest( sentryTest.skip(); } - const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); + const spans = collectStreamedSpans(page); + const pageloadRequestPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const eventData = envelopeRequestParser(await pageloadRequestPromise); + const eventData = await pageloadRequestPromise; - const { start_timestamp: startTimestamp, timestamp: endTimestamp } = eventData; + const { start_timestamp: startTimestamp, end_timestamp: endTimestamp } = eventData; const durationSeconds = endTimestamp! - startTimestamp!; - const spans = eventData.spans || []; + await page.evaluate(() => (window as any).Sentry.flush()); expect(durationSeconds).toBeGreaterThan(0); expect(durationSeconds).toBeLessThan(1.5); - expect(spans.some(span => span.description === 'take-me')).toBe(true); - expect(spans.some(span => span.description?.includes('ignore-me'))).toBe(false); + expect(spans.some(span => span.name === 'take-me')).toBe(true); + expect(spans.some(span => span.name?.includes('ignore-me'))).toBe(false); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested/init.js b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink-static/init.js similarity index 75% rename from dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested/init.js rename to dev-packages/browser-integration-tests/suites/tracing/linking-addLink-static/init.js index 9afcee48dc4a..bd0421b32761 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink-static/init.js @@ -3,7 +3,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.spanStreamingIntegration()], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-addLink-streamed/subject.js b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink-static/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/linking-addLink-streamed/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/linking-addLink-static/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-addLink-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink-static/test.ts new file mode 100644 index 000000000000..063569d95996 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink-static/test.ts @@ -0,0 +1,77 @@ +import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +import { expect } from '@playwright/test'; +import type { SpanJSON, TransactionEvent } from '@sentry/core'; +import { sentryTest } from '../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers'; + +sentryTest('should link spans with addLink() in trace context', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const rootSpan1Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan1'); + const rootSpan2Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan2'); + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const rootSpan1 = envelopeRequestParser(await rootSpan1Promise); + const rootSpan2 = envelopeRequestParser(await rootSpan2Promise); + + const rootSpan1_traceId = rootSpan1.contexts?.trace?.trace_id as string; + const rootSpan1_spanId = rootSpan1.contexts?.trace?.span_id as string; + + expect(rootSpan1.transaction).toBe('rootSpan1'); + expect(rootSpan1.spans).toEqual([]); + + expect(rootSpan2.transaction).toBe('rootSpan2'); + expect(rootSpan2.spans).toEqual([]); + + expect(rootSpan2.contexts?.trace?.links?.length).toBe(1); + expect(rootSpan2.contexts?.trace?.links?.[0]).toMatchObject({ + attributes: { [SENTRY_LINK_TYPE]: 'previous_trace' }, + sampled: true, + span_id: rootSpan1_spanId, + trace_id: rootSpan1_traceId, + }); +}); + +sentryTest('should link spans with addLink() in nested startSpan() calls', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const rootSpan1Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan1'); + const rootSpan3Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan3'); + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const rootSpan1 = envelopeRequestParser(await rootSpan1Promise); + const rootSpan3 = envelopeRequestParser(await rootSpan3Promise); + + const rootSpan1_traceId = rootSpan1.contexts?.trace?.trace_id as string; + const rootSpan1_spanId = rootSpan1.contexts?.trace?.span_id as string; + + const [childSpan_3_1, childSpan_3_2] = rootSpan3.spans as [SpanJSON, SpanJSON]; + const rootSpan3_traceId = rootSpan3.contexts?.trace?.trace_id as string; + const rootSpan3_spanId = rootSpan3.contexts?.trace?.span_id as string; + + expect(rootSpan3.transaction).toBe('rootSpan3'); + + expect(childSpan_3_1.description).toBe('childSpan3.1'); + expect(childSpan_3_1.links?.length).toBe(1); + expect(childSpan_3_1.links?.[0]).toMatchObject({ + attributes: { [SENTRY_LINK_TYPE]: 'previous_trace' }, + sampled: true, + span_id: rootSpan1_spanId, + trace_id: rootSpan1_traceId, + }); + + expect(childSpan_3_2.description).toBe('childSpan3.2'); + expect(childSpan_3_2.links?.[0]).toMatchObject({ + sampled: true, + span_id: rootSpan3_spanId, + trace_id: rootSpan3_traceId, + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-addLink-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink-streamed/test.ts deleted file mode 100644 index bafa95aee03f..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/linking-addLink-streamed/test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../utils/helpers'; -import { waitForStreamedSpan, waitForStreamedSpans } from '../../../utils/spanUtils'; - -sentryTest('links spans with addLink() in trace context', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const rootSpan1Promise = waitForStreamedSpan(page, s => s.name === 'rootSpan1' && !!s.is_segment); - const rootSpan2Promise = waitForStreamedSpan(page, s => s.name === 'rootSpan2' && !!s.is_segment); - - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - - const rootSpan1 = await rootSpan1Promise; - const rootSpan2 = await rootSpan2Promise; - - expect(rootSpan1.name).toBe('rootSpan1'); - expect(rootSpan1.links).toBeUndefined(); - - expect(rootSpan2.name).toBe('rootSpan2'); - expect(rootSpan2.links).toHaveLength(1); - expect(rootSpan2.links?.[0]).toMatchObject({ - attributes: { 'sentry.link.type': { type: 'string', value: 'previous_trace' } }, - sampled: true, - span_id: rootSpan1.span_id, - trace_id: rootSpan1.trace_id, - }); -}); - -sentryTest('links spans with addLink() in nested startSpan() calls', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const rootSpan1Promise = waitForStreamedSpan(page, s => s.name === 'rootSpan1' && !!s.is_segment); - const rootSpan3SpansPromise = waitForStreamedSpans(page, spans => - spans.some(s => s.name === 'rootSpan3' && s.is_segment), - ); - - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - - const rootSpan1 = await rootSpan1Promise; - const rootSpan3Spans = await rootSpan3SpansPromise; - - const rootSpan3 = rootSpan3Spans.find(s => s.name === 'rootSpan3')!; - const childSpan1 = rootSpan3Spans.find(s => s.name === 'childSpan3.1')!; - const childSpan2 = rootSpan3Spans.find(s => s.name === 'childSpan3.2')!; - - expect(rootSpan3.name).toBe('rootSpan3'); - - expect(childSpan1.name).toBe('childSpan3.1'); - expect(childSpan1.links).toHaveLength(1); - expect(childSpan1.links?.[0]).toMatchObject({ - attributes: { 'sentry.link.type': { type: 'string', value: 'previous_trace' } }, - sampled: true, - span_id: rootSpan1.span_id, - trace_id: rootSpan1.trace_id, - }); - - expect(childSpan2.name).toBe('childSpan3.2'); - expect(childSpan2.links?.[0]).toMatchObject({ - sampled: true, - span_id: rootSpan3.span_id, - trace_id: rootSpan3.trace_id, - }); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/init.js b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/init.js index 848d72e6bfdc..3ec6adbbdb75 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/test.ts b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/test.ts index 9c556335651e..e7f009c0e911 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/test.ts @@ -1,76 +1,66 @@ -import { expect } from '@playwright/test'; -import type { SpanJSON, TransactionEvent } from '@sentry/core'; -import { sentryTest } from '../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers'; - -sentryTest('should link spans with addLink() in trace context', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +sentryTest('links spans with addLink() in trace context', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); - const rootSpan1Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan1'); - const rootSpan2Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan2'); + const rootSpan1Promise = waitForStreamedSpan(page, s => s.name === 'rootSpan1' && !!s.is_segment); + const rootSpan2Promise = waitForStreamedSpan(page, s => s.name === 'rootSpan2' && !!s.is_segment); const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const rootSpan1 = envelopeRequestParser(await rootSpan1Promise); - const rootSpan2 = envelopeRequestParser(await rootSpan2Promise); - - const rootSpan1_traceId = rootSpan1.contexts?.trace?.trace_id as string; - const rootSpan1_spanId = rootSpan1.contexts?.trace?.span_id as string; - - expect(rootSpan1.transaction).toBe('rootSpan1'); - expect(rootSpan1.spans).toEqual([]); + const rootSpan1 = await rootSpan1Promise; + const rootSpan2 = await rootSpan2Promise; - expect(rootSpan2.transaction).toBe('rootSpan2'); - expect(rootSpan2.spans).toEqual([]); + expect(rootSpan1.name).toBe('rootSpan1'); + expect(rootSpan1.links).toBeUndefined(); - expect(rootSpan2.contexts?.trace?.links?.length).toBe(1); - expect(rootSpan2.contexts?.trace?.links?.[0]).toMatchObject({ - attributes: { 'sentry.link.type': 'previous_trace' }, + expect(rootSpan2.name).toBe('rootSpan2'); + expect(rootSpan2.links).toHaveLength(1); + expect(rootSpan2.links?.[0]).toMatchObject({ + attributes: { [SENTRY_LINK_TYPE]: { type: 'string', value: 'previous_trace' } }, sampled: true, - span_id: rootSpan1_spanId, - trace_id: rootSpan1_traceId, + span_id: rootSpan1.span_id, + trace_id: rootSpan1.trace_id, }); }); +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../utils/helpers'; +import { waitForStreamedSpan } from '../../../utils/spanUtils'; -sentryTest('should link spans with addLink() in nested startSpan() calls', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +sentryTest('links spans with addLink() in nested startSpan() calls', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); - const rootSpan1Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan1'); - const rootSpan3Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan3'); + const rootSpan1Promise = waitForStreamedSpan(page, s => s.name === 'rootSpan1' && !!s.is_segment); + const rootSpan3Promise = waitForStreamedSpan(page, span => span.name === 'rootSpan3' && span.is_segment); + const childSpan1Promise = waitForStreamedSpan(page, span => span.name === 'childSpan3.1'); + const childSpan2Promise = waitForStreamedSpan(page, span => span.name === 'childSpan3.2'); const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const rootSpan1 = envelopeRequestParser(await rootSpan1Promise); - const rootSpan3 = envelopeRequestParser(await rootSpan3Promise); - - const rootSpan1_traceId = rootSpan1.contexts?.trace?.trace_id as string; - const rootSpan1_spanId = rootSpan1.contexts?.trace?.span_id as string; - - const [childSpan_3_1, childSpan_3_2] = rootSpan3.spans as [SpanJSON, SpanJSON]; - const rootSpan3_traceId = rootSpan3.contexts?.trace?.trace_id as string; - const rootSpan3_spanId = rootSpan3.contexts?.trace?.span_id as string; + const rootSpan1 = await rootSpan1Promise; + const [rootSpan3, childSpan1, childSpan2] = await Promise.all([ + rootSpan3Promise, + childSpan1Promise, + childSpan2Promise, + ]); - expect(rootSpan3.transaction).toBe('rootSpan3'); + expect(rootSpan3.name).toBe('rootSpan3'); - expect(childSpan_3_1.description).toBe('childSpan3.1'); - expect(childSpan_3_1.links?.length).toBe(1); - expect(childSpan_3_1.links?.[0]).toMatchObject({ - attributes: { 'sentry.link.type': 'previous_trace' }, + expect(childSpan1.name).toBe('childSpan3.1'); + expect(childSpan1.links).toHaveLength(1); + expect(childSpan1.links?.[0]).toMatchObject({ + attributes: { [SENTRY_LINK_TYPE]: { type: 'string', value: 'previous_trace' } }, sampled: true, - span_id: rootSpan1_spanId, - trace_id: rootSpan1_traceId, + span_id: rootSpan1.span_id, + trace_id: rootSpan1.trace_id, }); - expect(childSpan_3_2.description).toBe('childSpan3.2'); - expect(childSpan_3_2.links?.[0]).toMatchObject({ + expect(childSpan2.name).toBe('childSpan3.2'); + expect(childSpan2.links?.[0]).toMatchObject({ sampled: true, - span_id: rootSpan3_spanId, - trace_id: rootSpan3_traceId, + span_id: rootSpan3.span_id, + trace_id: rootSpan3.trace_id, }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/init.js b/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/init.js index 848d72e6bfdc..3ec6adbbdb75 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/test.ts b/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/test.ts index 529eae04ae03..56aaebf93cde 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/test.ts @@ -1,47 +1,50 @@ +import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import type { SpanJSON, TransactionEvent } from '@sentry/core'; import { sentryTest } from '../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../utils/helpers'; +import { collectStreamedSpans, waitForStreamedSpan } from '../../../utils/spanUtils'; sentryTest('should link spans with addLinks() in trace context', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); } - const rootSpan1Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan1'); - const rootSpan2Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan2'); - const rootSpan3Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan3'); + const rootSpan1Promise = waitForStreamedSpan(page, event => event.name === 'rootSpan1'); + const rootSpan2Promise = waitForStreamedSpan(page, event => event.name === 'rootSpan2'); + const rootSpan3Promise = waitForStreamedSpan(page, event => event.name === 'rootSpan3'); + const allSpans = collectStreamedSpans(page); const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const rootSpan1 = envelopeRequestParser(await rootSpan1Promise); - const rootSpan2 = envelopeRequestParser(await rootSpan2Promise); - const rootSpan3 = envelopeRequestParser(await rootSpan3Promise); + const rootSpan1 = await rootSpan1Promise; + const rootSpan2 = await rootSpan2Promise; + const rootSpan3 = await rootSpan3Promise; - const rootSpan1_traceId = rootSpan1.contexts?.trace?.trace_id as string; - const rootSpan1_spanId = rootSpan1.contexts?.trace?.span_id as string; + const rootSpan1_traceId = rootSpan1.trace_id as string; + const rootSpan1_spanId = rootSpan1.span_id as string; - expect(rootSpan1.transaction).toBe('rootSpan1'); - expect(rootSpan1.spans).toEqual([]); + expect(rootSpan1.name).toBe('rootSpan1'); + expect(allSpans.filter(span => span.parent_span_id === rootSpan1.span_id)).toEqual([]); - const rootSpan2_traceId = rootSpan2.contexts?.trace?.trace_id as string; - const rootSpan2_spanId = rootSpan2.contexts?.trace?.span_id as string; + const rootSpan2_traceId = rootSpan2.trace_id as string; + const rootSpan2_spanId = rootSpan2.span_id as string; - expect(rootSpan2.transaction).toBe('rootSpan2'); - expect(rootSpan2.spans).toEqual([]); + expect(rootSpan2.name).toBe('rootSpan2'); + expect(allSpans.filter(span => span.parent_span_id === rootSpan2.span_id)).toEqual([]); - expect(rootSpan3.transaction).toBe('rootSpan3'); - expect(rootSpan3.spans).toEqual([]); - expect(rootSpan3.contexts?.trace?.links?.length).toBe(2); - expect(rootSpan3.contexts?.trace?.links).toEqual([ + expect(rootSpan3.name).toBe('rootSpan3'); + expect(allSpans.filter(span => span.parent_span_id === rootSpan3.span_id)).toEqual([]); + expect(rootSpan3.links?.length).toBe(2); + expect(rootSpan3.links).toEqual([ { + attributes: {}, sampled: true, span_id: rootSpan1_spanId, trace_id: rootSpan1_traceId, }, { - attributes: { 'sentry.link.type': 'previous_trace' }, + attributes: { [SENTRY_LINK_TYPE]: { type: 'string', value: 'previous_trace' } }, sampled: true, span_id: rootSpan2_spanId, trace_id: rootSpan2_traceId, @@ -54,37 +57,40 @@ sentryTest('should link spans with addLinks() in nested startSpan() calls', asyn sentryTest.skip(); } - const rootSpan2Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan2'); - const rootSpan4Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan4'); + const rootSpan2Promise = waitForStreamedSpan(page, event => event.name === 'rootSpan2'); + const rootSpan4Promise = waitForStreamedSpan(page, event => event.name === 'rootSpan4'); + const allSpans = collectStreamedSpans(page); const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const rootSpan2 = envelopeRequestParser(await rootSpan2Promise); - const rootSpan4 = envelopeRequestParser(await rootSpan4Promise); + const rootSpan2 = await rootSpan2Promise; + const rootSpan4 = await rootSpan4Promise; - const rootSpan2_traceId = rootSpan2.contexts?.trace?.trace_id as string; - const rootSpan2_spanId = rootSpan2.contexts?.trace?.span_id as string; + const rootSpan2_traceId = rootSpan2.trace_id as string; + const rootSpan2_spanId = rootSpan2.span_id as string; - const [childSpan_4_1, childSpan_4_2] = rootSpan4.spans as [SpanJSON, SpanJSON]; - const rootSpan4_traceId = rootSpan4.contexts?.trace?.trace_id as string; - const rootSpan4_spanId = rootSpan4.contexts?.trace?.span_id as string; + const childSpan_4_1 = allSpans.find(span => span.name === 'childSpan4.1')!; + const childSpan_4_2 = allSpans.find(span => span.name === 'childSpan4.2')!; + const rootSpan4_traceId = rootSpan4.trace_id as string; + const rootSpan4_spanId = rootSpan4.span_id as string; - expect(rootSpan4.transaction).toBe('rootSpan4'); + expect(rootSpan4.name).toBe('rootSpan4'); - expect(childSpan_4_1.description).toBe('childSpan4.1'); + expect(childSpan_4_1.name).toBe('childSpan4.1'); expect(childSpan_4_1.links).toBe(undefined); - expect(childSpan_4_2.description).toBe('childSpan4.2'); + expect(childSpan_4_2.name).toBe('childSpan4.2'); expect(childSpan_4_2.links?.length).toBe(2); expect(childSpan_4_2.links).toEqual([ { + attributes: {}, sampled: true, span_id: rootSpan4_spanId, trace_id: rootSpan4_traceId, }, { - attributes: { 'sentry.link.type': 'previous_trace' }, + attributes: { [SENTRY_LINK_TYPE]: { type: 'string', value: 'previous_trace' } }, sampled: true, span_id: rootSpan2_spanId, trace_id: rootSpan2_traceId, diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/init.js b/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/init.js index 848d72e6bfdc..3ec6adbbdb75 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/test.ts b/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/test.ts index c2a2ed02f0e6..c07214170d5c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/test.ts @@ -1,45 +1,48 @@ +import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import type { TransactionEvent } from '@sentry/core'; import { sentryTest } from '../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../utils/helpers'; +import { collectStreamedSpans, waitForStreamedSpan } from '../../../utils/spanUtils'; sentryTest('should link spans by adding "links" to span options', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); } - const rootSpan1Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan1'); - const rootSpan2Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan2'); - const rootSpan3Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan3'); + const rootSpan1Promise = waitForStreamedSpan(page, event => event.name === 'rootSpan1'); + const rootSpan2Promise = waitForStreamedSpan(page, event => event.name === 'rootSpan2'); + const rootSpan3Promise = waitForStreamedSpan(page, event => event.name === 'rootSpan3'); + const allSpans = collectStreamedSpans(page); const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const rootSpan1 = envelopeRequestParser(await rootSpan1Promise); - const rootSpan2 = envelopeRequestParser(await rootSpan2Promise); - const rootSpan3 = envelopeRequestParser(await rootSpan3Promise); + const rootSpan1 = await rootSpan1Promise; + const rootSpan2 = await rootSpan2Promise; + const rootSpan3 = await rootSpan3Promise; - const rootSpan1_traceId = rootSpan1.contexts?.trace?.trace_id as string; - const rootSpan1_spanId = rootSpan1.contexts?.trace?.span_id as string; - const rootSpan2_traceId = rootSpan2.contexts?.trace?.trace_id as string; - const rootSpan2_spanId = rootSpan2.contexts?.trace?.span_id as string; + const rootSpan1_traceId = rootSpan1.trace_id as string; + const rootSpan1_spanId = rootSpan1.span_id as string; + const rootSpan2_traceId = rootSpan2.trace_id as string; + const rootSpan2_spanId = rootSpan2.span_id as string; - expect(rootSpan1.transaction).toBe('rootSpan1'); - expect(rootSpan1.spans).toEqual([]); + expect(rootSpan1.name).toBe('rootSpan1'); + expect(allSpans.filter(span => span.parent_span_id === rootSpan1.span_id)).toEqual([]); - expect(rootSpan3.transaction).toBe('rootSpan3'); - expect(rootSpan3.spans?.length).toBe(1); - expect(rootSpan3.spans?.[0].description).toBe('childSpan3.1'); + expect(rootSpan3.name).toBe('rootSpan3'); + expect(allSpans.filter(span => span.parent_span_id === rootSpan3.span_id)?.length).toBe(1); + expect(allSpans.find(span => span.name === 'childSpan3.1')?.name).toBe('childSpan3.1'); - expect(rootSpan3.contexts?.trace?.links?.length).toBe(2); - expect(rootSpan3.contexts?.trace?.links).toEqual([ + expect(rootSpan3.links?.length).toBe(2); + expect(rootSpan3.links).toEqual([ { + attributes: {}, sampled: true, span_id: rootSpan1_spanId, trace_id: rootSpan1_traceId, }, { - attributes: { 'sentry.link.type': 'previous_trace' }, + attributes: { [SENTRY_LINK_TYPE]: { type: 'string', value: 'previous_trace' } }, sampled: true, span_id: rootSpan2_spanId, trace_id: rootSpan2_traceId, diff --git a/dev-packages/browser-integration-tests/suites/tracing/maxSpans-static/init.js b/dev-packages/browser-integration-tests/suites/tracing/maxSpans-static/init.js new file mode 100644 index 000000000000..d4062aa6eb8f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/maxSpans-static/init.js @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [], + tracesSampleRate: 1, +}); + +Sentry.startSpan({ name: 'parent' }, () => { + for (let i = 0; i < 5000; i++) { + Sentry.startInactiveSpan({ name: `child ${i}` }).end(); + } +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/maxSpans-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/maxSpans-static/test.ts new file mode 100644 index 000000000000..5a5c6f851c69 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/maxSpans-static/test.ts @@ -0,0 +1,16 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequestOnUrl } from '../../../utils/helpers'; + +sentryTest('limits static transactions to 1000 child spans', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); + + const url = await getLocalTestUrl({ testDir: __dirname }); + const req = await waitForTransactionRequestOnUrl(page, url); + const transaction = envelopeRequestParser(req); + + expect(transaction.spans).toHaveLength(1000); + expect(transaction.spans).toContainEqual(expect.objectContaining({ description: 'child 0' })); + expect(transaction.spans).toContainEqual(expect.objectContaining({ description: 'child 999' })); + expect(transaction.spans).not.toContainEqual(expect.objectContaining({ description: 'child 1000' })); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/maxSpans/init.js b/dev-packages/browser-integration-tests/suites/tracing/maxSpans/init.js index d4062aa6eb8f..162bc4a04cd9 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/maxSpans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/maxSpans/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/maxSpans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/maxSpans/test.ts index 328c5c364c87..f5a05ad0854c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/maxSpans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/maxSpans/test.ts @@ -1,20 +1,20 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequestOnUrl } from '../../../utils/helpers'; - -sentryTest('it limits spans to 1000', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +import { shouldSkipTracingTest } from '../../../utils/helpers'; +import { collectStreamedSpans, waitForStreamedSpan } from '../../../utils/spanUtils'; +sentryTest('streams all children without the static 1000-span limit', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); + const spans = collectStreamedSpans(page); + const rootPromise = waitForStreamedSpan(page, span => span.name === 'parent'); const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); + const root = await rootPromise; + await page.evaluate(() => (window as any).Sentry.flush()); - const req = await waitForTransactionRequestOnUrl(page, url); - const transaction = envelopeRequestParser(req); - - expect(transaction.spans).toHaveLength(1000); - expect(transaction.spans).toContainEqual(expect.objectContaining({ description: 'child 0' })); - expect(transaction.spans).toContainEqual(expect.objectContaining({ description: 'child 999' })); - expect(transaction.spans).not.toContainEqual(expect.objectContaining({ description: 'child 1000' })); + const children = spans.filter(span => span.parent_span_id === root.span_id); + expect(children).toHaveLength(5000); + expect(children).toContainEqual(expect.objectContaining({ name: 'child 0' })); + expect(children).toContainEqual(expect.objectContaining({ name: 'child 999' })); + expect(children).toContainEqual(expect.objectContaining({ name: 'child 4999' })); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/init.js b/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/init.js index 19670513eef9..b8f186bc2896 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/test.ts b/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/test.ts index e49b258b4ff2..d166f200ff28 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/test.ts @@ -1,6 +1,8 @@ +import { URL_FULL } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../utils/spanUtils'; sentryTest('should attribute spans to their originating microfrontend', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { @@ -11,27 +13,29 @@ sentryTest('should attribute spans to their originating microfrontend', async ({ const url = await getLocalTestUrl({ testDir: __dirname }); - const reqPromise = waitForTransactionRequest(page, event => { - const spans = event.spans || []; - return ( - spans.some(s => s.description?.includes('/api/todos/1')) && - spans.some(s => s.description?.includes('/api/todos/2')) && - spans.some(s => s.description?.includes('/api/todos/3')) && - spans.some(s => s.description?.includes('/api/shell-config')) - ); - }); - + const spans = collectStreamedSpans(page); + const rootSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); await page.goto(url); - - const req = await reqPromise; - const event = envelopeRequestParser(req); - const httpSpans = event.spans?.filter(({ op }) => op === 'http.client') || []; + await rootSpanPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + const httpSpans = spans.filter(span => getSpanOp(span) === 'http.client'); + expect(httpSpans).toHaveLength(4); // Each MFE's fetch is attributed via withScope + spanStart hook - expect(httpSpans.find(s => s.description?.includes('/api/todos/1'))?.data?.['mfe.name']).toBe('mfe-header'); - expect(httpSpans.find(s => s.description?.includes('/api/todos/2'))?.data?.['mfe.name']).toBe('mfe-one'); - expect(httpSpans.find(s => s.description?.includes('/api/todos/3'))?.data?.['mfe.name']).toBe('mfe-two'); + expect( + httpSpans.find(s => s.attributes[URL_FULL]?.value?.toString().includes('/api/todos/1'))?.attributes['mfe.name'], + ).toEqual({ type: 'string', value: 'mfe-header' }); + expect( + httpSpans.find(s => s.attributes[URL_FULL]?.value?.toString().includes('/api/todos/2'))?.attributes['mfe.name'], + ).toEqual({ type: 'string', value: 'mfe-one' }); + expect( + httpSpans.find(s => s.attributes[URL_FULL]?.value?.toString().includes('/api/todos/3'))?.attributes['mfe.name'], + ).toEqual({ type: 'string', value: 'mfe-two' }); // Shell span has no MFE tag - expect(httpSpans.find(s => s.description?.includes('/api/shell-config'))?.data?.['mfe.name']).toBeUndefined(); + expect( + httpSpans.find(s => s.attributes[URL_FULL]?.value?.toString().includes('/api/shell-config'))?.attributes[ + 'mfe.name' + ], + ).toBeUndefined(); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report/init.js b/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report-static/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report/init.js rename to dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report-static/init.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report/subject.js b/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report-static/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report-static/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report/test.ts b/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report-static/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report-static/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-addLink-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/default/init.js similarity index 75% rename from dev-packages/browser-integration-tests/suites/tracing/linking-addLink-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/default/init.js index 9afcee48dc4a..bd0421b32761 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/linking-addLink-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/default/init.js @@ -3,7 +3,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.spanStreamingIntegration()], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/default/subject.js b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/default/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/default/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/default/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/default/test.ts similarity index 52% rename from dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/default/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/default/test.ts index c9f48a4ebefd..2019c78c837f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/default/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/default/test.ts @@ -1,26 +1,26 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { waitForStreamedSpans } from '../../../../utils/spanUtils'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; sentryTest('sets an inactive span active and adds child spans to it', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } - const spansPromise = waitForStreamedSpans(page, spans => spans.some(s => s.name === 'checkout-flow' && s.is_segment)); + const req = waitForTransactionRequest(page, e => e.transaction === 'checkout-flow'); const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const spans = await spansPromise; - const checkoutSpan = spans.find(s => s.name === 'checkout-flow'); - const checkoutSpanId = checkoutSpan?.span_id; + const checkoutEvent = envelopeRequestParser(await req); + const checkoutSpanId = checkoutEvent.contexts?.trace?.span_id; expect(checkoutSpanId).toMatch(/[a-f\d]{16}/); - expect(spans.filter(s => !s.is_segment)).toHaveLength(3); + expect(checkoutEvent.spans).toHaveLength(3); - const checkoutStep1 = spans.find(s => s.name === 'checkout-step-1'); - const checkoutStep11 = spans.find(s => s.name === 'checkout-step-1-1'); - const checkoutStep2 = spans.find(s => s.name === 'checkout-step-2'); + const checkoutStep1 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-1'); + const checkoutStep11 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-1-1'); + const checkoutStep2 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-2'); expect(checkoutStep1).toBeDefined(); expect(checkoutStep11).toBeDefined(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested-parentAlwaysRoot/init.js b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/nested-parentAlwaysRoot/init.js similarity index 78% rename from dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested-parentAlwaysRoot/init.js rename to dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/nested-parentAlwaysRoot/init.js index 5b4cff73e95d..58be3b8dc82c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested-parentAlwaysRoot/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/nested-parentAlwaysRoot/init.js @@ -3,8 +3,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.spanStreamingIntegration()], tracesSampleRate: 1, parentSpanIsAlwaysRootSpan: false, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested-parentAlwaysRoot/subject.js b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/nested-parentAlwaysRoot/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested-parentAlwaysRoot/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/nested-parentAlwaysRoot/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested-parentAlwaysRoot/test.ts b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/nested-parentAlwaysRoot/test.ts similarity index 53% rename from dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested-parentAlwaysRoot/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/nested-parentAlwaysRoot/test.ts index f0ce77d4e00c..5132178d1981 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested-parentAlwaysRoot/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/nested-parentAlwaysRoot/test.ts @@ -1,37 +1,36 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { waitForStreamedSpans } from '../../../../utils/spanUtils'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; sentryTest( 'nested calls to setActiveSpanInBrowser with parentSpanIsAlwaysRootSpan=false result in correct parenting', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } - const checkoutSpansPromise = waitForStreamedSpans(page, spans => - spans.some(s => s.name === 'checkout-flow' && s.is_segment), - ); + const req = waitForTransactionRequest(page, e => e.transaction === 'checkout-flow'); + const postCheckoutReq = waitForTransactionRequest(page, e => e.transaction === 'post-checkout'); const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const checkoutSpans = await checkoutSpansPromise; + const checkoutEvent = envelopeRequestParser(await req); + const postCheckoutEvent = envelopeRequestParser(await postCheckoutReq); - const checkoutSpan = checkoutSpans.find(s => s.name === 'checkout-flow'); - const postCheckoutSpan = checkoutSpans.find(s => s.name === 'post-checkout'); - - const checkoutSpanId = checkoutSpan?.span_id; - const postCheckoutSpanId = postCheckoutSpan?.span_id; + const checkoutSpanId = checkoutEvent.contexts?.trace?.span_id; + const postCheckoutSpanId = postCheckoutEvent.contexts?.trace?.span_id; expect(checkoutSpanId).toMatch(/[a-f\d]{16}/); expect(postCheckoutSpanId).toMatch(/[a-f\d]{16}/); - expect(checkoutSpans.filter(s => !s.is_segment)).toHaveLength(5); + expect(checkoutEvent.spans).toHaveLength(4); + expect(postCheckoutEvent.spans).toHaveLength(1); - const checkoutStep1 = checkoutSpans.find(s => s.name === 'checkout-step-1'); - const checkoutStep2 = checkoutSpans.find(s => s.name === 'checkout-step-2'); - const checkoutStep21 = checkoutSpans.find(s => s.name === 'checkout-step-2-1'); - const checkoutStep3 = checkoutSpans.find(s => s.name === 'checkout-step-3'); + const checkoutStep1 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-1'); + const checkoutStep2 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-2'); + const checkoutStep21 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-2-1'); + const checkoutStep3 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-3'); expect(checkoutStep1).toBeDefined(); expect(checkoutStep2).toBeDefined(); @@ -51,7 +50,7 @@ sentryTest( // post-checkout trace is started as a new trace because ending checkoutSpan removes the active // span on the scope - const postCheckoutStep1 = checkoutSpans.find(s => s.name === 'post-checkout-1'); + const postCheckoutStep1 = postCheckoutEvent.spans?.find(s => s.description === 'post-checkout-1'); expect(postCheckoutStep1).toBeDefined(); expect(postCheckoutStep1?.parent_span_id).toBe(postCheckoutSpanId); }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/default/init.js b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/nested/init.js similarity index 75% rename from dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/default/init.js rename to dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/nested/init.js index 9afcee48dc4a..bd0421b32761 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/default/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/nested/init.js @@ -3,7 +3,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.spanStreamingIntegration()], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested/subject.js b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/nested/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/nested/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/nested/test.ts b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/nested/test.ts new file mode 100644 index 000000000000..cea01f304691 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-static/nested/test.ts @@ -0,0 +1,52 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest( + 'nested calls to setActiveSpanInBrowser still parent to root span by default', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const req = waitForTransactionRequest(page, e => e.transaction === 'checkout-flow'); + const postCheckoutReq = waitForTransactionRequest(page, e => e.transaction === 'post-checkout'); + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const checkoutEvent = envelopeRequestParser(await req); + const postCheckoutEvent = envelopeRequestParser(await postCheckoutReq); + + const checkoutSpanId = checkoutEvent.contexts?.trace?.span_id; + const postCheckoutSpanId = postCheckoutEvent.contexts?.trace?.span_id; + + expect(checkoutSpanId).toMatch(/[a-f\d]{16}/); + expect(postCheckoutSpanId).toMatch(/[a-f\d]{16}/); + + expect(checkoutEvent.spans).toHaveLength(4); + expect(postCheckoutEvent.spans).toHaveLength(1); + + const checkoutStep1 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-1'); + const checkoutStep2 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-2'); + const checkoutStep21 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-2-1'); + const checkoutStep3 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-3'); + + expect(checkoutStep1).toBeDefined(); + expect(checkoutStep2).toBeDefined(); + expect(checkoutStep21).toBeDefined(); + expect(checkoutStep3).toBeDefined(); + + expect(checkoutStep1?.parent_span_id).toBe(checkoutSpanId); + expect(checkoutStep2?.parent_span_id).toBe(checkoutSpanId); + expect(checkoutStep3?.parent_span_id).toBe(checkoutSpanId); + + // despite 2-1 being called within 2 AND setting 2 as active span, it's still parented to the + // root span due to this being default behaviour in browser environments + expect(checkoutStep21?.parent_span_id).toBe(checkoutSpanId); + + const postCheckoutStep1 = postCheckoutEvent.spans?.find(s => s.description === 'post-checkout-1'); + expect(postCheckoutStep1).toBeDefined(); + expect(postCheckoutStep1?.parent_span_id).toBe(postCheckoutSpanId); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested/test.ts b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested/test.ts deleted file mode 100644 index 37a76f81152d..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested/test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { waitForStreamedSpans } from '../../../../utils/spanUtils'; - -sentryTest( - 'nested calls to setActiveSpanInBrowser still parent to root span by default', - async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const checkoutSpansPromise = waitForStreamedSpans(page, spans => - spans.some(s => s.name === 'checkout-flow' && s.is_segment), - ); - - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - - const checkoutSpans = await checkoutSpansPromise; - - const checkoutSpan = checkoutSpans.find(s => s.name === 'checkout-flow'); - const postCheckoutSpan = checkoutSpans.find(s => s.name === 'post-checkout'); - - const checkoutSpanId = checkoutSpan?.span_id; - const postCheckoutSpanId = postCheckoutSpan?.span_id; - - expect(checkoutSpanId).toMatch(/[a-f\d]{16}/); - expect(postCheckoutSpanId).toMatch(/[a-f\d]{16}/); - - expect(checkoutSpans.filter(s => !s.is_segment)).toHaveLength(5); - - const checkoutStep1 = checkoutSpans.find(s => s.name === 'checkout-step-1'); - const checkoutStep2 = checkoutSpans.find(s => s.name === 'checkout-step-2'); - const checkoutStep21 = checkoutSpans.find(s => s.name === 'checkout-step-2-1'); - const checkoutStep3 = checkoutSpans.find(s => s.name === 'checkout-step-3'); - - expect(checkoutStep1).toBeDefined(); - expect(checkoutStep2).toBeDefined(); - expect(checkoutStep21).toBeDefined(); - expect(checkoutStep3).toBeDefined(); - - expect(checkoutStep1?.parent_span_id).toBe(checkoutSpanId); - expect(checkoutStep2?.parent_span_id).toBe(checkoutSpanId); - expect(checkoutStep3?.parent_span_id).toBe(checkoutSpanId); - - // despite 2-1 being called within 2 AND setting 2 as active span, it's still parented to the - // root span due to this being default behaviour in browser environments - expect(checkoutStep21?.parent_span_id).toBe(checkoutSpanId); - - const postCheckoutStep1 = checkoutSpans.find(s => s.name === 'post-checkout-1'); - expect(postCheckoutStep1).toBeDefined(); - expect(postCheckoutStep1?.parent_span_id).toBe(postCheckoutSpanId); - }, -); diff --git a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/default/init.js b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/default/init.js index bd0421b32761..3ec6adbbdb75 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/default/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/default/init.js @@ -3,7 +3,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/default/test.ts index 2019c78c837f..ab996fc17d4c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/default/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/default/test.ts @@ -1,26 +1,28 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest('sets an inactive span active and adds child spans to it', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); - const req = waitForTransactionRequest(page, e => e.transaction === 'checkout-flow'); + const spans = collectStreamedSpans(page); + const rootPromise = waitForStreamedSpan(page, span => span.name === 'checkout-flow' && span.is_segment); const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const checkoutEvent = envelopeRequestParser(await req); - const checkoutSpanId = checkoutEvent.contexts?.trace?.span_id; + await rootPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + const checkoutSpan = spans.find(s => s.name === 'checkout-flow'); + const checkoutSpanId = checkoutSpan?.span_id; expect(checkoutSpanId).toMatch(/[a-f\d]{16}/); - expect(checkoutEvent.spans).toHaveLength(3); + expect(spans.filter(s => !s.is_segment)).toHaveLength(3); - const checkoutStep1 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-1'); - const checkoutStep11 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-1-1'); - const checkoutStep2 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-2'); + const checkoutStep1 = spans.find(s => s.name === 'checkout-step-1'); + const checkoutStep11 = spans.find(s => s.name === 'checkout-step-1-1'); + const checkoutStep2 = spans.find(s => s.name === 'checkout-step-2'); expect(checkoutStep1).toBeDefined(); expect(checkoutStep11).toBeDefined(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/nested-parentAlwaysRoot/init.js b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/nested-parentAlwaysRoot/init.js index 58be3b8dc82c..27b13c52906f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/nested-parentAlwaysRoot/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/nested-parentAlwaysRoot/init.js @@ -3,8 +3,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [], tracesSampleRate: 1, parentSpanIsAlwaysRootSpan: false, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/nested-parentAlwaysRoot/test.ts b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/nested-parentAlwaysRoot/test.ts index 5132178d1981..12baff69f5f8 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/nested-parentAlwaysRoot/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/nested-parentAlwaysRoot/test.ts @@ -1,36 +1,38 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest( 'nested calls to setActiveSpanInBrowser with parentSpanIsAlwaysRootSpan=false result in correct parenting', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); - const req = waitForTransactionRequest(page, e => e.transaction === 'checkout-flow'); - const postCheckoutReq = waitForTransactionRequest(page, e => e.transaction === 'post-checkout'); + const checkoutSpans = collectStreamedSpans(page); + const checkoutPromise = waitForStreamedSpan(page, span => span.name === 'checkout-flow' && span.is_segment); + const postCheckoutPromise = waitForStreamedSpan(page, span => span.name === 'post-checkout' && span.is_segment); const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const checkoutEvent = envelopeRequestParser(await req); - const postCheckoutEvent = envelopeRequestParser(await postCheckoutReq); + await Promise.all([checkoutPromise, postCheckoutPromise]); + await page.evaluate(() => (window as any).Sentry.flush()); - const checkoutSpanId = checkoutEvent.contexts?.trace?.span_id; - const postCheckoutSpanId = postCheckoutEvent.contexts?.trace?.span_id; + const checkoutSpan = checkoutSpans.find(s => s.name === 'checkout-flow'); + const postCheckoutSpan = checkoutSpans.find(s => s.name === 'post-checkout'); + + const checkoutSpanId = checkoutSpan?.span_id; + const postCheckoutSpanId = postCheckoutSpan?.span_id; expect(checkoutSpanId).toMatch(/[a-f\d]{16}/); expect(postCheckoutSpanId).toMatch(/[a-f\d]{16}/); - expect(checkoutEvent.spans).toHaveLength(4); - expect(postCheckoutEvent.spans).toHaveLength(1); + expect(checkoutSpans.filter(s => !s.is_segment)).toHaveLength(5); - const checkoutStep1 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-1'); - const checkoutStep2 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-2'); - const checkoutStep21 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-2-1'); - const checkoutStep3 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-3'); + const checkoutStep1 = checkoutSpans.find(s => s.name === 'checkout-step-1'); + const checkoutStep2 = checkoutSpans.find(s => s.name === 'checkout-step-2'); + const checkoutStep21 = checkoutSpans.find(s => s.name === 'checkout-step-2-1'); + const checkoutStep3 = checkoutSpans.find(s => s.name === 'checkout-step-3'); expect(checkoutStep1).toBeDefined(); expect(checkoutStep2).toBeDefined(); @@ -50,7 +52,7 @@ sentryTest( // post-checkout trace is started as a new trace because ending checkoutSpan removes the active // span on the scope - const postCheckoutStep1 = postCheckoutEvent.spans?.find(s => s.description === 'post-checkout-1'); + const postCheckoutStep1 = checkoutSpans.find(s => s.name === 'post-checkout-1'); expect(postCheckoutStep1).toBeDefined(); expect(postCheckoutStep1?.parent_span_id).toBe(postCheckoutSpanId); }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/nested/init.js b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/nested/init.js index bd0421b32761..3ec6adbbdb75 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/nested/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/nested/init.js @@ -3,7 +3,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/nested/test.ts b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/nested/test.ts index cea01f304691..686a64079db9 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/nested/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive/nested/test.ts @@ -1,36 +1,38 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest( 'nested calls to setActiveSpanInBrowser still parent to root span by default', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); - const req = waitForTransactionRequest(page, e => e.transaction === 'checkout-flow'); - const postCheckoutReq = waitForTransactionRequest(page, e => e.transaction === 'post-checkout'); + const checkoutSpans = collectStreamedSpans(page); + const checkoutPromise = waitForStreamedSpan(page, span => span.name === 'checkout-flow' && span.is_segment); + const postCheckoutPromise = waitForStreamedSpan(page, span => span.name === 'post-checkout' && span.is_segment); const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const checkoutEvent = envelopeRequestParser(await req); - const postCheckoutEvent = envelopeRequestParser(await postCheckoutReq); + await Promise.all([checkoutPromise, postCheckoutPromise]); + await page.evaluate(() => (window as any).Sentry.flush()); - const checkoutSpanId = checkoutEvent.contexts?.trace?.span_id; - const postCheckoutSpanId = postCheckoutEvent.contexts?.trace?.span_id; + const checkoutSpan = checkoutSpans.find(s => s.name === 'checkout-flow'); + const postCheckoutSpan = checkoutSpans.find(s => s.name === 'post-checkout'); + + const checkoutSpanId = checkoutSpan?.span_id; + const postCheckoutSpanId = postCheckoutSpan?.span_id; expect(checkoutSpanId).toMatch(/[a-f\d]{16}/); expect(postCheckoutSpanId).toMatch(/[a-f\d]{16}/); - expect(checkoutEvent.spans).toHaveLength(4); - expect(postCheckoutEvent.spans).toHaveLength(1); + expect(checkoutSpans.filter(s => !s.is_segment)).toHaveLength(5); - const checkoutStep1 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-1'); - const checkoutStep2 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-2'); - const checkoutStep21 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-2-1'); - const checkoutStep3 = checkoutEvent.spans?.find(s => s.description === 'checkout-step-3'); + const checkoutStep1 = checkoutSpans.find(s => s.name === 'checkout-step-1'); + const checkoutStep2 = checkoutSpans.find(s => s.name === 'checkout-step-2'); + const checkoutStep21 = checkoutSpans.find(s => s.name === 'checkout-step-2-1'); + const checkoutStep3 = checkoutSpans.find(s => s.name === 'checkout-step-3'); expect(checkoutStep1).toBeDefined(); expect(checkoutStep2).toBeDefined(); @@ -45,7 +47,7 @@ sentryTest( // root span due to this being default behaviour in browser environments expect(checkoutStep21?.parent_span_id).toBe(checkoutSpanId); - const postCheckoutStep1 = postCheckoutEvent.spans?.find(s => s.description === 'post-checkout-1'); + const postCheckoutStep1 = checkoutSpans.find(s => s.name === 'post-checkout-1'); expect(postCheckoutStep1).toBeDefined(); expect(postCheckoutStep1?.parent_span_id).toBe(postCheckoutSpanId); }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/stringSampleRate/init.js b/dev-packages/browser-integration-tests/suites/tracing/stringSampleRate/init.js index cbf6c3a35415..dbefdbdfcda5 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/stringSampleRate/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/stringSampleRate/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: '1', }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/stringSampleRate/test.ts b/dev-packages/browser-integration-tests/suites/tracing/stringSampleRate/test.ts index 207aa7517ac7..e078da24e465 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/stringSampleRate/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/stringSampleRate/test.ts @@ -1,6 +1,8 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequestOnUrl } from '../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../utils/helpers'; +import { waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../utils/spanUtils'; sentryTest('parses a string sample rate', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { @@ -9,8 +11,7 @@ sentryTest('parses a string sample rate', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - const req = await waitForTransactionRequestOnUrl(page, url); - const eventData = envelopeRequestParser(req); + const [span] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url); - expect(eventData.contexts?.trace?.data?.['sentry.sample_rate']).toStrictEqual(1); + expect(span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toEqual({ type: 'integer', value: 1 }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/init.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/init.js index 8718d9d354eb..30317eda63ef 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/init.js @@ -5,7 +5,6 @@ import { feedbackIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration(), feedbackIntegration()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-static/init.js similarity index 89% rename from dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-static/init.js index 3dd77207e103..8718d9d354eb 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-static/init.js @@ -5,8 +5,9 @@ import { feedbackIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), feedbackIntegration(), Sentry.spanStreamingIntegration()], + integrations: [Sentry.browserTracingIntegration(), feedbackIntegration()], tracePropagationTargets: ['http://sentry-test-site.example'], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-static/subject.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-static/subject.js new file mode 100644 index 000000000000..eead800a75a5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-static/subject.js @@ -0,0 +1,23 @@ +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://sentry-test-site.example'); +}); + +const xhrBtn = document.getElementById('xhrBtn'); +xhrBtn.addEventListener('click', () => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', 'http://sentry-test-site.example'); + xhr.send(); +}); + +const spanAndFetchBtn = document.getElementById('spanAndFetchBtn'); +spanAndFetchBtn.addEventListener('click', () => { + Sentry.startSpan({ name: 'custom-root-span' }, async () => { + await fetch('http://sentry-test-site.example'); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-static/template.html b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-static/template.html new file mode 100644 index 000000000000..87e7bf5efbea --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-static/template.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-static/test.ts new file mode 100644 index 000000000000..2f7749685ba8 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-static/test.ts @@ -0,0 +1,363 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import type { EventAndTraceHeader } from '../../../../utils/helpers'; +import { + eventAndTraceHeaderRequestParser, + getFirstSentryEnvelopeRequest, + getMultipleSentryEnvelopeRequests, + shouldSkipFeedbackTest, + shouldSkipTracingTest, +} from '../../../../utils/helpers'; + +sentryTest('creates a new trace and sample_rand on each navigation', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await getFirstSentryEnvelopeRequest(page, url); + + const [navigation1Event, navigation1TraceHeader] = await getFirstSentryEnvelopeRequest( + page, + `${url}#foo`, + eventAndTraceHeaderRequestParser, + ); + const [navigation2Event, navigation2TraceHeader] = await getFirstSentryEnvelopeRequest( + page, + `${url}#bar`, + eventAndTraceHeaderRequestParser, + ); + + const navigation1TraceContext = navigation1Event.contexts?.trace; + const navigation2TraceContext = navigation2Event.contexts?.trace; + + expect(navigation1Event.type).toEqual('transaction'); + expect(navigation2Event.type).toEqual('transaction'); + + expect(navigation1TraceContext).toMatchObject({ + op: 'navigation', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + span_id: expect.stringMatching(/^[\da-f]{16}$/), + }); + expect(navigation1TraceContext).not.toHaveProperty('parent_span_id'); + + expect(navigation1TraceHeader).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: navigation1TraceContext?.trace_id, + sample_rand: expect.any(String), + }); + + expect(navigation2TraceContext).toMatchObject({ + op: 'navigation', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + span_id: expect.stringMatching(/^[\da-f]{16}$/), + }); + expect(navigation2TraceContext).not.toHaveProperty('parent_span_id'); + + expect(navigation2TraceHeader).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: navigation2TraceContext?.trace_id, + sample_rand: expect.any(String), + }); + + expect(navigation1TraceContext?.trace_id).not.toEqual(navigation2TraceContext?.trace_id); + expect(navigation1TraceHeader?.sample_rand).not.toEqual(navigation2TraceHeader?.sample_rand); +}); + +sentryTest('error after navigation has navigation traceId', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + // ensure pageload transaction is finished + await getFirstSentryEnvelopeRequest(page, url); + + const [navigationEvent, navigationTraceHeader] = await getFirstSentryEnvelopeRequest( + page, + `${url}#foo`, + eventAndTraceHeaderRequestParser, + ); + const navigationTraceContext = navigationEvent.contexts?.trace; + + expect(navigationEvent.type).toEqual('transaction'); + + expect(navigationTraceContext).toMatchObject({ + op: 'navigation', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + span_id: expect.stringMatching(/^[\da-f]{16}$/), + }); + expect(navigationTraceContext).not.toHaveProperty('parent_span_id'); + + expect(navigationTraceHeader).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: navigationTraceContext?.trace_id, + sample_rand: expect.any(String), + }); + + const errorEventPromise = getFirstSentryEnvelopeRequest( + page, + undefined, + eventAndTraceHeaderRequestParser, + ); + await page.locator('#errorBtn').click(); + const [errorEvent, errorTraceHeader] = await errorEventPromise; + + expect(errorEvent.type).toEqual(undefined); + + const errorTraceContext = errorEvent.contexts?.trace; + expect(errorTraceContext).toEqual({ + trace_id: navigationTraceContext?.trace_id, + span_id: expect.stringMatching(/^[\da-f]{16}$/), + }); + expect(errorTraceHeader).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: navigationTraceContext?.trace_id, + sample_rand: expect.any(String), + }); +}); + +sentryTest('error during navigation has new navigation traceId', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + // ensure pageload transaction is finished + await getFirstSentryEnvelopeRequest(page, url); + + const envelopeRequestsPromise = getMultipleSentryEnvelopeRequests( + page, + 2, + undefined, + eventAndTraceHeaderRequestParser, + ); + + await page.goto(`${url}#foo`); + await page.locator('#errorBtn').click(); + const envelopes = await envelopeRequestsPromise; + + const [navigationEvent, navigationTraceHeader] = envelopes.find(envelope => envelope[0].type === 'transaction')!; + const [errorEvent, errorTraceHeader] = envelopes.find(envelope => !envelope[0].type)!; + + expect(navigationEvent.type).toEqual('transaction'); + expect(errorEvent.type).toEqual(undefined); + + const navigationTraceContext = navigationEvent?.contexts?.trace; + expect(navigationTraceContext).toMatchObject({ + op: 'navigation', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + span_id: expect.stringMatching(/^[\da-f]{16}$/), + }); + expect(navigationTraceContext).not.toHaveProperty('parent_span_id'); + + expect(navigationTraceHeader).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: navigationTraceContext?.trace_id, + sample_rand: expect.any(String), + }); + + const errorTraceContext = errorEvent?.contexts?.trace; + expect(errorTraceContext).toEqual({ + trace_id: navigationTraceContext?.trace_id, + span_id: expect.stringMatching(/^[\da-f]{16}$/), + }); + + expect(errorTraceHeader).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: navigationTraceContext?.trace_id, + sample_rand: expect.any(String), + }); +}); + +sentryTest( + 'outgoing fetch request during navigation has navigation traceId in headers', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.route('http://sentry-test-site.example/**', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + // ensure pageload transaction is finished + await getFirstSentryEnvelopeRequest(page, url); + + const navigationEventPromise = getFirstSentryEnvelopeRequest( + page, + undefined, + eventAndTraceHeaderRequestParser, + ); + const requestPromise = page.waitForRequest('http://sentry-test-site.example/*'); + await page.goto(`${url}#foo`); + await page.locator('#fetchBtn').click(); + const [[navigationEvent, navigationTraceHeader], request] = await Promise.all([ + navigationEventPromise, + requestPromise, + ]); + + const navigationTraceContext = navigationEvent.contexts?.trace; + + expect(navigationEvent.type).toEqual('transaction'); + expect(navigationTraceContext).toMatchObject({ + op: 'navigation', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + span_id: expect.stringMatching(/^[\da-f]{16}$/), + }); + expect(navigationTraceContext).not.toHaveProperty('parent_span_id'); + + expect(navigationTraceHeader).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: navigationTraceContext?.trace_id, + sample_rand: expect.any(String), + }); + + const headers = request.headers(); + + // sampling decision is propagated from active span sampling decision + 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-sampled=true,sentry-sample_rand=${navigationTraceHeader?.sample_rand},sentry-sample_rate=1`, + ); + }, +); + +sentryTest( + 'outgoing XHR request during navigation has navigation traceId in headers', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.route('http://sentry-test-site.example/**', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + // ensure navigation transaction is finished + await getFirstSentryEnvelopeRequest(page, url); + + const navigationEventPromise = getFirstSentryEnvelopeRequest( + page, + undefined, + eventAndTraceHeaderRequestParser, + ); + const requestPromise = page.waitForRequest('http://sentry-test-site.example/*'); + await page.goto(`${url}#foo`); + await page.locator('#xhrBtn').click(); + const [[navigationEvent, navigationTraceHeader], request] = await Promise.all([ + navigationEventPromise, + requestPromise, + ]); + + const navigationTraceContext = navigationEvent.contexts?.trace; + + expect(navigationEvent.type).toEqual('transaction'); + expect(navigationTraceContext).toMatchObject({ + op: 'navigation', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + span_id: expect.stringMatching(/^[\da-f]{16}$/), + }); + expect(navigationTraceContext).not.toHaveProperty('parent_span_id'); + + expect(navigationTraceHeader).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: navigationTraceContext?.trace_id, + sample_rand: expect.any(String), + }); + + const headers = request.headers(); + + // sampling decision is propagated from active span sampling decision + 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-sampled=true,sentry-sample_rand=${navigationTraceHeader?.sample_rand},sentry-sample_rate=1`, + ); + }, +); + +sentryTest( + 'user feedback event after navigation has navigation traceId in headers', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest() || shouldSkipFeedbackTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname, handleLazyLoadedFeedback: true }); + + // ensure pageload transaction is finished + await getFirstSentryEnvelopeRequest(page, url); + + const navigationEvent = await getFirstSentryEnvelopeRequest(page, `${url}#foo`); + + const navigationTraceContext = navigationEvent.contexts?.trace; + expect(navigationTraceContext).toMatchObject({ + op: 'navigation', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + span_id: expect.stringMatching(/^[\da-f]{16}$/), + }); + expect(navigationTraceContext).not.toHaveProperty('parent_span_id'); + + const feedbackEventsPromise = getMultipleSentryEnvelopeRequests(page, 1, { envelopeType: 'feedback' }); + + await page.getByText('Report a Bug').click(); + expect(await page.locator(':visible:text-is("Report a Bug")').count()).toEqual(1); + await page.locator('[name="name"]').fill('Jane Doe'); + await page.locator('[name="email"]').fill('janedoe@example.org'); + await page.locator('[name="message"]').fill('my example feedback'); + await page.locator('[data-sentry-feedback] .btn--primary').click(); + + const [feedbackEvent] = await feedbackEventsPromise; + + expect(feedbackEvent.type).toEqual('feedback'); + + const feedbackTraceContext = feedbackEvent.contexts?.trace; + + expect(feedbackTraceContext).toMatchObject({ + trace_id: navigationTraceContext?.trace_id, + span_id: expect.stringMatching(/^[\da-f]{16}$/), + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-streamed/test.ts deleted file mode 100644 index 3c2cafaa0430..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-streamed/test.ts +++ /dev/null @@ -1,317 +0,0 @@ -import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; -import { sentryTest } from '../../../../utils/fixtures'; -import type { EventAndTraceHeader } from '../../../../utils/helpers'; -import { - eventAndTraceHeaderRequestParser, - getFirstSentryEnvelopeRequest, - shouldSkipFeedbackTest, - shouldSkipTracingTest, -} from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpan, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; - -sentryTest('creates a new trace and sample_rand on each navigation', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - // Wait for and skip the initial pageload span - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - await page.goto(url); - await pageloadSpanPromise; - - const navigation1SpanEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'navigation'), - ); - await page.goto(`${url}#foo`); - const navigation1SpanEnvelope = await navigation1SpanEnvelopePromise; - - const navigation2SpanEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'navigation'), - ); - await page.goto(`${url}#bar`); - const navigation2SpanEnvelope = await navigation2SpanEnvelopePromise; - - const navigation1TraceId = navigation1SpanEnvelope[0].trace?.trace_id; - const navigation1SampleRand = navigation1SpanEnvelope[0].trace?.sample_rand; - const navigation2TraceId = navigation2SpanEnvelope[0].trace?.trace_id; - const navigation2SampleRand = navigation2SpanEnvelope[0].trace?.sample_rand; - - const navigation1Span = navigation1SpanEnvelope[1][0][1].items.find(s => getSpanOp(s) === 'navigation')!; - const navigation2Span = navigation2SpanEnvelope[1][0][1].items.find(s => getSpanOp(s) === 'navigation')!; - - expect(getSpanOp(navigation1Span)).toEqual('navigation'); - expect(navigation1TraceId).toMatch(/^[\da-f]{32}$/); - expect(navigation1Span.span_id).toMatch(/^[\da-f]{16}$/); - expect(navigation1Span.parent_span_id).toBeUndefined(); - - expect(navigation1SpanEnvelope[0].trace).toEqual({ - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: navigation1TraceId, - sample_rand: expect.any(String), - }); - - expect(getSpanOp(navigation2Span)).toEqual('navigation'); - expect(navigation2TraceId).toMatch(/^[\da-f]{32}$/); - expect(navigation2Span.span_id).toMatch(/^[\da-f]{16}$/); - expect(navigation2Span.parent_span_id).toBeUndefined(); - - expect(navigation2SpanEnvelope[0].trace).toEqual({ - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: navigation2TraceId, - sample_rand: expect.any(String), - }); - - expect(navigation1TraceId).not.toEqual(navigation2TraceId); - expect(navigation1SampleRand).not.toEqual(navigation2SampleRand); -}); - -sentryTest('error after navigation has navigation traceId', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - // ensure pageload span is finished - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - await page.goto(url); - await pageloadSpanPromise; - - const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); - const navigationSpanEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'navigation'), - ); - await page.goto(`${url}#foo`); - const [navigationSpan, navigationSpanEnvelope] = await Promise.all([ - navigationSpanPromise, - navigationSpanEnvelopePromise, - ]); - - const navigationTraceId = navigationSpan.trace_id; - - expect(getSpanOp(navigationSpan)).toEqual('navigation'); - expect(navigationTraceId).toMatch(/^[\da-f]{32}$/); - expect(navigationSpan.span_id).toMatch(/^[\da-f]{16}$/); - expect(navigationSpan.parent_span_id).toBeUndefined(); - - expect(navigationSpanEnvelope[0].trace).toEqual({ - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: navigationTraceId, - sample_rand: expect.any(String), - }); - - const errorEventPromise = getFirstSentryEnvelopeRequest( - page, - undefined, - eventAndTraceHeaderRequestParser, - ); - await page.locator('#errorBtn').click(); - const [errorEvent, errorTraceHeader] = await errorEventPromise; - - expect(errorEvent.type).toEqual(undefined); - - const errorTraceContext = errorEvent.contexts?.trace; - expect(errorTraceContext).toEqual({ - trace_id: navigationTraceId, - span_id: expect.stringMatching(/^[\da-f]{16}$/), - }); - expect(errorTraceHeader).toEqual({ - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: navigationTraceId, - sample_rand: expect.any(String), - }); -}); - -sentryTest('error during navigation has new navigation traceId', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - // ensure pageload span is finished - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - await page.goto(url); - await pageloadSpanPromise; - - const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); - const errorEventPromise = getFirstSentryEnvelopeRequest( - page, - undefined, - eventAndTraceHeaderRequestParser, - ); - - await page.goto(`${url}#foo`); - await page.locator('#errorBtn').click(); - const [navigationSpan, [errorEvent, errorTraceHeader]] = await Promise.all([ - navigationSpanPromise, - errorEventPromise, - ]); - - expect(getSpanOp(navigationSpan)).toEqual('navigation'); - expect(errorEvent.type).toEqual(undefined); - - const navigationTraceId = navigationSpan.trace_id; - expect(navigationTraceId).toMatch(/^[\da-f]{32}$/); - expect(navigationSpan.span_id).toMatch(/^[\da-f]{16}$/); - expect(navigationSpan.parent_span_id).toBeUndefined(); - - const errorTraceContext = errorEvent?.contexts?.trace; - expect(errorTraceContext).toEqual({ - trace_id: navigationTraceId, - span_id: expect.stringMatching(/^[\da-f]{16}$/), - }); - - expect(errorTraceHeader).toEqual({ - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: navigationTraceId, - sample_rand: expect.any(String), - }); -}); - -sentryTest( - 'outgoing fetch request during navigation has navigation traceId in headers', - async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - await page.route('http://sentry-test-site.example/**', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({}), - }); - }); - - // ensure pageload span is finished - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - await page.goto(url); - await pageloadSpanPromise; - - const navigationSpanEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'navigation'), - ); - const requestPromise = page.waitForRequest('http://sentry-test-site.example/*'); - await page.goto(`${url}#foo`); - await page.locator('#fetchBtn').click(); - const [navigationSpanEnvelope, request] = await Promise.all([navigationSpanEnvelopePromise, requestPromise]); - - const navigationTraceId = navigationSpanEnvelope[0].trace?.trace_id; - const sampleRand = navigationSpanEnvelope[0].trace?.sample_rand; - - expect(navigationTraceId).toMatch(/^[\da-f]{32}$/); - - const headers = request.headers(); - - // sampling decision is propagated from active span sampling decision - 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-sampled=true,sentry-sample_rand=${sampleRand},sentry-sample_rate=1`, - ); - }, -); - -sentryTest( - 'outgoing XHR request during navigation has navigation traceId in headers', - async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - await page.route('http://sentry-test-site.example/**', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({}), - }); - }); - - // ensure navigation span is finished - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - await page.goto(url); - await pageloadSpanPromise; - - const navigationSpanEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'navigation'), - ); - const requestPromise = page.waitForRequest('http://sentry-test-site.example/*'); - await page.goto(`${url}#foo`); - await page.locator('#xhrBtn').click(); - const [navigationSpanEnvelope, request] = await Promise.all([navigationSpanEnvelopePromise, requestPromise]); - - const navigationTraceId = navigationSpanEnvelope[0].trace?.trace_id; - const sampleRand = navigationSpanEnvelope[0].trace?.sample_rand; - - expect(navigationTraceId).toMatch(/^[\da-f]{32}$/); - - const headers = request.headers(); - - // sampling decision is propagated from active span sampling decision - 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-sampled=true,sentry-sample_rand=${sampleRand},sentry-sample_rate=1`, - ); - }, -); - -sentryTest( - 'user feedback event after navigation has navigation traceId in headers', - async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest() || shouldSkipFeedbackTest()); - - const url = await getLocalTestUrl({ testDir: __dirname, handleLazyLoadedFeedback: true }); - - // ensure pageload span is finished - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - await page.goto(url); - await pageloadSpanPromise; - - const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); - await page.goto(`${url}#foo`); - const navigationSpan = await navigationSpanPromise; - - const navigationTraceId = navigationSpan.trace_id; - expect(getSpanOp(navigationSpan)).toEqual('navigation'); - expect(navigationTraceId).toMatch(/^[\da-f]{32}$/); - expect(navigationSpan.span_id).toMatch(/^[\da-f]{16}$/); - expect(navigationSpan.parent_span_id).toBeUndefined(); - - const feedbackEventPromise = getFirstSentryEnvelopeRequest(page); - - await page.getByText('Report a Bug').click(); - expect(await page.locator(':visible:text-is("Report a Bug")').count()).toEqual(1); - await page.locator('[name="name"]').fill('Jane Doe'); - await page.locator('[name="email"]').fill('janedoe@example.org'); - await page.locator('[name="message"]').fill('my example feedback'); - await page.locator('[data-sentry-feedback] .btn--primary').click(); - - const feedbackEvent = await feedbackEventPromise; - - expect(feedbackEvent.type).toEqual('feedback'); - - const feedbackTraceContext = feedbackEvent.contexts?.trace; - - expect(feedbackTraceContext).toMatchObject({ - trace_id: navigationTraceId, - span_id: expect.stringMatching(/^[\da-f]{16}$/), - }); - }, -); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/init.js similarity index 89% rename from dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/init.js index 3dd77207e103..30317eda63ef 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/init.js @@ -6,7 +6,7 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), feedbackIntegration(), Sentry.spanStreamingIntegration()], + integrations: [Sentry.browserTracingIntegration(), feedbackIntegration()], tracePropagationTargets: ['http://sentry-test-site.example'], tracesSampleRate: 1, }); 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 0255507287d3..37bb7cb62f8c 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,117 +1,117 @@ import { expect } from '@playwright/test'; import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import type { EventAndTraceHeader } from '../../../../utils/helpers'; import { eventAndTraceHeaderRequestParser, - getFirstSentryEnvelopeRequest, getMultipleSentryEnvelopeRequests, shouldSkipFeedbackTest, shouldSkipTracingTest, + waitForErrorRequest, } from '../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; sentryTest('creates a new trace and sample_rand on each navigation', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - await getFirstSentryEnvelopeRequest(page, url); + // Wait for and skip the initial pageload span + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + await page.goto(url); + await pageloadSpanPromise; - const [navigation1Event, navigation1TraceHeader] = await getFirstSentryEnvelopeRequest( + const navigation1SpanEnvelopePromise = waitForStreamedSpanEnvelope( page, - `${url}#foo`, - eventAndTraceHeaderRequestParser, + env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'navigation'), ); - const [navigation2Event, navigation2TraceHeader] = await getFirstSentryEnvelopeRequest( + await page.goto(`${url}#foo`); + const navigation1SpanEnvelope = await navigation1SpanEnvelopePromise; + + const navigation2SpanEnvelopePromise = waitForStreamedSpanEnvelope( page, - `${url}#bar`, - eventAndTraceHeaderRequestParser, + env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'navigation'), ); + await page.goto(`${url}#bar`); + const navigation2SpanEnvelope = await navigation2SpanEnvelopePromise; - const navigation1TraceContext = navigation1Event.contexts?.trace; - const navigation2TraceContext = navigation2Event.contexts?.trace; + const navigation1TraceId = navigation1SpanEnvelope[0].trace?.trace_id; + const navigation1SampleRand = navigation1SpanEnvelope[0].trace?.sample_rand; + const navigation2TraceId = navigation2SpanEnvelope[0].trace?.trace_id; + const navigation2SampleRand = navigation2SpanEnvelope[0].trace?.sample_rand; - expect(navigation1Event.type).toEqual('transaction'); - expect(navigation2Event.type).toEqual('transaction'); + const navigation1Span = navigation1SpanEnvelope[1][0][1].items.find(s => getSpanOp(s) === 'navigation')!; + const navigation2Span = navigation2SpanEnvelope[1][0][1].items.find(s => getSpanOp(s) === 'navigation')!; - expect(navigation1TraceContext).toMatchObject({ - op: 'navigation', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - span_id: expect.stringMatching(/^[\da-f]{16}$/), - }); - expect(navigation1TraceContext).not.toHaveProperty('parent_span_id'); + expect(getSpanOp(navigation1Span)).toEqual('navigation'); + expect(navigation1TraceId).toMatch(/^[\da-f]{32}$/); + expect(navigation1Span.span_id).toMatch(/^[\da-f]{16}$/); + expect(navigation1Span.parent_span_id).toBeUndefined(); - expect(navigation1TraceHeader).toEqual({ + expect(navigation1SpanEnvelope[0].trace).toEqual({ environment: 'production', public_key: 'public', sample_rate: '1', sampled: 'true', - trace_id: navigation1TraceContext?.trace_id, + trace_id: navigation1TraceId, sample_rand: expect.any(String), }); - expect(navigation2TraceContext).toMatchObject({ - op: 'navigation', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - span_id: expect.stringMatching(/^[\da-f]{16}$/), - }); - expect(navigation2TraceContext).not.toHaveProperty('parent_span_id'); + expect(getSpanOp(navigation2Span)).toEqual('navigation'); + expect(navigation2TraceId).toMatch(/^[\da-f]{32}$/); + expect(navigation2Span.span_id).toMatch(/^[\da-f]{16}$/); + expect(navigation2Span.parent_span_id).toBeUndefined(); - expect(navigation2TraceHeader).toEqual({ + expect(navigation2SpanEnvelope[0].trace).toEqual({ environment: 'production', public_key: 'public', sample_rate: '1', sampled: 'true', - trace_id: navigation2TraceContext?.trace_id, + trace_id: navigation2TraceId, sample_rand: expect.any(String), }); - expect(navigation1TraceContext?.trace_id).not.toEqual(navigation2TraceContext?.trace_id); - expect(navigation1TraceHeader?.sample_rand).not.toEqual(navigation2TraceHeader?.sample_rand); + expect(navigation1TraceId).not.toEqual(navigation2TraceId); + expect(navigation1SampleRand).not.toEqual(navigation2SampleRand); }); sentryTest('error after navigation has navigation traceId', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - // ensure pageload transaction is finished - await getFirstSentryEnvelopeRequest(page, url); + // ensure pageload span is finished + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + await page.goto(url); + await pageloadSpanPromise; - const [navigationEvent, navigationTraceHeader] = await getFirstSentryEnvelopeRequest( + const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); + const navigationSpanEnvelopePromise = waitForStreamedSpanEnvelope( page, - `${url}#foo`, - eventAndTraceHeaderRequestParser, + env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'navigation'), ); - const navigationTraceContext = navigationEvent.contexts?.trace; + await page.goto(`${url}#foo`); + const [navigationSpan, navigationSpanEnvelope] = await Promise.all([ + navigationSpanPromise, + navigationSpanEnvelopePromise, + ]); - expect(navigationEvent.type).toEqual('transaction'); + const navigationTraceId = navigationSpan.trace_id; - expect(navigationTraceContext).toMatchObject({ - op: 'navigation', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - span_id: expect.stringMatching(/^[\da-f]{16}$/), - }); - expect(navigationTraceContext).not.toHaveProperty('parent_span_id'); + expect(getSpanOp(navigationSpan)).toEqual('navigation'); + expect(navigationTraceId).toMatch(/^[\da-f]{32}$/); + expect(navigationSpan.span_id).toMatch(/^[\da-f]{16}$/); + expect(navigationSpan.parent_span_id).toBeUndefined(); - expect(navigationTraceHeader).toEqual({ + expect(navigationSpanEnvelope[0].trace).toEqual({ environment: 'production', public_key: 'public', sample_rate: '1', sampled: 'true', - trace_id: navigationTraceContext?.trace_id, + trace_id: navigationTraceId, sample_rand: expect.any(String), }); - const errorEventPromise = getFirstSentryEnvelopeRequest( - page, - undefined, - eventAndTraceHeaderRequestParser, - ); + const errorEventPromise = waitForErrorRequest(page).then(eventAndTraceHeaderRequestParser); await page.locator('#errorBtn').click(); const [errorEvent, errorTraceHeader] = await errorEventPromise; @@ -119,7 +119,7 @@ sentryTest('error after navigation has navigation traceId', async ({ getLocalTes const errorTraceContext = errorEvent.contexts?.trace; expect(errorTraceContext).toEqual({ - trace_id: navigationTraceContext?.trace_id, + trace_id: navigationTraceId, span_id: expect.stringMatching(/^[\da-f]{16}$/), }); expect(errorTraceHeader).toEqual({ @@ -127,58 +127,42 @@ sentryTest('error after navigation has navigation traceId', async ({ getLocalTes public_key: 'public', sample_rate: '1', sampled: 'true', - trace_id: navigationTraceContext?.trace_id, + trace_id: navigationTraceId, sample_rand: expect.any(String), }); }); sentryTest('error during navigation has new navigation traceId', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - // ensure pageload transaction is finished - await getFirstSentryEnvelopeRequest(page, url); + // ensure pageload span is finished + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + await page.goto(url); + await pageloadSpanPromise; - const envelopeRequestsPromise = getMultipleSentryEnvelopeRequests( - page, - 2, - undefined, - eventAndTraceHeaderRequestParser, - ); + const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); + const errorEventPromise = waitForErrorRequest(page).then(eventAndTraceHeaderRequestParser); await page.goto(`${url}#foo`); await page.locator('#errorBtn').click(); - const envelopes = await envelopeRequestsPromise; - - const [navigationEvent, navigationTraceHeader] = envelopes.find(envelope => envelope[0].type === 'transaction')!; - const [errorEvent, errorTraceHeader] = envelopes.find(envelope => !envelope[0].type)!; + const [navigationSpan, [errorEvent, errorTraceHeader]] = await Promise.all([ + navigationSpanPromise, + errorEventPromise, + ]); - expect(navigationEvent.type).toEqual('transaction'); + expect(getSpanOp(navigationSpan)).toEqual('navigation'); expect(errorEvent.type).toEqual(undefined); - const navigationTraceContext = navigationEvent?.contexts?.trace; - expect(navigationTraceContext).toMatchObject({ - op: 'navigation', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - span_id: expect.stringMatching(/^[\da-f]{16}$/), - }); - expect(navigationTraceContext).not.toHaveProperty('parent_span_id'); - - expect(navigationTraceHeader).toEqual({ - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: navigationTraceContext?.trace_id, - sample_rand: expect.any(String), - }); + const navigationTraceId = navigationSpan.trace_id; + expect(navigationTraceId).toMatch(/^[\da-f]{32}$/); + expect(navigationSpan.span_id).toMatch(/^[\da-f]{16}$/); + expect(navigationSpan.parent_span_id).toBeUndefined(); const errorTraceContext = errorEvent?.contexts?.trace; expect(errorTraceContext).toEqual({ - trace_id: navigationTraceContext?.trace_id, + trace_id: navigationTraceId, span_id: expect.stringMatching(/^[\da-f]{16}$/), }); @@ -187,7 +171,7 @@ sentryTest('error during navigation has new navigation traceId', async ({ getLoc public_key: 'public', sample_rate: '1', sampled: 'true', - trace_id: navigationTraceContext?.trace_id, + trace_id: navigationTraceId, sample_rand: expect.any(String), }); }); @@ -195,9 +179,7 @@ sentryTest('error during navigation has new navigation traceId', async ({ getLoc sentryTest( 'outgoing fetch request during navigation has navigation traceId in headers', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); @@ -209,48 +191,31 @@ sentryTest( }); }); - // ensure pageload transaction is finished - await getFirstSentryEnvelopeRequest(page, url); + // ensure pageload span is finished + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + await page.goto(url); + await pageloadSpanPromise; - const navigationEventPromise = getFirstSentryEnvelopeRequest( + const navigationSpanEnvelopePromise = waitForStreamedSpanEnvelope( page, - undefined, - eventAndTraceHeaderRequestParser, + env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'navigation'), ); const requestPromise = page.waitForRequest('http://sentry-test-site.example/*'); await page.goto(`${url}#foo`); await page.locator('#fetchBtn').click(); - const [[navigationEvent, navigationTraceHeader], request] = await Promise.all([ - navigationEventPromise, - requestPromise, - ]); + const [navigationSpanEnvelope, request] = await Promise.all([navigationSpanEnvelopePromise, requestPromise]); - const navigationTraceContext = navigationEvent.contexts?.trace; + const navigationTraceId = navigationSpanEnvelope[0].trace?.trace_id; + const sampleRand = navigationSpanEnvelope[0].trace?.sample_rand; - expect(navigationEvent.type).toEqual('transaction'); - expect(navigationTraceContext).toMatchObject({ - op: 'navigation', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - span_id: expect.stringMatching(/^[\da-f]{16}$/), - }); - expect(navigationTraceContext).not.toHaveProperty('parent_span_id'); - - expect(navigationTraceHeader).toEqual({ - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: navigationTraceContext?.trace_id, - sample_rand: expect.any(String), - }); + expect(navigationTraceId).toMatch(/^[\da-f]{32}$/); const headers = request.headers(); // sampling decision is propagated from active span sampling decision - 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-sampled=true,sentry-sample_rand=${navigationTraceHeader?.sample_rand},sentry-sample_rate=1`, + `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId},sentry-sampled=true,sentry-sample_rand=${sampleRand},sentry-sample_rate=1`, ); }, ); @@ -258,9 +223,7 @@ sentryTest( sentryTest( 'outgoing XHR request during navigation has navigation traceId in headers', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); @@ -272,48 +235,31 @@ sentryTest( }); }); - // ensure navigation transaction is finished - await getFirstSentryEnvelopeRequest(page, url); + // ensure navigation span is finished + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + await page.goto(url); + await pageloadSpanPromise; - const navigationEventPromise = getFirstSentryEnvelopeRequest( + const navigationSpanEnvelopePromise = waitForStreamedSpanEnvelope( page, - undefined, - eventAndTraceHeaderRequestParser, + env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'navigation'), ); const requestPromise = page.waitForRequest('http://sentry-test-site.example/*'); await page.goto(`${url}#foo`); await page.locator('#xhrBtn').click(); - const [[navigationEvent, navigationTraceHeader], request] = await Promise.all([ - navigationEventPromise, - requestPromise, - ]); + const [navigationSpanEnvelope, request] = await Promise.all([navigationSpanEnvelopePromise, requestPromise]); - const navigationTraceContext = navigationEvent.contexts?.trace; + const navigationTraceId = navigationSpanEnvelope[0].trace?.trace_id; + const sampleRand = navigationSpanEnvelope[0].trace?.sample_rand; - expect(navigationEvent.type).toEqual('transaction'); - expect(navigationTraceContext).toMatchObject({ - op: 'navigation', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - span_id: expect.stringMatching(/^[\da-f]{16}$/), - }); - expect(navigationTraceContext).not.toHaveProperty('parent_span_id'); - - expect(navigationTraceHeader).toEqual({ - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: navigationTraceContext?.trace_id, - sample_rand: expect.any(String), - }); + expect(navigationTraceId).toMatch(/^[\da-f]{32}$/); const headers = request.headers(); // sampling decision is propagated from active span sampling decision - 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-sampled=true,sentry-sample_rand=${navigationTraceHeader?.sample_rand},sentry-sample_rate=1`, + `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId},sentry-sampled=true,sentry-sample_rand=${sampleRand},sentry-sample_rate=1`, ); }, ); @@ -321,26 +267,26 @@ sentryTest( sentryTest( 'user feedback event after navigation has navigation traceId in headers', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest() || shouldSkipFeedbackTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest() || shouldSkipFeedbackTest()); const url = await getLocalTestUrl({ testDir: __dirname, handleLazyLoadedFeedback: true }); - // ensure pageload transaction is finished - await getFirstSentryEnvelopeRequest(page, url); + // ensure pageload span is finished + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + await page.goto(url); + await pageloadSpanPromise; - const navigationEvent = await getFirstSentryEnvelopeRequest(page, `${url}#foo`); + const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); + await page.goto(`${url}#foo`); + const navigationSpan = await navigationSpanPromise; - const navigationTraceContext = navigationEvent.contexts?.trace; - expect(navigationTraceContext).toMatchObject({ - op: 'navigation', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - span_id: expect.stringMatching(/^[\da-f]{16}$/), - }); - expect(navigationTraceContext).not.toHaveProperty('parent_span_id'); + const navigationTraceId = navigationSpan.trace_id; + expect(getSpanOp(navigationSpan)).toEqual('navigation'); + expect(navigationTraceId).toMatch(/^[\da-f]{32}$/); + expect(navigationSpan.span_id).toMatch(/^[\da-f]{16}$/); + expect(navigationSpan.parent_span_id).toBeUndefined(); - const feedbackEventPromise = getFirstSentryEnvelopeRequest(page); + const feedbackEventsPromise = getMultipleSentryEnvelopeRequests(page, 1, { envelopeType: 'feedback' }); await page.getByText('Report a Bug').click(); expect(await page.locator(':visible:text-is("Report a Bug")').count()).toEqual(1); @@ -349,14 +295,14 @@ sentryTest( await page.locator('[name="message"]').fill('my example feedback'); await page.locator('[data-sentry-feedback] .btn--primary').click(); - const feedbackEvent = await feedbackEventPromise; + const [feedbackEvent] = await feedbackEventsPromise; expect(feedbackEvent.type).toEqual('feedback'); const feedbackTraceContext = feedbackEvent.contexts?.trace; expect(feedbackTraceContext).toMatchObject({ - trace_id: navigationTraceContext?.trace_id, + trace_id: navigationTraceId, span_id: expect.stringMatching(/^[\da-f]{16}$/), }); }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-headers/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-headers/test.ts index 76e015dcb07a..efe6897d497e 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-headers/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-headers/test.ts @@ -1,11 +1,8 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import type { EventAndTraceHeader } from '../../../../utils/helpers'; -import { - eventAndTraceHeaderRequestParser, - getFirstSentryEnvelopeRequest, - shouldSkipTracingTest, -} from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../../utils/spanUtils'; const META_TAG_TRACE_ID = '12345678901234567890123456789012'; const META_TAG_PARENT_SPAN_ID = '1234567890123456'; @@ -28,23 +25,13 @@ sentryTest( }, }); - const [pageloadEvent, pageloadTraceHeader] = await getFirstSentryEnvelopeRequest( - page, - url, - eventAndTraceHeaderRequestParser, - ); - const [navigationEvent, navigationTraceHeader] = await getFirstSentryEnvelopeRequest( - page, - `${url}#foo`, - eventAndTraceHeaderRequestParser, - ); + const [pageloadSpan, pageloadTraceHeader] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url); + const [navigationSpan, navigationTraceHeader] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, `${url}#foo`); - const pageloadTraceContext = pageloadEvent.contexts?.trace; - const navigationTraceContext = navigationEvent.contexts?.trace; - - expect(pageloadEvent.type).toEqual('transaction'); - expect(pageloadTraceContext).toMatchObject({ - op: 'pageload', + expect(pageloadSpan.is_segment).toBe(true); + expect(pageloadSpan).toMatchObject({ + is_segment: true, + attributes: expect.objectContaining({ [SENTRY_OP]: { type: 'string', value: 'pageload' } }), trace_id: META_TAG_TRACE_ID, parent_span_id: META_TAG_PARENT_SPAN_ID, span_id: expect.stringMatching(/^[\da-f]{16}$/), @@ -61,25 +48,26 @@ sentryTest( sample_rand: '0.42', }); - expect(navigationEvent.type).toEqual('transaction'); - expect(navigationTraceContext).toMatchObject({ - op: 'navigation', + expect(navigationSpan.is_segment).toBe(true); + expect(navigationSpan).toMatchObject({ + is_segment: true, + attributes: expect.objectContaining({ [SENTRY_OP]: { type: 'string', value: 'navigation' } }), trace_id: expect.stringMatching(/^[\da-f]{32}$/), span_id: expect.stringMatching(/^[\da-f]{16}$/), }); // navigation span is head of trace, so there's no parent span: - expect(navigationTraceContext).not.toHaveProperty('parent_span_id'); + expect(navigationSpan).not.toHaveProperty('parent_span_id'); expect(navigationTraceHeader).toEqual({ environment: 'production', public_key: 'public', sample_rate: '1', sampled: 'true', - trace_id: navigationTraceContext?.trace_id, + trace_id: navigationSpan?.trace_id, sample_rand: expect.any(String), }); - expect(pageloadTraceContext?.trace_id).not.toEqual(navigationTraceContext?.trace_id); + expect(pageloadSpan?.trace_id).not.toEqual(navigationSpan?.trace_id); expect(pageloadTraceHeader?.sample_rand).not.toEqual(navigationTraceHeader?.sample_rand); }, ); 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 c66d8793e610..5d81177d2515 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,14 +1,19 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import type { EventAndTraceHeader } from '../../../../utils/helpers'; import { eventAndTraceHeaderRequestParser, - getFirstSentryEnvelopeRequest, getMultipleSentryEnvelopeRequests, shouldSkipFeedbackTest, shouldSkipTracingTest, + waitForErrorRequest, } from '../../../../utils/helpers'; +import { + getSpanOp, + waitForStreamedSpanAndTraceHeader, + waitForStreamedSpanAndTraceHeaderOnUrl, +} from '../../../../utils/spanUtils'; const META_TAG_TRACE_ID = '12345678901234567890123456789012'; const META_TAG_PARENT_SPAN_ID = '1234567890123456'; @@ -24,23 +29,13 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); - const [pageloadEvent, pageloadTraceHeader] = await getFirstSentryEnvelopeRequest( - page, - url, - eventAndTraceHeaderRequestParser, - ); - const [navigationEvent, navigationTraceHeader] = await getFirstSentryEnvelopeRequest( - page, - `${url}#foo`, - eventAndTraceHeaderRequestParser, - ); - - const pageloadTraceContext = pageloadEvent.contexts?.trace; - const navigationTraceContext = navigationEvent.contexts?.trace; + const [pageloadSpan, pageloadTraceHeader] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url); + const [navigationSpan, navigationTraceHeader] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, `${url}#foo`); - expect(pageloadEvent.type).toEqual('transaction'); - expect(pageloadTraceContext).toMatchObject({ - op: 'pageload', + expect(pageloadSpan.is_segment).toBe(true); + expect(pageloadSpan).toMatchObject({ + is_segment: true, + attributes: expect.objectContaining({ [SENTRY_OP]: { type: 'string', value: 'pageload' } }), trace_id: META_TAG_TRACE_ID, parent_span_id: META_TAG_PARENT_SPAN_ID, span_id: expect.stringMatching(/^[\da-f]{16}$/), @@ -57,25 +52,26 @@ sentryTest( sample_rand: '0.42', }); - expect(navigationEvent.type).toEqual('transaction'); - expect(navigationTraceContext).toMatchObject({ - op: 'navigation', + expect(navigationSpan.is_segment).toBe(true); + expect(navigationSpan).toMatchObject({ + is_segment: true, + attributes: expect.objectContaining({ [SENTRY_OP]: { type: 'string', value: 'navigation' } }), trace_id: expect.stringMatching(/^[\da-f]{32}$/), span_id: expect.stringMatching(/^[\da-f]{16}$/), }); // navigation span is head of trace, so there's no parent span: - expect(navigationTraceContext).not.toHaveProperty('parent_span_id'); + expect(navigationSpan).not.toHaveProperty('parent_span_id'); expect(navigationTraceHeader).toEqual({ environment: 'production', public_key: 'public', sample_rate: '1', sampled: 'true', - trace_id: navigationTraceContext?.trace_id, + trace_id: navigationSpan?.trace_id, sample_rand: expect.any(String), }); - expect(pageloadTraceContext?.trace_id).not.toEqual(navigationTraceContext?.trace_id); + expect(pageloadSpan?.trace_id).not.toEqual(navigationSpan?.trace_id); expect(pageloadTraceHeader?.sample_rand).not.toEqual(navigationTraceHeader?.sample_rand); }, ); @@ -87,14 +83,11 @@ sentryTest('error after tag pageload has pageload traceId', async ({ getL const url = await getLocalTestUrl({ testDir: __dirname }); - const [pageloadEvent, pageloadTraceHeader] = await getFirstSentryEnvelopeRequest( - page, - url, - eventAndTraceHeaderRequestParser, - ); + const [pageloadSpan, pageloadTraceHeader] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url); - expect(pageloadEvent.contexts?.trace).toMatchObject({ - op: 'pageload', + expect(pageloadSpan).toMatchObject({ + is_segment: true, + attributes: expect.objectContaining({ [SENTRY_OP]: { type: 'string', value: 'pageload' } }), trace_id: META_TAG_TRACE_ID, parent_span_id: META_TAG_PARENT_SPAN_ID, span_id: expect.stringMatching(/^[\da-f]{16}$/), @@ -111,11 +104,7 @@ sentryTest('error after tag pageload has pageload traceId', async ({ getL sample_rand: '0.42', }); - const errorEventPromise = getFirstSentryEnvelopeRequest( - page, - undefined, - eventAndTraceHeaderRequestParser, - ); + const errorEventPromise = waitForErrorRequest(page).then(eventAndTraceHeaderRequestParser); await page.locator('#errorBtn').click(); const [errorEvent, errorTraceHeader] = await errorEventPromise; @@ -145,24 +134,22 @@ sentryTest('error during tag pageload has pageload traceId', async ({ get const url = await getLocalTestUrl({ testDir: __dirname }); - const envelopeRequestsPromise = getMultipleSentryEnvelopeRequests( + const pageloadPromise = waitForStreamedSpanAndTraceHeader( page, - 2, - undefined, - eventAndTraceHeaderRequestParser, + span => span.is_segment && getSpanOp(span) === 'pageload', ); + const errorPromise = waitForErrorRequest(page).then(eventAndTraceHeaderRequestParser); await page.goto(url); await page.locator('#errorBtn').click(); - const envelopes = await envelopeRequestsPromise; - - const [pageloadEvent, pageloadTraceHeader] = envelopes.find( - eventAndHeader => eventAndHeader[0].type === 'transaction', - )!; - const [errorEvent, errorTraceHeader] = envelopes.find(eventAndHeader => !eventAndHeader[0].type)!; - - expect(pageloadEvent.type).toEqual('transaction'); - expect(pageloadEvent?.contexts?.trace).toMatchObject({ - op: 'pageload', + const [[pageloadSpan, pageloadTraceHeader], [errorEvent, errorTraceHeader]] = await Promise.all([ + pageloadPromise, + errorPromise, + ]); + + expect(pageloadSpan.is_segment).toBe(true); + expect(pageloadSpan).toMatchObject({ + is_segment: true, + attributes: expect.objectContaining({ [SENTRY_OP]: { type: 'string', value: 'pageload' } }), trace_id: META_TAG_TRACE_ID, parent_span_id: META_TAG_PARENT_SPAN_ID, span_id: expect.stringMatching(/^[\da-f]{16}$/), @@ -215,19 +202,19 @@ sentryTest( }); }); - const pageloadEventPromise = getFirstSentryEnvelopeRequest( + const pageloadSpanPromise = waitForStreamedSpanAndTraceHeader( page, - undefined, - eventAndTraceHeaderRequestParser, + span => span.is_segment && getSpanOp(span) === 'pageload', ); const requestPromise = page.waitForRequest('http://sentry-test-site.example/*'); await page.goto(url); await page.locator('#fetchBtn').click(); - const [[pageloadEvent, pageloadTraceHeader], request] = await Promise.all([pageloadEventPromise, requestPromise]); + const [[pageloadSpan, pageloadTraceHeader], request] = await Promise.all([pageloadSpanPromise, requestPromise]); - expect(pageloadEvent.type).toEqual('transaction'); - expect(pageloadEvent?.contexts?.trace).toMatchObject({ - op: 'pageload', + expect(pageloadSpan.is_segment).toBe(true); + expect(pageloadSpan).toMatchObject({ + is_segment: true, + attributes: expect.objectContaining({ [SENTRY_OP]: { type: 'string', value: 'pageload' } }), trace_id: META_TAG_TRACE_ID, parent_span_id: META_TAG_PARENT_SPAN_ID, span_id: expect.stringMatching(/^[\da-f]{16}$/), @@ -270,19 +257,19 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); - const pageloadEventPromise = getFirstSentryEnvelopeRequest( + const pageloadSpanPromise = waitForStreamedSpanAndTraceHeader( page, - undefined, - eventAndTraceHeaderRequestParser, + span => span.is_segment && getSpanOp(span) === 'pageload', ); const requestPromise = page.waitForRequest('http://sentry-test-site.example/*'); await page.goto(url); await page.locator('#xhrBtn').click(); - const [[pageloadEvent, pageloadTraceHeader], request] = await Promise.all([pageloadEventPromise, requestPromise]); + const [[pageloadSpan, pageloadTraceHeader], request] = await Promise.all([pageloadSpanPromise, requestPromise]); - expect(pageloadEvent.type).toEqual('transaction'); - expect(pageloadEvent?.contexts?.trace).toMatchObject({ - op: 'pageload', + expect(pageloadSpan.is_segment).toBe(true); + expect(pageloadSpan).toMatchObject({ + is_segment: true, + attributes: expect.objectContaining({ [SENTRY_OP]: { type: 'string', value: 'pageload' } }), trace_id: META_TAG_TRACE_ID, parent_span_id: META_TAG_PARENT_SPAN_ID, span_id: expect.stringMatching(/^[\da-f]{16}$/), @@ -315,17 +302,17 @@ sentryTest('user feedback event after pageload has pageload traceId in headers', const url = await getLocalTestUrl({ testDir: __dirname, handleLazyLoadedFeedback: true }); - const pageloadEvent = await getFirstSentryEnvelopeRequest(page, url); - const pageloadTraceContext = pageloadEvent.contexts?.trace; + const [pageloadSpan] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url); - expect(pageloadTraceContext).toMatchObject({ - op: 'pageload', + expect(pageloadSpan).toMatchObject({ + is_segment: true, + attributes: expect.objectContaining({ [SENTRY_OP]: { type: 'string', value: 'pageload' } }), trace_id: META_TAG_TRACE_ID, parent_span_id: META_TAG_PARENT_SPAN_ID, span_id: expect.stringMatching(/^[\da-f]{16}$/), }); - const feedbackEventPromise = getFirstSentryEnvelopeRequest(page); + const feedbackEventsPromise = getMultipleSentryEnvelopeRequests(page, 1, { envelopeType: 'feedback' }); await page.getByText('Report a Bug').click(); expect(await page.locator(':visible:text-is("Report a Bug")').count()).toEqual(1); @@ -334,7 +321,7 @@ sentryTest('user feedback event after pageload has pageload traceId in headers', await page.locator('[name="message"]').fill('my example feedback'); await page.locator('[data-sentry-feedback] .btn--primary').click(); - const feedbackEvent = await feedbackEventPromise; + const [feedbackEvent] = await feedbackEventsPromise; const feedbackTraceContext = feedbackEvent.contexts?.trace; expect(feedbackEvent.type).toEqual('feedback'); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-static/init.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-static/init.js new file mode 100644 index 000000000000..8718d9d354eb --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-static/init.js @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/browser'; +// Import this separately so that generatePlugin can handle it for CDN scenarios +import { feedbackIntegration } from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration(), feedbackIntegration()], + tracePropagationTargets: ['http://sentry-test-site.example'], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-static/subject.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-static/subject.js new file mode 100644 index 000000000000..eead800a75a5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-static/subject.js @@ -0,0 +1,23 @@ +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://sentry-test-site.example'); +}); + +const xhrBtn = document.getElementById('xhrBtn'); +xhrBtn.addEventListener('click', () => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', 'http://sentry-test-site.example'); + xhr.send(); +}); + +const spanAndFetchBtn = document.getElementById('spanAndFetchBtn'); +spanAndFetchBtn.addEventListener('click', () => { + Sentry.startSpan({ name: 'custom-root-span' }, async () => { + await fetch('http://sentry-test-site.example'); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-static/template.html b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-static/template.html new file mode 100644 index 000000000000..87e7bf5efbea --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-static/template.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-static/test.ts new file mode 100644 index 000000000000..f65ed9c13e3a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-static/test.ts @@ -0,0 +1,484 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import type { EventAndTraceHeader } from '../../../../utils/helpers'; +import { + eventAndTraceHeaderRequestParser, + getFirstSentryEnvelopeRequest, + getMultipleSentryEnvelopeRequests, + shouldSkipFeedbackTest, + shouldSkipTracingTest, +} from '../../../../utils/helpers'; + +sentryTest( + 'should create a new trace for a navigation after the initial pageload', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const [pageloadEvent, pageloadTraceHeaders] = await getFirstSentryEnvelopeRequest( + page, + url, + eventAndTraceHeaderRequestParser, + ); + const [navigationEvent, navigationTraceHeaders] = await getFirstSentryEnvelopeRequest( + page, + `${url}#foo`, + eventAndTraceHeaderRequestParser, + ); + + const pageloadTraceContext = pageloadEvent.contexts?.trace; + const navigationTraceContext = navigationEvent.contexts?.trace; + + expect(pageloadEvent.type).toEqual('transaction'); + expect(navigationEvent.type).toEqual('transaction'); + + expect(pageloadTraceContext).toMatchObject({ + op: 'pageload', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + span_id: expect.stringMatching(/^[\da-f]{16}$/), + }); + expect(pageloadTraceContext).not.toHaveProperty('parent_span_id'); + + expect(pageloadTraceHeaders).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: pageloadTraceContext?.trace_id, + sample_rand: expect.any(String), + }); + + expect(navigationTraceContext).toMatchObject({ + op: 'navigation', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + span_id: expect.stringMatching(/^[\da-f]{16}$/), + }); + expect(navigationTraceContext).not.toHaveProperty('parent_span_id'); + + expect(navigationTraceHeaders).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: navigationTraceContext?.trace_id, + sample_rand: expect.any(String), + }); + + expect(pageloadTraceContext?.span_id).not.toEqual(navigationTraceContext?.span_id); + }, +); + +sentryTest('error after pageload has pageload traceId', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const [pageloadEvent, pageloadTraceHeader] = await getFirstSentryEnvelopeRequest( + page, + url, + eventAndTraceHeaderRequestParser, + ); + const pageloadTraceContext = pageloadEvent.contexts?.trace; + + expect(pageloadEvent.type).toEqual('transaction'); + expect(pageloadTraceContext).toMatchObject({ + op: 'pageload', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + span_id: expect.stringMatching(/^[\da-f]{16}$/), + }); + expect(pageloadTraceContext).not.toHaveProperty('parent_span_id'); + + expect(pageloadTraceHeader).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: pageloadTraceContext?.trace_id, + sample_rand: expect.any(String), + }); + + const errorEventPromise = getFirstSentryEnvelopeRequest( + page, + undefined, + eventAndTraceHeaderRequestParser, + ); + await page.locator('#errorBtn').click(); + const [errorEvent, errorTraceHeader] = await errorEventPromise; + + const errorTraceContext = errorEvent.contexts?.trace; + expect(errorEvent.type).toEqual(undefined); + + expect(errorTraceContext).toEqual({ + trace_id: pageloadTraceContext?.trace_id, + span_id: expect.stringMatching(/^[\da-f]{16}$/), + }); + + expect(errorTraceHeader).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: pageloadTraceContext?.trace_id, + sample_rand: expect.any(String), + }); +}); + +sentryTest('error during pageload has pageload traceId', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const envelopeRequestsPromise = getMultipleSentryEnvelopeRequests( + page, + 2, + undefined, + eventAndTraceHeaderRequestParser, + ); + await page.goto(url); + await page.locator('#errorBtn').click(); + const envelopes = await envelopeRequestsPromise; + + const [pageloadEvent, pageloadTraceHeader] = envelopes.find( + eventAndHeader => eventAndHeader[0].type === 'transaction', + )!; + const [errorEvent, errorTraceHeader] = envelopes.find(eventAndHeader => !eventAndHeader[0].type)!; + + const pageloadTraceContext = pageloadEvent?.contexts?.trace; + + expect(pageloadEvent.type).toEqual('transaction'); + expect(pageloadTraceContext).toMatchObject({ + op: 'pageload', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + span_id: expect.stringMatching(/^[\da-f]{16}$/), + }); + expect(pageloadTraceContext).not.toHaveProperty('parent_span_id'); + + expect(pageloadTraceHeader).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: pageloadTraceContext?.trace_id, + sample_rand: expect.any(String), + }); + + const errorTraceContext = errorEvent?.contexts?.trace; + + expect(errorEvent.type).toEqual(undefined); + expect(errorTraceContext).toEqual({ + trace_id: pageloadTraceContext?.trace_id, + span_id: expect.stringMatching(/^[\da-f]{16}$/), + }); + + expect(errorTraceHeader).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: pageloadTraceContext?.trace_id, + sample_rand: expect.any(String), + }); +}); + +sentryTest( + 'outgoing fetch request during pageload has pageload traceId in headers', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.route('http://sentry-test-site.example/**', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + const pageloadEventPromise = getFirstSentryEnvelopeRequest( + page, + undefined, + eventAndTraceHeaderRequestParser, + ); + const requestPromise = page.waitForRequest('http://sentry-test-site.example/*'); + await page.goto(url); + await page.locator('#fetchBtn').click(); + const [[pageloadEvent, pageloadTraceHeader], request] = await Promise.all([pageloadEventPromise, requestPromise]); + + const pageloadTraceContext = pageloadEvent.contexts?.trace; + const pageloadTraceId = pageloadTraceContext?.trace_id; + + expect(pageloadEvent.type).toEqual('transaction'); + expect(pageloadTraceContext).toMatchObject({ + op: 'pageload', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + span_id: expect.stringMatching(/^[\da-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, + sample_rand: expect.any(String), + }); + + 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']).toBe( + `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sampled=true,sentry-sample_rand=${pageloadTraceHeader?.sample_rand},sentry-sample_rate=1`, + ); + }, +); + +sentryTest( + 'outgoing XHR request during pageload has pageload traceId in headers', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.route('http://sentry-test-site.example/**', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + const pageloadEventPromise = getFirstSentryEnvelopeRequest( + page, + undefined, + eventAndTraceHeaderRequestParser, + ); + const requestPromise = page.waitForRequest('http://sentry-test-site.example/*'); + await page.goto(url); + await page.locator('#xhrBtn').click(); + const [[pageloadEvent, pageloadTraceHeader], request] = await Promise.all([pageloadEventPromise, requestPromise]); + + const pageloadTraceContext = pageloadEvent.contexts?.trace; + const pageloadTraceId = pageloadTraceContext?.trace_id; + + expect(pageloadEvent.type).toEqual('transaction'); + expect(pageloadTraceContext).toMatchObject({ + op: 'pageload', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + span_id: expect.stringMatching(/^[\da-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, + sample_rand: expect.any(String), + }); + + 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']).toBe( + `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sampled=true,sentry-sample_rand=${pageloadTraceHeader?.sample_rand},sentry-sample_rate=1`, + ); + }, +); + +// 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://sentry-test-site.example/**', 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://sentry-test-site.example/*'); +// 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://sentry-test-site.example/**', 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://sentry-test-site.example/**'); +// 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(); + } + + const url = await getLocalTestUrl({ testDir: __dirname, handleLazyLoadedFeedback: true }); + + const pageloadEvent = await getFirstSentryEnvelopeRequest(page, url); + const pageloadTraceContext = pageloadEvent.contexts?.trace; + + expect(pageloadTraceContext).toMatchObject({ + op: 'pageload', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + span_id: expect.stringMatching(/^[\da-f]{16}$/), + }); + expect(pageloadTraceContext).not.toHaveProperty('parent_span_id'); + + const feedbackEventsPromise = getMultipleSentryEnvelopeRequests(page, 1, { envelopeType: 'feedback' }); + + await page.getByText('Report a Bug').click(); + expect(await page.locator(':visible:text-is("Report a Bug")').count()).toEqual(1); + await page.locator('[name="name"]').fill('Jane Doe'); + await page.locator('[name="email"]').fill('janedoe@example.org'); + await page.locator('[name="message"]').fill('my example feedback'); + await page.locator('[data-sentry-feedback] .btn--primary').click(); + + const [feedbackEvent] = await feedbackEventsPromise; + + expect(feedbackEvent.type).toEqual('feedback'); + + const feedbackTraceContext = feedbackEvent.contexts?.trace; + + expect(feedbackTraceContext).toMatchObject({ + trace_id: pageloadTraceContext?.trace_id, + span_id: expect.stringMatching(/^[\da-f]{16}$/), + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-streamed/test.ts deleted file mode 100644 index 646d4be6fa56..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-streamed/test.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; -import { sentryTest } from '../../../../utils/fixtures'; -import type { EventAndTraceHeader } from '../../../../utils/helpers'; -import { - eventAndTraceHeaderRequestParser, - getFirstSentryEnvelopeRequest, - shouldSkipFeedbackTest, - shouldSkipTracingTest, -} from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpan, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; - -sentryTest('creates a new trace for a navigation after the initial pageload', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); - - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - - const pageloadSpan = await pageloadSpanPromise; - - page.goto(`${url}#foo`); - - const navigationSpan = await navigationSpanPromise; - - expect(getSpanOp(pageloadSpan)).toEqual('pageload'); - expect(pageloadSpan.trace_id).toMatch(/^[\da-f]{32}$/); - expect(pageloadSpan.span_id).toMatch(/^[\da-f]{16}$/); - expect(pageloadSpan.parent_span_id).toBeUndefined(); - - expect(getSpanOp(navigationSpan)).toEqual('navigation'); - expect(navigationSpan.trace_id).toMatch(/^[\da-f]{32}$/); - expect(navigationSpan.span_id).toMatch(/^[\da-f]{16}$/); - expect(navigationSpan.parent_span_id).toBeUndefined(); - - expect(pageloadSpan.span_id).not.toEqual(navigationSpan.span_id); - expect(pageloadSpan.trace_id).not.toEqual(navigationSpan.trace_id); -}); - -sentryTest('error after pageload has pageload traceId', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - - const pageloadSpan = await pageloadSpanPromise; - const pageloadTraceId = pageloadSpan.trace_id; - - expect(getSpanOp(pageloadSpan)).toEqual('pageload'); - expect(pageloadTraceId).toMatch(/^[\da-f]{32}$/); - expect(pageloadSpan.span_id).toMatch(/^[\da-f]{16}$/); - expect(pageloadSpan.parent_span_id).toBeUndefined(); - - const errorEventPromise = getFirstSentryEnvelopeRequest( - page, - undefined, - eventAndTraceHeaderRequestParser, - ); - await page.locator('#errorBtn').click(); - const [errorEvent, errorTraceHeader] = await errorEventPromise; - - const errorTraceContext = errorEvent.contexts?.trace; - expect(errorEvent.type).toEqual(undefined); - - expect(errorTraceContext).toEqual({ - trace_id: pageloadTraceId, - span_id: expect.stringMatching(/^[\da-f]{16}$/), - }); - - expect(errorTraceHeader).toEqual({ - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: pageloadTraceId, - sample_rand: expect.any(String), - }); -}); - -sentryTest('error during pageload has pageload traceId', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - const errorEventPromise = getFirstSentryEnvelopeRequest( - page, - undefined, - eventAndTraceHeaderRequestParser, - ); - - await page.goto(url); - await page.locator('#errorBtn').click(); - const [pageloadSpan, [errorEvent, errorTraceHeader]] = await Promise.all([pageloadSpanPromise, errorEventPromise]); - - const pageloadTraceId = pageloadSpan.trace_id; - - expect(getSpanOp(pageloadSpan)).toEqual('pageload'); - expect(pageloadTraceId).toMatch(/^[\da-f]{32}$/); - expect(pageloadSpan.span_id).toMatch(/^[\da-f]{16}$/); - expect(pageloadSpan.parent_span_id).toBeUndefined(); - - const errorTraceContext = errorEvent?.contexts?.trace; - expect(errorEvent.type).toEqual(undefined); - - expect(errorTraceContext).toEqual({ - trace_id: pageloadTraceId, - span_id: expect.stringMatching(/^[\da-f]{16}$/), - }); - - expect(errorTraceHeader).toEqual({ - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: pageloadTraceId, - sample_rand: expect.any(String), - }); -}); - -sentryTest( - 'outgoing fetch request during pageload has pageload traceId in headers', - async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - await page.route('http://sentry-test-site.example/**', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({}), - }); - }); - - const pageloadSpanEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'pageload'), - ); - const requestPromise = page.waitForRequest('http://sentry-test-site.example/*'); - await page.goto(url); - await page.locator('#fetchBtn').click(); - const [pageloadSpanEnvelope, request] = await Promise.all([pageloadSpanEnvelopePromise, requestPromise]); - - const pageloadTraceId = pageloadSpanEnvelope[0].trace?.trace_id; - const sampleRand = pageloadSpanEnvelope[0].trace?.sample_rand; - - expect(pageloadTraceId).toMatch(/^[\da-f]{32}$/); - - 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']).toBe( - `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sampled=true,sentry-sample_rand=${sampleRand},sentry-sample_rate=1`, - ); - }, -); - -sentryTest( - 'outgoing XHR request during pageload has pageload traceId in headers', - async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - await page.route('http://sentry-test-site.example/**', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({}), - }); - }); - - const pageloadSpanEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'pageload'), - ); - const requestPromise = page.waitForRequest('http://sentry-test-site.example/*'); - await page.goto(url); - await page.locator('#xhrBtn').click(); - const [pageloadSpanEnvelope, request] = await Promise.all([pageloadSpanEnvelopePromise, requestPromise]); - - const pageloadTraceId = pageloadSpanEnvelope[0].trace?.trace_id; - const sampleRand = pageloadSpanEnvelope[0].trace?.sample_rand; - - expect(pageloadTraceId).toMatch(/^[\da-f]{32}$/); - - 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']).toBe( - `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sampled=true,sentry-sample_rand=${sampleRand},sentry-sample_rate=1`, - ); - }, -); - -sentryTest('user feedback event after pageload has pageload traceId in headers', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest() || shouldSkipFeedbackTest()); - - const url = await getLocalTestUrl({ testDir: __dirname, handleLazyLoadedFeedback: true }); - - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - await page.goto(url); - const pageloadSpan = await pageloadSpanPromise; - const pageloadTraceId = pageloadSpan.trace_id; - - expect(getSpanOp(pageloadSpan)).toEqual('pageload'); - expect(pageloadTraceId).toMatch(/^[\da-f]{32}$/); - expect(pageloadSpan.span_id).toMatch(/^[\da-f]{16}$/); - expect(pageloadSpan.parent_span_id).toBeUndefined(); - - const feedbackEventPromise = getFirstSentryEnvelopeRequest(page); - - await page.getByText('Report a Bug').click(); - expect(await page.locator(':visible:text-is("Report a Bug")').count()).toEqual(1); - await page.locator('[name="name"]').fill('Jane Doe'); - await page.locator('[name="email"]').fill('janedoe@example.org'); - await page.locator('[name="message"]').fill('my example feedback'); - await page.locator('[data-sentry-feedback] .btn--primary').click(); - - const feedbackEvent = await feedbackEventPromise; - - expect(feedbackEvent.type).toEqual('feedback'); - - const feedbackTraceContext = feedbackEvent.contexts?.trace; - - expect(feedbackTraceContext).toMatchObject({ - trace_id: pageloadTraceId, - span_id: expect.stringMatching(/^[\da-f]{16}$/), - }); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload/init.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload/init.js new file mode 100644 index 000000000000..30317eda63ef --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload/init.js @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/browser'; +// Import this separately so that generatePlugin can handle it for CDN scenarios +import { feedbackIntegration } from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration(), feedbackIntegration()], + tracePropagationTargets: ['http://sentry-test-site.example'], + tracesSampleRate: 1, +}); 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 e6edd02cf360..c6038e48826b 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,113 +1,61 @@ import { expect } from '@playwright/test'; import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import type { EventAndTraceHeader } from '../../../../utils/helpers'; import { eventAndTraceHeaderRequestParser, - getFirstSentryEnvelopeRequest, getMultipleSentryEnvelopeRequests, shouldSkipFeedbackTest, shouldSkipTracingTest, + waitForErrorRequest, } from '../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; -sentryTest( - 'should create a new trace for a navigation after the initial pageload', - async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +sentryTest('creates a new trace for a navigation after the initial pageload', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); - const url = await getLocalTestUrl({ testDir: __dirname }); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); - const [pageloadEvent, pageloadTraceHeaders] = await getFirstSentryEnvelopeRequest( - page, - url, - eventAndTraceHeaderRequestParser, - ); - const [navigationEvent, navigationTraceHeaders] = await getFirstSentryEnvelopeRequest( - page, - `${url}#foo`, - eventAndTraceHeaderRequestParser, - ); + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); - const pageloadTraceContext = pageloadEvent.contexts?.trace; - const navigationTraceContext = navigationEvent.contexts?.trace; + const pageloadSpan = await pageloadSpanPromise; - expect(pageloadEvent.type).toEqual('transaction'); - expect(navigationEvent.type).toEqual('transaction'); + await page.goto(`${url}#foo`); - expect(pageloadTraceContext).toMatchObject({ - op: 'pageload', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - span_id: expect.stringMatching(/^[\da-f]{16}$/), - }); - expect(pageloadTraceContext).not.toHaveProperty('parent_span_id'); - - expect(pageloadTraceHeaders).toEqual({ - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: pageloadTraceContext?.trace_id, - sample_rand: expect.any(String), - }); + const navigationSpan = await navigationSpanPromise; - expect(navigationTraceContext).toMatchObject({ - op: 'navigation', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - span_id: expect.stringMatching(/^[\da-f]{16}$/), - }); - expect(navigationTraceContext).not.toHaveProperty('parent_span_id'); - - expect(navigationTraceHeaders).toEqual({ - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: navigationTraceContext?.trace_id, - sample_rand: expect.any(String), - }); + expect(getSpanOp(pageloadSpan)).toEqual('pageload'); + expect(pageloadSpan.trace_id).toMatch(/^[\da-f]{32}$/); + expect(pageloadSpan.span_id).toMatch(/^[\da-f]{16}$/); + expect(pageloadSpan.parent_span_id).toBeUndefined(); - expect(pageloadTraceContext?.span_id).not.toEqual(navigationTraceContext?.span_id); - }, -); + expect(getSpanOp(navigationSpan)).toEqual('navigation'); + expect(navigationSpan.trace_id).toMatch(/^[\da-f]{32}$/); + expect(navigationSpan.span_id).toMatch(/^[\da-f]{16}$/); + expect(navigationSpan.parent_span_id).toBeUndefined(); + + expect(pageloadSpan.span_id).not.toEqual(navigationSpan.span_id); + expect(pageloadSpan.trace_id).not.toEqual(navigationSpan.trace_id); +}); sentryTest('error after pageload has pageload traceId', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); + + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); - const [pageloadEvent, pageloadTraceHeader] = await getFirstSentryEnvelopeRequest( - page, - url, - eventAndTraceHeaderRequestParser, - ); - const pageloadTraceContext = pageloadEvent.contexts?.trace; - - expect(pageloadEvent.type).toEqual('transaction'); - expect(pageloadTraceContext).toMatchObject({ - op: 'pageload', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - span_id: expect.stringMatching(/^[\da-f]{16}$/), - }); - expect(pageloadTraceContext).not.toHaveProperty('parent_span_id'); + const pageloadSpan = await pageloadSpanPromise; + const pageloadTraceId = pageloadSpan.trace_id; - expect(pageloadTraceHeader).toEqual({ - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: pageloadTraceContext?.trace_id, - sample_rand: expect.any(String), - }); + expect(getSpanOp(pageloadSpan)).toEqual('pageload'); + expect(pageloadTraceId).toMatch(/^[\da-f]{32}$/); + expect(pageloadSpan.span_id).toMatch(/^[\da-f]{16}$/); + expect(pageloadSpan.parent_span_id).toBeUndefined(); - const errorEventPromise = getFirstSentryEnvelopeRequest( - page, - undefined, - eventAndTraceHeaderRequestParser, - ); + const errorEventPromise = waitForErrorRequest(page).then(eventAndTraceHeaderRequestParser); await page.locator('#errorBtn').click(); const [errorEvent, errorTraceHeader] = await errorEventPromise; @@ -115,7 +63,7 @@ sentryTest('error after pageload has pageload traceId', async ({ getLocalTestUrl expect(errorEvent.type).toEqual(undefined); expect(errorTraceContext).toEqual({ - trace_id: pageloadTraceContext?.trace_id, + trace_id: pageloadTraceId, span_id: expect.stringMatching(/^[\da-f]{16}$/), }); @@ -124,57 +72,35 @@ sentryTest('error after pageload has pageload traceId', async ({ getLocalTestUrl public_key: 'public', sample_rate: '1', sampled: 'true', - trace_id: pageloadTraceContext?.trace_id, + trace_id: pageloadTraceId, sample_rand: expect.any(String), }); }); sentryTest('error during pageload has pageload traceId', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - const envelopeRequestsPromise = getMultipleSentryEnvelopeRequests( - page, - 2, - undefined, - eventAndTraceHeaderRequestParser, - ); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + const errorEventPromise = waitForErrorRequest(page).then(eventAndTraceHeaderRequestParser); + await page.goto(url); await page.locator('#errorBtn').click(); - const envelopes = await envelopeRequestsPromise; - - const [pageloadEvent, pageloadTraceHeader] = envelopes.find( - eventAndHeader => eventAndHeader[0].type === 'transaction', - )!; - const [errorEvent, errorTraceHeader] = envelopes.find(eventAndHeader => !eventAndHeader[0].type)!; - - const pageloadTraceContext = pageloadEvent?.contexts?.trace; + const [pageloadSpan, [errorEvent, errorTraceHeader]] = await Promise.all([pageloadSpanPromise, errorEventPromise]); - expect(pageloadEvent.type).toEqual('transaction'); - expect(pageloadTraceContext).toMatchObject({ - op: 'pageload', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - span_id: expect.stringMatching(/^[\da-f]{16}$/), - }); - expect(pageloadTraceContext).not.toHaveProperty('parent_span_id'); + const pageloadTraceId = pageloadSpan.trace_id; - expect(pageloadTraceHeader).toEqual({ - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: pageloadTraceContext?.trace_id, - sample_rand: expect.any(String), - }); + expect(getSpanOp(pageloadSpan)).toEqual('pageload'); + expect(pageloadTraceId).toMatch(/^[\da-f]{32}$/); + expect(pageloadSpan.span_id).toMatch(/^[\da-f]{16}$/); + expect(pageloadSpan.parent_span_id).toBeUndefined(); const errorTraceContext = errorEvent?.contexts?.trace; - expect(errorEvent.type).toEqual(undefined); + expect(errorTraceContext).toEqual({ - trace_id: pageloadTraceContext?.trace_id, + trace_id: pageloadTraceId, span_id: expect.stringMatching(/^[\da-f]{16}$/), }); @@ -183,7 +109,7 @@ sentryTest('error during pageload has pageload traceId', async ({ getLocalTestUr public_key: 'public', sample_rate: '1', sampled: 'true', - trace_id: pageloadTraceContext?.trace_id, + trace_id: pageloadTraceId, sample_rand: expect.any(String), }); }); @@ -191,9 +117,7 @@ sentryTest('error during pageload has pageload traceId', async ({ getLocalTestUr sentryTest( 'outgoing fetch request during pageload has pageload traceId in headers', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); @@ -205,42 +129,26 @@ sentryTest( }); }); - const pageloadEventPromise = getFirstSentryEnvelopeRequest( + const pageloadSpanEnvelopePromise = waitForStreamedSpanEnvelope( page, - undefined, - eventAndTraceHeaderRequestParser, + env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'pageload'), ); const requestPromise = page.waitForRequest('http://sentry-test-site.example/*'); await page.goto(url); await page.locator('#fetchBtn').click(); - const [[pageloadEvent, pageloadTraceHeader], request] = await Promise.all([pageloadEventPromise, requestPromise]); + const [pageloadSpanEnvelope, request] = await Promise.all([pageloadSpanEnvelopePromise, requestPromise]); - const pageloadTraceContext = pageloadEvent.contexts?.trace; - const pageloadTraceId = pageloadTraceContext?.trace_id; + const pageloadTraceId = pageloadSpanEnvelope[0].trace?.trace_id; + const sampleRand = pageloadSpanEnvelope[0].trace?.sample_rand; - expect(pageloadEvent.type).toEqual('transaction'); - expect(pageloadTraceContext).toMatchObject({ - op: 'pageload', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - span_id: expect.stringMatching(/^[\da-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, - sample_rand: expect.any(String), - }); + expect(pageloadTraceId).toMatch(/^[\da-f]{32}$/); 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']).toBe( - `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sampled=true,sentry-sample_rand=${pageloadTraceHeader?.sample_rand},sentry-sample_rate=1`, + `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sampled=true,sentry-sample_rand=${sampleRand},sentry-sample_rate=1`, ); }, ); @@ -248,9 +156,7 @@ sentryTest( sentryTest( 'outgoing XHR request during pageload has pageload traceId in headers', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); @@ -262,207 +168,46 @@ sentryTest( }); }); - const pageloadEventPromise = getFirstSentryEnvelopeRequest( + const pageloadSpanEnvelopePromise = waitForStreamedSpanEnvelope( page, - undefined, - eventAndTraceHeaderRequestParser, + env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'pageload'), ); const requestPromise = page.waitForRequest('http://sentry-test-site.example/*'); await page.goto(url); await page.locator('#xhrBtn').click(); - const [[pageloadEvent, pageloadTraceHeader], request] = await Promise.all([pageloadEventPromise, requestPromise]); + const [pageloadSpanEnvelope, request] = await Promise.all([pageloadSpanEnvelopePromise, requestPromise]); - const pageloadTraceContext = pageloadEvent.contexts?.trace; - const pageloadTraceId = pageloadTraceContext?.trace_id; + const pageloadTraceId = pageloadSpanEnvelope[0].trace?.trace_id; + const sampleRand = pageloadSpanEnvelope[0].trace?.sample_rand; - expect(pageloadEvent.type).toEqual('transaction'); - expect(pageloadTraceContext).toMatchObject({ - op: 'pageload', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - span_id: expect.stringMatching(/^[\da-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, - sample_rand: expect.any(String), - }); + expect(pageloadTraceId).toMatch(/^[\da-f]{32}$/); 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']).toBe( - `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sampled=true,sentry-sample_rand=${pageloadTraceHeader?.sample_rand},sentry-sample_rate=1`, + `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sampled=true,sentry-sample_rand=${sampleRand},sentry-sample_rate=1`, ); }, ); -// 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://sentry-test-site.example/**', 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://sentry-test-site.example/*'); -// 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://sentry-test-site.example/**', 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://sentry-test-site.example/**'); -// 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(); - } + sentryTest.skip(shouldSkipTracingTest() || shouldSkipFeedbackTest()); const url = await getLocalTestUrl({ testDir: __dirname, handleLazyLoadedFeedback: true }); - const pageloadEvent = await getFirstSentryEnvelopeRequest(page, url); - const pageloadTraceContext = pageloadEvent.contexts?.trace; + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + await page.goto(url); + const pageloadSpan = await pageloadSpanPromise; + const pageloadTraceId = pageloadSpan.trace_id; - expect(pageloadTraceContext).toMatchObject({ - op: 'pageload', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - span_id: expect.stringMatching(/^[\da-f]{16}$/), - }); - expect(pageloadTraceContext).not.toHaveProperty('parent_span_id'); + expect(getSpanOp(pageloadSpan)).toEqual('pageload'); + expect(pageloadTraceId).toMatch(/^[\da-f]{32}$/); + expect(pageloadSpan.span_id).toMatch(/^[\da-f]{16}$/); + expect(pageloadSpan.parent_span_id).toBeUndefined(); - const feedbackEventPromise = getFirstSentryEnvelopeRequest(page); + const feedbackEventsPromise = getMultipleSentryEnvelopeRequests(page, 1, { envelopeType: 'feedback' }); await page.getByText('Report a Bug').click(); expect(await page.locator(':visible:text-is("Report a Bug")').count()).toEqual(1); @@ -471,14 +216,14 @@ sentryTest('user feedback event after pageload has pageload traceId in headers', await page.locator('[name="message"]').fill('my example feedback'); await page.locator('[data-sentry-feedback] .btn--primary').click(); - const feedbackEvent = await feedbackEventPromise; + const [feedbackEvent] = await feedbackEventsPromise; expect(feedbackEvent.type).toEqual('feedback'); const feedbackTraceContext = feedbackEvent.contexts?.trace; expect(feedbackTraceContext).toMatchObject({ - trace_id: pageloadTraceContext?.trace_id, + trace_id: pageloadTraceId, span_id: expect.stringMatching(/^[\da-f]{16}$/), }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-multiple-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-multiple-spans/test.ts index cf17b26aee87..f9c3d5deca85 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-multiple-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-multiple-spans/test.ts @@ -1,12 +1,11 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import type { EventAndTraceHeader } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; import { - eventAndTraceHeaderRequestParser, - getFirstSentryEnvelopeRequest, - getMultipleSentryEnvelopeRequests, - shouldSkipTracingTest, -} from '../../../../utils/helpers'; + type StreamedSpanAndTraceHeader, + waitForStreamedSpanAndTraceHeader, + waitForStreamedSpanAndTraceHeaderOnUrl, +} from '../../../../utils/spanUtils'; sentryTest( 'every root span created within a single `startNewTrace` callback shares the one new trace', @@ -17,44 +16,37 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); - const [pageloadEvent] = await getFirstSentryEnvelopeRequest( - page, - url, - eventAndTraceHeaderRequestParser, - ); - const pageloadTraceId = pageloadEvent.contexts?.trace?.trace_id; + const [pageloadSpan] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url); + const pageloadTraceId = pageloadSpan?.trace_id; expect(pageloadTraceId).toMatch(/^[\da-f]{32}$/); - const transactionPromises = getMultipleSentryEnvelopeRequests( - page, - 3, - { envelopeType: 'transaction' }, - eventAndTraceHeaderRequestParser, + const spanPromises = Promise.all( + [1, 2, 3].map(index => waitForStreamedSpanAndTraceHeader(page, span => span.name === `new-trace-span-${index}`)), ); await page.locator('#newTrace').click(); - const transactions = await transactionPromises; + const spans = await spanPromises; - const byName = (name: string): EventAndTraceHeader => transactions.find(([event]) => event.transaction === name)!; + const byName = (name: string): StreamedSpanAndTraceHeader => spans.find(([span]) => span.name === name)!; const [span1] = byName('new-trace-span-1'); const [span2] = byName('new-trace-span-2'); const [span3] = byName('new-trace-span-3'); - const newTraceId = span1.contexts?.trace?.trace_id; + const newTraceId = span1.trace_id; expect(newTraceId).toMatch(/^[\da-f]{32}$/); // All three root spans share the one new trace id ... - expect(span2.contexts?.trace?.trace_id).toBe(newTraceId); - expect(span3.contexts?.trace?.trace_id).toBe(newTraceId); + expect(span2.trace_id).toBe(newTraceId); + expect(span3.trace_id).toBe(newTraceId); // ... which is a fresh trace, not the pageload trace. expect(newTraceId).not.toBe(pageloadTraceId); // They are independent root spans, not parented to one another. - expect(span1.contexts?.trace).not.toHaveProperty('parent_span_id'); - expect(span2.contexts?.trace).not.toHaveProperty('parent_span_id'); - expect(span3.contexts?.trace).not.toHaveProperty('parent_span_id'); + expect(span1).not.toHaveProperty('parent_span_id'); + expect(span2).not.toHaveProperty('parent_span_id'); + expect(span3).not.toHaveProperty('parent_span_id'); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-static/init.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-static/init.js new file mode 100644 index 000000000000..8718d9d354eb --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-static/init.js @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/browser'; +// Import this separately so that generatePlugin can handle it for CDN scenarios +import { feedbackIntegration } from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration(), feedbackIntegration()], + tracePropagationTargets: ['http://sentry-test-site.example'], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-streamed/subject.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-static/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-streamed/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-static/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-streamed/template.html b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-static/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-streamed/template.html rename to dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-static/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-static/test.ts new file mode 100644 index 000000000000..40cc8a64d75f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-static/test.ts @@ -0,0 +1,109 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import type { EventAndTraceHeader } from '../../../../utils/helpers'; +import { + eventAndTraceHeaderRequestParser, + getFirstSentryEnvelopeRequest, + getMultipleSentryEnvelopeRequests, + shouldSkipTracingTest, +} from '../../../../utils/helpers'; + +sentryTest( + 'creates a new trace if `startNewTrace` is called and leaves old trace valid outside the callback', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.route('http://sentry-test-site.example/**', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + const [pageloadEvent, pageloadTraceHeaders] = await getFirstSentryEnvelopeRequest( + page, + url, + eventAndTraceHeaderRequestParser, + ); + + const pageloadTraceContext = pageloadEvent.contexts?.trace; + + expect(pageloadEvent.type).toEqual('transaction'); + + expect(pageloadTraceContext).toMatchObject({ + op: 'pageload', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + span_id: expect.stringMatching(/^[\da-f]{16}$/), + }); + expect(pageloadTraceContext).not.toHaveProperty('parent_span_id'); + + expect(pageloadTraceHeaders).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: pageloadTraceContext?.trace_id, + sample_rand: expect.any(String), + }); + + const transactionPromises = getMultipleSentryEnvelopeRequests( + page, + 2, + { envelopeType: 'transaction' }, + eventAndTraceHeaderRequestParser, + ); + + await page.locator('#newTrace').click(); + await page.locator('#oldTrace').click(); + + const [txnEvent1, txnEvent2] = await transactionPromises; + + const [newTraceTransactionEvent, newTraceTransactionTraceHeaders] = + txnEvent1[0].transaction === 'new-trace' ? txnEvent1 : txnEvent2; + const [oldTraceTransactionEvent, oldTraceTransactionTraceHeaders] = + txnEvent1[0].transaction === 'old-trace' ? txnEvent1 : txnEvent2; + + const newTraceTransactionTraceContext = newTraceTransactionEvent.contexts?.trace; + expect(newTraceTransactionTraceContext).toMatchObject({ + op: 'ui.interaction.click', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + span_id: expect.stringMatching(/^[\da-f]{16}$/), + }); + + expect(newTraceTransactionTraceHeaders).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: newTraceTransactionTraceContext?.trace_id, + transaction: 'new-trace', + sample_rand: expect.any(String), + }); + + const oldTraceTransactionEventTraceContext = oldTraceTransactionEvent.contexts?.trace; + expect(oldTraceTransactionEventTraceContext).toMatchObject({ + op: 'ui.interaction.click', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + span_id: expect.stringMatching(/^[\da-f]{16}$/), + }); + + expect(oldTraceTransactionTraceHeaders).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: oldTraceTransactionTraceHeaders?.trace_id, + sample_rand: expect.any(String), + // transaction: 'old-trace', <-- this is not in the DSC because the DSC is continued from the pageload transaction + // which does not have a `transaction` field because its source is URL. + }); + + expect(oldTraceTransactionEventTraceContext?.trace_id).toEqual(pageloadTraceContext?.trace_id); + expect(newTraceTransactionTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-streamed/test.ts deleted file mode 100644 index 07ac4b0c8cd3..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-streamed/test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; - -sentryTest( - 'creates a new trace if `startNewTrace` is called and leaves old trace valid outside the callback', - async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - await page.route('http://sentry-test-site.example/**', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({}), - }); - }); - - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - await page.goto(url); - const pageloadSpan = await pageloadSpanPromise; - - const newTraceSpanPromise = waitForStreamedSpan(page, span => span.name === 'new-trace'); - const oldTraceSpanPromise = waitForStreamedSpan(page, span => span.name === 'old-trace'); - - await page.locator('#newTrace').click(); - await page.locator('#oldTrace').click(); - - const [newTraceSpan, oldTraceSpan] = await Promise.all([newTraceSpanPromise, oldTraceSpanPromise]); - - expect(getSpanOp(newTraceSpan)).toEqual('ui.interaction.click'); - expect(newTraceSpan.trace_id).toMatch(/^[\da-f]{32}$/); - expect(newTraceSpan.span_id).toMatch(/^[\da-f]{16}$/); - - expect(getSpanOp(oldTraceSpan)).toEqual('ui.interaction.click'); - expect(oldTraceSpan.trace_id).toMatch(/^[\da-f]{32}$/); - expect(oldTraceSpan.span_id).toMatch(/^[\da-f]{16}$/); - - expect(oldTraceSpan.trace_id).toEqual(pageloadSpan.trace_id); - expect(newTraceSpan.trace_id).not.toEqual(pageloadSpan.trace_id); - }, -); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/init.js similarity index 71% rename from dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/init.js index 187e07624fdf..b2280b70e307 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/init.js @@ -4,7 +4,7 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()], + integrations: [Sentry.browserTracingIntegration()], tracePropagationTargets: ['http://sentry-test-site.example'], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/test.ts index 40cc8a64d75f..07ac4b0c8cd3 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace/test.ts @@ -1,19 +1,12 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import type { EventAndTraceHeader } from '../../../../utils/helpers'; -import { - eventAndTraceHeaderRequestParser, - getFirstSentryEnvelopeRequest, - getMultipleSentryEnvelopeRequests, - shouldSkipTracingTest, -} from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest( 'creates a new trace if `startNewTrace` is called and leaves old trace valid outside the callback', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); @@ -25,85 +18,27 @@ sentryTest( }); }); - const [pageloadEvent, pageloadTraceHeaders] = await getFirstSentryEnvelopeRequest( - page, - url, - eventAndTraceHeaderRequestParser, - ); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + await page.goto(url); + const pageloadSpan = await pageloadSpanPromise; - const pageloadTraceContext = pageloadEvent.contexts?.trace; - - expect(pageloadEvent.type).toEqual('transaction'); - - expect(pageloadTraceContext).toMatchObject({ - op: 'pageload', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - span_id: expect.stringMatching(/^[\da-f]{16}$/), - }); - expect(pageloadTraceContext).not.toHaveProperty('parent_span_id'); - - expect(pageloadTraceHeaders).toEqual({ - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: pageloadTraceContext?.trace_id, - sample_rand: expect.any(String), - }); - - const transactionPromises = getMultipleSentryEnvelopeRequests( - page, - 2, - { envelopeType: 'transaction' }, - eventAndTraceHeaderRequestParser, - ); + const newTraceSpanPromise = waitForStreamedSpan(page, span => span.name === 'new-trace'); + const oldTraceSpanPromise = waitForStreamedSpan(page, span => span.name === 'old-trace'); await page.locator('#newTrace').click(); await page.locator('#oldTrace').click(); - const [txnEvent1, txnEvent2] = await transactionPromises; + const [newTraceSpan, oldTraceSpan] = await Promise.all([newTraceSpanPromise, oldTraceSpanPromise]); - const [newTraceTransactionEvent, newTraceTransactionTraceHeaders] = - txnEvent1[0].transaction === 'new-trace' ? txnEvent1 : txnEvent2; - const [oldTraceTransactionEvent, oldTraceTransactionTraceHeaders] = - txnEvent1[0].transaction === 'old-trace' ? txnEvent1 : txnEvent2; + expect(getSpanOp(newTraceSpan)).toEqual('ui.interaction.click'); + expect(newTraceSpan.trace_id).toMatch(/^[\da-f]{32}$/); + expect(newTraceSpan.span_id).toMatch(/^[\da-f]{16}$/); - const newTraceTransactionTraceContext = newTraceTransactionEvent.contexts?.trace; - expect(newTraceTransactionTraceContext).toMatchObject({ - op: 'ui.interaction.click', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - span_id: expect.stringMatching(/^[\da-f]{16}$/), - }); - - expect(newTraceTransactionTraceHeaders).toEqual({ - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: newTraceTransactionTraceContext?.trace_id, - transaction: 'new-trace', - sample_rand: expect.any(String), - }); - - const oldTraceTransactionEventTraceContext = oldTraceTransactionEvent.contexts?.trace; - expect(oldTraceTransactionEventTraceContext).toMatchObject({ - op: 'ui.interaction.click', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - span_id: expect.stringMatching(/^[\da-f]{16}$/), - }); - - expect(oldTraceTransactionTraceHeaders).toEqual({ - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: oldTraceTransactionTraceHeaders?.trace_id, - sample_rand: expect.any(String), - // transaction: 'old-trace', <-- this is not in the DSC because the DSC is continued from the pageload transaction - // which does not have a `transaction` field because its source is URL. - }); + expect(getSpanOp(oldTraceSpan)).toEqual('ui.interaction.click'); + expect(oldTraceSpan.trace_id).toMatch(/^[\da-f]{32}$/); + expect(oldTraceSpan.span_id).toMatch(/^[\da-f]{16}$/); - expect(oldTraceTransactionEventTraceContext?.trace_id).toEqual(pageloadTraceContext?.trace_id); - expect(newTraceTransactionTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + expect(oldTraceSpan.trace_id).toEqual(pageloadSpan.trace_id); + expect(newTraceSpan.trace_id).not.toEqual(pageloadSpan.trace_id); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling/init.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling/init.js index 9e1a8762f0b1..d32f77f4cb6b 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling/init.js @@ -15,7 +15,6 @@ crypto.randomUUID = function randomUUID() { }; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling/test.ts index dc3d75f54799..17f822e26a2a 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling/test.ts @@ -1,12 +1,9 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; +import { SENTRY_OP } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import type { EventAndTraceHeader } from '../../../../utils/helpers'; -import { - eventAndTraceHeaderRequestParser, - getFirstSentryEnvelopeRequest, - shouldSkipTracingTest, - waitForTransactionRequest, -} from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { waitForStreamedSpanAndTraceHeader, waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../../utils/spanUtils'; sentryTest( 'new trace started with `startNewTrace` is sampled according to the `tracesSampler`', @@ -25,65 +22,56 @@ sentryTest( }); }); - const [pageloadEvent, pageloadTraceHeaders] = await getFirstSentryEnvelopeRequest( - page, - url, - eventAndTraceHeaderRequestParser, - ); + const [pageloadSpan, pageloadTraceHeaders] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url); - const pageloadTraceContext = pageloadEvent.contexts?.trace; + expect(pageloadSpan.is_segment).toBe(true); - expect(pageloadEvent.type).toEqual('transaction'); - - expect(pageloadTraceContext).toMatchObject({ - op: 'pageload', + expect(pageloadSpan).toMatchObject({ + is_segment: true, + attributes: expect.objectContaining({ + [SENTRY_OP]: { type: 'string', value: 'pageload' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'double', value: 0.5 }, + }), trace_id: expect.stringMatching(/^[\da-f]{32}$/), span_id: expect.stringMatching(/^[\da-f]{16}$/), - data: { - 'sentry.sample_rate': 0.5, - }, }); - expect(pageloadTraceContext).not.toHaveProperty('parent_span_id'); + expect(pageloadSpan).not.toHaveProperty('parent_span_id'); expect(pageloadTraceHeaders).toEqual({ environment: 'production', public_key: 'public', sample_rate: '0.5', sampled: 'true', - trace_id: pageloadTraceContext?.trace_id, + trace_id: pageloadSpan?.trace_id, sample_rand: '0.45', }); - const transactionPromise = waitForTransactionRequest(page, event => { - return event.transaction === 'new-trace'; - }); + const spanPromise = waitForStreamedSpanAndTraceHeader(page, span => span.name === 'new-trace'); await page.locator('#newTrace').click(); - const [newTraceTransactionEvent, newTraceTransactionTraceHeaders] = eventAndTraceHeaderRequestParser( - await transactionPromise, - ); + const [newTraceSpan, newTraceHeaders] = await spanPromise; - const newTraceTransactionTraceContext = newTraceTransactionEvent.contexts?.trace; - expect(newTraceTransactionTraceContext).toMatchObject({ - op: 'ui.interaction.click', + expect(newTraceSpan).toMatchObject({ + is_segment: true, + attributes: expect.objectContaining({ + [SENTRY_OP]: { type: 'string', value: 'ui.interaction.click' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'double', value: 0.9 }, + }), trace_id: expect.stringMatching(/^[\da-f]{32}$/), span_id: expect.stringMatching(/^[\da-f]{16}$/), - data: { - 'sentry.sample_rate': 0.9, - }, }); - expect(newTraceTransactionTraceHeaders).toEqual({ + expect(newTraceHeaders).toEqual({ environment: 'production', public_key: 'public', sample_rate: '0.9', sampled: 'true', - trace_id: newTraceTransactionTraceContext?.trace_id, + trace_id: newTraceSpan?.trace_id, transaction: 'new-trace', sample_rand: '0.85', }); - expect(newTraceTransactionTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + expect(newTraceSpan?.trace_id).not.toEqual(pageloadSpan?.trace_id); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance-headers/init.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance-headers/init.js index f6aa87028094..94222159056c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance-headers/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance-headers/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', // in browser TwP means not setting tracesSampleRate but adding browserTracingIntegration, dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance-headers/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance-headers/test.ts index 3d0262b717d9..b0737b5c9fe7 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance-headers/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance-headers/test.ts @@ -1,10 +1,9 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import type { EventAndTraceHeader } from '../../../../utils/helpers'; import { eventAndTraceHeaderRequestParser, - getFirstSentryEnvelopeRequest, shouldSkipTracingTest, + waitForErrorRequest, } from '../../../../utils/helpers'; const META_TAG_TRACE_ID = '12345678901234567890123456789012'; @@ -25,11 +24,7 @@ sentryTest('error on initial page has traceId from server timing headers', async }); await page.goto(url); - const errorEventPromise = getFirstSentryEnvelopeRequest( - page, - undefined, - eventAndTraceHeaderRequestParser, - ); + const errorEventPromise = waitForErrorRequest(page).then(eventAndTraceHeaderRequestParser); await page.locator('#errorBtn').click(); const [errorEvent, errorTraceHeader] = await errorEventPromise; diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance-propagateTraceparent/init.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance-propagateTraceparent/init.js index 59e00870be1d..b5b7d0354f77 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance-propagateTraceparent/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance-propagateTraceparent/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', // in browser TwP means not setting tracesSampleRate but adding browserTracingIntegration, dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance/init.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance/init.js index f6aa87028094..94222159056c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', // in browser TwP means not setting tracesSampleRate but adding browserTracingIntegration, dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance/test.ts index 51cc61e0de0c..126f1eb99bff 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance/test.ts @@ -5,6 +5,7 @@ import { eventAndTraceHeaderRequestParser, getFirstSentryEnvelopeRequest, shouldSkipTracingTest, + waitForErrorRequest, } from '../../../../utils/helpers'; const META_TAG_TRACE_ID = '12345678901234567890123456789012'; @@ -20,11 +21,7 @@ sentryTest('error on initial page has traceId from meta tag', async ({ getLocalT const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const errorEventPromise = getFirstSentryEnvelopeRequest( - page, - undefined, - eventAndTraceHeaderRequestParser, - ); + const errorEventPromise = waitForErrorRequest(page).then(eventAndTraceHeaderRequestParser); await page.locator('#errorBtn').click(); const [errorEvent, errorTraceHeader] = await errorEventPromise; @@ -53,11 +50,7 @@ sentryTest('error has new traceId after navigation', async ({ getLocalTestUrl, p const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const errorEventPromise = getFirstSentryEnvelopeRequest( - page, - undefined, - eventAndTraceHeaderRequestParser, - ); + const errorEventPromise = waitForErrorRequest(page).then(eventAndTraceHeaderRequestParser); await page.locator('#errorBtn').click(); const [errorEvent, errorTraceHeader] = await errorEventPromise; diff --git a/dev-packages/browser-integration-tests/suites/tracing/tracesSampler/op-attribute/init.js b/dev-packages/browser-integration-tests/suites/tracing/tracesSampler/op-attribute/init.js index 1c46615de295..9eeaf599e7c2 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/tracesSampler/op-attribute/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/tracesSampler/op-attribute/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampler: ({ attributes }) => { return attributes?.['sentry.op'] === 'custom.op' ? 1 : 0; diff --git a/dev-packages/browser-integration-tests/suites/tracing/tracesSampler/op-attribute/test.ts b/dev-packages/browser-integration-tests/suites/tracing/tracesSampler/op-attribute/test.ts index 9d5bc57bc5c2..4828dcd58af7 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/tracesSampler/op-attribute/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/tracesSampler/op-attribute/test.ts @@ -1,10 +1,7 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { - envelopeRequestParser, - shouldSkipTracingTest, - waitForTransactionRequestOnUrl, -} from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../../utils/spanUtils'; sentryTest('tracesSampler can sample based on the `sentry.op` attribute', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { @@ -15,10 +12,9 @@ sentryTest('tracesSampler can sample based on the `sentry.op` attribute', async // The sampler drops `other.op` and keeps `custom.op`, so the only transaction // that arrives is the one whose op the sampler saw in the attributes. - const req = await waitForTransactionRequestOnUrl(page, url); - const transactionEvent = envelopeRequestParser(req); + const [span] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url); - expect(transactionEvent.type).toBe('transaction'); - expect(transactionEvent.transaction).toBe('span-with-sampled-op'); - expect(transactionEvent.contexts?.trace?.op).toBe('custom.op'); + expect(span.is_segment).toBe(true); + expect(span.name).toBe('span-with-sampled-op'); + expect(getSpanOp(span)).toBe('custom.op'); }); diff --git a/dev-packages/browser-integration-tests/utils/helpers.ts b/dev-packages/browser-integration-tests/utils/helpers.ts index 97c40002e305..05b5aebcf5f4 100644 --- a/dev-packages/browser-integration-tests/utils/helpers.ts +++ b/dev-packages/browser-integration-tests/utils/helpers.ts @@ -215,12 +215,12 @@ export function waitForErrorRequest(page: Page, callback?: (event: SentryEvent) } try { - const event = envelopeRequestParser(req); - - if (event.type) { + if (getEnvelopeType(req) !== 'event') { return false; } + const event = envelopeRequestParser(req); + if (callback) { return callback(event); } diff --git a/dev-packages/browser-integration-tests/utils/spanUtils.ts b/dev-packages/browser-integration-tests/utils/spanUtils.ts index 532f2ce5c526..4a10f9069e8c 100644 --- a/dev-packages/browser-integration-tests/utils/spanUtils.ts +++ b/dev-packages/browser-integration-tests/utils/spanUtils.ts @@ -1,5 +1,5 @@ import type { Page } from '@playwright/test'; -import type { SerializedStreamedSpan, StreamedSpanEnvelope } from '@sentry/core'; +import type { DynamicSamplingContext, SerializedStreamedSpan, StreamedSpanEnvelope } from '@sentry/core'; import { properFullEnvelopeParser } from './helpers'; /** @@ -131,3 +131,42 @@ export function getSpanOp(span: SerializedStreamedSpan): string | undefined { export function getSpansFromEnvelope(envelope: StreamedSpanEnvelope): SerializedStreamedSpan[] { return envelope[1][0][1].items; } + +export type StreamedSpanAndTraceHeader = [SerializedStreamedSpan, Partial | undefined]; + +export async function waitForStreamedSpanAndTraceHeader( + page: Page, + callback: (span: SerializedStreamedSpan) => boolean = span => span.is_segment, +): Promise { + const envelope = await waitForStreamedSpanEnvelope(page, envelope => envelope[1][0][1].items.some(callback)); + const span = envelope[1][0][1].items.find(callback)!; + return [span, envelope[0].trace]; +} + +export async function waitForStreamedSpanAndTraceHeaderOnUrl( + page: Page, + url: string, + callback?: (span: SerializedStreamedSpan) => boolean, +): Promise { + const [spanAndTraceHeader] = await Promise.all([waitForStreamedSpanAndTraceHeader(page, callback), page.goto(url)]); + return spanAndTraceHeader; +} + +export function collectStreamedSpans(page: Page): SerializedStreamedSpan[] { + const spans: SerializedStreamedSpan[] = []; + page.on('request', request => { + if (!request.postData()) { + return; + } + try { + const envelope = properFullEnvelopeParser(request); + const header = envelope[1][0][0]; + if (header.type === 'span' && header.content_type === 'application/vnd.sentry.items.span.v2+json') { + spans.push(...envelope[1][0][1].items); + } + } catch { + // Other requests may contain bodies which are not envelopes. + } + }); + return spans; +} From 8a92d0d2d4d8ff72c31756d2bc77980fef4bf439 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Fri, 9 Oct 2026 13:02:34 +0200 Subject: [PATCH 27/84] test(browser): port browser tracing suites to span streaming (#24888) Exercise browserTracingIntegration with default span streaming, including low-cardinality names and children sent across envelopes. Retain the existing transaction counterparts as explicitly pinned `*-static` suites to preserve compatibility coverage for navigation, pageloads, linked traces, timing, and sampling. Stacked on #24881 for the shared span collector and dynamic sampling context helpers. Fixes #24142 --------- Co-authored-by: GPT-6 --- .../async-spans/init.js | 1 - .../async-spans/test.ts | 6 +- .../backgroundtab-custom/init.js | 1 - .../backgroundtab-custom/test.ts | 7 +- .../init.js | 3 +- .../subject.js | 0 .../template.html | 0 .../backgroundtab-pageload-static/test.ts | 23 ++ .../backgroundtab-pageload-streamed/test.ts | 18 - .../init.js | 5 +- .../backgroundtab-pageload/test.ts | 22 +- .../browserTracingIntegration/error/test.ts | 54 +-- .../init.js | 4 +- .../subject.js | 0 .../http-timings-static/test.ts | 75 ++++ .../http-timings-streamed/test.ts | 64 --- .../http-timings/init.js | 1 - .../http-timings/test.ts | 164 +++++--- .../tracing/browserTracingIntegration/init.js | 1 - .../consistent-sampling/default/init.js | 2 +- .../consistent-sampling/default/subject.js | 0 .../consistent-sampling/default/template.html | 0 .../consistent-sampling/default/test.ts | 149 +++++++ .../consistent-sampling/meta-negative/init.js | 3 +- .../meta-negative/subject.js | 0 .../meta-negative/template.html | 0 .../consistent-sampling/meta-negative/test.ts | 43 +- .../meta-precedence/init.js | 2 +- .../meta-precedence/page-1.html | 0 .../meta-precedence/page-2.html | 0 .../meta-precedence/subject.js | 0 .../meta-precedence/template.html | 0 .../meta-precedence/test.ts | 51 +-- .../consistent-sampling/meta/init.js | 2 +- .../consistent-sampling/meta/subject.js | 0 .../consistent-sampling/meta/template.html | 0 .../consistent-sampling/meta/test.ts | 176 +++++++++ .../tracesSampler-precedence/init.js | 2 +- .../tracesSampler-precedence/subject.js | 0 .../tracesSampler-precedence/template.html | 0 .../tracesSampler-precedence/test.ts | 93 ++--- .../custom-trace}/init.js | 3 +- .../custom-trace/subject.js | 0 .../custom-trace/template.html | 0 .../linked-traces-static/custom-trace/test.ts | 62 +++ .../default}/init.js | 3 +- .../linked-traces-static/default/test.ts | 99 +++++ .../interaction-spans/init.js | 3 +- .../interaction-spans/template.html | 0 .../interaction-spans/test.ts | 89 +++++ .../linked-traces-static/meta/init.js | 11 + .../meta/template.html | 0 .../linked-traces-static/meta/test.ts | 54 +++ .../negatively-sampled/init.js | 3 +- .../negatively-sampled/test.ts | 42 ++ .../session-storage/init.js | 10 + .../session-storage/test.ts | 39 ++ .../consistent-sampling/default/test.ts | 154 -------- .../consistent-sampling/meta/test.ts | 171 -------- .../custom-trace/test.ts | 63 --- .../linked-traces-streamed/default/test.ts | 95 ----- .../interaction-spans/test.ts | 79 ---- .../linked-traces-streamed/meta/test.ts | 50 --- .../negatively-sampled/test.ts | 44 --- .../session-storage/test.ts | 42 -- .../consistent-sampling/default/init.js | 1 - .../consistent-sampling/default/test.ts | 138 +++---- .../consistent-sampling/meta-negative/init.js | 1 - .../consistent-sampling/meta-negative/test.ts | 38 +- .../meta-precedence/init.js | 1 - .../meta-precedence/test.ts | 51 ++- .../consistent-sampling/meta/init.js | 1 - .../consistent-sampling/meta/test.ts | 144 ++++--- .../tracesSampler-precedence/init.js | 1 - .../tracesSampler-precedence/test.ts | 101 ++--- .../linked-traces/custom-trace/test.ts | 60 +-- .../linked-traces/default/test.ts | 93 +++-- .../linked-traces/init.js | 1 - .../linked-traces/interaction-spans/init.js | 1 - .../linked-traces/interaction-spans/test.ts | 73 ++-- .../linked-traces/meta/test.ts | 45 +-- .../linked-traces/negatively-sampled/init.js | 1 - .../linked-traces/negatively-sampled/test.ts | 33 +- .../linked-traces/session-storage/init.js | 1 - .../linked-traces/session-storage/test.ts | 44 ++- .../init.js | 2 +- .../subject.js | 0 .../template.html | 0 .../test.ts | 34 ++ .../test.ts | 25 -- .../init.js | 1 - .../test.ts | 26 +- .../assets/script.js | 0 .../init.js | 2 +- .../template.html | 0 .../test.ts | 26 ++ .../test.ts | 28 -- .../long-animation-frame-disabled/init.js | 1 - .../long-animation-frame-disabled/test.ts | 22 +- .../assets/script.js | 0 .../init.js | 2 +- .../template.html | 0 .../test.ts | 116 ++++++ .../test.ts | 108 ----- .../long-animation-frame-enabled/init.js | 1 - .../long-animation-frame-enabled/test.ts | 141 +++---- .../long-animation-frame-non-chromium/init.js | 1 - .../long-animation-frame-non-chromium/test.ts | 10 +- .../assets/script.js | 0 .../init.js | 2 +- .../template.html | 0 .../test.ts | 118 ++++++ .../test.ts | 110 ------ .../init.js | 1 - .../test.ts | 141 +++---- .../init.js | 2 +- .../subject.js | 0 .../template.html | 0 .../test.ts | 19 +- .../long-tasks-before-navigation/init.js | 1 - .../long-tasks-before-navigation/test.ts | 25 +- .../assets/script.js | 0 .../init.js | 2 +- .../template.html | 0 .../long-tasks-disabled-static/test.ts | 21 + .../long-tasks-disabled-streamed/test.ts | 23 -- .../long-tasks-disabled/init.js | 1 - .../long-tasks-disabled/test.ts | 22 +- .../assets/script.js | 0 .../init.js | 2 +- .../template.html | 0 .../long-tasks-enabled-static/test.ts | 36 ++ .../long-tasks-enabled-streamed/test.ts | 42 -- .../long-tasks-enabled/init.js | 1 - .../long-tasks-enabled/test.ts | 36 +- .../long-tasks-no-animation-frame/init.js | 1 - .../long-tasks-no-animation-frame/test.ts | 27 +- .../browserTracingIntegration/meta/init.js | 1 - .../browserTracingIntegration/meta/test.ts | 116 +++--- .../navigation-aborting-pageload/init.js | 1 - .../navigation-aborting-pageload/test.ts | 112 +++--- .../multiple-redirects/init.js | 1 - .../multiple-redirects/test.ts | 81 ++-- .../navigation-navigation-click/init.js | 1 - .../navigation-navigation-click/test.ts | 40 +- .../navigation-navigation-keypress/init.js | 1 - .../navigation-navigation-keypress/test.ts | 43 +- .../navigation-redirect/init.js | 1 - .../navigation-redirect/test.ts | 31 +- .../navigation-redirect/opt-out/init.js | 1 - .../navigation-redirect/opt-out/test.ts | 21 +- .../pageload-navigation-click/init.js | 1 - .../pageload-navigation-click/test.ts | 23 +- .../pageload-navigation-keypress/init.js | 1 - .../pageload-navigation-keypress/test.ts | 21 +- .../pageload-navigation-time/init.js | 1 - .../pageload-navigation-time/test.ts | 21 +- .../pageload-redirect/init.js | 1 - .../pageload-redirect/test.ts | 66 ++-- .../navigation-static/init.js | 10 + .../navigation-static/test.ts | 156 ++++++++ .../navigation-streamed/test.ts | 274 ------------- .../navigation/init.js | 9 + .../navigation/test.ts | 370 +++++++++++++----- .../on-request-span-end/init.js | 1 - .../on-request-span-end/test.ts | 31 +- .../on-request-span-start/init.js | 1 - .../on-request-span-start/test.ts | 27 +- .../init.js | 4 +- .../pageload-static/test.ts | 43 ++ .../pageload-streamed/test.ts | 192 --------- .../pageload-update-txn-name/init.js | 1 - .../pageload-update-txn-name/test.ts | 39 +- .../pageload-updateSpanName-static/init.js | 11 + .../pageload-updateSpanName-static/subject.js | 4 + .../pageload-updateSpanName-static/test.ts | 38 ++ .../pageload-updateSpanName/init.js | 1 - .../pageload-updateSpanName/test.ts | 39 +- .../pageload/init.js | 1 - .../pageload/test.ts | 223 +++++++++-- .../pageloadDelayed/init.js | 1 - .../pageloadDelayed/test.ts | 23 +- .../pageloadWithChildSpanTimeout/init.js | 1 - .../pageloadWithChildSpanTimeout/test.ts | 37 +- .../default/init.js | 3 +- .../default/test.ts | 28 +- .../finalTimeout/init.js | 6 +- .../finalTimeout/test.ts | 43 ++ .../navigation/init.js | 6 +- .../navigation/test.ts | 41 ++ .../finalTimeout/test.ts | 36 -- .../navigation/test.ts | 34 -- .../reportPageLoaded/default/init.js | 1 - .../reportPageLoaded/default/test.ts | 36 +- .../reportPageLoaded/finalTimeout/init.js | 1 - .../reportPageLoaded/finalTimeout/test.ts | 35 +- .../reportPageLoaded/navigation/init.js | 1 - .../reportPageLoaded/navigation/test.ts | 35 +- .../resource-spans-ignored/init.js | 1 - .../resource-spans-ignored/test.ts | 10 +- .../init.js | 2 +- .../subject.js | 0 .../template.html | 0 .../test.ts | 54 +++ .../test.ts | 65 --- .../spotlight-interaction-filter/init.js | 1 - .../spotlight-interaction-filter/test.ts | 103 +++-- .../customTargets/init.js | 1 - .../defaultTargetsMatch/init.js | 1 - .../defaultTargetsNoMatch/init.js | 1 - .../twp-errors-meta/init.js | 1 - .../twp-errors/init.js | 1 - 212 files changed, 3526 insertions(+), 3240 deletions(-) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{navigation-streamed => backgroundtab-pageload-static}/init.js (64%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{backgroundtab-pageload-streamed => backgroundtab-pageload-static}/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{backgroundtab-pageload-streamed => backgroundtab-pageload-static}/template.html (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-streamed/test.ts rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed/session-storage => backgroundtab-pageload}/init.js (53%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{http-timings-streamed => http-timings-static}/init.js (78%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{http-timings-streamed => http-timings-static}/subject.js (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-streamed/test.ts rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/consistent-sampling/default/init.js (93%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/consistent-sampling/default/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/consistent-sampling/default/template.html (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/default/test.ts rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/consistent-sampling/meta-negative/init.js (85%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/consistent-sampling/meta-negative/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/consistent-sampling/meta-negative/template.html (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/consistent-sampling/meta-negative/test.ts (73%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/consistent-sampling/meta-precedence/init.js (92%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/consistent-sampling/meta-precedence/page-1.html (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/consistent-sampling/meta-precedence/page-2.html (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/consistent-sampling/meta-precedence/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/consistent-sampling/meta-precedence/template.html (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/consistent-sampling/meta-precedence/test.ts (63%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/consistent-sampling/meta/init.js (92%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/consistent-sampling/meta/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/consistent-sampling/meta/template.html (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta/test.ts rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/consistent-sampling/tracesSampler-precedence/init.js (94%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/consistent-sampling/tracesSampler-precedence/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/consistent-sampling/tracesSampler-precedence/template.html (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/consistent-sampling/tracesSampler-precedence/test.ts (50%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{backgroundtab-pageload-streamed => linked-traces-static/custom-trace}/init.js (66%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/custom-trace/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/custom-trace/template.html (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/custom-trace/test.ts rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static/default}/init.js (66%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/default/test.ts rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/interaction-spans/init.js (84%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/interaction-spans/template.html (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/interaction-spans/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/meta/init.js rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/meta/template.html (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/meta/test.ts rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{linked-traces-streamed => linked-traces-static}/negatively-sampled/init.js (88%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/negatively-sampled/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/session-storage/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/session-storage/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/default/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/custom-trace/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/default/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/interaction-spans/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/meta/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/negatively-sampled/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/session-storage/test.ts rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-animation-frame-before-navigation-streamed => long-animation-frame-before-navigation-static}/init.js (90%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-animation-frame-before-navigation-streamed => long-animation-frame-before-navigation-static}/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-animation-frame-before-navigation-streamed => long-animation-frame-before-navigation-static}/template.html (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-streamed/test.ts rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-animation-frame-disabled-streamed => long-animation-frame-disabled-static}/assets/script.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-tasks-disabled-streamed => long-animation-frame-disabled-static}/init.js (88%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-animation-frame-disabled-streamed => long-animation-frame-disabled-static}/template.html (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-streamed/test.ts rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-animation-frame-enabled-streamed => long-animation-frame-enabled-static}/assets/script.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-animation-frame-enabled-streamed => long-animation-frame-enabled-static}/init.js (88%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-animation-frame-enabled-streamed => long-animation-frame-enabled-static}/template.html (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/test.ts rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-tasks-and-animation-frame-enabled-streamed => long-tasks-and-animation-frame-enabled-static}/assets/script.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-tasks-and-animation-frame-enabled-streamed => long-tasks-and-animation-frame-enabled-static}/init.js (88%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-tasks-and-animation-frame-enabled-streamed => long-tasks-and-animation-frame-enabled-static}/template.html (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/test.ts rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-tasks-before-navigation-streamed => long-tasks-before-navigation-static}/init.js (90%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-tasks-before-navigation-streamed => long-tasks-before-navigation-static}/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-tasks-before-navigation-streamed => long-tasks-before-navigation-static}/template.html (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-tasks-before-navigation-streamed => long-tasks-before-navigation-static}/test.ts (50%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-tasks-disabled-streamed => long-tasks-disabled-static}/assets/script.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-animation-frame-disabled-streamed => long-tasks-disabled-static}/init.js (88%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-tasks-disabled-streamed => long-tasks-disabled-static}/template.html (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-streamed/test.ts rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-tasks-enabled-streamed => long-tasks-enabled-static}/assets/script.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-tasks-enabled-streamed => long-tasks-enabled-static}/init.js (87%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{long-tasks-enabled-streamed => long-tasks-enabled-static}/template.html (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-streamed/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-static/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/init.js rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{pageload-streamed => pageload-static}/init.js (65%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName-static/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName-static/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName-static/test.ts rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{reportPageLoaded-streamed => reportPageLoaded-static}/default/init.js (86%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{reportPageLoaded-streamed => reportPageLoaded-static}/default/test.ts (50%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{reportPageLoaded-streamed => reportPageLoaded-static}/finalTimeout/init.js (65%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-static/finalTimeout/test.ts rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{reportPageLoaded-streamed => reportPageLoaded-static}/navigation/init.js (71%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-static/navigation/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/finalTimeout/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/navigation/test.ts rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{spotlight-interaction-filter-streamed => spotlight-interaction-filter-static}/init.js (90%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{spotlight-interaction-filter-streamed => spotlight-interaction-filter-static}/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{spotlight-interaction-filter-streamed => spotlight-interaction-filter-static}/template.html (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-streamed/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/async-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/async-spans/init.js index bb6d4918d1bf..83076460599f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/async-spans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/async-spans/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/async-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/async-spans/test.ts index 5a2f34abecba..24516c270dba 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/async-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/async-spans/test.ts @@ -1,7 +1,6 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; type WindowWithSpan = Window & { firstWaitingSpan: any; @@ -19,8 +18,7 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const envelope = await getFirstSentryEnvelopeRequest(page); - expect(envelope).toBeDefined(); + await page.waitForFunction(() => !!(window as unknown as WindowWithSpan).secondWaitingSpan); const firstWaitingSpanValue = await page.evaluate( () => (window as unknown as WindowWithSpan).firstWaitingSpan.name, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/init.js index 1c1791241ab7..9a11fe3682bb 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/test.ts index b6661c2638e2..944ed474f19e 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/test.ts @@ -1,9 +1,10 @@ +import { SENTRY_STATUS_MESSAGE, SENTRY_CANCELLATION_REASON } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import type { StreamedSpanJSON } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; -sentryTest('should finish a custom transaction when the page goes background', async ({ getLocalTestUrl, page }) => { +sentryTest('should finish a custom span when the page goes background', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); } @@ -32,6 +33,6 @@ sentryTest('should finish a custom transaction when the page goes background', a expect(name_after).toBe(name_before); // a cancelled span is reported as `ok`, with the raw status kept as an attribute expect(spanJsonAfter.status).toBe('ok'); - expect(attributes_after['sentry.status.message']).toBeUndefined(); - expect(attributes_after['sentry.cancellation_reason']).toBe('document.hidden'); + expect(attributes_after[SENTRY_STATUS_MESSAGE]).toBeUndefined(); + expect(attributes_after[SENTRY_CANCELLATION_REASON]).toBe('document.hidden'); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-static/init.js similarity index 64% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-static/init.js index a93fc742bafb..bb6d4918d1bf 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-static/init.js @@ -3,7 +3,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()], + integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-streamed/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-static/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-streamed/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-static/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-streamed/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-static/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-streamed/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-static/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-static/test.ts new file mode 100644 index 000000000000..626c7cbdcdbf --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-static/test.ts @@ -0,0 +1,23 @@ +import { PAGELOAD } from '@sentry/conventions/op'; +import { SENTRY_CANCELLATION_REASON } from '@sentry/conventions/attributes'; +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest('should finish pageload transaction when the page goes background', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === PAGELOAD); + + await page.goto(url); + await page.locator('#go-background').click(); + + const pageloadTransaction = envelopeRequestParser(await pageloadPromise); + + expect(pageloadTransaction.contexts?.trace?.op).toBe(PAGELOAD); + expect(pageloadTransaction.contexts?.trace?.status).toBe('cancelled'); + expect(pageloadTransaction.contexts?.trace?.data?.[SENTRY_CANCELLATION_REASON]).toBe('document.hidden'); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-streamed/test.ts deleted file mode 100644 index f33f988be946..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-streamed/test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; - -sentryTest('finishes streamed pageload span when the page goes background', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - const url = await getLocalTestUrl({ testDir: __dirname }); - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - - await page.goto(url); - await page.locator('#go-background').click(); - const pageloadSpan = await pageloadSpanPromise; - - // TODO: Is this what we want? - expect(pageloadSpan.status).toBe('ok'); - expect(pageloadSpan.attributes['sentry.cancellation_reason']?.value).toBe('document.hidden'); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/session-storage/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/init.js similarity index 53% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/session-storage/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/init.js index e51af56c2a9d..83076460599f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/session-storage/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/init.js @@ -4,9 +4,6 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [ - Sentry.browserTracingIntegration({ linkPreviousTrace: 'session-storage' }), - Sentry.spanStreamingIntegration(), - ], + integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/test.ts index 80ea1f5fa09c..10380d5b0e1b 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/test.ts @@ -1,20 +1,20 @@ +import { PAGELOAD } from '@sentry/conventions/op'; +import { SENTRY_CANCELLATION_REASON } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; -sentryTest('should finish pageload transaction when the page goes background', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +sentryTest('finishes streamed pageload span when the page goes background', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === PAGELOAD); await page.goto(url); await page.locator('#go-background').click(); + const pageloadSpan = await pageloadSpanPromise; - const pageloadTransaction = await getFirstSentryEnvelopeRequest(page); - - expect(pageloadTransaction.contexts?.trace?.op).toBe('pageload'); - expect(pageloadTransaction.contexts?.trace?.status).toBe('cancelled'); - expect(pageloadTransaction.contexts?.trace?.data?.['sentry.cancellation_reason']).toBe('document.hidden'); + expect(getSpanOp(pageloadSpan)).toBe(PAGELOAD); + expect(pageloadSpan.status).toBe('ok'); + expect(pageloadSpan.attributes[SENTRY_CANCELLATION_REASON]?.value).toBe('document.hidden'); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/error/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/error/test.ts index bf54209f053a..07a56ecdc184 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/error/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/error/test.ts @@ -1,47 +1,35 @@ +import { PAGELOAD } from '@sentry/conventions/op'; +import { URL_PATH } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { - getMultipleSentryEnvelopeRequests, + envelopeRequestParser, + waitForErrorRequest, runScriptInSandbox, shouldSkipTracingTest, } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest( - 'should put the pageload transaction name onto an error event caught during pageload', + 'puts the page path onto an error caught during pageload', async ({ getLocalTestUrl, page, browserName }) => { - if (browserName === 'webkit') { - // This test fails on Webkit as errors thrown from `runScriptInSandbox` are Script Errors and skipped by Sentry - sentryTest.skip(); - } - - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - + // WebKit treats errors from runScriptInSandbox as Script Errors, which Sentry skips. + sentryTest.skip(shouldSkipTracingTest() || browserName === 'webkit'); const url = await getLocalTestUrl({ testDir: __dirname }); - - const errorEventsPromise = getMultipleSentryEnvelopeRequests(page, 2); - + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => getSpanOp(span) === PAGELOAD); + const errorPromise = waitForErrorRequest(page); await page.goto(url); - - await runScriptInSandbox(page, { - content: ` - throw new Error('Error during pageload'); - `, - }); - - const [e1, e2] = await errorEventsPromise; - - const pageloadTxnEvent = e1.type === 'transaction' ? e1 : e2; - const errorEvent = e1.type === 'transaction' ? e2 : e1; - - expect(pageloadTxnEvent.contexts?.trace?.op).toEqual('pageload'); - expect(pageloadTxnEvent.spans?.length).toBeGreaterThan(0); - expect(errorEvent.exception?.values?.[0]).toBeDefined(); - - expect(pageloadTxnEvent.transaction?.endsWith('index.html')).toBe(true); - - expect(errorEvent.transaction).toEqual(pageloadTxnEvent.transaction); + await runScriptInSandbox(page, { content: "throw new Error('Error during pageload');" }); + const [pageload, errorRequest] = await Promise.all([pageloadPromise, errorPromise]); + await page.evaluate(() => (window as any).Sentry.flush()); + expect(getSpanOp(pageload)).toBe(PAGELOAD); + expect(spans.filter(span => !span.is_segment)).not.toHaveLength(0); + const error = envelopeRequestParser(errorRequest); + expect(error.exception?.values?.[0]).toBeDefined(); + expect(pageload.name).toBe('Pageload'); + expect(error.transaction).toBe('/index.html'); + expect(error.transaction).toBe(pageload.attributes[URL_PATH]?.value); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-static/init.js similarity index 78% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-static/init.js index 8ec045483e0c..5e521633ad12 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-static/init.js @@ -3,6 +3,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ @@ -10,9 +11,6 @@ Sentry.init({ enableHTTPTimings: true, }, }), - Sentry.spanStreamingIntegration(), ], tracesSampleRate: 1, - traceLifecycle: 'stream', - debug: true, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-streamed/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-static/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-streamed/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-static/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-static/test.ts new file mode 100644 index 000000000000..bc64d2630c9b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-static/test.ts @@ -0,0 +1,75 @@ +import { PAGELOAD, HTTP_CLIENT } from '@sentry/conventions/op'; +import { + HTTP_REQUEST_REDIRECT_START, + HTTP_REQUEST_REDIRECT_END, + HTTP_REQUEST_WORKER_START, + HTTP_REQUEST_FETCH_START, + HTTP_REQUEST_DOMAIN_LOOKUP_START, + HTTP_REQUEST_DOMAIN_LOOKUP_END, + HTTP_REQUEST_CONNECT_START, + HTTP_REQUEST_SECURE_CONNECTION_START, + HTTP_REQUEST_CONNECTION_END, + HTTP_REQUEST_REQUEST_START, + HTTP_REQUEST_RESPONSE_START, + HTTP_REQUEST_RESPONSE_END, + HTTP_REQUEST_TIME_TO_FIRST_BYTE, + NETWORK_PROTOCOL_VERSION, +} from '@sentry/conventions/attributes'; +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest('creates fetch spans with http timing', async ({ browserName, getLocalTestUrl, page }) => { + const supportedBrowsers = ['chromium', 'firefox']; + + if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { + sentryTest.skip(); + } + await page.route('http://sentry-test-site.example/*', async route => { + const request = route.request(); + const postData = await request.postDataJSON(); + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(Object.assign({ id: 1 }, postData)), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === PAGELOAD); + await page.goto(url); + const tracingEvent = envelopeRequestParser(await pageloadPromise); + + const requestSpans = tracingEvent.spans?.filter(({ op }) => op === HTTP_CLIENT); + + expect(requestSpans).toHaveLength(3); + + requestSpans?.forEach((span, index) => + expect(span).toMatchObject({ + description: `GET http://sentry-test-site.example/${index}`, + parent_span_id: tracingEvent.contexts?.trace?.span_id, + span_id: expect.stringMatching(/[a-f\d]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: tracingEvent.contexts?.trace?.trace_id, + data: expect.objectContaining({ + [HTTP_REQUEST_REDIRECT_START]: expect.any(Number), + [HTTP_REQUEST_REDIRECT_END]: expect.any(Number), + [HTTP_REQUEST_WORKER_START]: expect.any(Number), + [HTTP_REQUEST_FETCH_START]: expect.any(Number), + [HTTP_REQUEST_DOMAIN_LOOKUP_START]: expect.any(Number), + [HTTP_REQUEST_DOMAIN_LOOKUP_END]: expect.any(Number), + [HTTP_REQUEST_CONNECT_START]: expect.any(Number), + [HTTP_REQUEST_SECURE_CONNECTION_START]: expect.any(Number), + [HTTP_REQUEST_CONNECTION_END]: expect.any(Number), + [HTTP_REQUEST_REQUEST_START]: expect.any(Number), + [HTTP_REQUEST_RESPONSE_START]: expect.any(Number), + [HTTP_REQUEST_RESPONSE_END]: expect.any(Number), + [HTTP_REQUEST_TIME_TO_FIRST_BYTE]: expect.any(Number), + [NETWORK_PROTOCOL_VERSION]: expect.any(String), + }), + }), + ); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-streamed/test.ts deleted file mode 100644 index 7c02a63fc41c..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-streamed/test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; - -sentryTest( - 'adds http timing to http.client spans in span streaming mode', - async ({ browserName, getLocalTestUrl, page }) => { - const supportedBrowsers = ['chromium', 'firefox']; - - sentryTest.skip(shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)); - - await page.route('http://sentry-test-site.example/*', async route => { - const request = route.request(); - const postData = await request.postDataJSON(); - - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify(Object.assign({ id: 1 }, postData)), - }); - }); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const spansPromise = waitForStreamedSpans(page, spans => spans.some(s => getSpanOp(s) === 'http.client')); - await page.goto(url); - - const requestSpans = (await spansPromise).filter(s => getSpanOp(s) === 'http.client'); - const pageloadSpan = (await spansPromise).find(s => getSpanOp(s) === 'pageload'); - - expect(pageloadSpan).toBeDefined(); - expect(requestSpans).toHaveLength(3); - - requestSpans?.forEach(span => - expect(span).toMatchObject({ - // Streamed span names drop the high-cardinality URL path. - name: 'GET sentry-test-site.example', - parent_span_id: pageloadSpan?.span_id, - span_id: expect.stringMatching(/[a-f\d]{16}/), - start_timestamp: expect.any(Number), - end_timestamp: expect.any(Number), - trace_id: pageloadSpan?.trace_id, - status: 'ok', - attributes: expect.objectContaining({ - 'http.request.redirect_start': expect.any(Object), - 'http.request.redirect_end': expect.any(Object), - 'http.request.worker_start': expect.any(Object), - 'http.request.fetch_start': expect.any(Object), - 'http.request.domain_lookup_start': expect.any(Object), - 'http.request.domain_lookup_end': expect.any(Object), - 'http.request.connect_start': expect.any(Object), - 'http.request.secure_connection_start': expect.any(Object), - 'http.request.connection_end': expect.any(Object), - 'http.request.request_start': expect.any(Object), - 'http.request.response_start': expect.any(Object), - 'http.request.response_end': expect.any(Object), - 'http.request.time_to_first_byte': expect.any(Object), - 'network.protocol.version': expect.any(Object), - }), - }), - ); - }, -); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/init.js index 5e521633ad12..575e3fa65693 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/test.ts index 6894d7407349..bb80bf6b8efb 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/test.ts @@ -1,59 +1,113 @@ +import { PAGELOAD, HTTP_CLIENT } from '@sentry/conventions/op'; +import { + URL_FULL, + HTTP_REQUEST_REDIRECT_START, + HTTP_REQUEST_REDIRECT_END, + HTTP_REQUEST_WORKER_START, + HTTP_REQUEST_FETCH_START, + HTTP_REQUEST_DOMAIN_LOOKUP_START, + HTTP_REQUEST_DOMAIN_LOOKUP_END, + HTTP_REQUEST_CONNECT_START, + HTTP_REQUEST_SECURE_CONNECTION_START, + HTTP_REQUEST_CONNECTION_END, + HTTP_REQUEST_REQUEST_START, + HTTP_REQUEST_RESPONSE_START, + HTTP_REQUEST_RESPONSE_END, + HTTP_REQUEST_TIME_TO_FIRST_BYTE, + NETWORK_PROTOCOL_VERSION, +} from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers'; - -sentryTest('creates fetch spans with http timing', async ({ browserName, getLocalTestUrl, page }) => { - const supportedBrowsers = ['chromium', 'firefox']; - - if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { - sentryTest.skip(); - } - await page.route('http://sentry-test-site.example/*', async route => { - const request = route.request(); - const postData = await request.postDataJSON(); - - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify(Object.assign({ id: 1 }, postData)), +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; + +sentryTest( + 'adds http timing to http.client spans in span streaming mode', + async ({ browserName, getLocalTestUrl, page }) => { + const supportedBrowsers = ['chromium', 'firefox']; + + sentryTest.skip(shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)); + + await page.route('http://sentry-test-site.example/*', async route => { + const request = route.request(); + const postData = await request.postDataJSON(); + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(Object.assign({ id: 1 }, postData)), + }); }); - }); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const envelopes = await getMultipleSentryEnvelopeRequests(page, 2, { url, timeout: 10000 }); - const tracingEvent = envelopes[envelopes.length - 1]; // last envelope contains tracing data on all browsers - - const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client'); - - expect(requestSpans).toHaveLength(3); - - await page.pause(); - requestSpans?.forEach((span, index) => - expect(span).toMatchObject({ - description: `GET http://sentry-test-site.example/${index}`, - parent_span_id: tracingEvent.contexts?.trace?.span_id, - span_id: expect.stringMatching(/[a-f\d]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: tracingEvent.contexts?.trace?.trace_id, - data: expect.objectContaining({ - 'http.request.redirect_start': expect.any(Number), - 'http.request.redirect_end': expect.any(Number), - 'http.request.worker_start': expect.any(Number), - 'http.request.fetch_start': expect.any(Number), - 'http.request.domain_lookup_start': expect.any(Number), - 'http.request.domain_lookup_end': expect.any(Number), - 'http.request.connect_start': expect.any(Number), - 'http.request.secure_connection_start': expect.any(Number), - 'http.request.connection_end': expect.any(Number), - 'http.request.request_start': expect.any(Number), - 'http.request.response_start': expect.any(Number), - 'http.request.response_end': expect.any(Number), - 'http.request.time_to_first_byte': expect.any(Number), - 'network.protocol.version': expect.any(String), + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const spans = collectStreamedSpans(page); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === PAGELOAD); + await page.goto(url); + + const pageloadSpan = await pageloadSpanPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + const requestSpans = spans + .filter(s => getSpanOp(s) === HTTP_CLIENT) + .sort((a, b) => String(a.attributes[URL_FULL]?.value).localeCompare(String(b.attributes[URL_FULL]?.value))); + + expect(pageloadSpan).toBeDefined(); + expect(requestSpans).toHaveLength(3); + + requestSpans?.forEach((span, index) => + expect(span).toMatchObject({ + name: 'GET sentry-test-site.example', + parent_span_id: pageloadSpan?.span_id, + span_id: expect.stringMatching(/[a-f\d]{16}/), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + trace_id: pageloadSpan?.trace_id, + status: 'ok', + attributes: expect.objectContaining({ + [URL_FULL]: { type: 'string', value: `http://sentry-test-site.example/${index}` }, + [HTTP_REQUEST_REDIRECT_START]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [HTTP_REQUEST_REDIRECT_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_WORKER_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_FETCH_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_DOMAIN_LOOKUP_START]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [HTTP_REQUEST_DOMAIN_LOOKUP_END]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [HTTP_REQUEST_CONNECT_START]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [HTTP_REQUEST_SECURE_CONNECTION_START]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [HTTP_REQUEST_CONNECTION_END]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [HTTP_REQUEST_REQUEST_START]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [HTTP_REQUEST_RESPONSE_START]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [HTTP_REQUEST_RESPONSE_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_TIME_TO_FIRST_BYTE]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [NETWORK_PROTOCOL_VERSION]: { type: 'string', value: expect.any(String) }, + }), }), - }), - ); -}); + ); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/init.js index bb6d4918d1bf..83076460599f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/default/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/default/init.js similarity index 93% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/default/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/default/init.js index 63afee65329a..04a299421004 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/default/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/default/init.js @@ -3,13 +3,13 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ linkPreviousTrace: 'in-memory', consistentTraceSampling: true, }), - Sentry.spanStreamingIntegration(), ], tracePropagationTargets: ['sentry-test-external.io'], tracesSampler: ctx => { diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/default/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/default/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/default/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/default/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/default/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/default/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/default/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/default/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/default/test.ts new file mode 100644 index 000000000000..4d13371bc693 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/default/test.ts @@ -0,0 +1,149 @@ +import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +import { expect } from '@playwright/test'; +import { extractTraceparentData, parseBaggageHeader, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + eventAndTraceHeaderRequestParser, + shouldSkipTracingTest, + waitForTracingHeadersOnUrl, + waitForTransactionRequest, +} from '../../../../../../utils/helpers'; + +sentryTest.describe('When `consistentTraceSampling` is `true`', () => { + sentryTest('Continues sampling decision from initial pageload', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const { pageloadTraceContext, pageloadSampleRand } = await sentryTest.step('Initial pageload', async () => { + const pageloadRequestPromise = waitForTransactionRequest(page, evt => { + return evt.contexts?.trace?.op === 'pageload'; + }); + await page.goto(url); + + const res = eventAndTraceHeaderRequestParser(await pageloadRequestPromise); + const pageloadSampleRand = Number(res[1]?.sample_rand); + const pageloadTraceContext = res[0].contexts?.trace; + + expect(pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBe(1); + expect(pageloadSampleRand).toBeGreaterThanOrEqual(0); + + return { pageloadTraceContext: res[0].contexts?.trace, pageloadSampleRand }; + }); + + const customTraceContext = await sentryTest.step('Custom trace', async () => { + const customTrace1RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'custom'); + await page.locator('#btn1').click(); + const [customTrace1Event, customTraceTraceHeader] = eventAndTraceHeaderRequestParser( + await customTrace1RequestPromise, + ); + + const customTraceContext = customTrace1Event.contexts?.trace; + + expect(customTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + // although we "continue the trace" from pageload, this is actually a root span, + // so there must not be a parent span id + expect(customTraceContext?.parent_span_id).toBeUndefined(); + + expect(pageloadSampleRand).toEqual(Number(customTraceTraceHeader?.sample_rand)); + + return customTraceContext; + }); + + await sentryTest.step('Navigation', async () => { + const navigation1RequestPromise = waitForTransactionRequest( + page, + evt => evt.contexts?.trace?.op === 'navigation', + ); + await page.goto(`${url}#foo`); + const [navigationEvent, navigationTraceHeader] = eventAndTraceHeaderRequestParser( + await navigation1RequestPromise, + ); + const navTraceContext = navigationEvent.contexts?.trace; + + expect(navTraceContext?.trace_id).not.toEqual(customTraceContext?.trace_id); + expect(navTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + + expect(navTraceContext?.links).toEqual([ + { + trace_id: customTraceContext?.trace_id, + span_id: customTraceContext?.span_id, + sampled: true, + attributes: { + [SENTRY_LINK_TYPE]: 'previous_trace', + }, + }, + ]); + expect(navTraceContext?.parent_span_id).toBeUndefined(); + + expect(pageloadSampleRand).toEqual(Number(navigationTraceHeader?.sample_rand)); + }); + }); + + sentryTest('Propagates continued sampling decision to outgoing requests', async ({ page, getLocalTestUrl }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const { pageloadTraceContext, pageloadSampleRand } = await sentryTest.step('Initial pageload', async () => { + const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + await page.goto(url); + + const res = eventAndTraceHeaderRequestParser(await pageloadRequestPromise); + const pageloadSampleRand = Number(res[1]?.sample_rand); + + expect(pageloadSampleRand).toBeGreaterThanOrEqual(0); + + const pageloadTraceContext = res[0].contexts?.trace; + + expect(pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBe(1); + + return { pageloadTraceContext: pageloadTraceContext, pageloadSampleRand }; + }); + + await sentryTest.step('Make fetch request', async () => { + const fetchTracePromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'custom'); + const tracingHeadersPromise = waitForTracingHeadersOnUrl(page, 'http://sentry-test-external.io'); + + await page.locator('#btn2').click(); + + const { baggage, sentryTrace } = await tracingHeadersPromise; + + const [fetchTraceEvent, fetchTraceTraceHeader] = eventAndTraceHeaderRequestParser(await fetchTracePromise); + + const fetchTraceSampleRand = Number(fetchTraceTraceHeader?.sample_rand); + const fetchTraceTraceContext = fetchTraceEvent.contexts?.trace; + const httpClientSpan = fetchTraceEvent.spans?.find(span => span.op === 'http.client'); + + expect(fetchTraceSampleRand).toEqual(pageloadSampleRand); + + expect(fetchTraceTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toEqual( + pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE], + ); + expect(fetchTraceTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + + expect(sentryTrace).toBeDefined(); + expect(baggage).toBeDefined(); + + expect(extractTraceparentData(sentryTrace)).toEqual({ + traceId: fetchTraceTraceContext?.trace_id, + parentSpanId: httpClientSpan?.span_id, + parentSampled: true, + }); + + expect(parseBaggageHeader(baggage)).toEqual({ + 'sentry-environment': 'production', + 'sentry-public_key': 'public', + 'sentry-sample_rand': `${pageloadSampleRand}`, + 'sentry-sample_rate': '1', + 'sentry-sampled': 'true', + 'sentry-trace_id': fetchTraceTraceContext?.trace_id, + 'sentry-transaction': 'custom root span 2', + }); + }); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-negative/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-negative/init.js similarity index 85% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-negative/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-negative/init.js index d570ac45144c..05c4ec8db85a 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-negative/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-negative/init.js @@ -3,15 +3,14 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ linkPreviousTrace: 'in-memory', consistentTraceSampling: true, }), - Sentry.spanStreamingIntegration(), ], - traceLifecycle: 'stream', tracePropagationTargets: ['sentry-test-external.io'], tracesSampleRate: 1, debug: true, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-negative/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-negative/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-negative/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-negative/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-negative/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-negative/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-negative/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-negative/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-negative/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-negative/test.ts similarity index 73% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-negative/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-negative/test.ts index efd4ccce4e37..c4eae38fbd89 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-negative/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-negative/test.ts @@ -1,15 +1,16 @@ import { expect } from '@playwright/test'; -import type { ClientReport, SerializedStreamedSpan } from '@sentry/core'; +import type { ClientReport } from '@sentry/core'; import { extractTraceparentData, parseBaggageHeader } from '@sentry/core'; import { sentryTest } from '../../../../../../utils/fixtures'; import { envelopeRequestParser, + envelopeUrlRegex, + getEnvelopeType, hidePage, shouldSkipTracingTest, waitForClientReportRequest, waitForTracingHeadersOnUrl, } from '../../../../../../utils/helpers'; -import { observeStreamedSpan } from '../../../../../../utils/spanUtils'; const metaTagSampleRand = 0.9; const metaTagSampleRate = 0.2; @@ -19,31 +20,38 @@ sentryTest.describe('When `consistentTraceSampling` is `true` and page contains sentryTest( 'Continues negative sampling decision from meta tag across all traces and downstream propagations', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } const url = await getLocalTestUrl({ testDir: __dirname }); - const spansReceived: SerializedStreamedSpan[] = []; - observeStreamedSpan(page, span => { - spansReceived.push(span); - return false; + let txnsReceived = 0; + page.on('request', request => { + if (envelopeUrlRegex.test(request.url()) && getEnvelopeType(request) === 'transaction') { + ++txnsReceived; + } }); - const clientReportPromise = waitForClientReportRequest(page); + const clientReportPromise = waitForClientReportRequest(page, report => + report.discarded_events.some( + event => event.category === 'transaction' && event.reason === 'sample_rate' && event.quantity === 4, + ), + ); await sentryTest.step('Initial pageload', async () => { await page.goto(url); - expect(spansReceived).toHaveLength(0); + expect(txnsReceived).toEqual(0); }); await sentryTest.step('Custom instrumented button click', async () => { await page.locator('#btn1').click(); - expect(spansReceived).toHaveLength(0); + expect(txnsReceived).toEqual(0); }); await sentryTest.step('Navigation', async () => { await page.goto(`${url}#foo`); - expect(spansReceived).toHaveLength(0); + expect(txnsReceived).toEqual(0); }); await sentryTest.step('Make fetch request', async () => { @@ -70,8 +78,6 @@ sentryTest.describe('When `consistentTraceSampling` is `true` and page contains 'sentry-trace_id': expect.not.stringContaining(metaTagTraceId), 'sentry-transaction': 'custom root span 2', }); - - expect(spansReceived).toHaveLength(0); }); await sentryTest.step('Client report', async () => { @@ -81,17 +87,18 @@ sentryTest.describe('When `consistentTraceSampling` is `true` and page contains timestamp: expect.any(Number), discarded_events: [ { - category: 'span', - quantity: expect.any(Number), + category: 'transaction', + quantity: 4, reason: 'sample_rate', }, ], }); - // exact number depends on performance observer emissions - expect(clientReport.discarded_events[0].quantity).toBeGreaterThanOrEqual(10); }); - expect(spansReceived).toHaveLength(0); + await sentryTest.step('No transactions sent after processing completes', async () => { + expect(await page.evaluate(() => (window as any).Sentry.flush())).toBe(true); + expect(txnsReceived).toEqual(0); + }); }, ); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-precedence/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-precedence/init.js similarity index 92% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-precedence/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-precedence/init.js index 177fe4c4aeaf..fbf897d47eab 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-precedence/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-precedence/init.js @@ -3,13 +3,13 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ linkPreviousTrace: 'session-storage', consistentTraceSampling: true, }), - Sentry.spanStreamingIntegration(), ], tracePropagationTargets: ['sentry-test-external.io'], tracesSampler: ({ inheritOrSampleWith }) => { diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-precedence/page-1.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-precedence/page-1.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-precedence/page-1.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-precedence/page-1.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-precedence/page-2.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-precedence/page-2.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-precedence/page-2.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-precedence/page-2.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-precedence/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-precedence/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-precedence/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-precedence/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-precedence/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-precedence/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-precedence/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-precedence/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-precedence/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-precedence/test.ts similarity index 63% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-precedence/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-precedence/test.ts index cd64596f9bfd..3dab9594ba7c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-precedence/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta-precedence/test.ts @@ -4,12 +4,13 @@ import { extractTraceparentData, parseBaggageHeader } from '@sentry/core'; import { sentryTest } from '../../../../../../utils/fixtures'; import { envelopeRequestParser, + eventAndTraceHeaderRequestParser, hidePage, shouldSkipTracingTest, waitForClientReportRequest, waitForTracingHeadersOnUrl, + waitForTransactionRequest, } from '../../../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpanEnvelope } from '../../../../../../utils/spanUtils'; const metaTagSampleRand = 0.9; const metaTagSampleRate = 0.2; @@ -20,14 +21,16 @@ sentryTest.describe('When `consistentTraceSampling` is `true` and page contains sentryTest( 'meta tag decision has precedence over sampling decision from previous trace in session storage', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } const url = await getLocalTestUrl({ testDir: __dirname }); const clientReportPromise = waitForClientReportRequest(page); await sentryTest.step('Initial pageload', async () => { - // negative sampling decision -> no pageload span + // negative sampling decision -> no pageload txn await page.goto(url); }); @@ -68,49 +71,37 @@ sentryTest.describe('When `consistentTraceSampling` is `true` and page contains timestamp: expect.any(Number), discarded_events: [ { - category: 'span', - quantity: expect.any(Number), + category: 'transaction', + quantity: 2, reason: 'sample_rate', }, ], }); - // exact number depends on performance observer emissions - expect(clientReport.discarded_events[0].quantity).toBeGreaterThanOrEqual(3); }); await sentryTest.step('Navigate to another page with meta tags', async () => { - const page1PageloadEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'pageload' && s.trace_id === metaTagTraceIdPage1), - ); + const page1Pageload = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); await page.locator('a').click(); - const envelope = await page1PageloadEnvelopePromise; - const pageloadSpan = envelope[1][0][1].items.find(s => getSpanOp(s) === 'pageload')!; + const [pageloadEvent, pageloadTraceHeader] = eventAndTraceHeaderRequestParser(await page1Pageload); + const pageloadTraceContext = pageloadEvent.contexts?.trace; - expect(Number(envelope[0].trace?.sample_rand)).toBe(0.12); - expect(Number(envelope[0].trace?.sample_rate)).toBe(0.2); - expect(pageloadSpan.trace_id).toEqual(metaTagTraceIdPage1); + expect(Number(pageloadTraceHeader?.sample_rand)).toBe(0.12); + expect(Number(pageloadTraceHeader?.sample_rate)).toBe(0.2); + expect(pageloadTraceContext?.trace_id).toEqual(metaTagTraceIdPage1); }); await sentryTest.step('Navigate to another page without meta tags', async () => { - const page2PageloadEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => - !!env[1][0][1].items.find( - s => - getSpanOp(s) === 'pageload' && s.trace_id !== metaTagTraceIdPage1 && s.trace_id !== metaTagTraceIdIndex, - ), - ); + const page2Pageload = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); await page.locator('a').click(); - const envelope = await page2PageloadEnvelopePromise; - const pageloadSpan = envelope[1][0][1].items.find(s => getSpanOp(s) === 'pageload')!; + const [pageloadEvent, pageloadTraceHeader] = eventAndTraceHeaderRequestParser(await page2Pageload); + const pageloadTraceContext = pageloadEvent.contexts?.trace; - expect(Number(envelope[0].trace?.sample_rand)).toBe(0.12); - expect(Number(envelope[0].trace?.sample_rate)).toBe(0.2); - expect(pageloadSpan.trace_id).not.toEqual(metaTagTraceIdPage1); - expect(pageloadSpan.trace_id).not.toEqual(metaTagTraceIdIndex); + expect(Number(pageloadTraceHeader?.sample_rand)).toBe(0.12); + expect(Number(pageloadTraceHeader?.sample_rate)).toBe(0.2); + expect(pageloadTraceContext?.trace_id).not.toEqual(metaTagTraceIdPage1); + expect(pageloadTraceContext?.trace_id).not.toEqual(metaTagTraceIdIndex); }); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta/init.js similarity index 92% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta/init.js index a1ddc5465950..61e2b2daa9ba 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta/init.js @@ -3,13 +3,13 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ linkPreviousTrace: 'in-memory', consistentTraceSampling: true, }), - Sentry.spanStreamingIntegration(), ], tracePropagationTargets: ['sentry-test-external.io'], // only take into account sampling from meta tag; otherwise sample negatively diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta/test.ts new file mode 100644 index 000000000000..ac2a865fb181 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/meta/test.ts @@ -0,0 +1,176 @@ +import { expect } from '@playwright/test'; +import { + extractTraceparentData, + parseBaggageHeader, + SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, +} from '@sentry/core'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + eventAndTraceHeaderRequestParser, + shouldSkipTracingTest, + waitForTracingHeadersOnUrl, + waitForTransactionRequest, +} from '../../../../../../utils/helpers'; + +const metaTagSampleRand = 0.051121; +const metaTagSampleRate = 0.2; + +sentryTest.describe('When `consistentTraceSampling` is `true` and page contains tags', () => { + sentryTest('Continues sampling decision across all traces from meta tag', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => { + const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + + await page.goto(url); + + const [pageloadEvent, pageloadTraceHeader] = eventAndTraceHeaderRequestParser(await pageloadRequestPromise); + const pageloadTraceContext = pageloadEvent.contexts?.trace; + + expect(Number(pageloadTraceHeader?.sample_rand)).toBe(metaTagSampleRand); + expect(Number(pageloadTraceHeader?.sample_rate)).toBe(metaTagSampleRate); + + // since the local sample rate was not applied, the sample rate attribute shouldn't be set + expect(pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBeUndefined(); + expect(pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]).toBeUndefined(); + + return pageloadTraceContext; + }); + + const customTraceContext = await sentryTest.step('Custom trace', async () => { + const customTrace1RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'custom'); + + await page.locator('#btn1').click(); + + const [customTrace1Event, customTraceTraceHeader] = eventAndTraceHeaderRequestParser( + await customTrace1RequestPromise, + ); + + const customTraceContext = customTrace1Event.contexts?.trace; + + expect(customTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + expect(customTraceContext?.parent_span_id).toBeUndefined(); + + expect(Number(customTraceTraceHeader?.sample_rand)).toBe(metaTagSampleRand); + expect(Number(customTraceTraceHeader?.sample_rate)).toBe(metaTagSampleRate); + expect(Boolean(customTraceTraceHeader?.sampled)).toBe(true); + + // since the local sample rate was not applied, the sample rate attribute shouldn't be set + expect(customTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBeUndefined(); + + // but we need to set this attribute to still be able to correctly add the sample rate to the DSC (checked above in trace header) + expect(customTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]).toBe(metaTagSampleRate); + + return customTraceContext; + }); + + await sentryTest.step('Navigation', async () => { + const navigation1RequestPromise = waitForTransactionRequest( + page, + evt => evt.contexts?.trace?.op === 'navigation', + ); + + await page.goto(`${url}#foo`); + + const [navigationEvent, navigationTraceHeader] = eventAndTraceHeaderRequestParser( + await navigation1RequestPromise, + ); + + const navigationTraceContext = navigationEvent.contexts?.trace; + + expect(navigationTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + expect(navigationTraceContext?.trace_id).not.toEqual(customTraceContext?.trace_id); + + expect(navigationTraceContext?.parent_span_id).toBeUndefined(); + + expect(Number(navigationTraceHeader?.sample_rand)).toEqual(metaTagSampleRand); + expect(Number(navigationTraceHeader?.sample_rate)).toEqual(metaTagSampleRate); + expect(Boolean(navigationTraceHeader?.sampled)).toEqual(true); + + // since the local sample rate was not applied, the sample rate attribute shouldn't be set + expect(navigationTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBeUndefined(); + + // but we need to set this attribute to still be able to correctly add the sample rate to the DSC (checked above in trace header) + expect(navigationTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]).toBe( + metaTagSampleRate, + ); + }); + }); + + sentryTest( + 'Propagates continued tag sampling decision to outgoing requests', + async ({ page, getLocalTestUrl }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => { + const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + + await page.goto(url); + + const [pageloadEvent, pageloadTraceHeader] = eventAndTraceHeaderRequestParser(await pageloadRequestPromise); + const pageloadTraceContext = pageloadEvent.contexts?.trace; + + expect(Number(pageloadTraceHeader?.sample_rand)).toBe(metaTagSampleRand); + expect(Number(pageloadTraceHeader?.sample_rate)).toBe(metaTagSampleRate); + + // since the local sample rate was not applied, the sample rate attribute shouldn't be set + expect(pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBeUndefined(); + expect(pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]).toBeUndefined(); + + return pageloadTraceContext; + }); + + await sentryTest.step('Make fetch request', async () => { + const fetchTracePromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'custom'); + const tracingHeadersPromise = waitForTracingHeadersOnUrl(page, 'http://sentry-test-external.io'); + + await page.locator('#btn2').click(); + + const { baggage, sentryTrace } = await tracingHeadersPromise; + + const [fetchTraceEvent, fetchTraceTraceHeader] = eventAndTraceHeaderRequestParser(await fetchTracePromise); + + const fetchTraceSampleRand = Number(fetchTraceTraceHeader?.sample_rand); + const fetchTraceTraceContext = fetchTraceEvent.contexts?.trace; + const httpClientSpan = fetchTraceEvent.spans?.find(span => span.op === 'http.client'); + + expect(fetchTraceSampleRand).toEqual(metaTagSampleRand); + + expect(fetchTraceTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBeUndefined(); + expect(fetchTraceTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]).toBe( + metaTagSampleRate, + ); + + expect(fetchTraceTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + + expect(sentryTrace).toBeDefined(); + expect(baggage).toBeDefined(); + + expect(extractTraceparentData(sentryTrace)).toEqual({ + traceId: fetchTraceTraceContext?.trace_id, + parentSpanId: httpClientSpan?.span_id, + parentSampled: true, + }); + + expect(parseBaggageHeader(baggage)).toEqual({ + 'sentry-environment': 'production', + 'sentry-public_key': 'public', + 'sentry-sample_rand': `${metaTagSampleRand}`, + 'sentry-sample_rate': `${metaTagSampleRate}`, + 'sentry-sampled': 'true', + 'sentry-trace_id': fetchTraceTraceContext?.trace_id, + 'sentry-transaction': 'custom root span 2', + }); + }); + }, + ); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/tracesSampler-precedence/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/tracesSampler-precedence/init.js similarity index 94% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/tracesSampler-precedence/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/tracesSampler-precedence/init.js index 623db0ecc028..e2efc43e50f2 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/tracesSampler-precedence/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/tracesSampler-precedence/init.js @@ -3,6 +3,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ @@ -10,7 +11,6 @@ Sentry.init({ consistentTraceSampling: true, enableInp: false, }), - Sentry.spanStreamingIntegration(), ], tracePropagationTargets: ['sentry-test-external.io'], tracesSampler: ctx => { diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/tracesSampler-precedence/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/tracesSampler-precedence/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/tracesSampler-precedence/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/tracesSampler-precedence/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/tracesSampler-precedence/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/tracesSampler-precedence/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/tracesSampler-precedence/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/tracesSampler-precedence/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/tracesSampler-precedence/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/tracesSampler-precedence/test.ts similarity index 50% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/tracesSampler-precedence/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/tracesSampler-precedence/test.ts index 37e3b204fe88..a61043dfc109 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/tracesSampler-precedence/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/consistent-sampling/tracesSampler-precedence/test.ts @@ -1,3 +1,4 @@ +import { PAGELOAD, NAVIGATION } from '@sentry/conventions/op'; import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; @@ -5,11 +6,12 @@ import type { ClientReport } from '@sentry/core'; import { sentryTest } from '../../../../../../utils/fixtures'; import { envelopeRequestParser, + eventAndTraceHeaderRequestParser, hidePage, shouldSkipTracingTest, waitForClientReportRequest, + waitForTransactionRequest, } from '../../../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpanEnvelope } from '../../../../../../utils/spanUtils'; /** * This test demonstrates that: @@ -18,24 +20,26 @@ import { getSpanOp, waitForStreamedSpanEnvelope } from '../../../../../../utils/ */ sentryTest.describe('When `consistentTraceSampling` is `true`', () => { sentryTest('explicit sampling decisions in `tracesSampler` have precedence', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } const url = await getLocalTestUrl({ testDir: __dirname }); - const { pageloadSpan } = await sentryTest.step('Initial pageload', async () => { - const pageloadEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'pageload'), - ); + const { pageloadTraceContext } = await sentryTest.step('Initial pageload', async () => { + const pageloadRequestPromise = waitForTransactionRequest(page, evt => { + return evt.contexts?.trace?.op === PAGELOAD; + }); await page.goto(url); - const envelope = await pageloadEnvelopePromise; - const pageloadSpan = envelope[1][0][1].items.find(s => getSpanOp(s) === 'pageload')!; + const res = eventAndTraceHeaderRequestParser(await pageloadRequestPromise); + const pageloadSampleRand = Number(res[1]?.sample_rand); + const pageloadTraceContext = res[0].contexts?.trace; - expect(pageloadSpan.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]?.value).toBe(1); - expect(Number(envelope[0].trace?.sample_rand)).toBeGreaterThanOrEqual(0); + expect(pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBe(1); + expect(pageloadSampleRand).toBeGreaterThanOrEqual(0); - return { pageloadSpan }; + return { pageloadTraceContext: res[0].contexts?.trace, pageloadSampleRand }; }); await sentryTest.step('Custom trace is sampled negatively (explicitly in tracesSampler)', async () => { @@ -43,7 +47,6 @@ sentryTest.describe('When `consistentTraceSampling` is `true`', () => { await page.locator('#btn1').click(); - await page.waitForTimeout(500); await hidePage(page); const clientReport = envelopeRequestParser(await clientReportPromise); @@ -52,7 +55,7 @@ sentryTest.describe('When `consistentTraceSampling` is `true`', () => { timestamp: expect.any(Number), discarded_events: [ { - category: 'span', + category: 'transaction', quantity: 1, reason: 'sample_rate', }, @@ -65,8 +68,6 @@ sentryTest.describe('When `consistentTraceSampling` is `true`', () => { await page.goto(`${url}#foo`); - await page.waitForTimeout(500); - await hidePage(page); const clientReport = envelopeRequestParser(await clientReportPromise); @@ -75,7 +76,7 @@ sentryTest.describe('When `consistentTraceSampling` is `true`', () => { timestamp: expect.any(Number), discarded_events: [ { - category: 'span', + category: 'transaction', quantity: 1, reason: 'sample_rate', }, @@ -83,67 +84,53 @@ sentryTest.describe('When `consistentTraceSampling` is `true`', () => { }); }); - const { customTrace2Span } = await sentryTest.step( + const { customTrace2Context } = await sentryTest.step( 'Custom trace 2 is sampled positively (explicitly in tracesSampler)', async () => { - const customEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'custom'), - ); + const customTrace2RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'custom'); await page.locator('#btn2').click(); - const envelope = await customEnvelopePromise; - const customTrace2Span = envelope[1][0][1].items.find(s => getSpanOp(s) === 'custom')!; + const [customTrace2Event] = eventAndTraceHeaderRequestParser(await customTrace2RequestPromise); + + const customTrace2Context = customTrace2Event.contexts?.trace; - expect(customTrace2Span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]?.value).toBe(1); - expect(customTrace2Span.trace_id).not.toEqual(pageloadSpan.trace_id); - expect(customTrace2Span.parent_span_id).toBeUndefined(); + expect(customTrace2Context?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBe(1); + expect(customTrace2Context?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + expect(customTrace2Context?.parent_span_id).toBeUndefined(); - expect(customTrace2Span.links).toEqual([ + expect(customTrace2Context?.links).toEqual([ { - attributes: { - [SENTRY_LINK_TYPE]: { - type: 'string', - value: 'previous_trace', - }, - }, + attributes: { [SENTRY_LINK_TYPE]: 'previous_trace' }, sampled: false, span_id: expect.stringMatching(/^[\da-f]{16}$/), trace_id: expect.stringMatching(/^[\da-f]{32}$/), }, ]); - return { customTrace2Span }; + return { customTrace2Context }; }, ); await sentryTest.step('Navigation trace is sampled positively (inherited from previous trace)', async () => { - const navigationEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => env[0].trace?.sampled === 'true' && !!env[1][0][1].items.find(s => getSpanOp(s) === 'navigation'), - ); + const navigationRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === NAVIGATION); await page.goto(`${url}#bar`); - const envelope = await navigationEnvelopePromise; - const navigationSpan = envelope[1][0][1].items.find(s => getSpanOp(s) === 'navigation')!; + const [navigationEvent] = eventAndTraceHeaderRequestParser(await navigationRequestPromise); - expect(navigationSpan.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]?.value).toBe(1); - expect(navigationSpan.trace_id).not.toEqual(customTrace2Span.trace_id); - expect(navigationSpan.parent_span_id).toBeUndefined(); + const navigationTraceContext = navigationEvent.contexts?.trace; - expect(navigationSpan.links).toEqual([ + expect(navigationTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBe(1); + expect(navigationTraceContext?.trace_id).not.toEqual(customTrace2Context?.trace_id); + expect(navigationTraceContext?.parent_span_id).toBeUndefined(); + + expect(navigationTraceContext?.links).toEqual([ { - attributes: { - [SENTRY_LINK_TYPE]: { - type: 'string', - value: 'previous_trace', - }, - }, + attributes: { [SENTRY_LINK_TYPE]: 'previous_trace' }, sampled: true, - span_id: customTrace2Span.span_id, - trace_id: customTrace2Span.trace_id, + span_id: customTrace2Context?.span_id, + trace_id: customTrace2Context?.trace_id, }, ]); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/custom-trace/init.js similarity index 66% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/custom-trace/init.js index 749560a5c459..bdd43f5ac757 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/custom-trace/init.js @@ -3,8 +3,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()], + integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, debug: true, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/custom-trace/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/custom-trace/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/custom-trace/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/custom-trace/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/custom-trace/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/custom-trace/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/custom-trace/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/custom-trace/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/custom-trace/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/custom-trace/test.ts new file mode 100644 index 000000000000..c4637dc25018 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/custom-trace/test.ts @@ -0,0 +1,62 @@ +import { expect } from '@playwright/test'; +import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; + +sentryTest('manually started custom traces are linked correctly in the chain', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => { + const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + await page.goto(url); + const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise); + return pageloadRequest.contexts?.trace; + }); + + const customTrace1Context = await sentryTest.step('Custom trace', async () => { + const customTrace1RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'custom'); + await page.locator('#btn1').click(); + const customTrace1Event = envelopeRequestParser(await customTrace1RequestPromise); + + const customTraceCtx = customTrace1Event.contexts?.trace; + + expect(customTraceCtx?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + expect(customTraceCtx?.links).toEqual([ + { + trace_id: pageloadTraceContext?.trace_id, + span_id: pageloadTraceContext?.span_id, + sampled: true, + attributes: { + [SENTRY_LINK_TYPE]: 'previous_trace', + }, + }, + ]); + + return customTraceCtx; + }); + + await sentryTest.step('Navigation', async () => { + const navigation1RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation'); + await page.goto(`${url}#foo`); + const navigationEvent = envelopeRequestParser(await navigation1RequestPromise); + const navTraceContext = navigationEvent.contexts?.trace; + + expect(navTraceContext?.trace_id).not.toEqual(customTrace1Context?.trace_id); + expect(navTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + + expect(navTraceContext?.links).toEqual([ + { + trace_id: customTrace1Context?.trace_id, + span_id: customTrace1Context?.span_id, + sampled: true, + attributes: { + [SENTRY_LINK_TYPE]: 'previous_trace', + }, + }, + ]); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/default/init.js similarity index 66% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/default/init.js index 749560a5c459..bdd43f5ac757 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/default/init.js @@ -3,8 +3,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()], + integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, debug: true, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/default/test.ts new file mode 100644 index 000000000000..7399c2bb26c3 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/default/test.ts @@ -0,0 +1,99 @@ +import { expect } from '@playwright/test'; +import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; + +sentryTest("navigation spans link back to previous trace's root span", async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => { + const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + await page.goto(url); + const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise); + return pageloadRequest.contexts?.trace; + }); + + const navigation1TraceContext = await sentryTest.step('First navigation', async () => { + const navigation1RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation'); + await page.goto(`${url}#foo`); + const navigation1Request = envelopeRequestParser(await navigation1RequestPromise); + return navigation1Request.contexts?.trace; + }); + + const navigation2TraceContext = await sentryTest.step('Second navigation', async () => { + const navigation2RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation'); + await page.goto(`${url}#bar`); + const navigation2Request = envelopeRequestParser(await navigation2RequestPromise); + return navigation2Request.contexts?.trace; + }); + + const pageloadTraceId = pageloadTraceContext?.trace_id; + const navigation1TraceId = navigation1TraceContext?.trace_id; + const navigation2TraceId = navigation2TraceContext?.trace_id; + + expect(pageloadTraceContext?.links).toBeUndefined(); + + expect(navigation1TraceContext?.links).toEqual([ + { + trace_id: pageloadTraceId, + span_id: pageloadTraceContext?.span_id, + sampled: true, + attributes: { + [SENTRY_LINK_TYPE]: 'previous_trace', + }, + }, + ]); + + expect(navigation1TraceContext?.data).toMatchObject({ + 'sentry.previous_trace': `${pageloadTraceId}-${pageloadTraceContext?.span_id}-1`, + }); + + expect(navigation2TraceContext?.links).toEqual([ + { + trace_id: navigation1TraceId, + span_id: navigation1TraceContext?.span_id, + sampled: true, + attributes: { + [SENTRY_LINK_TYPE]: 'previous_trace', + }, + }, + ]); + + expect(navigation2TraceContext?.data).toMatchObject({ + 'sentry.previous_trace': `${navigation1TraceId}-${navigation1TraceContext?.span_id}-1`, + }); + + expect(pageloadTraceId).not.toEqual(navigation1TraceId); + expect(navigation1TraceId).not.toEqual(navigation2TraceId); + expect(pageloadTraceId).not.toEqual(navigation2TraceId); +}); + +sentryTest("doesn't link between hard page reloads by default", async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await sentryTest.step('First pageload', async () => { + const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + await page.goto(url); + const pageload1Event = envelopeRequestParser(await pageloadRequestPromise); + + expect(pageload1Event.contexts?.trace).toBeDefined(); + expect(pageload1Event.contexts?.trace?.links).toBeUndefined(); + }); + + await sentryTest.step('Second pageload', async () => { + const pageload2RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + await page.reload(); + const pageload2Event = envelopeRequestParser(await pageload2RequestPromise); + + expect(pageload2Event.contexts?.trace).toBeDefined(); + expect(pageload2Event.contexts?.trace?.links).toBeUndefined(); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/interaction-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/interaction-spans/init.js similarity index 84% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/interaction-spans/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/interaction-spans/init.js index 812e8cb92dc2..2c3e9264d882 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/interaction-spans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/interaction-spans/init.js @@ -4,7 +4,8 @@ import { interactionsIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1, - integrations: [Sentry.browserTracingIntegration(), interactionsIntegration(), Sentry.spanStreamingIntegration()], + integrations: [Sentry.browserTracingIntegration(), interactionsIntegration()], }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/interaction-spans/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/interaction-spans/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/interaction-spans/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/interaction-spans/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/interaction-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/interaction-spans/test.ts new file mode 100644 index 000000000000..24a1e2c59072 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/interaction-spans/test.ts @@ -0,0 +1,89 @@ +import { expect } from '@playwright/test'; +import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; + +/* + This is quite peculiar behavior but it's a result of the route-based trace lifetime. + Once we shortened trace lifetime, this whole scenario will change as the interaction + spans will be their own trace. So most likely, we can replace this test with a new one + that covers the new default behavior. +*/ +sentryTest( + 'only the first root spans in the trace link back to the previous trace', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => { + const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + await page.goto(url); + + const pageloadEvent = envelopeRequestParser(await pageloadRequestPromise); + const traceContext = pageloadEvent.contexts?.trace; + + expect(traceContext).toBeDefined(); + expect(traceContext?.links).toBeUndefined(); + + return traceContext; + }); + + await sentryTest.step('Click Before navigation', async () => { + const interactionRequestPromise = waitForTransactionRequest(page, evt => { + return evt.contexts?.trace?.op === 'ui.action.click'; + }); + await page.click('#btn'); + + const interactionEvent = envelopeRequestParser(await interactionRequestPromise); + const interactionTraceContext = interactionEvent.contexts?.trace; + + // sanity check: route-based trace lifetime means the trace_id should be the same + expect(interactionTraceContext?.trace_id).toBe(pageloadTraceContext?.trace_id); + + // no links yet as previous root span belonged to same trace + expect(interactionTraceContext?.links).toBeUndefined(); + }); + + const navigationTraceContext = await sentryTest.step('Navigation', async () => { + const navigationRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation'); + await page.goto(`${url}#foo`); + const navigationEvent = envelopeRequestParser(await navigationRequestPromise); + + const traceContext = navigationEvent.contexts?.trace; + + expect(traceContext?.op).toBe('navigation'); + expect(traceContext?.links).toEqual([ + { + trace_id: pageloadTraceContext?.trace_id, + span_id: pageloadTraceContext?.span_id, + sampled: true, + attributes: { + [SENTRY_LINK_TYPE]: 'previous_trace', + }, + }, + ]); + + expect(traceContext?.trace_id).not.toEqual(traceContext?.links![0].trace_id); + return traceContext; + }); + + await sentryTest.step('Click After navigation', async () => { + const interactionRequestPromise = waitForTransactionRequest(page, evt => { + return evt.contexts?.trace?.op === 'ui.action.click'; + }); + await page.click('#btn'); + const interactionEvent = envelopeRequestParser(await interactionRequestPromise); + + const interactionTraceContext = interactionEvent.contexts?.trace; + + // sanity check: route-based trace lifetime means the trace_id should be the same + expect(interactionTraceContext?.trace_id).toBe(navigationTraceContext?.trace_id); + + // since this is the second root span in the trace, it doesn't link back to the previous trace + expect(interactionTraceContext?.links).toBeUndefined(); + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/meta/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/meta/init.js new file mode 100644 index 000000000000..bdd43f5ac757 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/meta/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, + debug: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/meta/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/meta/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/meta/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/meta/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/meta/test.ts new file mode 100644 index 000000000000..a232a5a0f6e7 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/meta/test.ts @@ -0,0 +1,54 @@ +import { expect } from '@playwright/test'; +import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; + +sentryTest( + "links back to previous trace's local root span if continued from meta tags", + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const metaTagTraceId = '12345678901234567890123456789012'; + + const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => { + const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + await page.goto(url); + const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise); + + const traceContext = pageloadRequest.contexts?.trace; + + // sanity check + expect(traceContext?.trace_id).toBe(metaTagTraceId); + + expect(traceContext?.links).toBeUndefined(); + + return traceContext; + }); + + const navigationTraceContext = await sentryTest.step('Navigation', async () => { + const navigationRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation'); + await page.goto(`${url}#foo`); + const navigationRequest = envelopeRequestParser(await navigationRequestPromise); + return navigationRequest.contexts?.trace; + }); + + const navigationTraceId = navigationTraceContext?.trace_id; + + expect(navigationTraceContext?.links).toEqual([ + { + trace_id: metaTagTraceId, + span_id: pageloadTraceContext?.span_id, + sampled: true, + attributes: { + [SENTRY_LINK_TYPE]: 'previous_trace', + }, + }, + ]); + + expect(navigationTraceId).not.toEqual(metaTagTraceId); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/negatively-sampled/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/negatively-sampled/init.js similarity index 88% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/negatively-sampled/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/negatively-sampled/init.js index 778092cf026b..79ca4d68e8ba 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/negatively-sampled/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/negatively-sampled/init.js @@ -3,9 +3,10 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', // We want to ignore redirects for this test - integrations: [Sentry.browserTracingIntegration({ detectRedirects: false }), Sentry.spanStreamingIntegration()], + integrations: [Sentry.browserTracingIntegration({ detectRedirects: false })], tracesSampler: ctx => { if (ctx.attributes['sentry.origin'] === 'auto.pageload.browser') { return 0; diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/negatively-sampled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/negatively-sampled/test.ts new file mode 100644 index 000000000000..894fe0d5929a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/negatively-sampled/test.ts @@ -0,0 +1,42 @@ +import { expect } from '@playwright/test'; +import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; + +sentryTest('includes a span link to a previously negatively sampled span', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await sentryTest.step('Initial pageload', async () => { + // No event to check for an event here because this pageload span is sampled negatively! + await page.goto(url); + }); + + await sentryTest.step('Navigation', async () => { + const navigationRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation'); + await page.goto(`${url}#foo`); + const navigationEvent = envelopeRequestParser(await navigationRequestPromise); + const navigationTraceContext = navigationEvent.contexts?.trace; + + expect(navigationTraceContext?.op).toBe('navigation'); + expect(navigationTraceContext?.links).toEqual([ + { + trace_id: expect.stringMatching(/[a-f\d]{32}/), + span_id: expect.stringMatching(/[a-f\d]{16}/), + sampled: false, + attributes: { + [SENTRY_LINK_TYPE]: 'previous_trace', + }, + }, + ]); + + expect(navigationTraceContext?.data).toMatchObject({ + 'sentry.previous_trace': expect.stringMatching(/[a-f\d]{32}-[a-f\d]{16}-0/), + }); + + expect(navigationTraceContext?.trace_id).not.toEqual(navigationTraceContext?.links![0].trace_id); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/session-storage/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/session-storage/init.js new file mode 100644 index 000000000000..9b88e489ae07 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/session-storage/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration({ linkPreviousTrace: 'session-storage' })], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/session-storage/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/session-storage/test.ts new file mode 100644 index 000000000000..f7e0d288de5b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-static/session-storage/test.ts @@ -0,0 +1,39 @@ +import { expect } from '@playwright/test'; +import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; + +sentryTest('adds link between hard page reloads when opting into sessionStorage', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageload1TraceContext = await sentryTest.step('First pageload', async () => { + const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + await page.goto(url); + const pageload1Event = envelopeRequestParser(await pageloadRequestPromise); + const pageload1TraceContext = pageload1Event.contexts?.trace; + expect(pageload1TraceContext).toBeDefined(); + expect(pageload1TraceContext?.links).toBeUndefined(); + return pageload1TraceContext; + }); + + const pageload2Event = await sentryTest.step('Hard page reload', async () => { + const pageload2RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + await page.reload(); + return envelopeRequestParser(await pageload2RequestPromise); + }); + + expect(pageload2Event.contexts?.trace?.links).toEqual([ + { + trace_id: pageload1TraceContext?.trace_id, + span_id: pageload1TraceContext?.span_id, + sampled: true, + attributes: { [SENTRY_LINK_TYPE]: 'previous_trace' }, + }, + ]); + + expect(pageload1TraceContext?.trace_id).not.toEqual(pageload2Event.contexts?.trace?.trace_id); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/default/test.ts deleted file mode 100644 index 3ef7ddefca6d..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/default/test.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; -import { expect } from '@playwright/test'; -import { extractTraceparentData, parseBaggageHeader } from '@sentry/core'; -import { sentryTest } from '../../../../../../utils/fixtures'; -import { shouldSkipTracingTest, waitForTracingHeadersOnUrl } from '../../../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpanEnvelope } from '../../../../../../utils/spanUtils'; - -sentryTest.describe('When `consistentTraceSampling` is `true`', () => { - sentryTest('continues sampling decision from initial pageload span', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const { pageloadSpan, pageloadSampleRand } = await sentryTest.step('Initial pageload', async () => { - const pageloadEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'pageload'), - ); - await page.goto(url); - - const envelope = await pageloadEnvelopePromise; - const pageloadSampleRand = Number(envelope[0].trace?.sample_rand); - const pageloadSpan = envelope[1][0][1].items.find(s => getSpanOp(s) === 'pageload')!; - - expect(pageloadSpan.attributes['sentry.sample_rate']?.value).toBe(1); - expect(Number.isNaN(pageloadSampleRand)).toBe(false); - expect(pageloadSampleRand).toBeGreaterThanOrEqual(0); - expect(pageloadSampleRand).toBeLessThanOrEqual(1); - - return { pageloadSpan, pageloadSampleRand }; - }); - - const customTraceSpan = await sentryTest.step('Custom trace', async () => { - const customEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'custom'), - ); - await page.locator('#btn1').click(); - const envelope = await customEnvelopePromise; - const span = envelope[1][0][1].items.find(s => getSpanOp(s) === 'custom')!; - - expect(span.trace_id).not.toEqual(pageloadSpan.trace_id); - // although we "continue the trace" from pageload, this is actually a root span, - // so there must not be a parent span id - expect(span.parent_span_id).toBeUndefined(); - - expect(Number(envelope[0].trace?.sample_rand)).toBe(pageloadSampleRand); - - return span; - }); - - await sentryTest.step('Navigation', async () => { - const navigationEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'navigation'), - ); - await page.goto(`${url}#foo`); - const envelope = await navigationEnvelopePromise; - const navSpan = envelope[1][0][1].items.find(s => getSpanOp(s) === 'navigation')!; - - expect(navSpan.trace_id).not.toEqual(customTraceSpan.trace_id); - expect(navSpan.trace_id).not.toEqual(pageloadSpan.trace_id); - - expect(navSpan.links).toEqual([ - { - trace_id: customTraceSpan.trace_id, - span_id: customTraceSpan.span_id, - sampled: true, - attributes: { - [SENTRY_LINK_TYPE]: { - type: 'string', - value: 'previous_trace', - }, - }, - }, - ]); - expect(navSpan.parent_span_id).toBeUndefined(); - - expect(Number(envelope[0].trace?.sample_rand)).toBe(pageloadSampleRand); - }); - }); - - sentryTest('Propagates continued sampling decision to outgoing requests', async ({ page, getLocalTestUrl }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const { pageloadSpan, pageloadSampleRand } = await sentryTest.step('Initial pageload', async () => { - const pageloadEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'pageload'), - ); - await page.goto(url); - - const envelope = await pageloadEnvelopePromise; - const pageloadSampleRand = Number(envelope[0].trace?.sample_rand); - - expect(Number(envelope[0].trace?.sample_rand)).toBe(pageloadSampleRand); - expect(pageloadSampleRand).toBeGreaterThanOrEqual(0); - expect(pageloadSampleRand).toBeLessThanOrEqual(1); - expect(Number.isNaN(pageloadSampleRand)).toBe(false); - - const pageloadSpan = envelope[1][0][1].items.find(s => getSpanOp(s) === 'pageload')!; - - expect(pageloadSpan.attributes['sentry.sample_rate']?.value).toBe(1); - - return { pageloadSpan, pageloadSampleRand }; - }); - - await sentryTest.step('Make fetch request', async () => { - const fetchEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'custom'), - ); - const tracingHeadersPromise = waitForTracingHeadersOnUrl(page, 'http://sentry-test-external.io'); - - await page.locator('#btn2').click(); - - const { baggage, sentryTrace } = await tracingHeadersPromise; - const fetchEnvelope = await fetchEnvelopePromise; - - const fetchTraceSampleRand = Number(fetchEnvelope[0].trace?.sample_rand); - const fetchTraceSpans = fetchEnvelope[1][0][1].items; - const fetchTraceSpan = fetchTraceSpans.find(s => getSpanOp(s) === 'custom')!; - const httpClientSpan = fetchTraceSpans.find(s => getSpanOp(s) === 'http.client'); - - expect(fetchTraceSampleRand).toBe(pageloadSampleRand); - - expect(fetchTraceSpan.attributes['sentry.sample_rate']?.value).toEqual( - pageloadSpan.attributes['sentry.sample_rate']?.value, - ); - expect(fetchTraceSpan.trace_id).not.toEqual(pageloadSpan.trace_id); - - expect(sentryTrace).toBeDefined(); - expect(baggage).toBeDefined(); - - expect(extractTraceparentData(sentryTrace)).toEqual({ - traceId: fetchTraceSpan.trace_id, - parentSpanId: httpClientSpan?.span_id, - parentSampled: true, - }); - - expect(parseBaggageHeader(baggage)).toEqual({ - 'sentry-environment': 'production', - 'sentry-public_key': 'public', - 'sentry-sample_rand': `${pageloadSampleRand}`, - 'sentry-sample_rate': '1', - 'sentry-sampled': 'true', - 'sentry-trace_id': fetchTraceSpan.trace_id, - 'sentry-transaction': 'custom root span 2', - }); - }); - }); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta/test.ts deleted file mode 100644 index 2657bed50d61..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta/test.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { expect } from '@playwright/test'; -import { - extractTraceparentData, - parseBaggageHeader, - SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE, -} from '@sentry/core'; -import { sentryTest } from '../../../../../../utils/fixtures'; -import { shouldSkipTracingTest, waitForTracingHeadersOnUrl } from '../../../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpanEnvelope } from '../../../../../../utils/spanUtils'; - -const metaTagSampleRand = 0.051121; -const metaTagSampleRate = 0.2; - -sentryTest.describe('When `consistentTraceSampling` is `true` and page contains tags', () => { - sentryTest('Continues sampling decision across all traces from meta tag', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const pageloadSpan = await sentryTest.step('Initial pageload', async () => { - const pageloadEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'pageload'), - ); - - await page.goto(url); - - const envelope = await pageloadEnvelopePromise; - const span = envelope[1][0][1].items.find(s => getSpanOp(s) === 'pageload')!; - - expect(Number(envelope[0].trace?.sample_rand)).toBe(metaTagSampleRand); - expect(Number(envelope[0].trace?.sample_rate)).toBe(metaTagSampleRate); - - // since the local sample rate was not applied, the sample rate attribute shouldn't be set - expect(span.attributes['sentry.sample_rate']).toBeUndefined(); - expect(span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]).toBeUndefined(); - - return span; - }); - - const customTraceSpan = await sentryTest.step('Custom trace', async () => { - const customEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'custom'), - ); - - await page.locator('#btn1').click(); - - const envelope = await customEnvelopePromise; - const span = envelope[1][0][1].items.find(s => getSpanOp(s) === 'custom')!; - - expect(span.trace_id).not.toEqual(pageloadSpan.trace_id); - expect(span.parent_span_id).toBeUndefined(); - - expect(Number(envelope[0].trace?.sample_rand)).toBe(metaTagSampleRand); - expect(Number(envelope[0].trace?.sample_rate)).toBe(metaTagSampleRate); - expect(envelope[0].trace?.sampled).toBe('true'); - - // since the local sample rate was not applied, the sample rate attribute shouldn't be set - expect(span.attributes['sentry.sample_rate']).toBeUndefined(); - - // but we need to set this attribute to still be able to correctly add the sample rate to the DSC (checked above in trace header) - expect(span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]?.value).toBe(metaTagSampleRate); - - return span; - }); - - await sentryTest.step('Navigation', async () => { - const navigationEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'navigation'), - ); - - await page.goto(`${url}#foo`); - - const envelope = await navigationEnvelopePromise; - const navSpan = envelope[1][0][1].items.find(s => getSpanOp(s) === 'navigation')!; - - expect(navSpan.trace_id).not.toEqual(pageloadSpan.trace_id); - expect(navSpan.trace_id).not.toEqual(customTraceSpan.trace_id); - - expect(navSpan.parent_span_id).toBeUndefined(); - - expect(Number(envelope[0].trace?.sample_rand)).toEqual(metaTagSampleRand); - expect(Number(envelope[0].trace?.sample_rate)).toEqual(metaTagSampleRate); - expect(envelope[0].trace?.sampled).toEqual('true'); - - // since the local sample rate was not applied, the sample rate attribute shouldn't be set - expect(navSpan.attributes['sentry.sample_rate']).toBeUndefined(); - - // but we need to set this attribute to still be able to correctly add the sample rate to the DSC (checked above in trace header) - expect(navSpan.attributes[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]?.value).toBe(metaTagSampleRate); - }); - }); - - sentryTest( - 'Propagates continued tag sampling decision to outgoing requests', - async ({ page, getLocalTestUrl }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const pageloadSpan = await sentryTest.step('Initial pageload', async () => { - const pageloadEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'pageload'), - ); - - await page.goto(url); - - const envelope = await pageloadEnvelopePromise; - const span = envelope[1][0][1].items.find(s => getSpanOp(s) === 'pageload')!; - - expect(Number(envelope[0].trace?.sample_rand)).toBe(metaTagSampleRand); - expect(Number(envelope[0].trace?.sample_rate)).toBe(metaTagSampleRate); - - // since the local sample rate was not applied, the sample rate attribute shouldn't be set - expect(span.attributes['sentry.sample_rate']).toBeUndefined(); - expect(span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]).toBeUndefined(); - - return span; - }); - - await sentryTest.step('Make fetch request', async () => { - const fetchEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'custom'), - ); - const tracingHeadersPromise = waitForTracingHeadersOnUrl(page, 'http://sentry-test-external.io'); - - await page.locator('#btn2').click(); - - const { baggage, sentryTrace } = await tracingHeadersPromise; - const fetchEnvelope = await fetchEnvelopePromise; - - const fetchTraceSampleRand = Number(fetchEnvelope[0].trace?.sample_rand); - const fetchTraceSpans = fetchEnvelope[1][0][1].items; - const fetchTraceSpan = fetchTraceSpans.find(s => getSpanOp(s) === 'custom')!; - const httpClientSpan = fetchTraceSpans.find(s => getSpanOp(s) === 'http.client'); - - expect(fetchTraceSampleRand).toEqual(metaTagSampleRand); - - expect(fetchTraceSpan.attributes['sentry.sample_rate']).toBeUndefined(); - expect(fetchTraceSpan.attributes[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]?.value).toBe( - metaTagSampleRate, - ); - - expect(fetchTraceSpan.trace_id).not.toEqual(pageloadSpan.trace_id); - - expect(sentryTrace).toBeDefined(); - expect(baggage).toBeDefined(); - - expect(extractTraceparentData(sentryTrace)).toEqual({ - traceId: fetchTraceSpan.trace_id, - parentSpanId: httpClientSpan?.span_id, - parentSampled: true, - }); - - expect(parseBaggageHeader(baggage)).toEqual({ - 'sentry-environment': 'production', - 'sentry-public_key': 'public', - 'sentry-sample_rand': `${metaTagSampleRand}`, - 'sentry-sample_rate': `${metaTagSampleRate}`, - 'sentry-sampled': 'true', - 'sentry-trace_id': fetchTraceSpan.trace_id, - 'sentry-transaction': 'custom root span 2', - }); - }); - }, - ); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/custom-trace/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/custom-trace/test.ts deleted file mode 100644 index 7bb7a5546910..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/custom-trace/test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { expect } from '@playwright/test'; -import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; - -sentryTest('manually started custom traces are linked correctly in the chain', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const pageloadSpan = await sentryTest.step('Initial pageload', async () => { - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - await page.goto(url); - return pageloadSpanPromise; - }); - - const customTraceSpan = await sentryTest.step('Custom trace', async () => { - const customSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'custom'); - await page.locator('#btn1').click(); - const span = await customSpanPromise; - - expect(span.trace_id).not.toEqual(pageloadSpan.trace_id); - expect(span.links).toEqual([ - { - trace_id: pageloadSpan.trace_id, - span_id: pageloadSpan.span_id, - sampled: true, - attributes: { - [SENTRY_LINK_TYPE]: { - type: 'string', - value: 'previous_trace', - }, - }, - }, - ]); - - return span; - }); - - await sentryTest.step('Navigation', async () => { - const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); - await page.goto(`${url}#foo`); - const navSpan = await navigationSpanPromise; - - expect(navSpan.trace_id).not.toEqual(customTraceSpan.trace_id); - expect(navSpan.trace_id).not.toEqual(pageloadSpan.trace_id); - - expect(navSpan.links).toEqual([ - { - trace_id: customTraceSpan.trace_id, - span_id: customTraceSpan.span_id, - sampled: true, - attributes: { - [SENTRY_LINK_TYPE]: { - type: 'string', - value: 'previous_trace', - }, - }, - }, - ]); - }); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/default/test.ts deleted file mode 100644 index 5fb19844a3ce..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/default/test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { expect } from '@playwright/test'; -import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; - -sentryTest("navigation spans link back to previous trace's root span", async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - await page.goto(url); - const pageloadSpan = await pageloadSpanPromise; - - const navigation1SpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); - await page.goto(`${url}#foo`); - const navigation1Span = await navigation1SpanPromise; - - const navigation2SpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); - await page.goto(`${url}#bar`); - const navigation2Span = await navigation2SpanPromise; - - const pageloadTraceId = pageloadSpan.trace_id; - const navigation1TraceId = navigation1Span.trace_id; - const navigation2TraceId = navigation2Span.trace_id; - - expect(pageloadSpan.links).toBeUndefined(); - - expect(navigation1Span.links).toEqual([ - { - trace_id: pageloadTraceId, - span_id: pageloadSpan.span_id, - sampled: true, - attributes: { - [SENTRY_LINK_TYPE]: { - type: 'string', - value: 'previous_trace', - }, - }, - }, - ]); - - expect(navigation1Span.attributes['sentry.previous_trace']).toEqual({ - type: 'string', - value: `${pageloadTraceId}-${pageloadSpan.span_id}-1`, - }); - - expect(navigation2Span.links).toEqual([ - { - trace_id: navigation1TraceId, - span_id: navigation1Span.span_id, - sampled: true, - attributes: { - [SENTRY_LINK_TYPE]: { - type: 'string', - value: 'previous_trace', - }, - }, - }, - ]); - - expect(navigation2Span.attributes['sentry.previous_trace']).toEqual({ - type: 'string', - value: `${navigation1TraceId}-${navigation1Span.span_id}-1`, - }); - - expect(pageloadTraceId).not.toEqual(navigation1TraceId); - expect(navigation1TraceId).not.toEqual(navigation2TraceId); - expect(pageloadTraceId).not.toEqual(navigation2TraceId); -}); - -sentryTest("doesn't link between hard page reloads by default", async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - await sentryTest.step('First pageload', async () => { - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - await page.goto(url); - const pageload1Span = await pageloadSpanPromise; - - expect(pageload1Span).toBeDefined(); - expect(pageload1Span.links).toBeUndefined(); - }); - - await sentryTest.step('Second pageload', async () => { - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - await page.reload(); - const pageload2Span = await pageloadSpanPromise; - - expect(pageload2Span).toBeDefined(); - expect(pageload2Span.links).toBeUndefined(); - }); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/interaction-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/interaction-spans/test.ts deleted file mode 100644 index bfc974fc953a..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/interaction-spans/test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { expect } from '@playwright/test'; -import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; - -/* - This is quite peculiar behavior but it's a result of the route-based trace lifetime. - Once we shortened trace lifetime, this whole scenario will change as the interaction - spans will be their own trace. So most likely, we can replace this test with a new one - that covers the new default behavior. -*/ -sentryTest( - 'only the first root spans in the trace link back to the previous trace', - async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const pageloadSpan = await sentryTest.step('Initial pageload', async () => { - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - await page.goto(url); - const span = await pageloadSpanPromise; - - expect(span).toBeDefined(); - expect(span.links).toBeUndefined(); - - return span; - }); - - await sentryTest.step('Click Before navigation', async () => { - const interactionSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.action.click'); - await page.click('#btn'); - const interactionSpan = await interactionSpanPromise; - - // sanity check: route-based trace lifetime means the trace_id should be the same - expect(interactionSpan.trace_id).toBe(pageloadSpan.trace_id); - - // no links yet as previous root span belonged to same trace - expect(interactionSpan.links).toBeUndefined(); - }); - - const navigationSpan = await sentryTest.step('Navigation', async () => { - const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); - await page.goto(`${url}#foo`); - const span = await navigationSpanPromise; - - expect(getSpanOp(span)).toBe('navigation'); - expect(span.links).toEqual([ - { - trace_id: pageloadSpan.trace_id, - span_id: pageloadSpan.span_id, - sampled: true, - attributes: { - [SENTRY_LINK_TYPE]: { - type: 'string', - value: 'previous_trace', - }, - }, - }, - ]); - - expect(span.trace_id).not.toEqual(span.links![0].trace_id); - return span; - }); - - await sentryTest.step('Click After navigation', async () => { - const interactionSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.action.click'); - await page.click('#btn'); - const interactionSpan = await interactionSpanPromise; - - // sanity check: route-based trace lifetime means the trace_id should be the same - expect(interactionSpan.trace_id).toBe(navigationSpan.trace_id); - - // since this is the second root span in the trace, it doesn't link back to the previous trace - expect(interactionSpan.links).toBeUndefined(); - }); - }, -); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/meta/test.ts deleted file mode 100644 index 732547954091..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/meta/test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { expect } from '@playwright/test'; -import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; - -sentryTest( - "links back to previous trace's local root span if continued from meta tags", - async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const metaTagTraceId = '12345678901234567890123456789012'; - - const pageloadSpan = await sentryTest.step('Initial pageload', async () => { - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - await page.goto(url); - const span = await pageloadSpanPromise; - - // sanity check - expect(span.trace_id).toBe(metaTagTraceId); - expect(span.links).toBeUndefined(); - - return span; - }); - - const navigationSpan = await sentryTest.step('Navigation', async () => { - const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); - await page.goto(`${url}#foo`); - return navigationSpanPromise; - }); - - expect(navigationSpan.links).toEqual([ - { - trace_id: metaTagTraceId, - span_id: pageloadSpan.span_id, - sampled: true, - attributes: { - [SENTRY_LINK_TYPE]: { - type: 'string', - value: 'previous_trace', - }, - }, - }, - ]); - - expect(navigationSpan.trace_id).not.toEqual(metaTagTraceId); - }, -); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/negatively-sampled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/negatively-sampled/test.ts deleted file mode 100644 index 4bc05cb95737..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/negatively-sampled/test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { expect } from '@playwright/test'; -import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; - -sentryTest('includes a span link to a previously negatively sampled span', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - await sentryTest.step('Initial pageload', async () => { - // No span envelope expected here because this pageload span is sampled negatively! - await page.goto(url); - }); - - await sentryTest.step('Navigation', async () => { - const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); - await page.goto(`${url}#foo`); - const navigationSpan = await navigationSpanPromise; - - expect(getSpanOp(navigationSpan)).toBe('navigation'); - expect(navigationSpan.links).toEqual([ - { - trace_id: expect.stringMatching(/[a-f\d]{32}/), - span_id: expect.stringMatching(/[a-f\d]{16}/), - sampled: false, - attributes: { - [SENTRY_LINK_TYPE]: { - type: 'string', - value: 'previous_trace', - }, - }, - }, - ]); - - expect(navigationSpan.attributes['sentry.previous_trace']).toEqual({ - type: 'string', - value: expect.stringMatching(/[a-f\d]{32}-[a-f\d]{16}-0/), - }); - - expect(navigationSpan.trace_id).not.toEqual(navigationSpan.links![0].trace_id); - }); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/session-storage/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/session-storage/test.ts deleted file mode 100644 index 81d1d31e10e3..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/session-storage/test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { expect } from '@playwright/test'; -import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; - -sentryTest('adds link between hard page reloads when opting into sessionStorage', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const pageload1Span = await sentryTest.step('First pageload', async () => { - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - await page.goto(url); - const span = await pageloadSpanPromise; - expect(span).toBeDefined(); - expect(span.links).toBeUndefined(); - return span; - }); - - const pageload2Span = await sentryTest.step('Hard page reload', async () => { - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - await page.reload(); - return pageloadSpanPromise; - }); - - expect(pageload2Span.links).toEqual([ - { - trace_id: pageload1Span.trace_id, - span_id: pageload1Span.span_id, - sampled: true, - attributes: { - [SENTRY_LINK_TYPE]: { - type: 'string', - value: 'previous_trace', - }, - }, - }, - ]); - - expect(pageload1Span.trace_id).not.toEqual(pageload2Span.trace_id); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/init.js index 04a299421004..2497d6da6b84 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/test.ts index 4d13371bc693..28c0b0ea1796 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/test.ts @@ -1,136 +1,140 @@ import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { extractTraceparentData, parseBaggageHeader, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, extractTraceparentData, parseBaggageHeader } from '@sentry/core'; import { sentryTest } from '../../../../../../utils/fixtures'; -import { - eventAndTraceHeaderRequestParser, - shouldSkipTracingTest, - waitForTracingHeadersOnUrl, - waitForTransactionRequest, -} from '../../../../../../utils/helpers'; +import { shouldSkipTracingTest, waitForTracingHeadersOnUrl } from '../../../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpanEnvelope } from '../../../../../../utils/spanUtils'; sentryTest.describe('When `consistentTraceSampling` is `true`', () => { - sentryTest('Continues sampling decision from initial pageload', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest('continues sampling decision from initial pageload span', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - const { pageloadTraceContext, pageloadSampleRand } = await sentryTest.step('Initial pageload', async () => { - const pageloadRequestPromise = waitForTransactionRequest(page, evt => { - return evt.contexts?.trace?.op === 'pageload'; - }); + const { pageloadSpan, pageloadSampleRand } = await sentryTest.step('Initial pageload', async () => { + const pageloadEnvelopePromise = waitForStreamedSpanEnvelope( + page, + env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'pageload'), + ); await page.goto(url); - const res = eventAndTraceHeaderRequestParser(await pageloadRequestPromise); - const pageloadSampleRand = Number(res[1]?.sample_rand); - const pageloadTraceContext = res[0].contexts?.trace; + const envelope = await pageloadEnvelopePromise; + const pageloadSampleRand = Number(envelope[0].trace?.sample_rand); + const pageloadSpan = envelope[1][0][1].items.find(s => getSpanOp(s) === 'pageload')!; - expect(pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBe(1); + expect(pageloadSpan.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]?.value).toBe(1); + expect(Number.isNaN(pageloadSampleRand)).toBe(false); expect(pageloadSampleRand).toBeGreaterThanOrEqual(0); + expect(pageloadSampleRand).toBeLessThanOrEqual(1); - return { pageloadTraceContext: res[0].contexts?.trace, pageloadSampleRand }; + return { pageloadSpan, pageloadSampleRand }; }); - const customTraceContext = await sentryTest.step('Custom trace', async () => { - const customTrace1RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'custom'); - await page.locator('#btn1').click(); - const [customTrace1Event, customTraceTraceHeader] = eventAndTraceHeaderRequestParser( - await customTrace1RequestPromise, + const customTraceSpan = await sentryTest.step('Custom trace', async () => { + const customEnvelopePromise = waitForStreamedSpanEnvelope( + page, + env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'custom'), ); + await page.locator('#btn1').click(); + const envelope = await customEnvelopePromise; + const span = envelope[1][0][1].items.find(s => getSpanOp(s) === 'custom')!; - const customTraceContext = customTrace1Event.contexts?.trace; - - expect(customTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + expect(span.trace_id).not.toEqual(pageloadSpan.trace_id); // although we "continue the trace" from pageload, this is actually a root span, // so there must not be a parent span id - expect(customTraceContext?.parent_span_id).toBeUndefined(); + expect(span.parent_span_id).toBeUndefined(); - expect(pageloadSampleRand).toEqual(Number(customTraceTraceHeader?.sample_rand)); + expect(Number(envelope[0].trace?.sample_rand)).toBe(pageloadSampleRand); - return customTraceContext; + return span; }); await sentryTest.step('Navigation', async () => { - const navigation1RequestPromise = waitForTransactionRequest( + const navigationEnvelopePromise = waitForStreamedSpanEnvelope( page, - evt => evt.contexts?.trace?.op === 'navigation', + env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'navigation'), ); await page.goto(`${url}#foo`); - const [navigationEvent, navigationTraceHeader] = eventAndTraceHeaderRequestParser( - await navigation1RequestPromise, - ); - const navTraceContext = navigationEvent.contexts?.trace; + const envelope = await navigationEnvelopePromise; + const navSpan = envelope[1][0][1].items.find(s => getSpanOp(s) === 'navigation')!; - expect(navTraceContext?.trace_id).not.toEqual(customTraceContext?.trace_id); - expect(navTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + expect(navSpan.trace_id).not.toEqual(customTraceSpan.trace_id); + expect(navSpan.trace_id).not.toEqual(pageloadSpan.trace_id); - expect(navTraceContext?.links).toEqual([ + expect(navSpan.links).toEqual([ { - trace_id: customTraceContext?.trace_id, - span_id: customTraceContext?.span_id, + trace_id: customTraceSpan.trace_id, + span_id: customTraceSpan.span_id, sampled: true, attributes: { - [SENTRY_LINK_TYPE]: 'previous_trace', + [SENTRY_LINK_TYPE]: { + type: 'string', + value: 'previous_trace', + }, }, }, ]); - expect(navTraceContext?.parent_span_id).toBeUndefined(); + expect(navSpan.parent_span_id).toBeUndefined(); - expect(pageloadSampleRand).toEqual(Number(navigationTraceHeader?.sample_rand)); + expect(Number(envelope[0].trace?.sample_rand)).toBe(pageloadSampleRand); }); }); sentryTest('Propagates continued sampling decision to outgoing requests', async ({ page, getLocalTestUrl }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - const { pageloadTraceContext, pageloadSampleRand } = await sentryTest.step('Initial pageload', async () => { - const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + const { pageloadSpan, pageloadSampleRand } = await sentryTest.step('Initial pageload', async () => { + const pageloadEnvelopePromise = waitForStreamedSpanEnvelope( + page, + env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'pageload'), + ); await page.goto(url); - const res = eventAndTraceHeaderRequestParser(await pageloadRequestPromise); - const pageloadSampleRand = Number(res[1]?.sample_rand); + const envelope = await pageloadEnvelopePromise; + const pageloadSampleRand = Number(envelope[0].trace?.sample_rand); expect(pageloadSampleRand).toBeGreaterThanOrEqual(0); + expect(pageloadSampleRand).toBeLessThanOrEqual(1); + expect(Number.isNaN(pageloadSampleRand)).toBe(false); - const pageloadTraceContext = res[0].contexts?.trace; + const pageloadSpan = envelope[1][0][1].items.find(s => getSpanOp(s) === 'pageload')!; - expect(pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBe(1); + expect(pageloadSpan.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]?.value).toBe(1); - return { pageloadTraceContext: pageloadTraceContext, pageloadSampleRand }; + return { pageloadSpan, pageloadSampleRand }; }); await sentryTest.step('Make fetch request', async () => { - const fetchTracePromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'custom'); + const fetchEnvelopePromise = waitForStreamedSpanEnvelope( + page, + env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'custom'), + ); const tracingHeadersPromise = waitForTracingHeadersOnUrl(page, 'http://sentry-test-external.io'); await page.locator('#btn2').click(); const { baggage, sentryTrace } = await tracingHeadersPromise; + const fetchEnvelope = await fetchEnvelopePromise; - const [fetchTraceEvent, fetchTraceTraceHeader] = eventAndTraceHeaderRequestParser(await fetchTracePromise); - - const fetchTraceSampleRand = Number(fetchTraceTraceHeader?.sample_rand); - const fetchTraceTraceContext = fetchTraceEvent.contexts?.trace; - const httpClientSpan = fetchTraceEvent.spans?.find(span => span.op === 'http.client'); + const fetchTraceSampleRand = Number(fetchEnvelope[0].trace?.sample_rand); + const fetchTraceSpans = fetchEnvelope[1][0][1].items; + const fetchTraceSpan = fetchTraceSpans.find(s => getSpanOp(s) === 'custom')!; + const httpClientSpan = fetchTraceSpans.find(s => getSpanOp(s) === 'http.client'); - expect(fetchTraceSampleRand).toEqual(pageloadSampleRand); + expect(fetchTraceSampleRand).toBe(pageloadSampleRand); - expect(fetchTraceTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toEqual( - pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE], + expect(fetchTraceSpan.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]?.value).toEqual( + pageloadSpan.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]?.value, ); - expect(fetchTraceTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + expect(fetchTraceSpan.trace_id).not.toEqual(pageloadSpan.trace_id); expect(sentryTrace).toBeDefined(); expect(baggage).toBeDefined(); expect(extractTraceparentData(sentryTrace)).toEqual({ - traceId: fetchTraceTraceContext?.trace_id, + traceId: fetchTraceSpan.trace_id, parentSpanId: httpClientSpan?.span_id, parentSampled: true, }); @@ -141,7 +145,7 @@ sentryTest.describe('When `consistentTraceSampling` is `true`', () => { 'sentry-sample_rand': `${pageloadSampleRand}`, 'sentry-sample_rate': '1', 'sentry-sampled': 'true', - 'sentry-trace_id': fetchTraceTraceContext?.trace_id, + 'sentry-trace_id': fetchTraceSpan.trace_id, 'sentry-transaction': 'custom root span 2', }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-negative/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-negative/init.js index 05c4ec8db85a..4cb0d3389fc2 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-negative/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-negative/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-negative/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-negative/test.ts index c8faee2f5feb..34401725079c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-negative/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-negative/test.ts @@ -1,15 +1,15 @@ import { expect } from '@playwright/test'; -import type { ClientReport } from '@sentry/core'; +import type { ClientReport, SerializedStreamedSpan } from '@sentry/core'; import { extractTraceparentData, parseBaggageHeader } from '@sentry/core'; import { sentryTest } from '../../../../../../utils/fixtures'; import { envelopeRequestParser, - getMultipleSentryEnvelopeRequests, hidePage, shouldSkipTracingTest, waitForClientReportRequest, waitForTracingHeadersOnUrl, } from '../../../../../../utils/helpers'; +import { observeStreamedSpan } from '../../../../../../utils/spanUtils'; const metaTagSampleRand = 0.9; const metaTagSampleRate = 0.2; @@ -19,34 +19,31 @@ sentryTest.describe('When `consistentTraceSampling` is `true` and page contains sentryTest( 'Continues negative sampling decision from meta tag across all traces and downstream propagations', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - let txnsReceived = 0; - // @ts-expect-error - no need to return something valid here - getMultipleSentryEnvelopeRequests(page, 1, { envelopeType: 'transaction' }, () => { - ++txnsReceived; - return {}; + const spansReceived: SerializedStreamedSpan[] = []; + observeStreamedSpan(page, span => { + spansReceived.push(span); + return false; }); const clientReportPromise = waitForClientReportRequest(page); await sentryTest.step('Initial pageload', async () => { await page.goto(url); - expect(txnsReceived).toEqual(0); + expect(spansReceived).toHaveLength(0); }); await sentryTest.step('Custom instrumented button click', async () => { await page.locator('#btn1').click(); - expect(txnsReceived).toEqual(0); + expect(spansReceived).toHaveLength(0); }); await sentryTest.step('Navigation', async () => { await page.goto(`${url}#foo`); - expect(txnsReceived).toEqual(0); + expect(spansReceived).toHaveLength(0); }); await sentryTest.step('Make fetch request', async () => { @@ -73,6 +70,8 @@ sentryTest.describe('When `consistentTraceSampling` is `true` and page contains 'sentry-trace_id': expect.not.stringContaining(metaTagTraceId), 'sentry-transaction': 'custom root span 2', }); + + expect(spansReceived).toHaveLength(0); }); await sentryTest.step('Client report', async () => { @@ -82,19 +81,18 @@ sentryTest.describe('When `consistentTraceSampling` is `true` and page contains timestamp: expect.any(Number), discarded_events: [ { - category: 'transaction', - quantity: 4, + category: 'span', + quantity: expect.any(Number), reason: 'sample_rate', }, ], }); + // exact number depends on performance observer emissions + expect(clientReport.discarded_events[0].quantity).toBeGreaterThanOrEqual(10); }); - await sentryTest.step('Wait for transactions to be discarded', async () => { - // give it a little longer just in case a txn is pending to be sent - await page.waitForTimeout(1000); - expect(txnsReceived).toEqual(0); - }); + await page.evaluate(() => (window as any).Sentry.flush()); + expect(spansReceived).toHaveLength(0); }, ); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/init.js index fbf897d47eab..c758d9875be1 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/test.ts index 3dab9594ba7c..cd64596f9bfd 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/test.ts @@ -4,13 +4,12 @@ import { extractTraceparentData, parseBaggageHeader } from '@sentry/core'; import { sentryTest } from '../../../../../../utils/fixtures'; import { envelopeRequestParser, - eventAndTraceHeaderRequestParser, hidePage, shouldSkipTracingTest, waitForClientReportRequest, waitForTracingHeadersOnUrl, - waitForTransactionRequest, } from '../../../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpanEnvelope } from '../../../../../../utils/spanUtils'; const metaTagSampleRand = 0.9; const metaTagSampleRate = 0.2; @@ -21,16 +20,14 @@ sentryTest.describe('When `consistentTraceSampling` is `true` and page contains sentryTest( 'meta tag decision has precedence over sampling decision from previous trace in session storage', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); const clientReportPromise = waitForClientReportRequest(page); await sentryTest.step('Initial pageload', async () => { - // negative sampling decision -> no pageload txn + // negative sampling decision -> no pageload span await page.goto(url); }); @@ -71,37 +68,49 @@ sentryTest.describe('When `consistentTraceSampling` is `true` and page contains timestamp: expect.any(Number), discarded_events: [ { - category: 'transaction', - quantity: 2, + category: 'span', + quantity: expect.any(Number), reason: 'sample_rate', }, ], }); + // exact number depends on performance observer emissions + expect(clientReport.discarded_events[0].quantity).toBeGreaterThanOrEqual(3); }); await sentryTest.step('Navigate to another page with meta tags', async () => { - const page1Pageload = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + const page1PageloadEnvelopePromise = waitForStreamedSpanEnvelope( + page, + env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'pageload' && s.trace_id === metaTagTraceIdPage1), + ); await page.locator('a').click(); - const [pageloadEvent, pageloadTraceHeader] = eventAndTraceHeaderRequestParser(await page1Pageload); - const pageloadTraceContext = pageloadEvent.contexts?.trace; + const envelope = await page1PageloadEnvelopePromise; + const pageloadSpan = envelope[1][0][1].items.find(s => getSpanOp(s) === 'pageload')!; - expect(Number(pageloadTraceHeader?.sample_rand)).toBe(0.12); - expect(Number(pageloadTraceHeader?.sample_rate)).toBe(0.2); - expect(pageloadTraceContext?.trace_id).toEqual(metaTagTraceIdPage1); + expect(Number(envelope[0].trace?.sample_rand)).toBe(0.12); + expect(Number(envelope[0].trace?.sample_rate)).toBe(0.2); + expect(pageloadSpan.trace_id).toEqual(metaTagTraceIdPage1); }); await sentryTest.step('Navigate to another page without meta tags', async () => { - const page2Pageload = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + const page2PageloadEnvelopePromise = waitForStreamedSpanEnvelope( + page, + env => + !!env[1][0][1].items.find( + s => + getSpanOp(s) === 'pageload' && s.trace_id !== metaTagTraceIdPage1 && s.trace_id !== metaTagTraceIdIndex, + ), + ); await page.locator('a').click(); - const [pageloadEvent, pageloadTraceHeader] = eventAndTraceHeaderRequestParser(await page2Pageload); - const pageloadTraceContext = pageloadEvent.contexts?.trace; + const envelope = await page2PageloadEnvelopePromise; + const pageloadSpan = envelope[1][0][1].items.find(s => getSpanOp(s) === 'pageload')!; - expect(Number(pageloadTraceHeader?.sample_rand)).toBe(0.12); - expect(Number(pageloadTraceHeader?.sample_rate)).toBe(0.2); - expect(pageloadTraceContext?.trace_id).not.toEqual(metaTagTraceIdPage1); - expect(pageloadTraceContext?.trace_id).not.toEqual(metaTagTraceIdIndex); + expect(Number(envelope[0].trace?.sample_rand)).toBe(0.12); + expect(Number(envelope[0].trace?.sample_rate)).toBe(0.2); + expect(pageloadSpan.trace_id).not.toEqual(metaTagTraceIdPage1); + expect(pageloadSpan.trace_id).not.toEqual(metaTagTraceIdIndex); }); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta/init.js index 61e2b2daa9ba..9aa63000ef2e 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta/test.ts index ac2a865fb181..bc2eb2118a09 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta/test.ts @@ -1,162 +1,158 @@ import { expect } from '@playwright/test'; import { + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, extractTraceparentData, parseBaggageHeader, SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, } from '@sentry/core'; import { sentryTest } from '../../../../../../utils/fixtures'; -import { - eventAndTraceHeaderRequestParser, - shouldSkipTracingTest, - waitForTracingHeadersOnUrl, - waitForTransactionRequest, -} from '../../../../../../utils/helpers'; +import { shouldSkipTracingTest, waitForTracingHeadersOnUrl } from '../../../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpanEnvelope } from '../../../../../../utils/spanUtils'; const metaTagSampleRand = 0.051121; const metaTagSampleRate = 0.2; sentryTest.describe('When `consistentTraceSampling` is `true` and page contains tags', () => { sentryTest('Continues sampling decision across all traces from meta tag', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => { - const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + const pageloadSpan = await sentryTest.step('Initial pageload', async () => { + const pageloadEnvelopePromise = waitForStreamedSpanEnvelope( + page, + env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'pageload'), + ); await page.goto(url); - const [pageloadEvent, pageloadTraceHeader] = eventAndTraceHeaderRequestParser(await pageloadRequestPromise); - const pageloadTraceContext = pageloadEvent.contexts?.trace; + const envelope = await pageloadEnvelopePromise; + const span = envelope[1][0][1].items.find(s => getSpanOp(s) === 'pageload')!; - expect(Number(pageloadTraceHeader?.sample_rand)).toBe(metaTagSampleRand); - expect(Number(pageloadTraceHeader?.sample_rate)).toBe(metaTagSampleRate); + expect(Number(envelope[0].trace?.sample_rand)).toBe(metaTagSampleRand); + expect(Number(envelope[0].trace?.sample_rate)).toBe(metaTagSampleRate); // since the local sample rate was not applied, the sample rate attribute shouldn't be set - expect(pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBeUndefined(); - expect(pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]).toBeUndefined(); + expect(span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBeUndefined(); + expect(span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]).toBeUndefined(); - return pageloadTraceContext; + return span; }); - const customTraceContext = await sentryTest.step('Custom trace', async () => { - const customTrace1RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'custom'); + const customTraceSpan = await sentryTest.step('Custom trace', async () => { + const customEnvelopePromise = waitForStreamedSpanEnvelope( + page, + env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'custom'), + ); await page.locator('#btn1').click(); - const [customTrace1Event, customTraceTraceHeader] = eventAndTraceHeaderRequestParser( - await customTrace1RequestPromise, - ); + const envelope = await customEnvelopePromise; + const span = envelope[1][0][1].items.find(s => getSpanOp(s) === 'custom')!; - const customTraceContext = customTrace1Event.contexts?.trace; + expect(span.trace_id).not.toEqual(pageloadSpan.trace_id); + expect(span.parent_span_id).toBeUndefined(); - expect(customTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); - expect(customTraceContext?.parent_span_id).toBeUndefined(); - - expect(Number(customTraceTraceHeader?.sample_rand)).toBe(metaTagSampleRand); - expect(Number(customTraceTraceHeader?.sample_rate)).toBe(metaTagSampleRate); - expect(Boolean(customTraceTraceHeader?.sampled)).toBe(true); + expect(Number(envelope[0].trace?.sample_rand)).toBe(metaTagSampleRand); + expect(Number(envelope[0].trace?.sample_rate)).toBe(metaTagSampleRate); + expect(envelope[0].trace?.sampled).toBe('true'); // since the local sample rate was not applied, the sample rate attribute shouldn't be set - expect(customTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBeUndefined(); + expect(span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBeUndefined(); // but we need to set this attribute to still be able to correctly add the sample rate to the DSC (checked above in trace header) - expect(customTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]).toBe(metaTagSampleRate); + expect(span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]?.value).toBe(metaTagSampleRate); - return customTraceContext; + return span; }); await sentryTest.step('Navigation', async () => { - const navigation1RequestPromise = waitForTransactionRequest( + const navigationEnvelopePromise = waitForStreamedSpanEnvelope( page, - evt => evt.contexts?.trace?.op === 'navigation', + env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'navigation'), ); await page.goto(`${url}#foo`); - const [navigationEvent, navigationTraceHeader] = eventAndTraceHeaderRequestParser( - await navigation1RequestPromise, - ); - - const navigationTraceContext = navigationEvent.contexts?.trace; + const envelope = await navigationEnvelopePromise; + const navSpan = envelope[1][0][1].items.find(s => getSpanOp(s) === 'navigation')!; - expect(navigationTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); - expect(navigationTraceContext?.trace_id).not.toEqual(customTraceContext?.trace_id); + expect(navSpan.trace_id).not.toEqual(pageloadSpan.trace_id); + expect(navSpan.trace_id).not.toEqual(customTraceSpan.trace_id); - expect(navigationTraceContext?.parent_span_id).toBeUndefined(); + expect(navSpan.parent_span_id).toBeUndefined(); - expect(Number(navigationTraceHeader?.sample_rand)).toEqual(metaTagSampleRand); - expect(Number(navigationTraceHeader?.sample_rate)).toEqual(metaTagSampleRate); - expect(Boolean(navigationTraceHeader?.sampled)).toEqual(true); + expect(Number(envelope[0].trace?.sample_rand)).toEqual(metaTagSampleRand); + expect(Number(envelope[0].trace?.sample_rate)).toEqual(metaTagSampleRate); + expect(envelope[0].trace?.sampled).toEqual('true'); // since the local sample rate was not applied, the sample rate attribute shouldn't be set - expect(navigationTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBeUndefined(); + expect(navSpan.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBeUndefined(); // but we need to set this attribute to still be able to correctly add the sample rate to the DSC (checked above in trace header) - expect(navigationTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]).toBe( - metaTagSampleRate, - ); + expect(navSpan.attributes[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]?.value).toBe(metaTagSampleRate); }); }); sentryTest( 'Propagates continued tag sampling decision to outgoing requests', async ({ page, getLocalTestUrl }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => { - const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + const pageloadSpan = await sentryTest.step('Initial pageload', async () => { + const pageloadEnvelopePromise = waitForStreamedSpanEnvelope( + page, + env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'pageload'), + ); await page.goto(url); - const [pageloadEvent, pageloadTraceHeader] = eventAndTraceHeaderRequestParser(await pageloadRequestPromise); - const pageloadTraceContext = pageloadEvent.contexts?.trace; + const envelope = await pageloadEnvelopePromise; + const span = envelope[1][0][1].items.find(s => getSpanOp(s) === 'pageload')!; - expect(Number(pageloadTraceHeader?.sample_rand)).toBe(metaTagSampleRand); - expect(Number(pageloadTraceHeader?.sample_rate)).toBe(metaTagSampleRate); + expect(Number(envelope[0].trace?.sample_rand)).toBe(metaTagSampleRand); + expect(Number(envelope[0].trace?.sample_rate)).toBe(metaTagSampleRate); // since the local sample rate was not applied, the sample rate attribute shouldn't be set - expect(pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBeUndefined(); - expect(pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]).toBeUndefined(); + expect(span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBeUndefined(); + expect(span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]).toBeUndefined(); - return pageloadTraceContext; + return span; }); await sentryTest.step('Make fetch request', async () => { - const fetchTracePromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'custom'); + const fetchEnvelopePromise = waitForStreamedSpanEnvelope( + page, + env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'custom'), + ); const tracingHeadersPromise = waitForTracingHeadersOnUrl(page, 'http://sentry-test-external.io'); await page.locator('#btn2').click(); const { baggage, sentryTrace } = await tracingHeadersPromise; + const fetchEnvelope = await fetchEnvelopePromise; - const [fetchTraceEvent, fetchTraceTraceHeader] = eventAndTraceHeaderRequestParser(await fetchTracePromise); - - const fetchTraceSampleRand = Number(fetchTraceTraceHeader?.sample_rand); - const fetchTraceTraceContext = fetchTraceEvent.contexts?.trace; - const httpClientSpan = fetchTraceEvent.spans?.find(span => span.op === 'http.client'); + const fetchTraceSampleRand = Number(fetchEnvelope[0].trace?.sample_rand); + const fetchTraceSpans = fetchEnvelope[1][0][1].items; + const fetchTraceSpan = fetchTraceSpans.find(s => getSpanOp(s) === 'custom')!; + const httpClientSpan = fetchTraceSpans.find(s => getSpanOp(s) === 'http.client'); expect(fetchTraceSampleRand).toEqual(metaTagSampleRand); - expect(fetchTraceTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBeUndefined(); - expect(fetchTraceTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]).toBe( + expect(fetchTraceSpan.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBeUndefined(); + expect(fetchTraceSpan.attributes[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]?.value).toBe( metaTagSampleRate, ); - expect(fetchTraceTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + expect(fetchTraceSpan.trace_id).not.toEqual(pageloadSpan.trace_id); expect(sentryTrace).toBeDefined(); expect(baggage).toBeDefined(); expect(extractTraceparentData(sentryTrace)).toEqual({ - traceId: fetchTraceTraceContext?.trace_id, + traceId: fetchTraceSpan.trace_id, parentSpanId: httpClientSpan?.span_id, parentSampled: true, }); @@ -167,7 +163,7 @@ sentryTest.describe('When `consistentTraceSampling` is `true` and page contains 'sentry-sample_rand': `${metaTagSampleRand}`, 'sentry-sample_rate': `${metaTagSampleRate}`, 'sentry-sampled': 'true', - 'sentry-trace_id': fetchTraceTraceContext?.trace_id, + 'sentry-trace_id': fetchTraceSpan.trace_id, 'sentry-transaction': 'custom root span 2', }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/init.js index e2efc43e50f2..e99cf357559a 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/test.ts index 2bb196c898fd..801f0a51d3bf 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/test.ts @@ -1,15 +1,17 @@ +import { PAGELOAD, NAVIGATION } from '@sentry/conventions/op'; +import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import type { ClientReport } from '@sentry/core'; + import { sentryTest } from '../../../../../../utils/fixtures'; import { envelopeRequestParser, - eventAndTraceHeaderRequestParser, hidePage, shouldSkipTracingTest, waitForClientReportRequest, - waitForTransactionRequest, } from '../../../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpanEnvelope } from '../../../../../../utils/spanUtils'; /** * This test demonstrates that: @@ -18,26 +20,24 @@ import { */ sentryTest.describe('When `consistentTraceSampling` is `true`', () => { sentryTest('explicit sampling decisions in `tracesSampler` have precedence', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - const { pageloadTraceContext } = await sentryTest.step('Initial pageload', async () => { - const pageloadRequestPromise = waitForTransactionRequest(page, evt => { - return evt.contexts?.trace?.op === 'pageload'; - }); + const { pageloadSpan } = await sentryTest.step('Initial pageload', async () => { + const pageloadEnvelopePromise = waitForStreamedSpanEnvelope( + page, + env => !!env[1][0][1].items.find(s => getSpanOp(s) === PAGELOAD), + ); await page.goto(url); - const res = eventAndTraceHeaderRequestParser(await pageloadRequestPromise); - const pageloadSampleRand = Number(res[1]?.sample_rand); - const pageloadTraceContext = res[0].contexts?.trace; + const envelope = await pageloadEnvelopePromise; + const pageloadSpan = envelope[1][0][1].items.find(s => getSpanOp(s) === PAGELOAD)!; - expect(pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBe(1); - expect(pageloadSampleRand).toBeGreaterThanOrEqual(0); + expect(pageloadSpan.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]?.value).toBe(1); + expect(Number(envelope[0].trace?.sample_rand)).toBeGreaterThanOrEqual(0); - return { pageloadTraceContext: res[0].contexts?.trace, pageloadSampleRand }; + return { pageloadSpan }; }); await sentryTest.step('Custom trace is sampled negatively (explicitly in tracesSampler)', async () => { @@ -45,7 +45,6 @@ sentryTest.describe('When `consistentTraceSampling` is `true`', () => { await page.locator('#btn1').click(); - await page.waitForTimeout(500); await hidePage(page); const clientReport = envelopeRequestParser(await clientReportPromise); @@ -54,12 +53,13 @@ sentryTest.describe('When `consistentTraceSampling` is `true`', () => { timestamp: expect.any(Number), discarded_events: [ { - category: 'transaction', - quantity: 1, + category: 'span', + quantity: expect.any(Number), reason: 'sample_rate', }, ], }); + expect(clientReport.discarded_events[0].quantity).toBeGreaterThanOrEqual(1); }); await sentryTest.step('Subsequent navigation trace is also sampled negatively', async () => { @@ -67,8 +67,6 @@ sentryTest.describe('When `consistentTraceSampling` is `true`', () => { await page.goto(`${url}#foo`); - await page.waitForTimeout(500); - await hidePage(page); const clientReport = envelopeRequestParser(await clientReportPromise); @@ -77,61 +75,76 @@ sentryTest.describe('When `consistentTraceSampling` is `true`', () => { timestamp: expect.any(Number), discarded_events: [ { - category: 'transaction', - quantity: 1, + category: 'span', + quantity: expect.any(Number), reason: 'sample_rate', }, ], }); + expect(clientReport.discarded_events[0].quantity).toBeGreaterThanOrEqual(1); }); - const { customTrace2Context } = await sentryTest.step( + const { customTrace2Span } = await sentryTest.step( 'Custom trace 2 is sampled positively (explicitly in tracesSampler)', async () => { - const customTrace2RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'custom'); + const customEnvelopePromise = waitForStreamedSpanEnvelope( + page, + env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'custom'), + ); await page.locator('#btn2').click(); - const [customTrace2Event] = eventAndTraceHeaderRequestParser(await customTrace2RequestPromise); + const envelope = await customEnvelopePromise; + const customTrace2Span = envelope[1][0][1].items.find(s => getSpanOp(s) === 'custom')!; - const customTrace2Context = customTrace2Event.contexts?.trace; + expect(customTrace2Span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]?.value).toBe(1); + expect(customTrace2Span.trace_id).not.toEqual(pageloadSpan.trace_id); + expect(customTrace2Span.parent_span_id).toBeUndefined(); - expect(customTrace2Context?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBe(1); - expect(customTrace2Context?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); - expect(customTrace2Context?.parent_span_id).toBeUndefined(); - - expect(customTrace2Context?.links).toEqual([ + expect(customTrace2Span.links).toEqual([ { - attributes: { 'sentry.link.type': 'previous_trace' }, + attributes: { + [SENTRY_LINK_TYPE]: { + type: 'string', + value: 'previous_trace', + }, + }, sampled: false, span_id: expect.stringMatching(/^[\da-f]{16}$/), trace_id: expect.stringMatching(/^[\da-f]{32}$/), }, ]); - return { customTrace2Context }; + return { customTrace2Span }; }, ); await sentryTest.step('Navigation trace is sampled positively (inherited from previous trace)', async () => { - const navigationRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation'); + const navigationEnvelopePromise = waitForStreamedSpanEnvelope( + page, + env => env[0].trace?.sampled === 'true' && !!env[1][0][1].items.find(s => getSpanOp(s) === NAVIGATION), + ); await page.goto(`${url}#bar`); - const [navigationEvent] = eventAndTraceHeaderRequestParser(await navigationRequestPromise); - - const navigationTraceContext = navigationEvent.contexts?.trace; + const envelope = await navigationEnvelopePromise; + const navigationSpan = envelope[1][0][1].items.find(s => getSpanOp(s) === NAVIGATION)!; - expect(navigationTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBe(1); - expect(navigationTraceContext?.trace_id).not.toEqual(customTrace2Context?.trace_id); - expect(navigationTraceContext?.parent_span_id).toBeUndefined(); + expect(navigationSpan.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]?.value).toBe(1); + expect(navigationSpan.trace_id).not.toEqual(customTrace2Span.trace_id); + expect(navigationSpan.parent_span_id).toBeUndefined(); - expect(navigationTraceContext?.links).toEqual([ + expect(navigationSpan.links).toEqual([ { - attributes: { 'sentry.link.type': 'previous_trace' }, + attributes: { + [SENTRY_LINK_TYPE]: { + type: 'string', + value: 'previous_trace', + }, + }, sampled: true, - span_id: customTrace2Context?.span_id, - trace_id: customTrace2Context?.trace_id, + span_id: customTrace2Span.span_id, + trace_id: customTrace2Span.trace_id, }, ]); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/custom-trace/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/custom-trace/test.ts index c4637dc25018..bdbdd1c78a90 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/custom-trace/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/custom-trace/test.ts @@ -1,60 +1,62 @@ -import { expect } from '@playwright/test'; import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +import { expect } from '@playwright/test'; + import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; sentryTest('manually started custom traces are linked correctly in the chain', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => { - const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + const pageloadSpan = await sentryTest.step('Initial pageload', async () => { + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); await page.goto(url); - const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise); - return pageloadRequest.contexts?.trace; + return pageloadSpanPromise; }); - const customTrace1Context = await sentryTest.step('Custom trace', async () => { - const customTrace1RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'custom'); + const customTraceSpan = await sentryTest.step('Custom trace', async () => { + const customSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'custom'); await page.locator('#btn1').click(); - const customTrace1Event = envelopeRequestParser(await customTrace1RequestPromise); - - const customTraceCtx = customTrace1Event.contexts?.trace; + const span = await customSpanPromise; - expect(customTraceCtx?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); - expect(customTraceCtx?.links).toEqual([ + expect(span.trace_id).not.toEqual(pageloadSpan.trace_id); + expect(span.links).toEqual([ { - trace_id: pageloadTraceContext?.trace_id, - span_id: pageloadTraceContext?.span_id, + trace_id: pageloadSpan.trace_id, + span_id: pageloadSpan.span_id, sampled: true, attributes: { - [SENTRY_LINK_TYPE]: 'previous_trace', + [SENTRY_LINK_TYPE]: { + type: 'string', + value: 'previous_trace', + }, }, }, ]); - return customTraceCtx; + return span; }); await sentryTest.step('Navigation', async () => { - const navigation1RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation'); + const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); await page.goto(`${url}#foo`); - const navigationEvent = envelopeRequestParser(await navigation1RequestPromise); - const navTraceContext = navigationEvent.contexts?.trace; + const navSpan = await navigationSpanPromise; - expect(navTraceContext?.trace_id).not.toEqual(customTrace1Context?.trace_id); - expect(navTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + expect(navSpan.trace_id).not.toEqual(customTraceSpan.trace_id); + expect(navSpan.trace_id).not.toEqual(pageloadSpan.trace_id); - expect(navTraceContext?.links).toEqual([ + expect(navSpan.links).toEqual([ { - trace_id: customTrace1Context?.trace_id, - span_id: customTrace1Context?.span_id, + trace_id: customTraceSpan.trace_id, + span_id: customTraceSpan.span_id, sampled: true, attributes: { - [SENTRY_LINK_TYPE]: 'previous_trace', + [SENTRY_LINK_TYPE]: { + type: 'string', + value: 'previous_trace', + }, }, }, ]); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/default/test.ts index 7399c2bb26c3..ca42ae5e32a0 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/default/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/default/test.ts @@ -1,70 +1,69 @@ -import { expect } from '@playwright/test'; import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +import { expect } from '@playwright/test'; + import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; sentryTest("navigation spans link back to previous trace's root span", async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => { - const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); - await page.goto(url); - const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise); - return pageloadRequest.contexts?.trace; - }); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + await page.goto(url); + const pageloadSpan = await pageloadSpanPromise; - const navigation1TraceContext = await sentryTest.step('First navigation', async () => { - const navigation1RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation'); - await page.goto(`${url}#foo`); - const navigation1Request = envelopeRequestParser(await navigation1RequestPromise); - return navigation1Request.contexts?.trace; - }); + const navigation1SpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); + await page.goto(`${url}#foo`); + const navigation1Span = await navigation1SpanPromise; - const navigation2TraceContext = await sentryTest.step('Second navigation', async () => { - const navigation2RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation'); - await page.goto(`${url}#bar`); - const navigation2Request = envelopeRequestParser(await navigation2RequestPromise); - return navigation2Request.contexts?.trace; - }); + const navigation2SpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); + await page.goto(`${url}#bar`); + const navigation2Span = await navigation2SpanPromise; - const pageloadTraceId = pageloadTraceContext?.trace_id; - const navigation1TraceId = navigation1TraceContext?.trace_id; - const navigation2TraceId = navigation2TraceContext?.trace_id; + const pageloadTraceId = pageloadSpan.trace_id; + const navigation1TraceId = navigation1Span.trace_id; + const navigation2TraceId = navigation2Span.trace_id; - expect(pageloadTraceContext?.links).toBeUndefined(); + expect(pageloadSpan.links).toBeUndefined(); - expect(navigation1TraceContext?.links).toEqual([ + expect(navigation1Span.links).toEqual([ { trace_id: pageloadTraceId, - span_id: pageloadTraceContext?.span_id, + span_id: pageloadSpan.span_id, sampled: true, attributes: { - [SENTRY_LINK_TYPE]: 'previous_trace', + [SENTRY_LINK_TYPE]: { + type: 'string', + value: 'previous_trace', + }, }, }, ]); - expect(navigation1TraceContext?.data).toMatchObject({ - 'sentry.previous_trace': `${pageloadTraceId}-${pageloadTraceContext?.span_id}-1`, + expect(navigation1Span.attributes['sentry.previous_trace']).toEqual({ + type: 'string', + value: `${pageloadTraceId}-${pageloadSpan.span_id}-1`, }); - expect(navigation2TraceContext?.links).toEqual([ + expect(navigation2Span.links).toEqual([ { trace_id: navigation1TraceId, - span_id: navigation1TraceContext?.span_id, + span_id: navigation1Span.span_id, sampled: true, attributes: { - [SENTRY_LINK_TYPE]: 'previous_trace', + [SENTRY_LINK_TYPE]: { + type: 'string', + value: 'previous_trace', + }, }, }, ]); - expect(navigation2TraceContext?.data).toMatchObject({ - 'sentry.previous_trace': `${navigation1TraceId}-${navigation1TraceContext?.span_id}-1`, + expect(navigation2Span.attributes['sentry.previous_trace']).toEqual({ + type: 'string', + value: `${navigation1TraceId}-${navigation1Span.span_id}-1`, }); expect(pageloadTraceId).not.toEqual(navigation1TraceId); @@ -73,27 +72,25 @@ sentryTest("navigation spans link back to previous trace's root span", async ({ }); sentryTest("doesn't link between hard page reloads by default", async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); await sentryTest.step('First pageload', async () => { - const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); await page.goto(url); - const pageload1Event = envelopeRequestParser(await pageloadRequestPromise); + const pageload1Span = await pageloadSpanPromise; - expect(pageload1Event.contexts?.trace).toBeDefined(); - expect(pageload1Event.contexts?.trace?.links).toBeUndefined(); + expect(pageload1Span).toBeDefined(); + expect(pageload1Span.links).toBeUndefined(); }); await sentryTest.step('Second pageload', async () => { - const pageload2RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); await page.reload(); - const pageload2Event = envelopeRequestParser(await pageload2RequestPromise); + const pageload2Span = await pageloadSpanPromise; - expect(pageload2Event.contexts?.trace).toBeDefined(); - expect(pageload2Event.contexts?.trace?.links).toBeUndefined(); + expect(pageload2Span).toBeDefined(); + expect(pageload2Span.links).toBeUndefined(); }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/init.js index bdd43f5ac757..1ce4125ee422 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/interaction-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/interaction-spans/init.js index 2c3e9264d882..96386e1d38d5 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/interaction-spans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/interaction-spans/init.js @@ -4,7 +4,6 @@ import { interactionsIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1, integrations: [Sentry.browserTracingIntegration(), interactionsIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/interaction-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/interaction-spans/test.ts index 24a1e2c59072..67e216567856 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/interaction-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/interaction-spans/test.ts @@ -1,7 +1,9 @@ -import { expect } from '@playwright/test'; import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +import { expect } from '@playwright/test'; + import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; /* This is quite peculiar behavior but it's a result of the route-based trace lifetime. @@ -12,78 +14,67 @@ import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest sentryTest( 'only the first root spans in the trace link back to the previous trace', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => { - const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + const pageloadSpan = await sentryTest.step('Initial pageload', async () => { + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); await page.goto(url); + const span = await pageloadSpanPromise; - const pageloadEvent = envelopeRequestParser(await pageloadRequestPromise); - const traceContext = pageloadEvent.contexts?.trace; - - expect(traceContext).toBeDefined(); - expect(traceContext?.links).toBeUndefined(); + expect(span).toBeDefined(); + expect(span.links).toBeUndefined(); - return traceContext; + return span; }); await sentryTest.step('Click Before navigation', async () => { - const interactionRequestPromise = waitForTransactionRequest(page, evt => { - return evt.contexts?.trace?.op === 'ui.action.click'; - }); + const interactionSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.action.click'); await page.click('#btn'); - - const interactionEvent = envelopeRequestParser(await interactionRequestPromise); - const interactionTraceContext = interactionEvent.contexts?.trace; + const interactionSpan = await interactionSpanPromise; // sanity check: route-based trace lifetime means the trace_id should be the same - expect(interactionTraceContext?.trace_id).toBe(pageloadTraceContext?.trace_id); + expect(interactionSpan.trace_id).toBe(pageloadSpan.trace_id); // no links yet as previous root span belonged to same trace - expect(interactionTraceContext?.links).toBeUndefined(); + expect(interactionSpan.links).toBeUndefined(); }); - const navigationTraceContext = await sentryTest.step('Navigation', async () => { - const navigationRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation'); + const navigationSpan = await sentryTest.step('Navigation', async () => { + const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); await page.goto(`${url}#foo`); - const navigationEvent = envelopeRequestParser(await navigationRequestPromise); - - const traceContext = navigationEvent.contexts?.trace; + const span = await navigationSpanPromise; - expect(traceContext?.op).toBe('navigation'); - expect(traceContext?.links).toEqual([ + expect(getSpanOp(span)).toBe('navigation'); + expect(span.links).toEqual([ { - trace_id: pageloadTraceContext?.trace_id, - span_id: pageloadTraceContext?.span_id, + trace_id: pageloadSpan.trace_id, + span_id: pageloadSpan.span_id, sampled: true, attributes: { - [SENTRY_LINK_TYPE]: 'previous_trace', + [SENTRY_LINK_TYPE]: { + type: 'string', + value: 'previous_trace', + }, }, }, ]); - expect(traceContext?.trace_id).not.toEqual(traceContext?.links![0].trace_id); - return traceContext; + expect(span.trace_id).not.toEqual(span.links![0].trace_id); + return span; }); await sentryTest.step('Click After navigation', async () => { - const interactionRequestPromise = waitForTransactionRequest(page, evt => { - return evt.contexts?.trace?.op === 'ui.action.click'; - }); + const interactionSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.action.click'); await page.click('#btn'); - const interactionEvent = envelopeRequestParser(await interactionRequestPromise); - - const interactionTraceContext = interactionEvent.contexts?.trace; + const interactionSpan = await interactionSpanPromise; // sanity check: route-based trace lifetime means the trace_id should be the same - expect(interactionTraceContext?.trace_id).toBe(navigationTraceContext?.trace_id); + expect(interactionSpan.trace_id).toBe(navigationSpan.trace_id); // since this is the second root span in the trace, it doesn't link back to the previous trace - expect(interactionTraceContext?.links).toBeUndefined(); + expect(interactionSpan.links).toBeUndefined(); }); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/meta/test.ts index a232a5a0f6e7..3f8688a0bf4d 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/meta/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/meta/test.ts @@ -1,54 +1,51 @@ -import { expect } from '@playwright/test'; import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +import { expect } from '@playwright/test'; + import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; sentryTest( "links back to previous trace's local root span if continued from meta tags", async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); const metaTagTraceId = '12345678901234567890123456789012'; - const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => { - const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + const pageloadSpan = await sentryTest.step('Initial pageload', async () => { + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); await page.goto(url); - const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise); - - const traceContext = pageloadRequest.contexts?.trace; + const span = await pageloadSpanPromise; // sanity check - expect(traceContext?.trace_id).toBe(metaTagTraceId); - - expect(traceContext?.links).toBeUndefined(); + expect(span.trace_id).toBe(metaTagTraceId); + expect(span.links).toBeUndefined(); - return traceContext; + return span; }); - const navigationTraceContext = await sentryTest.step('Navigation', async () => { - const navigationRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation'); + const navigationSpan = await sentryTest.step('Navigation', async () => { + const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); await page.goto(`${url}#foo`); - const navigationRequest = envelopeRequestParser(await navigationRequestPromise); - return navigationRequest.contexts?.trace; + return navigationSpanPromise; }); - const navigationTraceId = navigationTraceContext?.trace_id; - - expect(navigationTraceContext?.links).toEqual([ + expect(navigationSpan.links).toEqual([ { trace_id: metaTagTraceId, - span_id: pageloadTraceContext?.span_id, + span_id: pageloadSpan.span_id, sampled: true, attributes: { - [SENTRY_LINK_TYPE]: 'previous_trace', + [SENTRY_LINK_TYPE]: { + type: 'string', + value: 'previous_trace', + }, }, }, ]); - expect(navigationTraceId).not.toEqual(metaTagTraceId); + expect(navigationSpan.trace_id).not.toEqual(metaTagTraceId); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/negatively-sampled/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/negatively-sampled/init.js index 79ca4d68e8ba..a3b99e2e1dc3 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/negatively-sampled/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/negatively-sampled/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', // We want to ignore redirects for this test integrations: [Sentry.browserTracingIntegration({ detectRedirects: false })], diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/negatively-sampled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/negatively-sampled/test.ts index 894fe0d5929a..9fd3c0d2dd7d 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/negatively-sampled/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/negatively-sampled/test.ts @@ -1,42 +1,45 @@ -import { expect } from '@playwright/test'; import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +import { expect } from '@playwright/test'; + import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; sentryTest('includes a span link to a previously negatively sampled span', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); await sentryTest.step('Initial pageload', async () => { - // No event to check for an event here because this pageload span is sampled negatively! + // No span envelope expected here because this pageload span is sampled negatively! await page.goto(url); }); await sentryTest.step('Navigation', async () => { - const navigationRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation'); + const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); await page.goto(`${url}#foo`); - const navigationEvent = envelopeRequestParser(await navigationRequestPromise); - const navigationTraceContext = navigationEvent.contexts?.trace; + const navigationSpan = await navigationSpanPromise; - expect(navigationTraceContext?.op).toBe('navigation'); - expect(navigationTraceContext?.links).toEqual([ + expect(getSpanOp(navigationSpan)).toBe('navigation'); + expect(navigationSpan.links).toEqual([ { trace_id: expect.stringMatching(/[a-f\d]{32}/), span_id: expect.stringMatching(/[a-f\d]{16}/), sampled: false, attributes: { - [SENTRY_LINK_TYPE]: 'previous_trace', + [SENTRY_LINK_TYPE]: { + type: 'string', + value: 'previous_trace', + }, }, }, ]); - expect(navigationTraceContext?.data).toMatchObject({ - 'sentry.previous_trace': expect.stringMatching(/[a-f\d]{32}-[a-f\d]{16}-0/), + expect(navigationSpan.attributes['sentry.previous_trace']).toEqual({ + type: 'string', + value: expect.stringMatching(/[a-f\d]{32}-[a-f\d]{16}-0/), }); - expect(navigationTraceContext?.trace_id).not.toEqual(navigationTraceContext?.links![0].trace_id); + expect(navigationSpan.trace_id).not.toEqual(navigationSpan.links![0].trace_id); }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/session-storage/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/session-storage/init.js index 9b88e489ae07..fc053206d9ed 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/session-storage/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/session-storage/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration({ linkPreviousTrace: 'session-storage' })], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/session-storage/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/session-storage/test.ts index f7e0d288de5b..57121c395b53 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/session-storage/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/session-storage/test.ts @@ -1,39 +1,43 @@ -import { expect } from '@playwright/test'; import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +import { expect } from '@playwright/test'; + import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; sentryTest('adds link between hard page reloads when opting into sessionStorage', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - const pageload1TraceContext = await sentryTest.step('First pageload', async () => { - const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + const pageload1Span = await sentryTest.step('First pageload', async () => { + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); await page.goto(url); - const pageload1Event = envelopeRequestParser(await pageloadRequestPromise); - const pageload1TraceContext = pageload1Event.contexts?.trace; - expect(pageload1TraceContext).toBeDefined(); - expect(pageload1TraceContext?.links).toBeUndefined(); - return pageload1TraceContext; + const span = await pageloadSpanPromise; + expect(span).toBeDefined(); + expect(span.links).toBeUndefined(); + return span; }); - const pageload2Event = await sentryTest.step('Hard page reload', async () => { - const pageload2RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + const pageload2Span = await sentryTest.step('Hard page reload', async () => { + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); await page.reload(); - return envelopeRequestParser(await pageload2RequestPromise); + return pageloadSpanPromise; }); - expect(pageload2Event.contexts?.trace?.links).toEqual([ + expect(pageload2Span.links).toEqual([ { - trace_id: pageload1TraceContext?.trace_id, - span_id: pageload1TraceContext?.span_id, + trace_id: pageload1Span.trace_id, + span_id: pageload1Span.span_id, sampled: true, - attributes: { [SENTRY_LINK_TYPE]: 'previous_trace' }, + attributes: { + [SENTRY_LINK_TYPE]: { + type: 'string', + value: 'previous_trace', + }, + }, }, ]); - expect(pageload1TraceContext?.trace_id).not.toEqual(pageload2Event.contexts?.trace?.trace_id); + expect(pageload1Span.trace_id).not.toEqual(pageload2Span.trace_id); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-static/init.js similarity index 90% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-static/init.js index d3c861a8ae26..a8a7fc20c7f9 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-static/init.js @@ -3,6 +3,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ @@ -11,7 +12,6 @@ Sentry.init({ instrumentPageLoad: false, enableInp: false, }), - Sentry.spanStreamingIntegration(), ], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-streamed/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-static/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-streamed/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-static/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-streamed/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-static/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-streamed/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-static/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-static/test.ts new file mode 100644 index 000000000000..ca072e14b342 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-static/test.ts @@ -0,0 +1,34 @@ +import { NAVIGATION } from '@sentry/conventions/op'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest( + "doesn't capture long animation frame that starts before a navigation.", + async ({ browserName, getLocalTestUrl, page }) => { + // Long animation frames only work on chrome + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + const navigationTransactionEventPromise = waitForTransactionRequest( + page, + event => event.contexts?.trace?.op === NAVIGATION, + ); + + await page.locator('#clickme').click(); + + const navigationTransactionEvent = envelopeRequestParser(await navigationTransactionEventPromise); + + expect(navigationTransactionEvent.contexts?.trace?.op).toBe(NAVIGATION); + + const loafSpans = navigationTransactionEvent.spans?.filter(s => s.op?.startsWith('ui.long_animation_frame')); + + expect(loafSpans?.length).toEqual(0); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-streamed/test.ts deleted file mode 100644 index bfda616223b4..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-streamed/test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; - -sentryTest( - "doesn't capture long animation frame that starts before a navigation.", - async ({ browserName, getLocalTestUrl, page }) => { - // Long animation frames only work on chrome - sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium'); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const navigationSpansPromise = waitForStreamedSpans(page, spans => spans.some(s => getSpanOp(s) === 'navigation')); - - await page.goto(url); - - await page.locator('#clickme').click(); - - const spans = await navigationSpansPromise; - - const loafSpans = spans.filter(s => getSpanOp(s)?.startsWith('ui.long_animation_frame')); - expect(loafSpans).toHaveLength(0); - }, -); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation/init.js index a8a7fc20c7f9..019eece018f6 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation/test.ts index b7215249910f..031da81f9d78 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation/test.ts @@ -1,30 +1,30 @@ +import { SENTRY_SEGMENT_ID } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest( "doesn't capture long animation frame that starts before a navigation.", async ({ browserName, getLocalTestUrl, page }) => { // Long animation frames only work on chrome - if (shouldSkipTracingTest() || browserName !== 'chromium') { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium'); const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); + const allSpans = collectStreamedSpans(page); + const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); - const navigationTransactionEventPromise = getFirstSentryEnvelopeRequest(page); + await page.goto(url); await page.locator('#clickme').click(); - const navigationTransactionEvent = await navigationTransactionEventPromise; - - expect(navigationTransactionEvent.contexts?.trace?.op).toBe('navigation'); - - const loafSpans = navigationTransactionEvent.spans?.filter(s => s.op?.startsWith('ui.long_animation_frame')); + const navigationRoot = await navigationSpanPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + expect(getSpanOp(navigationRoot)).toBe('navigation'); + const spans = allSpans.filter(span => span.attributes[SENTRY_SEGMENT_ID]?.value === navigationRoot.span_id); - expect(loafSpans?.length).toEqual(0); + const loafSpans = spans.filter(s => getSpanOp(s)?.startsWith('ui.long_animation_frame')); + expect(loafSpans).toHaveLength(0); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-streamed/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-static/assets/script.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-streamed/assets/script.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-static/assets/script.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-static/init.js similarity index 88% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-static/init.js index 3c004b952f80..250f2a72311b 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-static/init.js @@ -3,13 +3,13 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ enableLongTask: false, enableLongAnimationFrame: false, }), - Sentry.spanStreamingIntegration(), ], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-streamed/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-static/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-streamed/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-static/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-static/test.ts new file mode 100644 index 000000000000..3d4ca79877b1 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-static/test.ts @@ -0,0 +1,26 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'should not capture long animation frame when flag is disabled.', + async ({ browserName, getLocalTestUrl, page }) => { + // Long animation frames only work on chrome + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/script.js` }), + ); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui')); + + expect(uiSpans?.length).toBe(0); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-streamed/test.ts deleted file mode 100644 index e03474070bb0..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-streamed/test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { Route } from '@playwright/test'; -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; - -sentryTest( - 'does not capture long animation frame when flag is disabled.', - async ({ browserName, getLocalTestUrl, page }) => { - // Long animation frames only work on chrome - sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium'); - - await page.route('**/path/to/script.js', (route: Route) => - route.fulfill({ path: `${__dirname}/assets/script.js` }), - ); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const spansPromise = waitForStreamedSpans(page, spans => spans.some(s => getSpanOp(s) === 'pageload')); - - await page.goto(url); - - const spans = await spansPromise; - const uiSpans = spans.filter(s => getSpanOp(s)?.startsWith('ui')); - - expect(uiSpans.length).toBe(0); - }, -); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled/init.js index 250f2a72311b..0339c3fa3dbf 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled/test.ts index 3d4ca79877b1..7ea2ac55bb53 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled/test.ts @@ -1,16 +1,14 @@ import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest( - 'should not capture long animation frame when flag is disabled.', + 'does not capture long animation frame when flag is disabled.', async ({ browserName, getLocalTestUrl, page }) => { // Long animation frames only work on chrome - if (shouldSkipTracingTest() || browserName !== 'chromium') { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium'); await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` }), @@ -18,9 +16,15 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui')); + const spans = collectStreamedSpans(page); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - expect(uiSpans?.length).toBe(0); + await page.goto(url); + + await pageloadSpanPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + const uiSpans = spans.filter(s => getSpanOp(s)?.startsWith('ui')); + + expect(uiSpans.length).toBe(0); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-static/assets/script.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/assets/script.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-static/assets/script.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-static/init.js similarity index 88% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-static/init.js index 2be5f32985d7..cfa9cb5faa3f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-static/init.js @@ -3,13 +3,13 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ enableLongTask: false, enableLongAnimationFrame: true, }), - Sentry.spanStreamingIntegration(), ], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-static/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-static/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-static/test.ts new file mode 100644 index 000000000000..0a3f97f6a786 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-static/test.ts @@ -0,0 +1,116 @@ +import { PAGELOAD, UI_LONG_ANIMATION_FRAME } from '@sentry/conventions/op'; +import { + BROWSER_SCRIPT_INVOKER, + CODE_FILE_PATH, + BROWSER_SCRIPT_SOURCE_CHAR_POSITION, + BROWSER_SCRIPT_INVOKER_TYPE, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest( + 'should capture long animation frame for top-level script.', + async ({ browserName, getLocalTestUrl, page }) => { + // Long animation frames only work on chrome + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/script.js` }), + ); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const promise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === PAGELOAD); + + await page.goto(url); + + const eventData = envelopeRequestParser(await promise); + + const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith(UI_LONG_ANIMATION_FRAME)); + + expect(uiSpans?.length).toBeGreaterThanOrEqual(1); + + const topLevelUISpan = (uiSpans || []).find( + span => span.data?.[BROWSER_SCRIPT_INVOKER] === 'https://sentry-test-site.example/path/to/script.js', + )!; + expect(topLevelUISpan).toEqual( + expect.objectContaining({ + op: UI_LONG_ANIMATION_FRAME, + description: 'Main UI thread blocked', + parent_span_id: eventData.contexts?.trace?.span_id, + data: { + [CODE_FILE_PATH]: 'https://sentry-test-site.example/path/to/script.js', + [BROWSER_SCRIPT_SOURCE_CHAR_POSITION]: 0, + [BROWSER_SCRIPT_INVOKER]: 'https://sentry-test-site.example/path/to/script.js', + [BROWSER_SCRIPT_INVOKER_TYPE]: 'classic-script', + [SENTRY_OP]: UI_LONG_ANIMATION_FRAME, + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + }, + }), + ); + const start = topLevelUISpan.start_timestamp ?? 0; + const end = topLevelUISpan.timestamp ?? 0; + const duration = end - start; + + expect(duration).toBeGreaterThanOrEqual(0.1); + expect(duration).toBeLessThanOrEqual(0.15); + }, +); + +sentryTest( + 'should capture long animation frame for event listener.', + async ({ browserName, getLocalTestUrl, page }) => { + // Long animation frames only work on chrome + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/script.js` }), + ); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const promise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === PAGELOAD); + + await page.goto(url); + + // trigger long animation frame function + await page.getByRole('button').click(); + + const eventData = envelopeRequestParser(await promise); + + const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith(UI_LONG_ANIMATION_FRAME)) || []; + + expect(uiSpans.length).toBeGreaterThanOrEqual(2); + + const eventListenerUISpan = uiSpans.find(span => span.data[BROWSER_SCRIPT_INVOKER] === 'BUTTON#clickme.onclick')!; + + expect(eventListenerUISpan).toEqual( + expect.objectContaining({ + op: UI_LONG_ANIMATION_FRAME, + description: 'Main UI thread blocked', + parent_span_id: eventData.contexts?.trace?.span_id, + data: { + [BROWSER_SCRIPT_INVOKER]: 'BUTTON#clickme.onclick', + [BROWSER_SCRIPT_INVOKER_TYPE]: 'event-listener', + [CODE_FILE_PATH]: 'https://sentry-test-site.example/path/to/script.js', + [SENTRY_OP]: UI_LONG_ANIMATION_FRAME, + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + }, + }), + ); + const start = eventListenerUISpan.start_timestamp ?? 0; + const end = eventListenerUISpan.timestamp ?? 0; + const duration = end - start; + + expect(duration).toBeGreaterThanOrEqual(0.1); + expect(duration).toBeLessThanOrEqual(0.15); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/test.ts deleted file mode 100644 index f885fc4c3bde..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; -import type { Route } from '@playwright/test'; -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; - -sentryTest( - 'captures long animation frame span for top-level script.', - async ({ browserName, getLocalTestUrl, page }) => { - // Long animation frames only work on chrome - sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium'); - - await page.route('**/path/to/script.js', (route: Route) => - route.fulfill({ path: `${__dirname}/assets/script.js` }), - ); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const spansPromise = waitForStreamedSpans(page, spans => spans.some(s => getSpanOp(s) === 'pageload')); - - await page.goto(url); - - const spans = await spansPromise; - const pageloadSpan = spans.find(s => getSpanOp(s) === 'pageload')!; - - const uiSpans = spans.filter(s => getSpanOp(s)?.startsWith('ui.long_animation_frame')); - - expect(uiSpans.length).toBeGreaterThanOrEqual(1); - - const topLevelUISpan = uiSpans.find( - s => s.attributes['browser.script.invoker']?.value === 'https://sentry-test-site.example/path/to/script.js', - )!; - - expect(topLevelUISpan).toEqual( - expect.objectContaining({ - name: 'Main UI thread blocked', - parent_span_id: pageloadSpan.span_id, - attributes: expect.objectContaining({ - 'code.file.path': { type: 'string', value: 'https://sentry-test-site.example/path/to/script.js' }, - 'browser.script.source_char_position': expect.objectContaining({ value: 0 }), - 'browser.script.invoker': { - type: 'string', - value: 'https://sentry-test-site.example/path/to/script.js', - }, - 'browser.script.invoker_type': { type: 'string', value: 'classic-script' }, - [SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, - [SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, - }), - }), - ); - - const start = topLevelUISpan.start_timestamp ?? 0; - const end = topLevelUISpan.end_timestamp ?? 0; - const duration = end - start; - - expect(duration).toBeGreaterThanOrEqual(0.1); - expect(duration).toBeLessThanOrEqual(0.15); - }, -); - -sentryTest('captures long animation frame span for event listener.', async ({ browserName, getLocalTestUrl, page }) => { - // Long animation frames only work on chrome - sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium'); - - await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const spansPromise = waitForStreamedSpans(page, spans => spans.some(s => getSpanOp(s) === 'pageload')); - - await page.goto(url); - - // trigger long animation frame function - await page.getByRole('button').click(); - - const spans = await spansPromise; - const pageloadSpan = spans.find(s => getSpanOp(s) === 'pageload')!; - - const uiSpans = spans.filter(s => getSpanOp(s)?.startsWith('ui.long_animation_frame')); - - expect(uiSpans.length).toBeGreaterThanOrEqual(2); - - const eventListenerUISpan = uiSpans.find( - s => s.attributes['browser.script.invoker']?.value === 'BUTTON#clickme.onclick', - )!; - - expect(eventListenerUISpan).toEqual( - expect.objectContaining({ - name: 'Main UI thread blocked', - parent_span_id: pageloadSpan.span_id, - attributes: expect.objectContaining({ - 'browser.script.invoker': { type: 'string', value: 'BUTTON#clickme.onclick' }, - 'browser.script.invoker_type': { type: 'string', value: 'event-listener' }, - 'code.file.path': { type: 'string', value: 'https://sentry-test-site.example/path/to/script.js' }, - [SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, - [SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, - }), - }), - ); - - const start = eventListenerUISpan.start_timestamp ?? 0; - const end = eventListenerUISpan.end_timestamp ?? 0; - const duration = end - start; - - expect(duration).toBeGreaterThanOrEqual(0.1); - expect(duration).toBeLessThanOrEqual(0.15); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/init.js index cfa9cb5faa3f..f16f7af33b5c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/test.ts index 937662e117a4..8a1cc8026aaa 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/test.ts @@ -1,17 +1,22 @@ -import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { + BROWSER_SCRIPT_INVOKER, + CODE_FILE_PATH, + BROWSER_SCRIPT_SOURCE_CHAR_POSITION, + BROWSER_SCRIPT_INVOKER_TYPE, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest( - 'should capture long animation frame for top-level script.', + 'captures long animation frame span for top-level script.', async ({ browserName, getLocalTestUrl, page }) => { // Long animation frames only work on chrome - if (shouldSkipTracingTest() || browserName !== 'chromium') { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium'); await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` }), @@ -19,38 +24,43 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); - const promise = getFirstSentryEnvelopeRequest(page); + const spans = collectStreamedSpans(page); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); await page.goto(url); - await new Promise(resolve => setTimeout(resolve, 1000)); + await pageloadSpanPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + const pageloadSpan = spans.find(s => getSpanOp(s) === 'pageload')!; - const eventData = await promise; + const uiSpans = spans.filter(s => getSpanOp(s)?.startsWith('ui.long_animation_frame')); - const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui.long_animation_frame')); + expect(uiSpans.length).toBeGreaterThanOrEqual(1); - expect(uiSpans?.length).toBeGreaterThanOrEqual(1); - - const topLevelUISpan = (uiSpans || []).find( - span => span.data?.['browser.script.invoker'] === 'https://sentry-test-site.example/path/to/script.js', + const topLevelUISpan = uiSpans.find( + s => s.attributes[BROWSER_SCRIPT_INVOKER]?.value === 'https://sentry-test-site.example/path/to/script.js', )!; + expect(topLevelUISpan).toEqual( expect.objectContaining({ - op: 'ui.long_animation_frame', - description: 'Main UI thread blocked', - parent_span_id: eventData.contexts?.trace?.span_id, - data: { - 'code.file.path': 'https://sentry-test-site.example/path/to/script.js', - 'browser.script.source_char_position': 0, - 'browser.script.invoker': 'https://sentry-test-site.example/path/to/script.js', - 'browser.script.invoker_type': 'classic-script', - [SENTRY_OP]: 'ui.long_animation_frame', - [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', - }, + name: 'Main UI thread blocked', + parent_span_id: pageloadSpan.span_id, + attributes: expect.objectContaining({ + [CODE_FILE_PATH]: { type: 'string', value: 'https://sentry-test-site.example/path/to/script.js' }, + [BROWSER_SCRIPT_SOURCE_CHAR_POSITION]: expect.objectContaining({ value: 0 }), + [BROWSER_SCRIPT_INVOKER]: { + type: 'string', + value: 'https://sentry-test-site.example/path/to/script.js', + }, + [BROWSER_SCRIPT_INVOKER_TYPE]: { type: 'string', value: 'classic-script' }, + [SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, + }), }), ); + const start = topLevelUISpan.start_timestamp ?? 0; - const end = topLevelUISpan.timestamp ?? 0; + const end = topLevelUISpan.end_timestamp ?? 0; const duration = end - start; expect(duration).toBeGreaterThanOrEqual(0.1); @@ -58,56 +68,51 @@ sentryTest( }, ); -sentryTest( - 'should capture long animation frame for event listener.', - async ({ browserName, getLocalTestUrl, page }) => { - // Long animation frames only work on chrome - if (shouldSkipTracingTest() || browserName !== 'chromium') { - sentryTest.skip(); - } - - await page.route('**/path/to/script.js', (route: Route) => - route.fulfill({ path: `${__dirname}/assets/script.js` }), - ); +sentryTest('captures long animation frame span for event listener.', async ({ browserName, getLocalTestUrl, page }) => { + // Long animation frames only work on chrome + sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium'); - const url = await getLocalTestUrl({ testDir: __dirname }); + await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); - const promise = getFirstSentryEnvelopeRequest(page); + const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); + const spans = collectStreamedSpans(page); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - // trigger long animation frame function - await page.getByRole('button').click(); + await page.goto(url); - await new Promise(resolve => setTimeout(resolve, 1000)); + // trigger long animation frame function + await page.getByRole('button').click(); - const eventData = await promise; + const pageloadSpan = await pageloadSpanPromise; + await page.evaluate(() => (window as any).Sentry.flush()); - const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui.long_animation_frame')) || []; + const uiSpans = spans.filter(s => getSpanOp(s)?.startsWith('ui.long_animation_frame')); - expect(uiSpans.length).toBeGreaterThanOrEqual(2); + expect(uiSpans.length).toBeGreaterThanOrEqual(2); - const eventListenerUISpan = uiSpans.find(span => span.data['browser.script.invoker'] === 'BUTTON#clickme.onclick')!; + const eventListenerUISpan = uiSpans.find( + s => s.attributes[BROWSER_SCRIPT_INVOKER]?.value === 'BUTTON#clickme.onclick', + )!; - expect(eventListenerUISpan).toEqual( - expect.objectContaining({ - op: 'ui.long_animation_frame', - description: 'Main UI thread blocked', - parent_span_id: eventData.contexts?.trace?.span_id, - data: { - 'browser.script.invoker': 'BUTTON#clickme.onclick', - 'browser.script.invoker_type': 'event-listener', - 'code.file.path': 'https://sentry-test-site.example/path/to/script.js', - [SENTRY_OP]: 'ui.long_animation_frame', - [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', - }, + expect(eventListenerUISpan).toEqual( + expect.objectContaining({ + name: 'Main UI thread blocked', + parent_span_id: pageloadSpan.span_id, + attributes: expect.objectContaining({ + [BROWSER_SCRIPT_INVOKER]: { type: 'string', value: 'BUTTON#clickme.onclick' }, + [BROWSER_SCRIPT_INVOKER_TYPE]: { type: 'string', value: 'event-listener' }, + [CODE_FILE_PATH]: { type: 'string', value: 'https://sentry-test-site.example/path/to/script.js' }, + [SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, }), - ); - const start = eventListenerUISpan.start_timestamp ?? 0; - const end = eventListenerUISpan.timestamp ?? 0; - const duration = end - start; + }), + ); - expect(duration).toBeGreaterThanOrEqual(0.1); - expect(duration).toBeLessThanOrEqual(0.15); - }, -); + const start = eventListenerUISpan.start_timestamp ?? 0; + const end = eventListenerUISpan.end_timestamp ?? 0; + const duration = end - start; + + expect(duration).toBeGreaterThanOrEqual(0.1); + expect(duration).toBeLessThanOrEqual(0.15); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-non-chromium/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-non-chromium/init.js index 37df798364a8..382115cbc8b9 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-non-chromium/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-non-chromium/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-non-chromium/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-non-chromium/test.ts index 2c8f5427cc05..07d3a6cd313a 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-non-chromium/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-non-chromium/test.ts @@ -1,8 +1,8 @@ import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../../utils/spanUtils'; sentryTest( 'should not capture long animation frame or long task when browser is non-chromium', @@ -18,8 +18,10 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui')); + const spans = collectStreamedSpans(page); + await waitForStreamedSpanAndTraceHeaderOnUrl(page, url, span => getSpanOp(span) === 'pageload'); + await page.evaluate(() => (window as any).Sentry.flush()); + const uiSpans = spans?.filter(span => getSpanOp(span)?.startsWith('ui')); expect(uiSpans?.length).toBe(0); }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-static/assets/script.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/assets/script.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-static/assets/script.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-static/init.js similarity index 88% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-static/init.js index b0dc0a8d7ff5..37df798364a8 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-static/init.js @@ -3,13 +3,13 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ enableLongTask: true, enableLongAnimationFrame: true, }), - Sentry.spanStreamingIntegration(), ], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-static/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-static/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-static/test.ts new file mode 100644 index 000000000000..fb252c80fa30 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-static/test.ts @@ -0,0 +1,118 @@ +import { PAGELOAD, UI_LONG_ANIMATION_FRAME } from '@sentry/conventions/op'; +import { + BROWSER_SCRIPT_INVOKER, + CODE_FILE_PATH, + BROWSER_SCRIPT_SOURCE_CHAR_POSITION, + BROWSER_SCRIPT_INVOKER_TYPE, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest( + 'should capture long animation frame for top-level script.', + async ({ browserName, getLocalTestUrl, page }) => { + // Long animation frames only work on chrome + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + // Long animation frame should take priority over long tasks + + await page.route('**/path/to/script.js', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/script.js` }), + ); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const promise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === PAGELOAD); + + await page.goto(url); + + const eventData = envelopeRequestParser(await promise); + + const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith(UI_LONG_ANIMATION_FRAME)); + + expect(uiSpans?.length).toBeGreaterThanOrEqual(1); + + const topLevelUISpan = (uiSpans || []).find( + span => span.data?.[BROWSER_SCRIPT_INVOKER] === 'https://sentry-test-site.example/path/to/script.js', + )!; + expect(topLevelUISpan).toEqual( + expect.objectContaining({ + op: UI_LONG_ANIMATION_FRAME, + description: 'Main UI thread blocked', + parent_span_id: eventData.contexts?.trace?.span_id, + data: { + [CODE_FILE_PATH]: 'https://sentry-test-site.example/path/to/script.js', + [BROWSER_SCRIPT_SOURCE_CHAR_POSITION]: 0, + [BROWSER_SCRIPT_INVOKER]: 'https://sentry-test-site.example/path/to/script.js', + [BROWSER_SCRIPT_INVOKER_TYPE]: 'classic-script', + [SENTRY_OP]: UI_LONG_ANIMATION_FRAME, + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + }, + }), + ); + const start = topLevelUISpan.start_timestamp ?? 0; + const end = topLevelUISpan.timestamp ?? 0; + const duration = end - start; + + expect(duration).toBeGreaterThanOrEqual(0.1); + expect(duration).toBeLessThanOrEqual(0.15); + }, +); + +sentryTest( + 'should capture long animation frame for event listener.', + async ({ browserName, getLocalTestUrl, page }) => { + // Long animation frames only work on chrome + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/script.js` }), + ); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const promise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === PAGELOAD); + + await page.goto(url); + + // trigger long animation frame function + await page.getByRole('button').click(); + + const eventData = envelopeRequestParser(await promise); + + const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith(UI_LONG_ANIMATION_FRAME)) || []; + + expect(uiSpans.length).toBeGreaterThanOrEqual(2); + + const eventListenerUISpan = uiSpans.find(span => span.data[BROWSER_SCRIPT_INVOKER] === 'BUTTON#clickme.onclick')!; + + expect(eventListenerUISpan).toEqual( + expect.objectContaining({ + op: UI_LONG_ANIMATION_FRAME, + description: 'Main UI thread blocked', + parent_span_id: eventData.contexts?.trace?.span_id, + data: { + [BROWSER_SCRIPT_INVOKER]: 'BUTTON#clickme.onclick', + [BROWSER_SCRIPT_INVOKER_TYPE]: 'event-listener', + [CODE_FILE_PATH]: 'https://sentry-test-site.example/path/to/script.js', + [SENTRY_OP]: UI_LONG_ANIMATION_FRAME, + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + }, + }), + ); + const start = eventListenerUISpan.start_timestamp ?? 0; + const end = eventListenerUISpan.timestamp ?? 0; + const duration = end - start; + + expect(duration).toBeGreaterThanOrEqual(0.1); + expect(duration).toBeLessThanOrEqual(0.15); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/test.ts deleted file mode 100644 index 94ece2b74e6f..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; -import type { Route } from '@playwright/test'; -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; - -sentryTest( - 'captures long animation frame span for top-level script.', - async ({ browserName, getLocalTestUrl, page }) => { - // Long animation frames only work on chrome - sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium'); - - // Long animation frame should take priority over long tasks - - await page.route('**/path/to/script.js', (route: Route) => - route.fulfill({ path: `${__dirname}/assets/script.js` }), - ); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const spansPromise = waitForStreamedSpans(page, spans => spans.some(s => getSpanOp(s) === 'pageload')); - - await page.goto(url); - - const spans = await spansPromise; - const pageloadSpan = spans.find(s => getSpanOp(s) === 'pageload')!; - - const uiSpans = spans.filter(s => getSpanOp(s)?.startsWith('ui.long_animation_frame')); - - expect(uiSpans.length).toBeGreaterThanOrEqual(1); - - const topLevelUISpan = uiSpans.find( - s => s.attributes['browser.script.invoker']?.value === 'https://sentry-test-site.example/path/to/script.js', - )!; - - expect(topLevelUISpan).toEqual( - expect.objectContaining({ - name: 'Main UI thread blocked', - parent_span_id: pageloadSpan.span_id, - attributes: expect.objectContaining({ - 'code.file.path': { type: 'string', value: 'https://sentry-test-site.example/path/to/script.js' }, - 'browser.script.source_char_position': expect.objectContaining({ value: 0 }), - 'browser.script.invoker': { - type: 'string', - value: 'https://sentry-test-site.example/path/to/script.js', - }, - 'browser.script.invoker_type': { type: 'string', value: 'classic-script' }, - [SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, - [SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, - }), - }), - ); - - const start = topLevelUISpan.start_timestamp ?? 0; - const end = topLevelUISpan.end_timestamp ?? 0; - const duration = end - start; - - expect(duration).toBeGreaterThanOrEqual(0.1); - expect(duration).toBeLessThanOrEqual(0.15); - }, -); - -sentryTest('captures long animation frame span for event listener.', async ({ browserName, getLocalTestUrl, page }) => { - // Long animation frames only work on chrome - sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium'); - - await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const spansPromise = waitForStreamedSpans(page, spans => spans.some(s => getSpanOp(s) === 'pageload')); - - await page.goto(url); - - // trigger long animation frame function - await page.getByRole('button').click(); - - const spans = await spansPromise; - const pageloadSpan = spans.find(s => getSpanOp(s) === 'pageload')!; - - const uiSpans = spans.filter(s => getSpanOp(s)?.startsWith('ui.long_animation_frame')); - - expect(uiSpans.length).toBeGreaterThanOrEqual(2); - - const eventListenerUISpan = uiSpans.find( - s => s.attributes['browser.script.invoker']?.value === 'BUTTON#clickme.onclick', - )!; - - expect(eventListenerUISpan).toEqual( - expect.objectContaining({ - name: 'Main UI thread blocked', - parent_span_id: pageloadSpan.span_id, - attributes: expect.objectContaining({ - 'browser.script.invoker': { type: 'string', value: 'BUTTON#clickme.onclick' }, - 'browser.script.invoker_type': { type: 'string', value: 'event-listener' }, - 'code.file.path': { type: 'string', value: 'https://sentry-test-site.example/path/to/script.js' }, - [SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, - [SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, - }), - }), - ); - - const start = eventListenerUISpan.start_timestamp ?? 0; - const end = eventListenerUISpan.end_timestamp ?? 0; - const duration = end - start; - - expect(duration).toBeGreaterThanOrEqual(0.1); - expect(duration).toBeLessThanOrEqual(0.15); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/init.js index 37df798364a8..382115cbc8b9 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/test.ts index b571a7d6a306..192956b477d9 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/test.ts @@ -1,17 +1,22 @@ -import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { + BROWSER_SCRIPT_INVOKER, + CODE_FILE_PATH, + BROWSER_SCRIPT_SOURCE_CHAR_POSITION, + BROWSER_SCRIPT_INVOKER_TYPE, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest( - 'should capture long animation frame for top-level script.', + 'captures long animation frame span for top-level script.', async ({ browserName, getLocalTestUrl, page }) => { // Long animation frames only work on chrome - if (shouldSkipTracingTest() || browserName !== 'chromium') { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium'); // Long animation frame should take priority over long tasks @@ -21,38 +26,43 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); - const promise = getFirstSentryEnvelopeRequest(page); + const spans = collectStreamedSpans(page); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); await page.goto(url); - await new Promise(resolve => setTimeout(resolve, 1000)); + await pageloadSpanPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + const pageloadSpan = spans.find(s => getSpanOp(s) === 'pageload')!; - const eventData = await promise; + const uiSpans = spans.filter(s => getSpanOp(s)?.startsWith('ui.long_animation_frame')); - const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui.long_animation_frame')); + expect(uiSpans.length).toBeGreaterThanOrEqual(1); - expect(uiSpans?.length).toBeGreaterThanOrEqual(1); - - const topLevelUISpan = (uiSpans || []).find( - span => span.data?.['browser.script.invoker'] === 'https://sentry-test-site.example/path/to/script.js', + const topLevelUISpan = uiSpans.find( + s => s.attributes[BROWSER_SCRIPT_INVOKER]?.value === 'https://sentry-test-site.example/path/to/script.js', )!; + expect(topLevelUISpan).toEqual( expect.objectContaining({ - op: 'ui.long_animation_frame', - description: 'Main UI thread blocked', - parent_span_id: eventData.contexts?.trace?.span_id, - data: { - 'code.file.path': 'https://sentry-test-site.example/path/to/script.js', - 'browser.script.source_char_position': 0, - 'browser.script.invoker': 'https://sentry-test-site.example/path/to/script.js', - 'browser.script.invoker_type': 'classic-script', - [SENTRY_OP]: 'ui.long_animation_frame', - [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', - }, + name: 'Main UI thread blocked', + parent_span_id: pageloadSpan.span_id, + attributes: expect.objectContaining({ + [CODE_FILE_PATH]: { type: 'string', value: 'https://sentry-test-site.example/path/to/script.js' }, + [BROWSER_SCRIPT_SOURCE_CHAR_POSITION]: expect.objectContaining({ value: 0 }), + [BROWSER_SCRIPT_INVOKER]: { + type: 'string', + value: 'https://sentry-test-site.example/path/to/script.js', + }, + [BROWSER_SCRIPT_INVOKER_TYPE]: { type: 'string', value: 'classic-script' }, + [SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, + }), }), ); + const start = topLevelUISpan.start_timestamp ?? 0; - const end = topLevelUISpan.timestamp ?? 0; + const end = topLevelUISpan.end_timestamp ?? 0; const duration = end - start; expect(duration).toBeGreaterThanOrEqual(0.1); @@ -60,56 +70,51 @@ sentryTest( }, ); -sentryTest( - 'should capture long animation frame for event listener.', - async ({ browserName, getLocalTestUrl, page }) => { - // Long animation frames only work on chrome - if (shouldSkipTracingTest() || browserName !== 'chromium') { - sentryTest.skip(); - } - - await page.route('**/path/to/script.js', (route: Route) => - route.fulfill({ path: `${__dirname}/assets/script.js` }), - ); +sentryTest('captures long animation frame span for event listener.', async ({ browserName, getLocalTestUrl, page }) => { + // Long animation frames only work on chrome + sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium'); - const url = await getLocalTestUrl({ testDir: __dirname }); + await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); - const promise = getFirstSentryEnvelopeRequest(page); + const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); + const spans = collectStreamedSpans(page); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - // trigger long animation frame function - await page.getByRole('button').click(); + await page.goto(url); - await new Promise(resolve => setTimeout(resolve, 1000)); + // trigger long animation frame function + await page.getByRole('button').click(); - const eventData = await promise; + const pageloadSpan = await pageloadSpanPromise; + await page.evaluate(() => (window as any).Sentry.flush()); - const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui.long_animation_frame')) || []; + const uiSpans = spans.filter(s => getSpanOp(s)?.startsWith('ui.long_animation_frame')); - expect(uiSpans.length).toBeGreaterThanOrEqual(2); + expect(uiSpans.length).toBeGreaterThanOrEqual(2); - const eventListenerUISpan = uiSpans.find(span => span.data['browser.script.invoker'] === 'BUTTON#clickme.onclick')!; + const eventListenerUISpan = uiSpans.find( + s => s.attributes[BROWSER_SCRIPT_INVOKER]?.value === 'BUTTON#clickme.onclick', + )!; - expect(eventListenerUISpan).toEqual( - expect.objectContaining({ - op: 'ui.long_animation_frame', - description: 'Main UI thread blocked', - parent_span_id: eventData.contexts?.trace?.span_id, - data: { - 'browser.script.invoker': 'BUTTON#clickme.onclick', - 'browser.script.invoker_type': 'event-listener', - 'code.file.path': 'https://sentry-test-site.example/path/to/script.js', - [SENTRY_OP]: 'ui.long_animation_frame', - [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', - }, + expect(eventListenerUISpan).toEqual( + expect.objectContaining({ + name: 'Main UI thread blocked', + parent_span_id: pageloadSpan.span_id, + attributes: expect.objectContaining({ + [BROWSER_SCRIPT_INVOKER]: { type: 'string', value: 'BUTTON#clickme.onclick' }, + [BROWSER_SCRIPT_INVOKER_TYPE]: { type: 'string', value: 'event-listener' }, + [CODE_FILE_PATH]: { type: 'string', value: 'https://sentry-test-site.example/path/to/script.js' }, + [SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, }), - ); - const start = eventListenerUISpan.start_timestamp ?? 0; - const end = eventListenerUISpan.timestamp ?? 0; - const duration = end - start; + }), + ); - expect(duration).toBeGreaterThanOrEqual(0.1); - expect(duration).toBeLessThanOrEqual(0.15); - }, -); + const start = eventListenerUISpan.start_timestamp ?? 0; + const end = eventListenerUISpan.end_timestamp ?? 0; + const duration = end - start; + + expect(duration).toBeGreaterThanOrEqual(0.1); + expect(duration).toBeLessThanOrEqual(0.15); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-static/init.js similarity index 90% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-static/init.js index 9f36511d8ff5..63abc00dea38 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-static/init.js @@ -3,6 +3,7 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ @@ -12,7 +13,6 @@ Sentry.init({ enableInp: false, enableLongTask: true, }), - Sentry.spanStreamingIntegration(), ], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-streamed/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-static/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-streamed/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-static/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-streamed/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-static/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-streamed/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-static/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-static/test.ts similarity index 50% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-streamed/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-static/test.ts index 7e544f27a107..5236717091f1 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-static/test.ts @@ -1,29 +1,30 @@ import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; sentryTest( "doesn't capture long task spans starting before a navigation in the navigation transaction", async ({ browserName, getLocalTestUrl, page }) => { // Long tasks only work on chrome - sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium'); + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } const url = await getLocalTestUrl({ testDir: __dirname }); await page.route('**/path/to/script.js', route => route.fulfill({ path: `${__dirname}/assets/script.js` })); - const navigationSpansPromise = waitForStreamedSpans(page, spans => spans.some(s => getSpanOp(s) === 'navigation')); - await page.goto(url); + const navigationTransactionEventPromise = getFirstSentryEnvelopeRequest(page); + await page.locator('#myButton').click(); - const spans = await navigationSpansPromise; + const navigationTransactionEvent = await navigationTransactionEventPromise; - const navigationSpan = spans.find(s => getSpanOp(s) === 'navigation'); - expect(navigationSpan).toBeDefined(); + expect(navigationTransactionEvent.contexts?.trace?.op).toBe('navigation'); - const longTaskSpans = spans.filter(s => getSpanOp(s) === 'ui.long_task'); + const longTaskSpans = navigationTransactionEvent?.spans?.filter(span => span.op === 'ui.long_task'); expect(longTaskSpans).toHaveLength(0); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/init.js index 63abc00dea38..ccf669361e37 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/test.ts index 5236717091f1..fb458438ed43 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/test.ts @@ -1,30 +1,33 @@ +import { SENTRY_SEGMENT_ID } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest( - "doesn't capture long task spans starting before a navigation in the navigation transaction", + "doesn't capture long task spans starting before a navigation in the navigation span", async ({ browserName, getLocalTestUrl, page }) => { // Long tasks only work on chrome - if (shouldSkipTracingTest() || browserName !== 'chromium') { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium'); const url = await getLocalTestUrl({ testDir: __dirname }); await page.route('**/path/to/script.js', route => route.fulfill({ path: `${__dirname}/assets/script.js` })); - await page.goto(url); + const allSpans = collectStreamedSpans(page); + const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); - const navigationTransactionEventPromise = getFirstSentryEnvelopeRequest(page); + await page.goto(url); await page.locator('#myButton').click(); - const navigationTransactionEvent = await navigationTransactionEventPromise; + const navigationRoot = await navigationSpanPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + const spans = allSpans.filter(span => span.attributes[SENTRY_SEGMENT_ID]?.value === navigationRoot.span_id); - expect(navigationTransactionEvent.contexts?.trace?.op).toBe('navigation'); + const navigationSpan = spans.find(s => getSpanOp(s) === 'navigation'); + expect(navigationSpan).toBeDefined(); - const longTaskSpans = navigationTransactionEvent?.spans?.filter(span => span.op === 'ui.long_task'); + const longTaskSpans = spans.filter(s => getSpanOp(s) === 'ui.long_task'); expect(longTaskSpans).toHaveLength(0); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-streamed/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-static/assets/script.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-streamed/assets/script.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-static/assets/script.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-static/init.js similarity index 88% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-static/init.js index 3c004b952f80..250f2a72311b 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-static/init.js @@ -3,13 +3,13 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ enableLongTask: false, enableLongAnimationFrame: false, }), - Sentry.spanStreamingIntegration(), ], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-streamed/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-static/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-streamed/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-static/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-static/test.ts new file mode 100644 index 000000000000..bb32e21ee1d5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-static/test.ts @@ -0,0 +1,21 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should not capture long task when flag is disabled.', async ({ browserName, getLocalTestUrl, page }) => { + // Long tasks only work on chrome + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui')); + + expect(uiSpans?.length).toBe(0); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-streamed/test.ts deleted file mode 100644 index da7f44f05df7..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-streamed/test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Route } from '@playwright/test'; -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; - -sentryTest("doesn't capture long task spans when flag is disabled.", async ({ browserName, getLocalTestUrl, page }) => { - // Long tasks only work on chrome - sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium'); - - await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const spansPromise = waitForStreamedSpans(page, spans => spans.some(s => getSpanOp(s) === 'pageload')); - - await page.goto(url); - - const spans = await spansPromise; - const uiSpans = spans.filter(s => getSpanOp(s)?.startsWith('ui')); - - expect(uiSpans.length).toBe(0); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/init.js index 250f2a72311b..0339c3fa3dbf 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/test.ts index bb32e21ee1d5..6ebf0c018e6f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/test.ts @@ -1,21 +1,25 @@ import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; -sentryTest('should not capture long task when flag is disabled.', async ({ browserName, getLocalTestUrl, page }) => { +sentryTest("doesn't capture long task spans when flag is disabled.", async ({ browserName, getLocalTestUrl, page }) => { // Long tasks only work on chrome - if (shouldSkipTracingTest() || browserName !== 'chromium') { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium'); await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui')); + const spans = collectStreamedSpans(page); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - expect(uiSpans?.length).toBe(0); + await page.goto(url); + + await pageloadSpanPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + const uiSpans = spans.filter(s => getSpanOp(s)?.startsWith('ui')); + + expect(uiSpans.length).toBe(0); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-streamed/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-static/assets/script.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-streamed/assets/script.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-static/assets/script.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-static/init.js similarity index 87% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-static/init.js index 977ff9c6425e..73d20634b532 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-static/init.js @@ -3,12 +3,12 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ enableLongAnimationFrame: false, }), - Sentry.spanStreamingIntegration(), ], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-streamed/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-static/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-streamed/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-static/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-static/test.ts new file mode 100644 index 000000000000..e34b0d779f7e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-static/test.ts @@ -0,0 +1,36 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should capture long task.', async ({ browserName, getLocalTestUrl, page }) => { + // Long tasks only work on chrome + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui')); + + expect(uiSpans?.length).toBeGreaterThan(0); + + const [firstUISpan] = uiSpans || []; + expect(firstUISpan).toEqual( + expect.objectContaining({ + op: 'ui.long_task', + description: 'Main UI thread blocked', + parent_span_id: eventData.contexts?.trace?.span_id, + }), + ); + const start = firstUISpan.start_timestamp ?? 0; + const end = firstUISpan.timestamp ?? 0; + const duration = end - start; + + expect(duration).toBeGreaterThanOrEqual(0.1); + expect(duration).toBeLessThanOrEqual(0.15); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-streamed/test.ts deleted file mode 100644 index 1eeb65579b39..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-streamed/test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { Route } from '@playwright/test'; -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; - -sentryTest('captures long task.', async ({ browserName, getLocalTestUrl, page }) => { - // Long tasks only work on chrome - sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium'); - - await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const spansPromise = waitForStreamedSpans(page, spans => spans.some(s => getSpanOp(s) === 'pageload')); - - await page.goto(url); - - const spans = await spansPromise; - const pageloadSpan = spans.find(s => getSpanOp(s) === 'pageload')!; - - const uiSpans = spans.filter(s => getSpanOp(s)?.startsWith('ui')); - expect(uiSpans.length).toBeGreaterThan(0); - - const [firstUISpan] = uiSpans; - expect(firstUISpan).toEqual( - expect.objectContaining({ - name: 'Main UI thread blocked', - parent_span_id: pageloadSpan.span_id, - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'ui.long_task' }, - }), - }), - ); - - const start = firstUISpan.start_timestamp ?? 0; - const end = firstUISpan.end_timestamp ?? 0; - const duration = end - start; - - expect(duration).toBeGreaterThanOrEqual(0.1); - expect(duration).toBeLessThanOrEqual(0.15); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/init.js index 73d20634b532..47e9fd0b92d8 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/test.ts index e34b0d779f7e..add1a8130425 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/test.ts @@ -1,34 +1,42 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; -sentryTest('should capture long task.', async ({ browserName, getLocalTestUrl, page }) => { +sentryTest('captures long task.', async ({ browserName, getLocalTestUrl, page }) => { // Long tasks only work on chrome - if (shouldSkipTracingTest() || browserName !== 'chromium') { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium'); await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui')); + const spans = collectStreamedSpans(page); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - expect(uiSpans?.length).toBeGreaterThan(0); + await page.goto(url); - const [firstUISpan] = uiSpans || []; + const pageloadSpan = await pageloadSpanPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + + const uiSpans = spans.filter(s => getSpanOp(s)?.startsWith('ui')); + expect(uiSpans.length).toBeGreaterThan(0); + + const [firstUISpan] = uiSpans; expect(firstUISpan).toEqual( expect.objectContaining({ - op: 'ui.long_task', - description: 'Main UI thread blocked', - parent_span_id: eventData.contexts?.trace?.span_id, + name: 'Main UI thread blocked', + parent_span_id: pageloadSpan.span_id, + attributes: expect.objectContaining({ + [SENTRY_OP]: { type: 'string', value: 'ui.long_task' }, + }), }), ); + const start = firstUISpan.start_timestamp ?? 0; - const end = firstUISpan.timestamp ?? 0; + const end = firstUISpan.end_timestamp ?? 0; const duration = end - start; expect(duration).toBeGreaterThanOrEqual(0.1); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-no-animation-frame/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-no-animation-frame/init.js index a7797f5dce06..723707736f2e 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-no-animation-frame/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-no-animation-frame/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-no-animation-frame/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-no-animation-frame/test.ts index e34b0d779f7e..707a336d3df1 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-no-animation-frame/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-no-animation-frame/test.ts @@ -1,8 +1,9 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../../utils/spanUtils'; sentryTest('should capture long task.', async ({ browserName, getLocalTestUrl, page }) => { // Long tasks only work on chrome @@ -14,21 +15,27 @@ sentryTest('should capture long task.', async ({ browserName, getLocalTestUrl, p const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui')); + const spans = collectStreamedSpans(page); + const [pageloadSpan] = await waitForStreamedSpanAndTraceHeaderOnUrl( + page, + url, + span => getSpanOp(span) === 'pageload', + ); + await page.evaluate(() => (window as any).Sentry.flush()); + const uiSpans = spans.filter(span => getSpanOp(span)?.startsWith('ui')); - expect(uiSpans?.length).toBeGreaterThan(0); + expect(uiSpans.length).toBeGreaterThan(0); - const [firstUISpan] = uiSpans || []; + const [firstUISpan] = uiSpans; expect(firstUISpan).toEqual( expect.objectContaining({ - op: 'ui.long_task', - description: 'Main UI thread blocked', - parent_span_id: eventData.contexts?.trace?.span_id, + attributes: expect.objectContaining({ [SENTRY_OP]: { type: 'string', value: 'ui.long_task' } }), + name: 'Main UI thread blocked', + parent_span_id: pageloadSpan.span_id, }), ); const start = firstUISpan.start_timestamp ?? 0; - const end = firstUISpan.timestamp ?? 0; + const end = firstUISpan.end_timestamp ?? 0; const duration = end - start; expect(duration).toBeGreaterThanOrEqual(0.1); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/init.js index 52fc529d095a..d4c7810ef518 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/test.ts index 1d7a14a46055..86b7d100bb37 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/test.ts @@ -1,43 +1,31 @@ +import { SENTRY_SEGMENT_ID } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import type { Event, EventEnvelopeHeaders } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { - envelopeHeaderRequestParser, - getFirstSentryEnvelopeRequest, - shouldSkipTracingTest, -} from '../../../../utils/helpers'; - -sentryTest('should create a pageload transaction based on `sentry-trace` ', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../../utils/spanUtils'; +sentryTest('creates a pageload span based on `sentry-trace` ', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(eventData.contexts?.trace).toMatchObject({ - op: 'pageload', + const spans = collectStreamedSpans(page); + const [pageload] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url); + await page.evaluate(() => (window as any).Sentry.flush()); + expect(getSpanOp(pageload)).toBe('pageload'); + expect(pageload).toMatchObject({ + is_segment: true, parent_span_id: '1121201211212012', trace_id: '12312012123120121231201212312012', }); - - expect(eventData.spans?.length).toBeGreaterThan(0); + expect(spans.filter(span => !span.is_segment)).not.toHaveLength(0); }); sentryTest( - 'should pick up `baggage` tag, propagate the content in transaction and not add own data', + 'propagates `baggage` in the span envelope without adding own data', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - - const envHeader = await getFirstSentryEnvelopeRequest(page, url, envelopeHeaderRequestParser); - - expect(envHeader.trace).toBeDefined(); - expect(envHeader.trace).toEqual({ + const [, trace] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url); + expect(trace).toEqual({ release: '2.1.12', sample_rate: '0.3232', trace_id: '123', @@ -47,47 +35,31 @@ sentryTest( }, ); -sentryTest( - "should create a navigation that's not influenced by `sentry-trace` ", - async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const pageloadRequest = await getFirstSentryEnvelopeRequest(page, url); - const navigationRequest = await getFirstSentryEnvelopeRequest(page, `${url}#foo`); - - expect(pageloadRequest.contexts?.trace).toMatchObject({ - op: 'pageload', - parent_span_id: '1121201211212012', - trace_id: '12312012123120121231201212312012', - }); - - expect(navigationRequest.contexts?.trace?.op).toBe('navigation'); - expect(navigationRequest.contexts?.trace?.trace_id).toBeDefined(); - expect(navigationRequest.contexts?.trace?.trace_id).not.toBe(pageloadRequest.contexts?.trace?.trace_id); - - const pageloadSpans = pageloadRequest.spans; - const navigationSpans = navigationRequest.spans; - - const pageloadSpanId = pageloadRequest.contexts?.trace?.span_id; - const navigationSpanId = navigationRequest.contexts?.trace?.span_id; - - expect(pageloadSpanId).toBeDefined(); - expect(navigationSpanId).toBeDefined(); - - pageloadSpans?.forEach(span => - expect(span).toMatchObject({ - parent_span_id: pageloadSpanId, - }), - ); - - navigationSpans?.forEach(span => - expect(span).toMatchObject({ - parent_span_id: navigationSpanId, - }), - ); - }, -); +sentryTest("creates a navigation that's not influenced by `sentry-trace` ", async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); + const url = await getLocalTestUrl({ testDir: __dirname }); + const spans = collectStreamedSpans(page); + const [pageload] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url); + const [navigation] = await waitForStreamedSpanAndTraceHeaderOnUrl( + page, + `${url}#foo`, + span => getSpanOp(span) === 'navigation', + ); + await page.evaluate(() => (window as any).Sentry.flush()); + + expect(getSpanOp(pageload)).toBe('pageload'); + expect(pageload).toMatchObject({ + parent_span_id: '1121201211212012', + trace_id: '12312012123120121231201212312012', + }); + expect(getSpanOp(navigation)).toBe('navigation'); + expect(navigation.trace_id).toBeDefined(); + expect(navigation.trace_id).not.toBe(pageload.trace_id); + expect(pageload.span_id).toBeDefined(); + expect(navigation.span_id).toBeDefined(); + for (const root of [pageload, navigation]) { + spans + .filter(span => !span.is_segment && span.attributes[SENTRY_SEGMENT_ID]?.value === root.span_id) + .forEach(span => expect(span.parent_span_id).toBe(root.span_id)); + } +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/init.js index f1e482cc6950..fcce12c3dca2 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/test.ts index 829710ce5011..c62d4a2a7b3f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/test.ts @@ -1,69 +1,71 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { + URL_PATH, + SENTRY_IDLE_SPAN_FINISH_REASON, + USER_AGENT_ORIGINAL, + URL_FULL, + SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; -sentryTest( - 'should create a navigation transaction that aborts an ongoing pageload', - async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +sentryTest('should create a navigation span that aborts an ongoing pageload', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } - const url = await getLocalTestUrl({ testDir: __dirname }); + const url = await getLocalTestUrl({ testDir: __dirname }); - const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); - const navigationRequestPromise = waitForTransactionRequest( - page, - event => event.contexts?.trace?.op === 'navigation', - ); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); - await page.goto(url); + await page.goto(url); - const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise); - const navigationRequest = envelopeRequestParser(await navigationRequestPromise); + const pageloadSpan = await pageloadSpanPromise; + const navigationSpan = await navigationSpanPromise; + await page.evaluate(() => (window as any).Sentry.flush()); - expect(pageloadRequest.contexts?.trace?.op).toBe('pageload'); - expect(navigationRequest.contexts?.trace?.op).toBe('navigation'); + expect(getSpanOp(pageloadSpan)).toBe('pageload'); + expect(getSpanOp(navigationSpan)).toBe('navigation'); - expect(navigationRequest.transaction_info?.source).toEqual('url'); + expect(navigationSpan.attributes[SENTRY_SEGMENT_NAME_SOURCE]?.value).toEqual('url'); - const pageloadTraceId = pageloadRequest.contexts?.trace?.trace_id; - const navigationTraceId = navigationRequest.contexts?.trace?.trace_id; + const pageloadTraceId = pageloadSpan.trace_id; + const navigationTraceId = navigationSpan.trace_id; - expect(pageloadTraceId).toBeDefined(); - expect(navigationTraceId).toBeDefined(); - expect(pageloadTraceId).not.toEqual(navigationTraceId); + expect(pageloadTraceId).toBeDefined(); + expect(navigationTraceId).toBeDefined(); + expect(pageloadTraceId).not.toEqual(navigationTraceId); - expect(pageloadRequest.transaction).toEqual('/index.html'); - expect(navigationRequest.transaction).toEqual('/sub-page'); + expect(pageloadSpan.name).toEqual('Pageload'); + expect(pageloadSpan.attributes[URL_PATH]?.value).toEqual('/index.html'); + expect(navigationSpan.name).toEqual('Navigation'); + expect(navigationSpan.attributes[URL_PATH]?.value).toEqual('/sub-page'); - expect(pageloadRequest.contexts?.trace?.data).toMatchObject({ - [SENTRY_ORIGIN]: 'auto.pageload.browser', - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, - [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SENTRY_OP]: 'pageload', - ['sentry.idle_span_finish_reason']: 'cancelled', - }); - expect(navigationRequest.contexts?.trace?.data).toMatchObject({ - [SENTRY_ORIGIN]: 'auto.navigation.browser', - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, - [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SENTRY_OP]: 'navigation', - ['sentry.idle_span_finish_reason']: 'idleTimeout', - }); - expect(pageloadRequest.request).toEqual({ - headers: { - 'User-Agent': expect.any(String), - }, - url: 'http://sentry-test.io/index.html', - }); - expect(navigationRequest.request).toEqual({ - headers: { - 'User-Agent': expect.any(String), - }, - url: 'http://sentry-test.io/sub-page', - }); - }, -); + expect(pageloadSpan.attributes).toMatchObject({ + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, + [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'url' }, + [SENTRY_OP]: { type: 'string', value: 'pageload' }, + [SENTRY_IDLE_SPAN_FINISH_REASON]: { type: 'string', value: 'cancelled' }, + }); + expect(navigationSpan.attributes).toMatchObject({ + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.navigation.browser' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, + [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'url' }, + [SENTRY_OP]: { type: 'string', value: 'navigation' }, + [SENTRY_IDLE_SPAN_FINISH_REASON]: { type: 'string', value: 'idleTimeout' }, + }); + expect(pageloadSpan.attributes).toMatchObject({ + [USER_AGENT_ORIGINAL]: { type: 'string', value: expect.any(String) }, + [URL_FULL]: { type: 'string', value: 'http://sentry-test.io/index.html' }, + }); + expect(navigationSpan.attributes).toMatchObject({ + [USER_AGENT_ORIGINAL]: { type: 'string', value: expect.any(String) }, + [URL_FULL]: { type: 'string', value: 'http://sentry-test.io/sub-page' }, + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/init.js index b538f322b360..484a12682a2d 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/test.ts index caa2b6afb328..75f306ef5078 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/test.ts @@ -1,8 +1,13 @@ import { expect } from '@playwright/test'; import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; import { + SENTRY_IDLE_SPAN_FINISH_REASON, + USER_AGENT_ORIGINAL, + SENTRY_SEGMENT_ID, + SENTRY_SEGMENT_NAME, SENTRY_SEGMENT_NAME_SOURCE, URL_FULL, URL_PATH, @@ -17,66 +22,72 @@ sentryTest( sentryTest.skip(); } + const allSpans = collectStreamedSpans(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); - const navigationRequestPromise = waitForTransactionRequest( + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + const navigationSpanPromise = waitForStreamedSpan( page, - event => event.contexts?.trace?.op === 'navigation' && event.transaction === '/next-page', + span => getSpanOp(span) === 'navigation' && span.attributes[URL_PATH]?.value === '/next-page', ); await page.goto(url); - const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise); - const navigationRequest = envelopeRequestParser(await navigationRequestPromise); + const pageloadSpan = await pageloadSpanPromise; + const navigationSpan = await navigationSpanPromise; + await page.evaluate(() => (window as any).Sentry.flush()); - expect(pageloadRequest.contexts?.trace?.op).toBe('pageload'); + expect(getSpanOp(pageloadSpan)).toBe('pageload'); - expect(pageloadRequest.contexts?.trace?.data).toMatchObject({ - [SENTRY_ORIGIN]: 'auto.pageload.browser', - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, - [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SENTRY_OP]: 'pageload', - ['sentry.idle_span_finish_reason']: 'cancelled', + expect(pageloadSpan.attributes).toMatchObject({ + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, + [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'url' }, + [SENTRY_OP]: { type: 'string', value: 'pageload' }, + [SENTRY_IDLE_SPAN_FINISH_REASON]: { type: 'string', value: 'cancelled' }, }); - expect(pageloadRequest.request).toEqual({ - headers: { - 'User-Agent': expect.any(String), - }, - url: 'http://sentry-test.io/index.html', + expect(pageloadSpan.attributes).toMatchObject({ + [USER_AGENT_ORIGINAL]: { type: 'string', value: expect.any(String) }, + [URL_FULL]: { type: 'string', value: 'http://sentry-test.io/index.html' }, }); - const spans = pageloadRequest.spans || []; + const spans = allSpans.filter(span => span.attributes[SENTRY_SEGMENT_ID]?.value === pageloadSpan.span_id); - const redirectSpans = spans.filter(span => span.op === 'navigation.redirect'); + const redirectSpans = spans.filter(span => getSpanOp(span) === 'navigation.redirect'); expect(redirectSpans).toHaveLength(3); redirectSpans.forEach(redirectSpan => { - expect(redirectSpan?.timestamp).toEqual(redirectSpan?.start_timestamp); - expect(redirectSpan).toEqual({ - data: { - 'sentry.op': 'navigation.redirect', - 'sentry.origin': 'auto.navigation.browser', - [URL_FULL]: expect.any(String), - [URL_PATH]: expect.any(String), + expect(redirectSpan?.end_timestamp).toEqual(redirectSpan?.start_timestamp); + expect(redirectSpan).toMatchObject({ + attributes: { + [SENTRY_OP]: { type: 'string', value: 'navigation.redirect' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.navigation.browser' }, + [SENTRY_SEGMENT_ID]: { type: 'string', value: pageloadSpan.span_id }, + [SENTRY_SEGMENT_NAME]: { type: 'string', value: 'Pageload' }, + [URL_FULL]: { type: 'string', value: expect.any(String) }, + [URL_PATH]: { type: 'string', value: expect.any(String) }, }, - description: expect.stringContaining('/sub-page-redirect-'), - op: 'navigation.redirect', - origin: 'auto.navigation.browser', + name: 'Navigation', + is_segment: false, status: 'ok', - parent_span_id: pageloadRequest.contexts!.trace!.span_id, + parent_span_id: pageloadSpan.span_id, span_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), + end_timestamp: expect.any(Number), trace_id: expect.any(String), }); }); - expect(navigationRequest.contexts?.trace?.op).toBe('navigation'); - expect(navigationRequest.transaction).toEqual('/next-page'); + expect(getSpanOp(navigationSpan)).toBe('navigation'); + expect(navigationSpan.name).toEqual('Navigation'); + expect(navigationSpan.attributes[URL_PATH]?.value).toEqual('/next-page'); // 2 subsequent redirects belonging to the navigation root span - expect(navigationRequest.spans?.filter(span => span.op === 'navigation.redirect')).toHaveLength(2); + expect( + allSpans + .filter(span => span.attributes[SENTRY_SEGMENT_ID]?.value === navigationSpan.span_id) + .filter(span => getSpanOp(span) === 'navigation.redirect'), + ).toHaveLength(2); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-navigation-click/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-navigation-click/init.js index b85590114a76..c3e90ce95430 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-navigation-click/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-navigation-click/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-navigation-click/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-navigation-click/test.ts index a88987209bcd..93e77f615917 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-navigation-click/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-navigation-click/test.ts @@ -1,6 +1,8 @@ +import { URL_PATH, SENTRY_SEGMENT_ID } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; sentryTest( 'creates navigation root span if click happened within 1.5s of the last navigation', @@ -9,39 +11,47 @@ sentryTest( sentryTest.skip(); } + const allSpans = collectStreamedSpans(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); - const navigationRequestPromise = waitForTransactionRequest( + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + const navigationSpanPromise = waitForStreamedSpan( page, - event => event.contexts?.trace?.op === 'navigation' && event.transaction === '/sub-page', + span => getSpanOp(span) === 'navigation' && span.attributes[URL_PATH]?.value === '/sub-page', ); - const navigation2RequestPromise = waitForTransactionRequest( + const navigation2SpanPromise = waitForStreamedSpan( page, - event => event.contexts?.trace?.op === 'navigation' && event.transaction === '/sub-page-2', + span => getSpanOp(span) === 'navigation' && span.attributes[URL_PATH]?.value === '/sub-page-2', ); await page.goto(url); - await pageloadRequestPromise; + await pageloadSpanPromise; // Now trigger navigation (since no span is active), and then a redirect in the navigation, with await page.click('#btn1'); - const navigationRequest = envelopeRequestParser(await navigationRequestPromise); - const navigation2Request = envelopeRequestParser(await navigation2RequestPromise); + const navigationSpan = await navigationSpanPromise; + const navigation2Span = await navigation2SpanPromise; + await page.evaluate(() => (window as any).Sentry.flush()); - expect(navigationRequest.contexts?.trace?.op).toBe('navigation'); - expect(navigationRequest.transaction).toEqual('/sub-page'); + expect(getSpanOp(navigationSpan)).toBe('navigation'); + expect(navigationSpan.name).toEqual('Navigation'); + expect(navigationSpan.attributes[URL_PATH]?.value).toEqual('/sub-page'); - const spans = (navigationRequest.spans || []).filter(s => s.op === 'navigation.redirect'); + const spans = allSpans + .filter(span => span.attributes[SENTRY_SEGMENT_ID]?.value === navigationSpan.span_id) + .filter(s => getSpanOp(s) === 'navigation.redirect'); expect(spans).toHaveLength(0); - expect(navigation2Request.contexts?.trace?.op).toBe('navigation'); - expect(navigation2Request.transaction).toEqual('/sub-page-2'); + expect(getSpanOp(navigation2Span)).toBe('navigation'); + expect(navigation2Span.name).toEqual('Navigation'); + expect(navigation2Span.attributes[URL_PATH]?.value).toEqual('/sub-page-2'); - const spans2 = (navigation2Request.spans || []).filter(s => s.op === 'navigation.redirect'); + const spans2 = allSpans + .filter(span => span.attributes[SENTRY_SEGMENT_ID]?.value === navigation2Span.span_id) + .filter(s => getSpanOp(s) === 'navigation.redirect'); expect(spans2).toHaveLength(0); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-navigation-keypress/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-navigation-keypress/init.js index b53ef315b481..f9d8db46d3d9 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-navigation-keypress/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-navigation-keypress/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-navigation-keypress/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-navigation-keypress/test.ts index fe152feda462..fe01834e77cb 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-navigation-keypress/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-navigation-keypress/test.ts @@ -1,6 +1,8 @@ +import { URL_PATH, SENTRY_SEGMENT_ID } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; sentryTest( 'creates a navigation root span if a keypress happened within the last 1.5s', @@ -9,22 +11,23 @@ sentryTest( sentryTest.skip(); } + const allSpans = collectStreamedSpans(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); - const navigationRequestPromise = waitForTransactionRequest( + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + const navigationSpanPromise = waitForStreamedSpan( page, - event => event.contexts?.trace?.op === 'navigation' && event.transaction === '/sub-page', + span => getSpanOp(span) === 'navigation' && span.attributes[URL_PATH]?.value === '/sub-page', ); - const navigationRequest2Promise = waitForTransactionRequest( + const navigationSpan2Promise = waitForStreamedSpan( page, - event => event.contexts?.trace?.op === 'navigation' && event.transaction === '/sub-page-2', + span => getSpanOp(span) === 'navigation' && span.attributes[URL_PATH]?.value === '/sub-page-2', ); await page.goto(url); - await pageloadRequestPromise; + await pageloadSpanPromise; await page.focus('#btn1'); await page.keyboard.press('Enter'); @@ -34,21 +37,29 @@ sentryTest( await page.focus('#btn2'); await page.keyboard.press('Enter'); - const navigationRequest = envelopeRequestParser(await navigationRequestPromise); - const navigationRequest2 = envelopeRequestParser(await navigationRequest2Promise); + const navigationSpan = await navigationSpanPromise; + const navigationSpan2 = await navigationSpan2Promise; + await page.evaluate(() => (window as any).Sentry.flush()); - expect(navigationRequest.contexts?.trace?.op).toBe('navigation'); - expect(navigationRequest.transaction).toEqual('/sub-page'); + expect(getSpanOp(navigationSpan)).toBe('navigation'); + expect(navigationSpan.name).toEqual('Navigation'); + expect(navigationSpan.attributes[URL_PATH]?.value).toEqual('/sub-page'); - const redirectSpans = navigationRequest.spans?.filter(span => span.op === 'navigation.redirect') || []; + const redirectSpans = allSpans + .filter(span => span.attributes[SENTRY_SEGMENT_ID]?.value === navigationSpan.span_id) + .filter(span => getSpanOp(span) === 'navigation.redirect'); expect(redirectSpans).toHaveLength(1); - expect(redirectSpans[0].description).toEqual('/sub-page-redirect'); + expect(redirectSpans[0].name).toBe('Navigation'); + expect(redirectSpans[0].attributes[URL_PATH]?.value).toBe('/sub-page-redirect'); - expect(navigationRequest2.contexts?.trace?.op).toBe('navigation'); - expect(navigationRequest2.transaction).toEqual('/sub-page-2'); + expect(getSpanOp(navigationSpan2)).toBe('navigation'); + expect(navigationSpan2.name).toEqual('Navigation'); + expect(navigationSpan2.attributes[URL_PATH]?.value).toEqual('/sub-page-2'); - const redirectSpans2 = navigationRequest2.spans?.filter(span => span.op === 'navigation.redirect') || []; + const redirectSpans2 = allSpans + .filter(span => span.attributes[SENTRY_SEGMENT_ID]?.value === navigationSpan2.span_id) + .filter(span => getSpanOp(span) === 'navigation.redirect'); expect(redirectSpans2).toHaveLength(0); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-redirect/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-redirect/init.js index 1545e68dd89c..5cf45a1f0b06 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-redirect/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-redirect/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-redirect/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-redirect/test.ts index 71fe35c8bcc9..bab505425ba5 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-redirect/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/navigation-redirect/test.ts @@ -1,6 +1,8 @@ +import { URL_PATH, SENTRY_SEGMENT_ID, SENTRY_OP } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; sentryTest( 'creates a navigation root span and redirect child span if no click happened within the last 1.5s', @@ -9,32 +11,35 @@ sentryTest( sentryTest.skip(); } + const allSpans = collectStreamedSpans(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); - const navigationRequestPromise = waitForTransactionRequest( - page, - event => event.contexts?.trace?.op === 'navigation', - ); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); await page.goto(url); - await pageloadRequestPromise; + await pageloadSpanPromise; // Now trigger navigation (since no span is active), and then a redirect in the navigation, with await page.click('#btn1'); - const navigationRequest = envelopeRequestParser(await navigationRequestPromise); + const navigationSpan = await navigationSpanPromise; + await page.evaluate(() => (window as any).Sentry.flush()); - expect(navigationRequest.contexts?.trace?.op).toBe('navigation'); - expect(navigationRequest.transaction).toEqual('/sub-page'); + expect(getSpanOp(navigationSpan)).toBe('navigation'); + expect(navigationSpan.name).toEqual('Navigation'); + expect(navigationSpan.attributes[URL_PATH]?.value).toEqual('/sub-page'); - const spans = navigationRequest.spans || []; + const spans = allSpans.filter(span => span.attributes[SENTRY_SEGMENT_ID]?.value === navigationSpan.span_id); expect(spans).toContainEqual( expect.objectContaining({ - op: 'navigation.redirect', - description: '/sub-page-redirect', + name: 'Navigation', + attributes: expect.objectContaining({ + [SENTRY_OP]: { type: 'string', value: 'navigation.redirect' }, + [URL_PATH]: { type: 'string', value: '/sub-page-redirect' }, + }), }), ); }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/opt-out/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/opt-out/init.js index bfefd3b54534..331024032a6f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/opt-out/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/opt-out/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/opt-out/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/opt-out/test.ts index a25cc01f086f..ba2c70c2b924 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/opt-out/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/opt-out/test.ts @@ -1,6 +1,8 @@ +import { SENTRY_SEGMENT_ID, SENTRY_OP } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; sentryTest( "doesn't create a navigation.redirect span if `detectRedirects` is set to false", @@ -9,25 +11,24 @@ sentryTest( sentryTest.skip(); } + const allSpans = collectStreamedSpans(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); - const navigationRequestPromise = waitForTransactionRequest( - page, - event => event.contexts?.trace?.op === 'navigation', - ); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); await page.goto(url); - const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise); + const pageloadSpan = await pageloadSpanPromise; + await page.evaluate(() => (window as any).Sentry.flush()); // Ensure a navigation span is sent, too - await navigationRequestPromise; + await navigationSpanPromise; - const spans = pageloadRequest.spans || []; + const spans = allSpans.filter(span => span.attributes[SENTRY_SEGMENT_ID]?.value === pageloadSpan.span_id); expect(spans).not.toContainEqual( expect.objectContaining({ - op: 'navigation.redirect', + attributes: expect.objectContaining({ [SENTRY_OP]: { type: 'string', value: 'navigation.redirect' } }), }), ); }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-click/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-click/init.js index bdd43f5ac757..1ce4125ee422 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-click/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-click/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-click/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-click/test.ts index 771b07afaba6..a7e18331a79d 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-click/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-click/test.ts @@ -1,6 +1,8 @@ +import { SENTRY_SEGMENT_ID, SENTRY_OP } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; sentryTest( "doesn't create a navigation.redirect span if a click happened before navigation", @@ -9,25 +11,24 @@ sentryTest( sentryTest.skip(); } + const allSpans = collectStreamedSpans(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); - const navigationRequestPromise = waitForTransactionRequest( - page, - event => event.contexts?.trace?.op === 'navigation', - ); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); await page.goto(url); - const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise); + const pageloadSpan = await pageloadSpanPromise; + await page.evaluate(() => (window as any).Sentry.flush()); // Ensure a navigation span is sent, too - await navigationRequestPromise; + await navigationSpanPromise; - const pageloadTxnSpans = pageloadRequest.spans || []; + const pageloadSpans = allSpans.filter(span => span.attributes[SENTRY_SEGMENT_ID]?.value === pageloadSpan.span_id); - expect(pageloadTxnSpans).not.toContainEqual( + expect(pageloadSpans).not.toContainEqual( expect.objectContaining({ - op: 'navigation.redirect', + attributes: expect.objectContaining({ [SENTRY_OP]: { type: 'string', value: 'navigation.redirect' } }), }), ); }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-keypress/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-keypress/init.js index bdd43f5ac757..1ce4125ee422 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-keypress/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-keypress/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-keypress/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-keypress/test.ts index e77e08936466..e82f4f565293 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-keypress/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-keypress/test.ts @@ -1,6 +1,8 @@ +import { SENTRY_SEGMENT_ID, SENTRY_OP } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; sentryTest( "doesn't create a navigation.redirect span if a keypress happened before navigation", @@ -9,27 +11,26 @@ sentryTest( sentryTest.skip(); } + const allSpans = collectStreamedSpans(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); - const navigationRequestPromise = waitForTransactionRequest( - page, - event => event.contexts?.trace?.op === 'navigation', - ); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); await page.goto(url); await page.focus('#btn1'); await page.keyboard.press('Enter'); - const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise); + const pageloadSpan = await pageloadSpanPromise; + await page.evaluate(() => (window as any).Sentry.flush()); // Ensure a navigation span is sent, too - await navigationRequestPromise; + await navigationSpanPromise; - const spans = pageloadRequest.spans || []; + const spans = allSpans.filter(span => span.attributes[SENTRY_SEGMENT_ID]?.value === pageloadSpan.span_id); expect(spans).not.toContainEqual( expect.objectContaining({ - op: 'navigation.redirect', + attributes: expect.objectContaining({ [SENTRY_OP]: { type: 'string', value: 'navigation.redirect' } }), }), ); }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-time/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-time/init.js index 4aecc7f5ef40..f317a550e2a7 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-time/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-time/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-time/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-time/test.ts index 6b5925f52cd1..535bc77adaae 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-time/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-navigation-time/test.ts @@ -1,6 +1,8 @@ +import { SENTRY_SEGMENT_ID, SENTRY_OP } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; sentryTest( "doesn't create a navigation.redirect span if redirect happened more than 1.5s after pageload", @@ -9,25 +11,24 @@ sentryTest( sentryTest.skip(); } + const allSpans = collectStreamedSpans(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); - const navigationRequestPromise = waitForTransactionRequest( - page, - event => event.contexts?.trace?.op === 'navigation', - ); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation'); await page.goto(url); - const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise); + const pageloadSpan = await pageloadSpanPromise; + await page.evaluate(() => (window as any).Sentry.flush()); // Ensure a navigation span is sent, too - await navigationRequestPromise; + await navigationSpanPromise; - const spans = pageloadRequest.spans || []; + const spans = allSpans.filter(span => span.attributes[SENTRY_SEGMENT_ID]?.value === pageloadSpan.span_id); expect(spans).not.toContainEqual( expect.objectContaining({ - op: 'navigation.redirect', + attributes: expect.objectContaining({ [SENTRY_OP]: { type: 'string', value: 'navigation.redirect' } }), }), ); }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/init.js index bb91be7762dc..cba0015b22c8 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/test.ts index fc126da536f3..866e09aa6551 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/test.ts @@ -1,8 +1,13 @@ import { expect } from '@playwright/test'; import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; import { + SENTRY_IDLE_SPAN_FINISH_REASON, + USER_AGENT_ORIGINAL, + SENTRY_SEGMENT_ID, + SENTRY_SEGMENT_NAME, SENTRY_SEGMENT_NAME_SOURCE, URL_FULL, URL_PATH, @@ -15,56 +20,57 @@ sentryTest('creates a pageload root span with navigation.redirect childspan', as sentryTest.skip(); } + const allSpans = collectStreamedSpans(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); await page.goto(url); - const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise); + const pageloadSpan = await pageloadSpanPromise; + await page.evaluate(() => (window as any).Sentry.flush()); - expect(pageloadRequest.contexts?.trace?.op).toBe('pageload'); + expect(getSpanOp(pageloadSpan)).toBe('pageload'); - expect(pageloadRequest.contexts?.trace?.data).toMatchObject({ - [SENTRY_ORIGIN]: 'auto.pageload.browser', - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, - [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SENTRY_OP]: 'pageload', - ['sentry.idle_span_finish_reason']: 'idleTimeout', + expect(pageloadSpan.attributes).toMatchObject({ + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, + [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'url' }, + [SENTRY_OP]: { type: 'string', value: 'pageload' }, + [SENTRY_IDLE_SPAN_FINISH_REASON]: { type: 'string', value: 'idleTimeout' }, }); - expect(pageloadRequest.request).toEqual({ - headers: { - 'User-Agent': expect.any(String), - }, - url: 'http://sentry-test.io/index.html', + expect(pageloadSpan.attributes).toMatchObject({ + [USER_AGENT_ORIGINAL]: { type: 'string', value: expect.any(String) }, + [URL_FULL]: { type: 'string', value: 'http://sentry-test.io/index.html' }, }); - const spans = pageloadRequest.spans || []; + const spans = allSpans.filter(span => span.attributes[SENTRY_SEGMENT_ID]?.value === pageloadSpan.span_id); expect(spans).toContainEqual( expect.objectContaining({ - op: 'navigation.redirect', + attributes: expect.objectContaining({ [SENTRY_OP]: { type: 'string', value: 'navigation.redirect' } }), }), ); - const redirectSpan = spans.find(span => span.op === 'navigation.redirect'); - expect(redirectSpan?.timestamp).toEqual(redirectSpan?.start_timestamp); - expect(redirectSpan).toEqual({ - data: { - 'sentry.op': 'navigation.redirect', - 'sentry.origin': 'auto.navigation.browser', - [URL_FULL]: 'http://sentry-test.io/sub-page', - [URL_PATH]: '/sub-page', + const redirectSpan = spans.find(span => getSpanOp(span) === 'navigation.redirect'); + expect(redirectSpan?.end_timestamp).toEqual(redirectSpan?.start_timestamp); + expect(redirectSpan).toMatchObject({ + attributes: { + [SENTRY_OP]: { type: 'string', value: 'navigation.redirect' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.navigation.browser' }, + [SENTRY_SEGMENT_ID]: { type: 'string', value: pageloadSpan.span_id }, + [SENTRY_SEGMENT_NAME]: { type: 'string', value: 'Pageload' }, + [URL_FULL]: { type: 'string', value: 'http://sentry-test.io/sub-page' }, + [URL_PATH]: { type: 'string', value: '/sub-page' }, }, - description: '/sub-page', - op: 'navigation.redirect', - origin: 'auto.navigation.browser', + name: 'Navigation', + is_segment: false, status: 'ok', - parent_span_id: pageloadRequest.contexts!.trace!.span_id, + parent_span_id: pageloadSpan.span_id, span_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), + end_timestamp: expect.any(Number), trace_id: expect.any(String), }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-static/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-static/init.js new file mode 100644 index 000000000000..bb6d4918d1bf --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-static/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-static/test.ts new file mode 100644 index 000000000000..bf3324b9809b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-static/test.ts @@ -0,0 +1,156 @@ +import { PAGELOAD, NAVIGATION } from '@sentry/conventions/op'; +import { + SENTRY_IDLE_SPAN_FINISH_REASON, + SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest('should create a navigation transaction on page navigation', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === PAGELOAD); + await page.goto(url); + const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise); + + const navigationRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === NAVIGATION); + await page.goto(`${url}#foo`); + const navigationRequest = envelopeRequestParser(await navigationRequestPromise); + + expect(pageloadRequest.contexts?.trace?.op).toBe(PAGELOAD); + expect(navigationRequest.contexts?.trace?.op).toBe(NAVIGATION); + + expect(navigationRequest.transaction_info?.source).toEqual('url'); + + const pageloadTraceId = pageloadRequest.contexts?.trace?.trace_id; + const navigationTraceId = navigationRequest.contexts?.trace?.trace_id; + + expect(pageloadTraceId).toBeDefined(); + expect(navigationTraceId).toBeDefined(); + expect(pageloadTraceId).not.toEqual(navigationTraceId); + + expect(pageloadRequest.transaction).toEqual('/index.html'); + // Fragment is not in transaction name + expect(navigationRequest.transaction).toEqual('/index.html'); + + expect(pageloadRequest.contexts?.trace?.data).toMatchObject({ + [SENTRY_ORIGIN]: 'auto.pageload.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SENTRY_SEGMENT_NAME_SOURCE]: 'url', + [SENTRY_OP]: PAGELOAD, + [SENTRY_IDLE_SPAN_FINISH_REASON]: 'idleTimeout', + }); + expect(navigationRequest.contexts?.trace?.data).toMatchObject({ + [SENTRY_ORIGIN]: 'auto.navigation.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SENTRY_SEGMENT_NAME_SOURCE]: 'url', + [SENTRY_OP]: NAVIGATION, + [SENTRY_IDLE_SPAN_FINISH_REASON]: 'idleTimeout', + }); + expect(pageloadRequest.request).toEqual({ + headers: { + 'User-Agent': expect.any(String), + }, + url: 'http://sentry-test.io/index.html', + }); + expect(navigationRequest.request).toEqual({ + headers: { + 'User-Agent': expect.any(String), + }, + url: 'http://sentry-test.io/index.html#foo', + }); + + const pageloadSpans = pageloadRequest.spans; + const navigationSpans = navigationRequest.spans; + + const pageloadSpanId = pageloadRequest.contexts?.trace?.span_id; + const navigationSpanId = navigationRequest.contexts?.trace?.span_id; + + expect(pageloadSpanId).toBeDefined(); + expect(navigationSpanId).toBeDefined(); + + pageloadSpans?.forEach(span => + expect(span).toMatchObject({ + parent_span_id: pageloadSpanId, + }), + ); + + navigationSpans?.forEach(span => + expect(span).toMatchObject({ + parent_span_id: navigationSpanId, + }), + ); + + expect(pageloadSpanId).not.toEqual(navigationSpanId); +}); + +// +sentryTest('should handle pushState with full URL', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === PAGELOAD); + const navigationRequestPromise = waitForTransactionRequest( + page, + event => event.contexts?.trace?.op === NAVIGATION && event.transaction === '/sub-page', + ); + const navigationRequestPromise2 = waitForTransactionRequest( + page, + event => event.contexts?.trace?.op === NAVIGATION && event.transaction === '/sub-page-2', + ); + + await page.goto(url); + await pageloadRequestPromise; + + await page.evaluate("window.history.pushState({}, '', `${window.location.origin}/sub-page`);"); + + const navigationRequest = envelopeRequestParser(await navigationRequestPromise); + + expect(navigationRequest.transaction).toEqual('/sub-page'); + + expect(navigationRequest.contexts?.trace?.data).toMatchObject({ + [SENTRY_ORIGIN]: 'auto.navigation.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SENTRY_SEGMENT_NAME_SOURCE]: 'url', + [SENTRY_OP]: NAVIGATION, + [SENTRY_IDLE_SPAN_FINISH_REASON]: 'idleTimeout', + }); + expect(navigationRequest.request).toEqual({ + headers: { + 'User-Agent': expect.any(String), + }, + url: 'http://sentry-test.io/sub-page', + }); + + await page.evaluate("window.history.pushState({}, '', `${window.location.origin}/sub-page-2`);"); + + const navigationRequest2 = envelopeRequestParser(await navigationRequestPromise2); + + expect(navigationRequest2.transaction).toEqual('/sub-page-2'); + + expect(navigationRequest2.contexts?.trace?.data).toMatchObject({ + [SENTRY_ORIGIN]: 'auto.navigation.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SENTRY_SEGMENT_NAME_SOURCE]: 'url', + [SENTRY_OP]: NAVIGATION, + [SENTRY_IDLE_SPAN_FINISH_REASON]: 'idleTimeout', + }); + expect(navigationRequest2.request).toEqual({ + headers: { + 'User-Agent': expect.any(String), + }, + url: 'http://sentry-test.io/sub-page-2', + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts deleted file mode 100644 index 62fcd88c337f..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { expect } from '@playwright/test'; -import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; -import { - SENTRY_SEGMENT_NAME_SOURCE, - SENTRY_TRACE_LIFECYCLE, - URL_FULL, - URL_PATH, - USER_AGENT_ORIGINAL, - SENTRY_ENVIRONMENT, - SENTRY_SDK_INTEGRATIONS, - SENTRY_OP, - SENTRY_ORIGIN, -} from '@sentry/conventions/attributes'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { - getSpanOp, - getSpansFromEnvelope, - waitForStreamedSpan, - waitForStreamedSpanEnvelope, -} from '../../../../utils/spanUtils'; - -sentryTest('starts a streamed navigation span on page navigation', async ({ browserName, getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - const navigationSpanEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!getSpansFromEnvelope(env).find(s => getSpanOp(s) === 'navigation'), - ); - - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - - const pageloadSpan = await pageloadSpanPromise; - - // simulate navigation - page.goto(`${url}#foo`); - - const navigationSpanEnvelope = await navigationSpanEnvelopePromise; - - const navigationSpanEnvelopeHeader = navigationSpanEnvelope[0]; - const navigationSpanEnvelopeItem = navigationSpanEnvelope[1]; - const navigationSpans = navigationSpanEnvelopeItem[0][1].items; - const navigationSpan = navigationSpans.find(s => getSpanOp(s) === 'navigation')!; - - expect(navigationSpanEnvelopeHeader).toEqual({ - sent_at: expect.any(String), - trace: { - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - environment: 'production', - public_key: 'public', - sample_rand: expect.any(String), - sample_rate: '1', - sampled: 'true', - }, - sdk: { - name: 'sentry.javascript.browser', - version: SDK_VERSION, - }, - }); - - const numericSampleRand = parseFloat(navigationSpanEnvelopeHeader.trace!.sample_rand!); - expect(Number.isNaN(numericSampleRand)).toBe(false); - - const pageloadTraceId = pageloadSpan.trace_id; - const navigationTraceId = navigationSpan.trace_id; - - expect(pageloadTraceId).toBeDefined(); - expect(navigationTraceId).toBeDefined(); - expect(pageloadTraceId).not.toEqual(navigationTraceId); - - expect(pageloadSpan.name).toEqual('Pageload'); - - expect(navigationSpan).toEqual({ - attributes: { - 'sentry.is_localhost': { value: false, type: 'boolean' }, - [SENTRY_TRACE_LIFECYCLE]: { - type: 'string', - value: 'stream', - }, - 'culture.calendar': { - type: 'string', - value: expect.any(String), - }, - 'culture.locale': { - type: 'string', - value: expect.any(String), - }, - 'culture.timezone': { - type: 'string', - value: expect.any(String), - }, - [USER_AGENT_ORIGINAL]: { - type: 'string', - value: expect.any(String), - }, - [URL_FULL]: { - type: 'string', - value: expect.any(String), - }, - [URL_PATH]: { - type: 'string', - value: expect.any(String), - }, - 'device.processor_count': { - type: expect.stringMatching(/^(integer)|(double)$/), - value: expect.any(Number), - }, - ...(browserName !== 'webkit' && { - 'network.connection.effective_type': { - type: 'string', - value: expect.any(String), - }, - 'network.connection.rtt': { - type: expect.stringMatching(/^(integer)|(double)$/), - value: expect.any(Number), - }, - }), - 'sentry.idle_span_finish_reason': { - type: 'string', - value: 'idleTimeout', - }, - [SENTRY_OP]: { - type: 'string', - value: 'navigation', - }, - [SENTRY_ORIGIN]: { - type: 'string', - value: 'auto.navigation.browser', - }, - 'sentry.previous_trace': { - type: 'string', - value: `${pageloadTraceId}-${pageloadSpan.span_id}-1`, - }, - 'sentry.sample_rate': { - type: 'integer', - value: 1, - }, - 'sentry.sdk.name': { - type: 'string', - value: 'sentry.javascript.browser', - }, - 'sentry.sdk.version': { - type: 'string', - value: SDK_VERSION, - }, - [SENTRY_SDK_INTEGRATIONS]: { - type: 'array', - value: expect.arrayContaining(['BrowserTracing', 'SpanStreaming']), - }, - 'sentry.segment.id': { - type: 'string', - value: navigationSpan.span_id, - }, - 'sentry.segment.name': { - type: 'string', - value: 'Navigation', - }, - 'sentry.segment.name.source': { - type: 'string', - value: 'url', - }, - [SENTRY_ENVIRONMENT]: { - type: 'string', - value: 'production', - }, - }, - end_timestamp: expect.any(Number), - is_segment: true, - links: [ - { - attributes: { - 'sentry.link.type': { - type: 'string', - value: 'previous_trace', - }, - }, - sampled: true, - span_id: pageloadSpan.span_id, - trace_id: pageloadTraceId, - }, - ], - // The raw URL stays in `url.path`/`url.full`: with span streaming, a navigation span name is - // low cardinality and falls back to 'Navigation' when there is no parameterized route. - name: 'Navigation', - span_id: navigationSpan.span_id, - start_timestamp: expect.any(Number), - status: 'ok', - trace_id: navigationTraceId, - }); -}); - -sentryTest('handles pushState with full URL', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - const navigationSpan1Promise = waitForStreamedSpan( - page, - // Matched on `url.path` rather than the span name, which is low cardinality. - span => getSpanOp(span) === 'navigation' && span.attributes?.[URL_PATH]?.value === '/sub-page', - ); - const navigationSpan2Promise = waitForStreamedSpan( - page, - span => getSpanOp(span) === 'navigation' && span.attributes?.[URL_PATH]?.value === '/sub-page-2', - ); - - await page.goto(url); - await pageloadSpanPromise; - - await page.evaluate("window.history.pushState({}, '', `${window.location.origin}/sub-page`);"); - - const navigationSpan1 = await navigationSpan1Promise; - - expect(navigationSpan1.name).toEqual('Navigation'); - - expect(navigationSpan1.attributes).toMatchObject({ - [URL_PATH]: { - type: 'string', - value: '/sub-page', - }, - [SENTRY_ORIGIN]: { - type: 'string', - value: 'auto.navigation.browser', - }, - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { - type: 'integer', - value: 1, - }, - [SENTRY_SEGMENT_NAME_SOURCE]: { - type: 'string', - value: 'url', - }, - [SENTRY_OP]: { - type: 'string', - value: 'navigation', - }, - }); - - await page.evaluate("window.history.pushState({}, '', `${window.location.origin}/sub-page-2`);"); - - const navigationSpan2 = await navigationSpan2Promise; - - expect(navigationSpan2.name).toEqual('Navigation'); - - expect(navigationSpan2.attributes).toMatchObject({ - [URL_PATH]: { - type: 'string', - value: '/sub-page-2', - }, - [SENTRY_ORIGIN]: { - type: 'string', - value: 'auto.navigation.browser', - }, - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { - type: 'integer', - value: 1, - }, - [SENTRY_SEGMENT_NAME_SOURCE]: { - type: 'string', - value: 'url', - }, - [SENTRY_OP]: { - type: 'string', - value: 'navigation', - }, - ['sentry.idle_span_finish_reason']: { - type: 'string', - value: 'idleTimeout', - }, - }); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/init.js new file mode 100644 index 000000000000..83076460599f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/init.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts index ab8a3931e132..84eaf5b4943a 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts @@ -1,150 +1,310 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; +import { + SENTRY_IDLE_SPAN_FINISH_REASON, + SENTRY_SEGMENT_ID, + SENTRY_IS_LOCALHOST, + CULTURE_CALENDAR, + CULTURE_LOCALE, + CULTURE_TIMEZONE, + DEVICE_PROCESSOR_COUNT, + NETWORK_CONNECTION_EFFECTIVE_TYPE, + NETWORK_CONNECTION_RTT, + SENTRY_SDK_NAME, + SENTRY_SDK_VERSION, + SENTRY_SEGMENT_NAME, + SENTRY_LINK_TYPE, + SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_TRACE_LIFECYCLE, + URL_FULL, + URL_PATH, + USER_AGENT_ORIGINAL, + SENTRY_ENVIRONMENT, + SENTRY_SDK_INTEGRATIONS, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { sentryTest } from '../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; import { - envelopeRequestParser, - getFirstSentryEnvelopeRequest, - shouldSkipTracingTest, - waitForTransactionRequest, -} from '../../../../utils/helpers'; - -sentryTest('should create a navigation transaction on page navigation', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + collectStreamedSpans, + getSpanOp, + getSpansFromEnvelope, + waitForStreamedSpan, + waitForStreamedSpanEnvelope, +} from '../../../../utils/spanUtils'; - const url = await getLocalTestUrl({ testDir: __dirname }); +sentryTest('starts a streamed navigation span on page navigation', async ({ browserName, getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); - const pageloadRequest = await getFirstSentryEnvelopeRequest(page, url); - const navigationRequest = await getFirstSentryEnvelopeRequest(page, `${url}#foo`); + const collectedSpans = collectStreamedSpans(page); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + const navigationSpanEnvelopePromise = waitForStreamedSpanEnvelope( + page, + env => !!getSpansFromEnvelope(env).find(s => getSpanOp(s) === 'navigation'), + ); - expect(pageloadRequest.contexts?.trace?.op).toBe('pageload'); - expect(navigationRequest.contexts?.trace?.op).toBe('navigation'); + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); - expect(navigationRequest.transaction_info?.source).toEqual('url'); + const pageloadSpan = await pageloadSpanPromise; - const pageloadTraceId = pageloadRequest.contexts?.trace?.trace_id; - const navigationTraceId = navigationRequest.contexts?.trace?.trace_id; + await page.goto(`${url}#foo`); - expect(pageloadTraceId).toBeDefined(); - expect(navigationTraceId).toBeDefined(); - expect(pageloadTraceId).not.toEqual(navigationTraceId); + const navigationSpanEnvelope = await navigationSpanEnvelopePromise; - expect(pageloadRequest.transaction).toEqual('/index.html'); - // Fragment is not in transaction name - expect(navigationRequest.transaction).toEqual('/index.html'); + const navigationSpanEnvelopeHeader = navigationSpanEnvelope[0]; + const navigationSpanEnvelopeItem = navigationSpanEnvelope[1]; + const navigationSpans = navigationSpanEnvelopeItem[0][1].items; + const navigationSpan = navigationSpans.find(s => getSpanOp(s) === 'navigation')!; - expect(pageloadRequest.contexts?.trace?.data).toMatchObject({ - [SENTRY_ORIGIN]: 'auto.pageload.browser', - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, - [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SENTRY_OP]: 'pageload', - ['sentry.idle_span_finish_reason']: 'idleTimeout', - }); - expect(navigationRequest.contexts?.trace?.data).toMatchObject({ - [SENTRY_ORIGIN]: 'auto.navigation.browser', - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, - [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SENTRY_OP]: 'navigation', - ['sentry.idle_span_finish_reason']: 'idleTimeout', - }); - expect(pageloadRequest.request).toEqual({ - headers: { - 'User-Agent': expect.any(String), + expect(navigationSpanEnvelopeHeader).toEqual({ + sent_at: expect.any(String), + trace: { + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + environment: 'production', + public_key: 'public', + sample_rand: expect.any(String), + sample_rate: '1', + sampled: 'true', }, - url: 'http://sentry-test.io/index.html', - }); - expect(navigationRequest.request).toEqual({ - headers: { - 'User-Agent': expect.any(String), + sdk: { + name: 'sentry.javascript.browser', + version: SDK_VERSION, }, - url: 'http://sentry-test.io/index.html#foo', }); - const pageloadSpans = pageloadRequest.spans; - const navigationSpans = navigationRequest.spans; + const numericSampleRand = parseFloat(navigationSpanEnvelopeHeader.trace!.sample_rand!); + expect(Number.isNaN(numericSampleRand)).toBe(false); - const pageloadSpanId = pageloadRequest.contexts?.trace?.span_id; - const navigationSpanId = navigationRequest.contexts?.trace?.span_id; + const pageloadTraceId = pageloadSpan.trace_id; + const navigationTraceId = navigationSpan.trace_id; - expect(pageloadSpanId).toBeDefined(); - expect(navigationSpanId).toBeDefined(); - - pageloadSpans?.forEach(span => - expect(span).toMatchObject({ - parent_span_id: pageloadSpanId, - }), - ); + expect(pageloadTraceId).toBeDefined(); + expect(navigationTraceId).toBeDefined(); + expect(pageloadTraceId).not.toEqual(navigationTraceId); - navigationSpans?.forEach(span => - expect(span).toMatchObject({ - parent_span_id: navigationSpanId, - }), - ); + expect(pageloadSpan.name).toEqual('Pageload'); + expect(pageloadSpan.attributes).toMatchObject({ + [URL_PATH]: { type: 'string', value: '/index.html' }, + [URL_FULL]: { type: 'string', value: 'http://sentry-test.io/index.html' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, + [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'url' }, + [SENTRY_IDLE_SPAN_FINISH_REASON]: { type: 'string', value: 'idleTimeout' }, + }); + expect(pageloadSpan.span_id).not.toBe(navigationSpan.span_id); + await page.evaluate(() => (window as any).Sentry.flush()); + for (const root of [pageloadSpan, navigationSpan]) { + collectedSpans + .filter(span => !span.is_segment && span.attributes[SENTRY_SEGMENT_ID]?.value === root.span_id) + .forEach(span => expect(span.parent_span_id).toBe(root.span_id)); + } - expect(pageloadSpanId).not.toEqual(navigationSpanId); + expect(navigationSpan).toEqual({ + attributes: { + [SENTRY_IS_LOCALHOST]: { value: false, type: 'boolean' }, + [SENTRY_TRACE_LIFECYCLE]: { + type: 'string', + value: 'stream', + }, + [CULTURE_CALENDAR]: { + type: 'string', + value: expect.any(String), + }, + [CULTURE_LOCALE]: { + type: 'string', + value: expect.any(String), + }, + [CULTURE_TIMEZONE]: { + type: 'string', + value: expect.any(String), + }, + [USER_AGENT_ORIGINAL]: { + type: 'string', + value: expect.any(String), + }, + [URL_FULL]: { + type: 'string', + value: 'http://sentry-test.io/index.html#foo', + }, + [URL_PATH]: { + type: 'string', + value: '/index.html', + }, + [DEVICE_PROCESSOR_COUNT]: { + type: expect.stringMatching(/^(integer)|(double)$/), + value: expect.any(Number), + }, + ...(browserName !== 'webkit' && { + [NETWORK_CONNECTION_EFFECTIVE_TYPE]: { + type: 'string', + value: expect.any(String), + }, + [NETWORK_CONNECTION_RTT]: { + type: expect.stringMatching(/^(integer)|(double)$/), + value: expect.any(Number), + }, + }), + [SENTRY_IDLE_SPAN_FINISH_REASON]: { + type: 'string', + value: 'idleTimeout', + }, + [SENTRY_OP]: { + type: 'string', + value: 'navigation', + }, + [SENTRY_ORIGIN]: { + type: 'string', + value: 'auto.navigation.browser', + }, + 'sentry.previous_trace': { + type: 'string', + value: `${pageloadTraceId}-${pageloadSpan.span_id}-1`, + }, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { + type: 'integer', + value: 1, + }, + [SENTRY_SDK_NAME]: { + type: 'string', + value: 'sentry.javascript.browser', + }, + [SENTRY_SDK_VERSION]: { + type: 'string', + value: SDK_VERSION, + }, + [SENTRY_SDK_INTEGRATIONS]: { + type: 'array', + value: expect.arrayContaining(['BrowserTracing', 'SpanStreaming']), + }, + [SENTRY_SEGMENT_ID]: { + type: 'string', + value: navigationSpan.span_id, + }, + [SENTRY_SEGMENT_NAME]: { + type: 'string', + value: 'Navigation', + }, + [SENTRY_SEGMENT_NAME_SOURCE]: { + type: 'string', + value: 'url', + }, + [SENTRY_ENVIRONMENT]: { + type: 'string', + value: 'production', + }, + }, + end_timestamp: expect.any(Number), + is_segment: true, + links: [ + { + attributes: { + [SENTRY_LINK_TYPE]: { + type: 'string', + value: 'previous_trace', + }, + }, + sampled: true, + span_id: pageloadSpan.span_id, + trace_id: pageloadTraceId, + }, + ], + name: 'Navigation', + span_id: navigationSpan.span_id, + start_timestamp: expect.any(Number), + status: 'ok', + trace_id: navigationTraceId, + }); }); -// -sentryTest('should handle pushState with full URL', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +sentryTest('handles pushState with full URL', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); - const navigationRequestPromise = waitForTransactionRequest( + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + const navigationSpan1Promise = waitForStreamedSpan( page, - event => event.contexts?.trace?.op === 'navigation' && event.transaction === '/sub-page', + span => getSpanOp(span) === 'navigation' && span.attributes?.[URL_PATH]?.value === '/sub-page', ); - const navigationRequestPromise2 = waitForTransactionRequest( + const navigationSpan2Promise = waitForStreamedSpan( page, - event => event.contexts?.trace?.op === 'navigation' && event.transaction === '/sub-page-2', + span => getSpanOp(span) === 'navigation' && span.attributes?.[URL_PATH]?.value === '/sub-page-2', ); await page.goto(url); - await pageloadRequestPromise; + await pageloadSpanPromise; await page.evaluate("window.history.pushState({}, '', `${window.location.origin}/sub-page`);"); - const navigationRequest = envelopeRequestParser(await navigationRequestPromise); + const navigationSpan1 = await navigationSpan1Promise; - expect(navigationRequest.transaction).toEqual('/sub-page'); - - expect(navigationRequest.contexts?.trace?.data).toMatchObject({ - [SENTRY_ORIGIN]: 'auto.navigation.browser', - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, - [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SENTRY_OP]: 'navigation', - ['sentry.idle_span_finish_reason']: 'idleTimeout', + expect(navigationSpan1.name).toEqual('Navigation'); + expect(navigationSpan1.attributes).toMatchObject({ + [URL_FULL]: { type: 'string', value: 'http://sentry-test.io/sub-page' }, + [USER_AGENT_ORIGINAL]: { type: 'string', value: expect.any(String) }, + [SENTRY_IDLE_SPAN_FINISH_REASON]: { type: 'string', value: 'idleTimeout' }, }); - expect(navigationRequest.request).toEqual({ - headers: { - 'User-Agent': expect.any(String), + + expect(navigationSpan1.attributes).toMatchObject({ + [URL_PATH]: { + type: 'string', + value: '/sub-page', + }, + [SENTRY_ORIGIN]: { + type: 'string', + value: 'auto.navigation.browser', + }, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { + type: 'integer', + value: 1, + }, + [SENTRY_SEGMENT_NAME_SOURCE]: { + type: 'string', + value: 'url', + }, + [SENTRY_OP]: { + type: 'string', + value: 'navigation', }, - url: 'http://sentry-test.io/sub-page', }); await page.evaluate("window.history.pushState({}, '', `${window.location.origin}/sub-page-2`);"); - const navigationRequest2 = envelopeRequestParser(await navigationRequestPromise2); - - expect(navigationRequest2.transaction).toEqual('/sub-page-2'); + const navigationSpan2 = await navigationSpan2Promise; - expect(navigationRequest2.contexts?.trace?.data).toMatchObject({ - [SENTRY_ORIGIN]: 'auto.navigation.browser', - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, - [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SENTRY_OP]: 'navigation', - ['sentry.idle_span_finish_reason']: 'idleTimeout', + expect(navigationSpan2.name).toEqual('Navigation'); + expect(navigationSpan2.attributes).toMatchObject({ + [URL_FULL]: { type: 'string', value: 'http://sentry-test.io/sub-page-2' }, + [USER_AGENT_ORIGINAL]: { type: 'string', value: expect.any(String) }, + [SENTRY_IDLE_SPAN_FINISH_REASON]: { type: 'string', value: 'idleTimeout' }, }); - expect(navigationRequest2.request).toEqual({ - headers: { - 'User-Agent': expect.any(String), + + expect(navigationSpan2.attributes).toMatchObject({ + [URL_PATH]: { + type: 'string', + value: '/sub-page-2', + }, + [SENTRY_ORIGIN]: { + type: 'string', + value: 'auto.navigation.browser', + }, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { + type: 'integer', + value: 1, + }, + [SENTRY_SEGMENT_NAME_SOURCE]: { + type: 'string', + value: 'url', + }, + [SENTRY_OP]: { + type: 'string', + value: 'navigation', + }, + [SENTRY_IDLE_SPAN_FINISH_REASON]: { + type: 'string', + value: 'idleTimeout', }, - url: 'http://sentry-test.io/sub-page-2', }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-end/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-end/init.js index 25e1637ea608..207473572df5 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-end/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-end/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-end/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-end/test.ts index 03bfc13814af..8fd42411f5a9 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-end/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-end/test.ts @@ -1,7 +1,8 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../../utils/spanUtils'; sentryTest('should call onRequestSpanEnd hook', async ({ browserName, getLocalTestUrl, page }) => { const supportedBrowsers = ['chromium', 'firefox']; @@ -35,26 +36,26 @@ sentryTest('should call onRequestSpanEnd hook', async ({ browserName, getLocalTe const url = await getLocalTestUrl({ testDir: __dirname }); - const envelopes = await getMultipleSentryEnvelopeRequests(page, 2, { url, timeout: 10000 }); + const spans = collectStreamedSpans(page); + await waitForStreamedSpanAndTraceHeaderOnUrl(page, url, span => getSpanOp(span) === 'pageload'); + await page.evaluate(() => (window as any).Sentry.flush()); - const tracingEvent = envelopes[envelopes.length - 1]; // last envelope contains tracing data on all browsers - - expect(tracingEvent.spans).toContainEqual( + expect(spans).toContainEqual( expect.objectContaining({ - op: 'http.client', - data: expect.objectContaining({ - type: 'xhr', - 'hook.called.response-type': 'xhr', + attributes: expect.objectContaining({ + [SENTRY_OP]: { type: 'string', value: 'http.client' }, + type: { type: 'string', value: 'xhr' }, + 'hook.called.response-type': { type: 'string', value: 'xhr' }, }), }), ); - expect(tracingEvent.spans).toContainEqual( + expect(spans).toContainEqual( expect.objectContaining({ - op: 'http.client', - data: expect.objectContaining({ - type: 'fetch', - 'hook.called.response-type': 'fetch', + attributes: expect.objectContaining({ + [SENTRY_OP]: { type: 'string', value: 'http.client' }, + type: { type: 'string', value: 'fetch' }, + 'hook.called.response-type': { type: 'string', value: 'fetch' }, }), }), ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/init.js index a24444de55b1..680dfd7304df 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/test.ts index d1d90a698a77..c21fa92e97a3 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/on-request-span-start/test.ts @@ -1,7 +1,8 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../../utils/spanUtils'; sentryTest('should call onRequestSpanStart hook', async ({ browserName, getLocalTestUrl, page }) => { const supportedBrowsers = ['chromium', 'firefox']; @@ -27,24 +28,24 @@ sentryTest('should call onRequestSpanStart hook', async ({ browserName, getLocal const url = await getLocalTestUrl({ testDir: __dirname }); - const envelopes = await getMultipleSentryEnvelopeRequests(page, 2, { url, timeout: 10000 }); + const spans = collectStreamedSpans(page); + await waitForStreamedSpanAndTraceHeaderOnUrl(page, url, span => getSpanOp(span) === 'pageload'); + await page.evaluate(() => (window as any).Sentry.flush()); - const tracingEvent = envelopes[envelopes.length - 1]; // last envelope contains tracing data on all browsers - - expect(tracingEvent.spans).toContainEqual( + expect(spans).toContainEqual( expect.objectContaining({ - op: 'http.client', - data: expect.objectContaining({ - 'hook.called.headers': 'xhr', + attributes: expect.objectContaining({ + [SENTRY_OP]: { type: 'string', value: 'http.client' }, + 'hook.called.headers': { type: 'string', value: 'xhr' }, }), }), ); - expect(tracingEvent.spans).toContainEqual( + expect(spans).toContainEqual( expect.objectContaining({ - op: 'http.client', - data: expect.objectContaining({ - 'hook.called.headers': 'fetch', + attributes: expect.objectContaining({ + [SENTRY_OP]: { type: 'string', value: 'http.client' }, + 'hook.called.headers': { type: 'string', value: 'fetch' }, }), }), ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-static/init.js similarity index 65% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-static/init.js index bd3b6ed17872..b74e6633eadb 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-static/init.js @@ -4,8 +4,8 @@ window.Sentry = Sentry; window._testBaseTimestamp = performance.timeOrigin / 1000; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()], - traceLifecycle: 'stream', + integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-static/test.ts new file mode 100644 index 000000000000..2f89771057f7 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-static/test.ts @@ -0,0 +1,43 @@ +import { PAGELOAD } from '@sentry/conventions/op'; +import { + SENTRY_IDLE_SPAN_FINISH_REASON, + SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; +import { expect } from '@playwright/test'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest('creates a pageload transaction with url as source', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === PAGELOAD); + await page.goto(url); + const eventData = envelopeRequestParser(await pageloadRequestPromise); + const timeOrigin = await page.evaluate('window._testBaseTimestamp'); + + const { start_timestamp: startTimestamp } = eventData; + + const traceContextData = eventData.contexts?.trace?.data; + + expect(startTimestamp).toBeCloseTo(timeOrigin, 1); + + expect(traceContextData).toMatchObject({ + [SENTRY_ORIGIN]: 'auto.pageload.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SENTRY_SEGMENT_NAME_SOURCE]: 'url', + [SENTRY_OP]: PAGELOAD, + [SENTRY_IDLE_SPAN_FINISH_REASON]: 'idleTimeout', + }); + + expect(eventData.contexts?.trace?.op).toBe(PAGELOAD); + expect(eventData.spans?.length).toBeGreaterThan(0); + expect(eventData.transaction_info?.source).toEqual('url'); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts deleted file mode 100644 index 717011ffb9c3..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { expect } from '@playwright/test'; -import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; -import { - BROWSER_NAVIGATION_TYPE, - SENTRY_SEGMENT_NAME_SOURCE, - SENTRY_SEGMENT_ID, - SENTRY_SEGMENT_NAME, - SENTRY_SDK_NAME, - SENTRY_SDK_VERSION, - SENTRY_TRACE_LIFECYCLE, - URL_FULL, - URL_PATH, - USER_AGENT_ORIGINAL, - SENTRY_ENVIRONMENT, - SENTRY_SDK_INTEGRATIONS, - SENTRY_OP, - SENTRY_ORIGIN, -} from '@sentry/conventions/attributes'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, getSpansFromEnvelope, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; - -sentryTest( - 'creates a pageload streamed span envelope with url as pageload span name source', - async ({ browserName, getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const spanEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!getSpansFromEnvelope(env).find(s => getSpanOp(s) === 'pageload'), - ); - - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - - const spanEnvelope = await spanEnvelopePromise; - const envelopeHeader = spanEnvelope[0]; - const envelopeItem = spanEnvelope[1]; - const spans = envelopeItem[0][1].items; - const pageloadSpan = spans.find(s => getSpanOp(s) === 'pageload'); - - const timeOrigin = await page.evaluate('window._testBaseTimestamp'); - - expect(envelopeHeader).toEqual({ - sdk: { - name: 'sentry.javascript.browser', - version: SDK_VERSION, - }, - sent_at: expect.any(String), - trace: { - environment: 'production', - public_key: 'public', - sample_rand: expect.any(String), - sample_rate: '1', - sampled: 'true', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - }, - }); - - const numericSampleRand = parseFloat(envelopeHeader.trace!.sample_rand!); - const traceId = envelopeHeader.trace!.trace_id; - - expect(Number.isNaN(numericSampleRand)).toBe(false); - - expect(envelopeItem[0][0].item_count).toBeGreaterThan(1); - - expect(pageloadSpan?.start_timestamp).toBeCloseTo(timeOrigin, 1); - - expect(pageloadSpan).toEqual({ - attributes: { - 'sentry.is_localhost': { value: false, type: 'boolean' }, - 'culture.calendar': { - type: 'string', - value: expect.any(String), - }, - 'culture.locale': { - type: 'string', - value: expect.any(String), - }, - 'culture.timezone': { - type: 'string', - value: expect.any(String), - }, - [USER_AGENT_ORIGINAL]: { - type: 'string', - value: expect.any(String), - }, - [URL_FULL]: { - type: 'string', - value: expect.any(String), - }, - [URL_PATH]: { - type: 'string', - value: expect.any(String), - }, - // formerly known as 'hardwareConcurrency' - 'device.processor_count': { - type: expect.stringMatching(/^(integer)|(double)$/), - value: expect.any(Number), - }, - 'browser.performance.navigation.activation_start': { - type: expect.stringMatching(/^(integer)|(double)$/), - value: expect.any(Number), - }, - 'browser.performance.time_origin': { - type: expect.stringMatching(/^(integer)|(double)$/), - value: expect.any(Number), - }, - 'browser.web_vital.ttfb.request_time': { - type: expect.stringMatching(/^(integer)|(double)$/), - value: expect.any(Number), - }, - ...(browserName !== 'webkit' && { - // formerly known as 'effectiveConnectionType' - 'network.connection.effective_type': { - type: 'string', - value: expect.any(String), - }, - 'network.connection.rtt': { - type: expect.stringMatching(/^(integer)|(double)$/), - value: expect.any(Number), - }, - 'browser.web_vital.ttfb.value': { - type: expect.stringMatching(/^(integer)|(double)$/), - value: expect.any(Number), - }, - [BROWSER_NAVIGATION_TYPE]: { - type: 'string', - value: 'navigate', - }, - }), - 'sentry.idle_span_finish_reason': { - type: 'string', - value: 'idleTimeout', - }, - [SENTRY_OP]: { - type: 'string', - value: 'pageload', - }, - [SENTRY_ORIGIN]: { - type: 'string', - value: 'auto.pageload.browser', - }, - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { - type: 'integer', - value: 1, - }, - [SENTRY_SDK_NAME]: { - type: 'string', - value: 'sentry.javascript.browser', - }, - [SENTRY_SDK_VERSION]: { - type: 'string', - value: SDK_VERSION, - }, - [SENTRY_SDK_INTEGRATIONS]: { - type: 'array', - value: expect.arrayContaining(['BrowserTracing', 'SpanStreaming']), - }, - [SENTRY_SEGMENT_ID]: { - type: 'string', - value: pageloadSpan?.span_id, - }, - [SENTRY_SEGMENT_NAME]: { - type: 'string', - value: 'Pageload', - }, - [SENTRY_SEGMENT_NAME_SOURCE]: { - type: 'string', - value: 'url', - }, - [SENTRY_ENVIRONMENT]: { - type: 'string', - value: 'production', - }, - [SENTRY_TRACE_LIFECYCLE]: { - type: 'string', - value: 'stream', - }, - }, - end_timestamp: expect.any(Number), - is_segment: true, - // The raw URL stays in `url.path`/`url.full`: with span streaming, a pageload span name is - // low cardinality and falls back to 'Pageload' when there is no parameterized route. - name: 'Pageload', - span_id: expect.stringMatching(/^[\da-f]{16}$/), - start_timestamp: expect.any(Number), - status: 'ok', - trace_id: traceId, - }); - }, -); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/init.js index b74e6633eadb..1f0b64911a75 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/init.js @@ -4,7 +4,6 @@ window.Sentry = Sentry; window._testBaseTimestamp = performance.timeOrigin / 1000; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts index 0776668fb9e8..232565e32f9f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts @@ -1,12 +1,13 @@ import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; -import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../../utils/spanUtils'; sentryTest( - 'sets the source to custom when updating the transaction name with `span.updateName`', + 'sets the source to custom when updating the span name with `span.updateName`', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); @@ -14,25 +15,31 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const spans = collectStreamedSpans(page); + const [pageloadSpan] = await waitForStreamedSpanAndTraceHeaderOnUrl( + page, + url, + span => getSpanOp(span) === 'pageload', + ); + await page.evaluate(() => (window as any).Sentry.flush()); - const traceContextData = eventData.contexts?.trace?.data; + const attributes = pageloadSpan.attributes; - expect(traceContextData).toBeDefined(); + expect(attributes).toBeDefined(); - expect(eventData.transaction).toBe('new name'); + expect(pageloadSpan.name).toBe('new name'); - expect(traceContextData).toMatchObject({ - [SENTRY_ORIGIN]: 'auto.pageload.browser', - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, - [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', - [SENTRY_OP]: 'pageload', + expect(pageloadSpan.attributes).toMatchObject({ + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, + [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'custom' }, + [SENTRY_OP]: { type: 'string', value: 'pageload' }, }); - expect(traceContextData![SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined(); + expect(attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined(); - expect(eventData.contexts?.trace?.op).toBe('pageload'); - expect(eventData.spans?.length).toBeGreaterThan(0); - expect(eventData.transaction_info?.source).toEqual('custom'); + expect(getSpanOp(pageloadSpan)).toBe('pageload'); + expect(spans.filter(span => !span.is_segment).length).toBeGreaterThan(0); + expect(pageloadSpan.attributes[SENTRY_SEGMENT_NAME_SOURCE]?.value).toEqual('custom'); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName-static/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName-static/init.js new file mode 100644 index 000000000000..b74e6633eadb --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName-static/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window._testBaseTimestamp = performance.timeOrigin / 1000; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName-static/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName-static/subject.js new file mode 100644 index 000000000000..7f0ad0fea340 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName-static/subject.js @@ -0,0 +1,4 @@ +const activeSpan = Sentry.getActiveSpan(); +const rootSpan = activeSpan && Sentry.getRootSpan(activeSpan); + +Sentry.updateSpanName(rootSpan, 'new name'); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName-static/test.ts new file mode 100644 index 000000000000..ab1b1ed20c46 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName-static/test.ts @@ -0,0 +1,38 @@ +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { expect } from '@playwright/test'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; +import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'sets the source to custom when updating the transaction name with Sentry.updateSpanName', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + const traceContextData = eventData.contexts?.trace?.data; + + expect(traceContextData).toBeDefined(); + + expect(traceContextData).toMatchObject({ + [SENTRY_ORIGIN]: 'auto.pageload.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', + [SENTRY_OP]: 'pageload', + }); + + expect(traceContextData![SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined(); + + expect(eventData.transaction).toBe('new name'); + + expect(eventData.contexts?.trace?.op).toBe('pageload'); + expect(eventData.spans?.length).toBeGreaterThan(0); + expect(eventData.transaction_info?.source).toEqual('custom'); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/init.js index b74e6633eadb..1f0b64911a75 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/init.js @@ -4,7 +4,6 @@ window.Sentry = Sentry; window._testBaseTimestamp = performance.timeOrigin / 1000; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/test.ts index ab1b1ed20c46..0ee7a634717f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/test.ts @@ -1,12 +1,13 @@ import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; -import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../../utils/spanUtils'; sentryTest( - 'sets the source to custom when updating the transaction name with Sentry.updateSpanName', + 'sets the source to custom when updating the span name with Sentry.updateSpanName', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); @@ -14,25 +15,31 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const spans = collectStreamedSpans(page); + const [pageloadSpan] = await waitForStreamedSpanAndTraceHeaderOnUrl( + page, + url, + span => getSpanOp(span) === 'pageload', + ); + await page.evaluate(() => (window as any).Sentry.flush()); - const traceContextData = eventData.contexts?.trace?.data; + const attributes = pageloadSpan.attributes; - expect(traceContextData).toBeDefined(); + expect(attributes).toBeDefined(); - expect(traceContextData).toMatchObject({ - [SENTRY_ORIGIN]: 'auto.pageload.browser', - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, - [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', - [SENTRY_OP]: 'pageload', + expect(pageloadSpan.attributes).toMatchObject({ + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, + [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'custom' }, + [SENTRY_OP]: { type: 'string', value: 'pageload' }, }); - expect(traceContextData![SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined(); + expect(attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toEqual({ type: 'string', value: 'new name' }); - expect(eventData.transaction).toBe('new name'); + expect(pageloadSpan.name).toBe('new name'); - expect(eventData.contexts?.trace?.op).toBe('pageload'); - expect(eventData.spans?.length).toBeGreaterThan(0); - expect(eventData.transaction_info?.source).toEqual('custom'); + expect(getSpanOp(pageloadSpan)).toBe('pageload'); + expect(spans.filter(span => !span.is_segment).length).toBeGreaterThan(0); + expect(pageloadSpan.attributes[SENTRY_SEGMENT_NAME_SOURCE]?.value).toEqual('custom'); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/init.js index b74e6633eadb..1f0b64911a75 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/init.js @@ -4,7 +4,6 @@ window.Sentry = Sentry; window._testBaseTimestamp = performance.timeOrigin / 1000; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts index 8e7c3a4b2480..83e6991b114c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts @@ -1,35 +1,208 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; -import type { Event } from '@sentry/core'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; +import { + SENTRY_IS_LOCALHOST, + CULTURE_CALENDAR, + CULTURE_LOCALE, + CULTURE_TIMEZONE, + DEVICE_PROCESSOR_COUNT, + BROWSER_PERFORMANCE_NAVIGATION_ACTIVATION_START, + BROWSER_PERFORMANCE_TIME_ORIGIN, + BROWSER_WEB_VITAL_TTFB_REQUEST_TIME, + NETWORK_CONNECTION_EFFECTIVE_TYPE, + NETWORK_CONNECTION_RTT, + BROWSER_WEB_VITAL_TTFB_VALUE, + SENTRY_IDLE_SPAN_FINISH_REASON, + BROWSER_NAVIGATION_TYPE, + SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_SEGMENT_ID, + SENTRY_SEGMENT_NAME, + SENTRY_SDK_NAME, + SENTRY_SDK_VERSION, + SENTRY_TRACE_LIFECYCLE, + URL_FULL, + URL_PATH, + USER_AGENT_ORIGINAL, + SENTRY_ENVIRONMENT, + SENTRY_SDK_INTEGRATIONS, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { + collectStreamedSpans, + getSpanOp, + getSpansFromEnvelope, + waitForStreamedSpanEnvelope, +} from '../../../../utils/spanUtils'; -sentryTest('creates a pageload transaction with url as source', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +sentryTest( + 'creates a pageload streamed span envelope with url as pageload span name source', + async ({ browserName, getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); - const url = await getLocalTestUrl({ testDir: __dirname }); + const collectedSpans = collectStreamedSpans(page); + const spanEnvelopePromise = waitForStreamedSpanEnvelope( + page, + env => !!getSpansFromEnvelope(env).find(s => getSpanOp(s) === 'pageload'), + ); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const timeOrigin = await page.evaluate('window._testBaseTimestamp'); + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); - const { start_timestamp: startTimestamp } = eventData; + const spanEnvelope = await spanEnvelopePromise; + const envelopeHeader = spanEnvelope[0]; + const envelopeItem = spanEnvelope[1]; + const spans = envelopeItem[0][1].items; + const pageloadSpan = spans.find(s => getSpanOp(s) === 'pageload'); - const traceContextData = eventData.contexts?.trace?.data; + const timeOrigin = await page.evaluate('window._testBaseTimestamp'); - expect(startTimestamp).toBeCloseTo(timeOrigin, 1); + expect(envelopeHeader).toEqual({ + sdk: { + name: 'sentry.javascript.browser', + version: SDK_VERSION, + }, + sent_at: expect.any(String), + trace: { + environment: 'production', + public_key: 'public', + sample_rand: expect.any(String), + sample_rate: '1', + sampled: 'true', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + }, + }); - expect(traceContextData).toMatchObject({ - [SENTRY_ORIGIN]: 'auto.pageload.browser', - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, - [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SENTRY_OP]: 'pageload', - ['sentry.idle_span_finish_reason']: 'idleTimeout', - }); + const numericSampleRand = parseFloat(envelopeHeader.trace!.sample_rand!); + const traceId = envelopeHeader.trace!.trace_id; - expect(eventData.contexts?.trace?.op).toBe('pageload'); - expect(eventData.spans?.length).toBeGreaterThan(0); - expect(eventData.transaction_info?.source).toEqual('url'); -}); + expect(Number.isNaN(numericSampleRand)).toBe(false); + + expect(envelopeItem[0][0].item_count).toBe(spans.length); + await page.evaluate(() => (window as any).Sentry.flush()); + expect(collectedSpans.filter(span => !span.is_segment)).not.toHaveLength(0); + + expect(pageloadSpan?.start_timestamp).toBeCloseTo(timeOrigin, 1); + + expect(pageloadSpan).toEqual({ + attributes: { + [SENTRY_IS_LOCALHOST]: { value: false, type: 'boolean' }, + [CULTURE_CALENDAR]: { + type: 'string', + value: expect.any(String), + }, + [CULTURE_LOCALE]: { + type: 'string', + value: expect.any(String), + }, + [CULTURE_TIMEZONE]: { + type: 'string', + value: expect.any(String), + }, + [USER_AGENT_ORIGINAL]: { + type: 'string', + value: expect.any(String), + }, + [URL_FULL]: { + type: 'string', + value: 'http://sentry-test.io/index.html', + }, + [URL_PATH]: { + type: 'string', + value: '/index.html', + }, + [DEVICE_PROCESSOR_COUNT]: { + type: expect.stringMatching(/^(integer)|(double)$/), + value: expect.any(Number), + }, + [BROWSER_PERFORMANCE_NAVIGATION_ACTIVATION_START]: { + type: expect.stringMatching(/^(integer)|(double)$/), + value: expect.any(Number), + }, + [BROWSER_PERFORMANCE_TIME_ORIGIN]: { + type: expect.stringMatching(/^(integer)|(double)$/), + value: expect.any(Number), + }, + [BROWSER_WEB_VITAL_TTFB_REQUEST_TIME]: { + type: expect.stringMatching(/^(integer)|(double)$/), + value: expect.any(Number), + }, + ...(browserName !== 'webkit' && { + [NETWORK_CONNECTION_EFFECTIVE_TYPE]: { + type: 'string', + value: expect.any(String), + }, + [NETWORK_CONNECTION_RTT]: { + type: expect.stringMatching(/^(integer)|(double)$/), + value: expect.any(Number), + }, + [BROWSER_WEB_VITAL_TTFB_VALUE]: { + type: expect.stringMatching(/^(integer)|(double)$/), + value: expect.any(Number), + }, + [BROWSER_NAVIGATION_TYPE]: { + type: 'string', + value: 'navigate', + }, + }), + [SENTRY_IDLE_SPAN_FINISH_REASON]: { + type: 'string', + value: 'idleTimeout', + }, + [SENTRY_OP]: { + type: 'string', + value: 'pageload', + }, + [SENTRY_ORIGIN]: { + type: 'string', + value: 'auto.pageload.browser', + }, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { + type: 'integer', + value: 1, + }, + [SENTRY_SDK_NAME]: { + type: 'string', + value: 'sentry.javascript.browser', + }, + [SENTRY_SDK_VERSION]: { + type: 'string', + value: SDK_VERSION, + }, + [SENTRY_SDK_INTEGRATIONS]: { + type: 'array', + value: expect.arrayContaining(['BrowserTracing', 'SpanStreaming']), + }, + [SENTRY_SEGMENT_ID]: { + type: 'string', + value: pageloadSpan?.span_id, + }, + [SENTRY_SEGMENT_NAME]: { + type: 'string', + value: 'Pageload', + }, + [SENTRY_SEGMENT_NAME_SOURCE]: { + type: 'string', + value: 'url', + }, + [SENTRY_ENVIRONMENT]: { + type: 'string', + value: 'production', + }, + [SENTRY_TRACE_LIFECYCLE]: { + type: 'string', + value: 'stream', + }, + }, + end_timestamp: expect.any(Number), + is_segment: true, + name: 'Pageload', + span_id: expect.stringMatching(/^[\da-f]{16}$/), + start_timestamp: expect.any(Number), + status: 'ok', + trace_id: traceId, + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/init.js index ba739b4b8cc5..2c5a44a7f76d 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/init.js @@ -6,7 +6,6 @@ window._testBaseTimestamp = performance.timeOrigin / 1000; setTimeout(() => { window._testTimeoutTimestamp = (performance.timeOrigin + performance.now()) / 1000; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/test.ts index 0d608a782314..2b09200481b7 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/test.ts @@ -1,25 +1,32 @@ +import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../../utils/spanUtils'; -sentryTest('should create a pageload transaction when initialized delayed', async ({ getLocalTestUrl, page }) => { +sentryTest('should create a pageload span when initialized delayed', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); } const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const spans = collectStreamedSpans(page); + const [pageloadSpan] = await waitForStreamedSpanAndTraceHeaderOnUrl( + page, + url, + span => getSpanOp(span) === 'pageload', + ); + await page.evaluate(() => (window as any).Sentry.flush()); const timeOrigin = await page.evaluate('window._testBaseTimestamp'); const timeoutTimestamp = await page.evaluate('window._testTimeoutTimestamp'); - const { start_timestamp: startTimestamp } = eventData; + const { start_timestamp: startTimestamp } = pageloadSpan; expect(startTimestamp).toBeCloseTo(timeOrigin, 1); expect(startTimestamp).toBeLessThan(timeoutTimestamp); - expect(eventData.contexts?.trace?.op).toBe('pageload'); - expect(eventData.spans?.length).toBeGreaterThan(0); - expect(eventData.transaction_info?.source).toEqual('url'); + expect(getSpanOp(pageloadSpan)).toBe('pageload'); + expect(spans.filter(span => !span.is_segment).length).toBeGreaterThan(0); + expect(pageloadSpan.attributes[SENTRY_SEGMENT_NAME_SOURCE]?.value).toEqual('url'); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithChildSpanTimeout/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithChildSpanTimeout/init.js index b2820c740c2b..229372e215c7 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithChildSpanTimeout/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithChildSpanTimeout/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithChildSpanTimeout/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithChildSpanTimeout/test.ts index 16f5a2f7f539..daf8048d6622 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithChildSpanTimeout/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithChildSpanTimeout/test.ts @@ -1,28 +1,33 @@ +import { SENTRY_IDLE_SPAN_FINISH_REASON } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { - envelopeRequestParser, - shouldSkipTracingTest, - waitForTransactionRequestOnUrl, -} from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../../utils/spanUtils'; -// This tests asserts that the pageload span will finish itself after the child span timeout if it -// has a child span without adding any additional ones or finishing any of them finishing. All of the child spans that -// are still running should have the status "cancelled". sentryTest('should send a pageload span terminated via child span timeout', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); } const url = await getLocalTestUrl({ testDir: __dirname }); - const req = await waitForTransactionRequestOnUrl(page, url); + const spans = collectStreamedSpans(page); + const [pageloadSpan] = await waitForStreamedSpanAndTraceHeaderOnUrl( + page, + url, + span => getSpanOp(span) === 'pageload', + ); + await page.evaluate(() => (window as any).Sentry.flush()); - const eventData = envelopeRequestParser(req); - - expect(eventData.contexts?.trace?.op).toBe('pageload'); - expect(eventData.contexts?.trace?.data?.['sentry.idle_span_discarded_spans']).toBeUndefined(); - expect(eventData.spans?.length).toBeGreaterThanOrEqual(1); - const testSpan = eventData.spans?.find(span => span.description === 'pageload-child-span'); + expect(getSpanOp(pageloadSpan)).toBe('pageload'); + expect(pageloadSpan.attributes['sentry.idle_span_discarded_spans']).toBeUndefined(); + expect(spans.length).toBeGreaterThanOrEqual(2); + const testSpan = spans.find(span => span.name === 'pageload-child-span'); expect(testSpan).toBeDefined(); - expect(testSpan?.status).toBe('cancelled'); + expect(testSpan?.status).toBe('ok'); + expect(pageloadSpan.attributes[SENTRY_IDLE_SPAN_FINISH_REASON]?.value).toBe('heartbeatFailed'); + expect(testSpan).toMatchObject({ + parent_span_id: pageloadSpan.span_id, + trace_id: pageloadSpan.trace_id, + end_timestamp: pageloadSpan.end_timestamp, + }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/default/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-static/default/init.js similarity index 86% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/default/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-static/default/init.js index ded3ca204b6b..054a40d79664 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/default/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-static/default/init.js @@ -4,8 +4,9 @@ window.Sentry = Sentry; window._testBaseTimestamp = performance.timeOrigin / 1000; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration({ enableReportPageLoaded: true }), Sentry.spanStreamingIntegration()], + integrations: [Sentry.browserTracingIntegration({ enableReportPageLoaded: true })], tracesSampleRate: 1, debug: true, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-static/default/test.ts similarity index 50% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/default/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-static/default/test.ts index 3c4a37bc9de3..a20d10297c4b 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/default/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-static/default/test.ts @@ -7,30 +7,32 @@ import { import { expect } from '@playwright/test'; import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; sentryTest( 'waits for Sentry.reportPageLoaded() to be called when `enableReportPageLoaded` is true', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } - const url = await getLocalTestUrl({ testDir: __dirname }); + const pageloadEventPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const pageloadSpan = await pageloadSpanPromise; + const eventData = envelopeRequestParser(await pageloadEventPromise); - const spanDurationSeconds = pageloadSpan.end_timestamp - pageloadSpan.start_timestamp; + const traceContextData = eventData.contexts?.trace?.data; + const spanDurationSeconds = eventData.timestamp! - eventData.start_timestamp!; - expect(pageloadSpan.attributes).toMatchObject({ - [SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: expect.objectContaining({ value: 1 }), - [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'url' }, - [SENTRY_OP]: { type: 'string', value: 'pageload' }, - [SENTRY_IDLE_SPAN_FINISH_REASON]: { type: 'string', value: 'reportPageLoaded' }, + expect(traceContextData).toMatchObject({ + [SENTRY_ORIGIN]: 'auto.pageload.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SENTRY_SEGMENT_NAME_SOURCE]: 'url', + [SENTRY_OP]: 'pageload', + [SENTRY_IDLE_SPAN_FINISH_REASON]: 'reportPageLoaded', }); // We wait for 2.5 seconds before calling Sentry.reportPageLoaded(). The span starts at navigation start, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/finalTimeout/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-static/finalTimeout/init.js similarity index 65% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/finalTimeout/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-static/finalTimeout/init.js index b1c19f779713..2f633a9c6f1e 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/finalTimeout/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-static/finalTimeout/init.js @@ -4,11 +4,9 @@ window.Sentry = Sentry; window._testBaseTimestamp = performance.timeOrigin / 1000; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [ - Sentry.browserTracingIntegration({ enableReportPageLoaded: true, finalTimeout: 3000 }), - Sentry.spanStreamingIntegration(), - ], + integrations: [Sentry.browserTracingIntegration({ enableReportPageLoaded: true, finalTimeout: 3000 })], tracesSampleRate: 1, debug: true, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-static/finalTimeout/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-static/finalTimeout/test.ts new file mode 100644 index 000000000000..8244b6f83c7a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-static/finalTimeout/test.ts @@ -0,0 +1,43 @@ +import { + SENTRY_IDLE_SPAN_FINISH_REASON, + SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; +import { expect } from '@playwright/test'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; + +sentryTest( + 'final timeout cancels the pageload span even if `enableReportPageLoaded` is true', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const pageloadEventPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + const eventData = envelopeRequestParser(await pageloadEventPromise); + + const traceContextData = eventData.contexts?.trace?.data; + const spanDurationSeconds = eventData.timestamp! - eventData.start_timestamp!; + + expect(traceContextData).toMatchObject({ + [SENTRY_ORIGIN]: 'auto.pageload.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SENTRY_SEGMENT_NAME_SOURCE]: 'url', + [SENTRY_OP]: 'pageload', + [SENTRY_IDLE_SPAN_FINISH_REASON]: 'finalTimeout', + }); + + // We wait for 3 seconds before calling Sentry.reportPageLoaded() + // the margins are to account for timing weirdness in CI to avoid flakes + expect(spanDurationSeconds).toBeGreaterThan(2.5); + expect(spanDurationSeconds).toBeLessThan(3.5); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/navigation/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-static/navigation/init.js similarity index 71% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/navigation/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-static/navigation/init.js index ac42880742a3..3e937c873954 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/navigation/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-static/navigation/init.js @@ -4,11 +4,9 @@ window.Sentry = Sentry; window._testBaseTimestamp = performance.timeOrigin / 1000; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [ - Sentry.browserTracingIntegration({ enableReportPageLoaded: true, instrumentNavigation: false }), - Sentry.spanStreamingIntegration(), - ], + integrations: [Sentry.browserTracingIntegration({ enableReportPageLoaded: true, instrumentNavigation: false })], tracesSampleRate: 1, debug: true, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-static/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-static/navigation/test.ts new file mode 100644 index 000000000000..376c35d25a7f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-static/navigation/test.ts @@ -0,0 +1,41 @@ +import { + SENTRY_IDLE_SPAN_FINISH_REASON, + SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; +import { expect } from '@playwright/test'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; + +sentryTest( + 'starting a navigation span cancels the pageload span even if `enableReportPageLoaded` is true', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const pageloadEventPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + const eventData = envelopeRequestParser(await pageloadEventPromise); + + const traceContextData = eventData.contexts?.trace?.data; + const spanDurationSeconds = eventData.timestamp! - eventData.start_timestamp!; + + expect(traceContextData).toMatchObject({ + [SENTRY_ORIGIN]: 'auto.pageload.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SENTRY_SEGMENT_NAME_SOURCE]: 'url', + [SENTRY_OP]: 'pageload', + [SENTRY_IDLE_SPAN_FINISH_REASON]: 'cancelled', + }); + + // ending span after 1s but adding a margin of 0.5s to account for timing weirdness in CI to avoid flakes + expect(spanDurationSeconds).toBeLessThan(1.5); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/finalTimeout/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/finalTimeout/test.ts deleted file mode 100644 index 8ea2ea1ca842..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/finalTimeout/test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; -import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; - -sentryTest( - 'final timeout cancels the pageload span even if `enableReportPageLoaded` is true', - async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - - await page.goto(url); - - const pageloadSpan = await pageloadSpanPromise; - - const spanDurationSeconds = pageloadSpan.end_timestamp - pageloadSpan.start_timestamp; - - expect(pageloadSpan.attributes).toMatchObject({ - [SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: expect.objectContaining({ value: 1 }), - [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'url' }, - [SENTRY_OP]: { type: 'string', value: 'pageload' }, - 'sentry.idle_span_finish_reason': { type: 'string', value: 'finalTimeout' }, - }); - - // We wait for 3 seconds before calling Sentry.reportPageLoaded() - // the margins are to account for timing weirdness in CI to avoid flakes - expect(spanDurationSeconds).toBeGreaterThan(2.5); - expect(spanDurationSeconds).toBeLessThan(3.5); - }, -); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/navigation/test.ts deleted file mode 100644 index db6f6737ccd6..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/navigation/test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; -import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; - -sentryTest( - 'starting a navigation span cancels the pageload span even if `enableReportPageLoaded` is true', - async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - - await page.goto(url); - - const pageloadSpan = await pageloadSpanPromise; - - const spanDurationSeconds = pageloadSpan.end_timestamp - pageloadSpan.start_timestamp; - - expect(pageloadSpan.attributes).toMatchObject({ - [SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: expect.objectContaining({ value: 1 }), - [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'url' }, - [SENTRY_OP]: { type: 'string', value: 'pageload' }, - 'sentry.idle_span_finish_reason': { type: 'string', value: 'cancelled' }, - }); - - // ending span after 1s but adding a margin of 0.5s to account for timing weirdness in CI to avoid flakes - expect(spanDurationSeconds).toBeLessThan(1.5); - }, -); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/default/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/default/init.js index 054a40d79664..39a212ae5fa5 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/default/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/default/init.js @@ -4,7 +4,6 @@ window.Sentry = Sentry; window._testBaseTimestamp = performance.timeOrigin / 1000; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration({ enableReportPageLoaded: true })], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/default/test.ts index bca0d6052b95..52fb62fbec10 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/default/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/default/test.ts @@ -1,33 +1,37 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { + SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_IDLE_SPAN_FINISH_REASON, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; + import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; sentryTest( 'waits for Sentry.reportPageLoaded() to be called when `enableReportPageLoaded` is true', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - const pageloadEventPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + await page.goto(url); - const eventData = envelopeRequestParser(await pageloadEventPromise); + const pageloadSpan = await pageloadSpanPromise; - const traceContextData = eventData.contexts?.trace?.data; - const spanDurationSeconds = eventData.timestamp! - eventData.start_timestamp!; + const spanDurationSeconds = pageloadSpan.end_timestamp - pageloadSpan.start_timestamp; - expect(traceContextData).toMatchObject({ - [SENTRY_ORIGIN]: 'auto.pageload.browser', - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, - [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SENTRY_OP]: 'pageload', - ['sentry.idle_span_finish_reason']: 'reportPageLoaded', + expect(pageloadSpan.attributes).toMatchObject({ + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: expect.objectContaining({ value: 1 }), + [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'url' }, + [SENTRY_OP]: { type: 'string', value: 'pageload' }, + [SENTRY_IDLE_SPAN_FINISH_REASON]: { type: 'string', value: 'reportPageLoaded' }, }); // We wait for 2.5 seconds before calling Sentry.reportPageLoaded(). The span starts at navigation start, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/finalTimeout/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/finalTimeout/init.js index 2f633a9c6f1e..7ec015be44dd 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/finalTimeout/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/finalTimeout/init.js @@ -4,7 +4,6 @@ window.Sentry = Sentry; window._testBaseTimestamp = performance.timeOrigin / 1000; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration({ enableReportPageLoaded: true, finalTimeout: 3000 })], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/finalTimeout/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/finalTimeout/test.ts index ae6ffdec21b3..aaef5b809ed7 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/finalTimeout/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/finalTimeout/test.ts @@ -1,33 +1,36 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { + SENTRY_IDLE_SPAN_FINISH_REASON, + SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; sentryTest( 'final timeout cancels the pageload span even if `enableReportPageLoaded` is true', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - const pageloadEventPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + await page.goto(url); - const eventData = envelopeRequestParser(await pageloadEventPromise); + const pageloadSpan = await pageloadSpanPromise; - const traceContextData = eventData.contexts?.trace?.data; - const spanDurationSeconds = eventData.timestamp! - eventData.start_timestamp!; + const spanDurationSeconds = pageloadSpan.end_timestamp - pageloadSpan.start_timestamp; - expect(traceContextData).toMatchObject({ - [SENTRY_ORIGIN]: 'auto.pageload.browser', - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, - [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SENTRY_OP]: 'pageload', - ['sentry.idle_span_finish_reason']: 'finalTimeout', + expect(pageloadSpan.attributes).toMatchObject({ + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: expect.objectContaining({ value: 1 }), + [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'url' }, + [SENTRY_OP]: { type: 'string', value: 'pageload' }, + [SENTRY_IDLE_SPAN_FINISH_REASON]: { type: 'string', value: 'finalTimeout' }, }); // We wait for 3 seconds before calling Sentry.reportPageLoaded() diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/navigation/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/navigation/init.js index 3e937c873954..65e9938f7985 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/navigation/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/navigation/init.js @@ -4,7 +4,6 @@ window.Sentry = Sentry; window._testBaseTimestamp = performance.timeOrigin / 1000; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration({ enableReportPageLoaded: true, instrumentNavigation: false })], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/navigation/test.ts index 9e947592a576..b67276f0bcd0 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/navigation/test.ts @@ -1,33 +1,36 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { + SENTRY_IDLE_SPAN_FINISH_REASON, + SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; sentryTest( 'starting a navigation span cancels the pageload span even if `enableReportPageLoaded` is true', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - const pageloadEventPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + await page.goto(url); - const eventData = envelopeRequestParser(await pageloadEventPromise); + const pageloadSpan = await pageloadSpanPromise; - const traceContextData = eventData.contexts?.trace?.data; - const spanDurationSeconds = eventData.timestamp! - eventData.start_timestamp!; + const spanDurationSeconds = pageloadSpan.end_timestamp - pageloadSpan.start_timestamp; - expect(traceContextData).toMatchObject({ - [SENTRY_ORIGIN]: 'auto.pageload.browser', - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, - [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SENTRY_OP]: 'pageload', - ['sentry.idle_span_finish_reason']: 'cancelled', + expect(pageloadSpan.attributes).toMatchObject({ + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: expect.objectContaining({ value: 1 }), + [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'url' }, + [SENTRY_OP]: { type: 'string', value: 'pageload' }, + [SENTRY_IDLE_SPAN_FINISH_REASON]: { type: 'string', value: 'cancelled' }, }); // ending span after 1s but adding a margin of 0.5s to account for timing weirdness in CI to avoid flakes diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/init.js index 0eb1efc7a5a4..3a4c2cb1cdf4 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/test.ts index 4bc9621f8395..282625e0d8b7 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/test.ts @@ -1,8 +1,8 @@ import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../../utils/spanUtils'; sentryTest('should allow specific types of resource spans to be ignored.', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { @@ -13,8 +13,10 @@ sentryTest('should allow specific types of resource spans to be ignored.', async const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const allSpans = eventData.spans?.filter(({ op }) => op?.startsWith('resource.script')); + const spans = collectStreamedSpans(page); + await waitForStreamedSpanAndTraceHeaderOnUrl(page, url, span => getSpanOp(span) === 'pageload'); + await page.evaluate(() => (window as any).Sentry.flush()); + const allSpans = spans?.filter(span => getSpanOp(span)?.startsWith('resource.script')); expect(allSpans?.length).toBe(0); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-static/init.js similarity index 90% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-static/init.js index 53029a40b668..f2b876b3bf7d 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-static/init.js @@ -4,13 +4,13 @@ import { interactionsIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ enableLongTask: false, }), interactionsIntegration(), - Sentry.spanStreamingIntegration(), Sentry.spotlightBrowserIntegration(), ], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-streamed/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-static/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-streamed/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-static/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-streamed/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-static/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-streamed/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-static/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-static/test.ts new file mode 100644 index 000000000000..ae6640054caa --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-static/test.ts @@ -0,0 +1,54 @@ +import { PAGELOAD, UI_ACTION_CLICK, UI_INTERACTION_CLICK } from '@sentry/conventions/op'; +import { expect } from '@playwright/test'; +import type { TransactionEvent } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipCdnBundleTest, + shouldSkipTracingTest, + waitForTransactionRequest, +} from '../../../../utils/helpers'; + +sentryTest( + 'filters ui.interaction.click spans for spotlight elements via ignoreSpans', + async ({ getLocalTestUrl, page }) => { + // spotlightBrowserIntegration is not available in CDN bundles + if (shouldSkipTracingTest() || shouldSkipCdnBundleTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + const pageloadPromise = waitForTransactionRequest(page, txn => txn.contexts?.trace?.op === PAGELOAD); + await page.goto(url); + await pageloadPromise; + + // Click on the spotlight element — interaction span should be filtered + const spotlightTxnPromise = waitForTransactionRequest(page, txn => txn.contexts?.trace?.op === UI_ACTION_CLICK); + await page.locator('[data-test-id=spotlight-button]').click(); + await page.locator('.clicked[data-test-id=spotlight-button]').isVisible(); + const spotlightTransaction = envelopeRequestParser(await spotlightTxnPromise); + + expect(spotlightTransaction.contexts?.trace?.op).toBe(UI_ACTION_CLICK); + + const spotlightInteractionSpans = spotlightTransaction.spans?.filter(span => span.op === UI_INTERACTION_CLICK); + expect(spotlightInteractionSpans).toHaveLength(0); + + // Click on the regular button — wait specifically for a transaction that contains + // a ui.interaction.click child span, since the PerformanceObserver may deliver + // the event entry asynchronously + const regularTxnPromise = waitForTransactionRequest( + page, + txn => + txn.contexts?.trace?.op === UI_ACTION_CLICK && + (txn.spans?.some(span => span.op === UI_INTERACTION_CLICK) ?? false), + ); + await page.locator('[data-test-id=regular-button]').click(); + await page.locator('.clicked[data-test-id=regular-button]').isVisible(); + const regularTransaction = envelopeRequestParser(await regularTxnPromise); + + const regularInteractionSpans = regularTransaction.spans?.filter(span => span.op === UI_INTERACTION_CLICK); + expect(regularInteractionSpans?.length).toBeGreaterThanOrEqual(1); + expect(regularInteractionSpans![0]!.description).toContain('button'); + expect(regularInteractionSpans![0]!.description).not.toContain('#sentry-spotlight'); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-streamed/test.ts deleted file mode 100644 index 0a646701fde8..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter-streamed/test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipCdnBundleTest, shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, observeStreamedSpan, waitForStreamedSpan, waitForStreamedSpans } from '../../../../utils/spanUtils'; - -// Interaction spans are named after the component, so the element is on `browser.web_vital.inp.target`. -// The click handler adds the `clicked` class before the event timing entry is recorded. -const SPOTLIGHT_BUTTON = 'body > div#sentry-spotlight > button.clicked'; -const REGULAR_BUTTON = 'body > button.clicked'; - -sentryTest( - 'filters ui.interaction.click spans for spotlight elements via ignoreSpans in streaming mode', - async ({ getLocalTestUrl, page }) => { - // spotlightBrowserIntegration is not available in CDN bundles - if (shouldSkipTracingTest() || shouldSkipCdnBundleTest()) { - sentryTest.skip(); - } - - const url = await getLocalTestUrl({ testDir: __dirname }); - - // Set up an observer that fails if a spotlight interaction span is ever sent - let sawSpotlightInteractionSpan = false; - await observeStreamedSpan(page, span => { - if ( - getSpanOp(span) === 'ui.interaction.click' && - span.attributes['browser.web_vital.inp.target']?.value === SPOTLIGHT_BUTTON - ) { - sawSpotlightInteractionSpan = true; - return true; - } - return false; - }); - - await page.goto(url); - - // Wait for pageload to finish before clicking - await waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - - // Click on the spotlight element — its ui.interaction.click child should be filtered - await page.locator('[data-test-id=spotlight-button]').click(); - await page.locator('.clicked[data-test-id=spotlight-button]').isVisible(); - - // Wait for the spotlight click's segment span to arrive - await waitForStreamedSpans(page, spans => - spans.some(span => span.is_segment && getSpanOp(span) === 'ui.action.click'), - ); - - // Click on the regular button — its ui.interaction.click child should be kept - const regularInteractionSpansPromise = waitForStreamedSpans(page, spans => - spans.some(span => span.attributes['browser.web_vital.inp.target']?.value === REGULAR_BUTTON), - ); - - await page.locator('[data-test-id=regular-button]').click(); - await page.locator('.clicked[data-test-id=regular-button]').isVisible(); - - const regularSpans = await regularInteractionSpansPromise; - const regularInteractionSpan = regularSpans.find( - span => span.attributes['browser.web_vital.inp.target']?.value === REGULAR_BUTTON, - ); - expect(getSpanOp(regularInteractionSpan!)).toBe('ui.interaction.click'); - - // Verify no spotlight interaction span was ever sent - expect(sawSpotlightInteractionSpan).toBe(false); - }, -); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter/init.js index f2b876b3bf7d..652e289ca5f7 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter/init.js @@ -4,7 +4,6 @@ import { interactionsIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter/test.ts index d0480062c10a..26d8b21cc6b3 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/spotlight-interaction-filter/test.ts @@ -1,57 +1,50 @@ +import { BROWSER_WEB_VITAL_INP_TARGET } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import type { TransactionEvent } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { - envelopeRequestParser, - shouldSkipCdnBundleTest, - shouldSkipTracingTest, - waitForTransactionRequest, -} from '../../../../utils/helpers'; - -sentryTest( - 'filters ui.interaction.click spans for spotlight elements via ignoreSpans', - async ({ getLocalTestUrl, page }) => { - // spotlightBrowserIntegration is not available in CDN bundles - if (shouldSkipTracingTest() || shouldSkipCdnBundleTest()) { - sentryTest.skip(); - } - - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - - // Wait for the pageload transaction to complete - await waitForTransactionRequest(page); - - // Click on the spotlight element — interaction span should be filtered - const spotlightTxnPromise = waitForTransactionRequest(page, txn => txn.contexts?.trace?.op === 'ui.action.click'); - await page.locator('[data-test-id=spotlight-button]').click(); - await page.locator('.clicked[data-test-id=spotlight-button]').isVisible(); - const spotlightTransaction = envelopeRequestParser(await spotlightTxnPromise); - - expect(spotlightTransaction.contexts?.trace?.op).toBe('ui.action.click'); - - const spotlightInteractionSpans = spotlightTransaction.spans?.filter(span => span.op === 'ui.interaction.click'); - expect(spotlightInteractionSpans).toHaveLength(0); - - // Let the first idle span fully settle before clicking again - await page.waitForTimeout(1000); - - // Click on the regular button — wait specifically for a transaction that contains - // a ui.interaction.click child span, since the PerformanceObserver may deliver - // the event entry asynchronously - const regularTxnPromise = waitForTransactionRequest( - page, - txn => - txn.contexts?.trace?.op === 'ui.action.click' && - (txn.spans?.some(span => span.op === 'ui.interaction.click') ?? false), - ); - await page.locator('[data-test-id=regular-button]').click(); - await page.locator('.clicked[data-test-id=regular-button]').isVisible(); - const regularTransaction = envelopeRequestParser(await regularTxnPromise); - - const regularInteractionSpans = regularTransaction.spans?.filter(span => span.op === 'ui.interaction.click'); - expect(regularInteractionSpans?.length).toBeGreaterThanOrEqual(1); - expect(regularInteractionSpans![0]!.description).toContain('button'); - expect(regularInteractionSpans![0]!.description).not.toContain('#sentry-spotlight'); - }, -); +import { shouldSkipCdnBundleTest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; + +// The click handler adds the class before the event timing entry records the target. +const SPOTLIGHT_BUTTON = 'body > div#sentry-spotlight > button.clicked'; +const REGULAR_BUTTON = 'body > button.clicked'; + +sentryTest('filters spotlight interaction spans via ignoreSpans', async ({ getLocalTestUrl, page }) => { + // spotlightBrowserIntegration is not available in CDN bundles. + sentryTest.skip(shouldSkipTracingTest() || shouldSkipCdnBundleTest()); + const url = await getLocalTestUrl({ testDir: __dirname }); + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + await page.goto(url); + await pageloadPromise; + + const spotlightPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'ui.action.click'); + await page.locator('[data-test-id=spotlight-button]').click(); + await expect(page.locator('.clicked[data-test-id=spotlight-button]')).toBeVisible(); + const spotlight = await spotlightPromise; + expect(getSpanOp(spotlight)).toBe('ui.action.click'); + + const regularPromise = waitForStreamedSpan( + page, + span => span.is_segment && getSpanOp(span) === 'ui.action.click' && span.span_id !== spotlight.span_id, + ); + const regularInteractionPromise = waitForStreamedSpan( + page, + span => span.attributes[BROWSER_WEB_VITAL_INP_TARGET]?.value === REGULAR_BUTTON, + ); + await page.locator('[data-test-id=regular-button]').click(); + await expect(page.locator('.clicked[data-test-id=regular-button]')).toBeVisible(); + const [regular, interaction] = await Promise.all([regularPromise, regularInteractionPromise]); + await page.evaluate(() => (window as any).Sentry.flush()); + + expect(getSpanOp(interaction)).toBe('ui.interaction.click'); + expect(interaction.parent_span_id).toBe(regular.span_id); + expect(interaction.trace_id).toBe(regular.trace_id); + expect(interaction.attributes[BROWSER_WEB_VITAL_INP_TARGET]?.value).not.toContain('#sentry-spotlight'); + expect( + spans.filter( + span => + getSpanOp(span) === 'ui.interaction.click' && + span.attributes[BROWSER_WEB_VITAL_INP_TARGET]?.value === SPOTLIGHT_BUTTON, + ), + ).toHaveLength(0); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/init.js index 65cfbab96ee2..b2280b70e307 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/init.js index bb6d4918d1bf..83076460599f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/init.js index 238d85509b25..d7b0a648a2de 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta/init.js index 17b2b0a13c6b..c026daa1eed9 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors-meta/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: integrations => { integrations.push(Sentry.browserTracingIntegration()); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors/init.js index 17b2b0a13c6b..c026daa1eed9 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/twp-errors/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: integrations => { integrations.push(Sentry.browserTracingIntegration()); From 061f62c0b8261a3bb6935de70d93f6a5dc77361e Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Fri, 9 Oct 2026 13:02:35 +0200 Subject: [PATCH 28/84] test(browser): port requests and web vitals to span streaming (#24886) Exercise request instrumentation, web vitals, interactions, and user timing with default span streaming. Retain the existing transaction counterparts for fetch, XHR, resource timing, and TTFB as explicitly pinned `*-static` suites so the migration preserves coverage of both lifecycles. Check RTT as a span attribute on pageload and navigation. Fixes #24143 --------- Co-authored-by: GPT-6 --- .../tracing/interactions/disabled/init.js | 1 - .../tracing/interactions/disabled/test.ts | 18 +- .../interactions/navigation-click/init.js | 1 - .../interactions/navigation-click/test.ts | 63 +-- .../suites/tracing/interactions/spans/init.js | 2 +- .../suites/tracing/interactions/spans/test.ts | 38 +- .../metrics/connection-rtt-navigation/init.js | 1 - .../metrics/connection-rtt-navigation/test.ts | 45 +- .../tracing/metrics/connection-rtt/test.ts | 66 +-- .../tracing/metrics/element-timing/init.js | 1 - .../metrics/handlers-lcp/template.html | 2 +- .../tracing/metrics/handlers-lcp/test.ts | 58 +-- .../suites/tracing/metrics/init.js | 1 - .../metrics/pageload-browser-spans/test.ts | 48 +- .../assets/image.svg | 1 + .../assets/script.js | 1 + .../assets/style.css | 4 + .../pageload-resource-spans-static}/init.js | 2 +- .../template.html | 0 .../pageload-resource-spans-static/test.ts | 254 +++++++++ .../pageload-resource-spans-streamed/test.ts | 68 --- .../pageload-resource-spans}/init.js | 1 - .../metrics/pageload-resource-spans/test.ts | 490 ++++++++++-------- .../init.js | 3 +- .../metrics/web-vitals-cls-static/subject.js | 9 + .../template.html | 1 - .../metrics/web-vitals-cls-static/test.ts | 29 ++ .../web-vitals-cls-streamed-spans/subject.js | 17 - .../web-vitals-cls-streamed-spans/test.ts | 79 --- .../tracing/metrics/web-vitals-cls/init.js | 8 +- .../tracing/metrics/web-vitals-cls/subject.js | 18 +- .../metrics/web-vitals-cls/template.html | 1 + .../tracing/metrics/web-vitals-cls/test.ts | 97 ++-- .../init.js | 4 +- .../web-vitals-fp-fcp-static/template.html | 9 + .../metrics/web-vitals-fp-fcp-static/test.ts | 46 ++ .../tracing/metrics/web-vitals-fp-fcp/test.ts | 63 +-- .../init.js | 39 ++ .../subject.js | 5 +- .../template.html | 2 + .../test.ts | 48 ++ .../web-vitals-inp-before-send-span/init.js | 13 +- .../web-vitals-inp-before-send-span/test.ts | 36 +- .../metrics/web-vitals-inp-late/init.js | 1 - .../metrics/web-vitals-inp-late/test.ts | 85 +-- .../metrics/web-vitals-inp-navigate/init.js | 1 - .../metrics/web-vitals-inp-navigate/test.ts | 140 +++-- .../web-vitals-inp-parametrized-late/init.js | 1 - .../web-vitals-inp-parametrized-late/test.ts | 85 +-- .../web-vitals-inp-parametrized/init.js | 1 - .../web-vitals-inp-parametrized/test.ts | 85 +-- .../web-vitals-inp-streamed-spans/test.ts | 87 ---- .../tracing/metrics/web-vitals-inp/init.js | 19 +- .../tracing/metrics/web-vitals-inp/subject.js | 5 +- .../metrics/web-vitals-inp/template.html | 2 - .../tracing/metrics/web-vitals-inp/test.ts | 266 ++++------ .../assets/sentry-logo-600x179.png | Bin .../metrics/web-vitals-lcp-static/init.js | 10 + .../template.html | 0 .../metrics/web-vitals-lcp-static/test.ts | 42 ++ .../web-vitals-lcp-streamed-spans/init.js | 11 - .../web-vitals-lcp-streamed-spans/test.ts | 69 --- .../init.js | 3 +- .../tracing/metrics/web-vitals-lcp/test.ts | 79 ++- .../metrics/web-vitals-ttfb-static/init.js | 10 + .../web-vitals-ttfb-static/template.html | 9 + .../metrics/web-vitals-ttfb-static/test.ts | 28 + .../metrics/web-vitals-ttfb-streamed/test.ts | 36 -- .../init.js | 3 +- .../tracing/metrics/web-vitals-ttfb/test.ts | 41 +- .../suites/tracing/metrics/web-vitals/test.ts | 48 +- .../tracing/request/fetch-data-url/init.js | 1 - .../tracing/request/fetch-data-url/test.ts | 48 +- .../tracing/request/fetch-immediate/init.js | 1 - .../tracing/request/fetch-immediate/test.ts | 37 +- .../tracing/request/fetch-no-tracing/init.js | 1 - .../fetch-propagateTraceparent/init.js | 1 - .../request/fetch-relative-url-static/init.js | 10 + .../fetch-relative-url-static/subject.js | 3 + .../request/fetch-relative-url-static/test.ts | 78 +++ .../fetch-relative-url-streamed/subject.js | 1 - .../fetch-relative-url-streamed/test.ts | 39 -- .../request/fetch-relative-url/init.js | 2 +- .../request/fetch-relative-url/test.ts | 52 +- .../{xhr-streamed => fetch-static}/init.js | 3 +- .../subject.js | 0 .../tracing/request/fetch-static/test.ts | 87 ++++ .../tracing/request/fetch-streamed/test.ts | 48 -- .../fetch-strip-query-and-fragment/init.js | 1 - .../fetch-strip-query-and-fragment/test.ts | 171 +++--- .../fetch-trace-header-merging/init.js | 1 - .../init.js | 1 - .../request/fetch-tracing-unsampled/init.js | 1 - .../init.js | 1 - .../fetch-tracing-without-performance/init.js | 1 - .../init.js | 6 +- .../subject.js | 0 .../test.ts | 9 +- .../init.js | 1 - .../fetch-with-custom-sentry-headers/init.js | 1 - .../request/fetch-with-request/init.js | 1 - .../request/{fetch-streamed => fetch}/init.js | 2 +- .../suites/tracing/request/fetch/test.ts | 51 +- .../suites/tracing/request/init.js | 1 - .../tracing/request/xhr-data-url/init.js | 1 - .../tracing/request/xhr-data-url/test.ts | 45 +- .../tracing/request/xhr-no-tracing/init.js | 1 - .../request/xhr-propagateTraceparent/init.js | 1 - .../request/xhr-relative-url-static/init.js | 10 + .../subject.js | 1 + .../request/xhr-relative-url-static/test.ts | 78 +++ .../request/xhr-relative-url-streamed/test.ts | 39 -- .../tracing/request/xhr-relative-url/init.js | 2 +- .../tracing/request/xhr-relative-url/test.ts | 52 +- .../suites/tracing/request/xhr-static/init.js | 12 + .../{xhr-streamed => xhr-static}/subject.js | 0 .../suites/tracing/request/xhr-static/test.ts | 76 +++ .../tracing/request/xhr-streamed/test.ts | 48 -- .../xhr-strip-query-and-fragment/init.js | 1 - .../xhr-strip-query-and-fragment/test.ts | 161 +++--- .../init.js | 1 - .../request/xhr-tracing-unsampled/init.js | 1 - .../init.js | 1 - .../xhr-tracing-without-performance/init.js | 1 - .../init.js | 1 - .../suites/tracing/request/xhr/init.js | 11 + .../suites/tracing/request/xhr/test.ts | 48 +- .../tracing/user-timing/disabled/test.ts | 8 +- .../init.js | 1 - .../test.ts | 79 +-- .../pageload-measure-spans/init.js | 1 - .../pageload-measure-spans/test.ts | 56 +- .../suites/tracing/user-timing/spans/test.ts | 11 +- .../utils/helpers.ts | 49 +- .../nuxt-4-static/tests/middleware.test.ts | 8 +- 135 files changed, 2427 insertions(+), 1899 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/image.svg create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/script.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/style.css rename dev-packages/browser-integration-tests/suites/tracing/{request/fetch-relative-url-streamed => metrics/pageload-resource-spans-static}/init.js (87%) rename dev-packages/browser-integration-tests/suites/tracing/metrics/{pageload-resource-spans-streamed => pageload-resource-spans-static}/template.html (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/test.ts rename dev-packages/browser-integration-tests/suites/tracing/{request/xhr-relative-url-streamed => metrics/pageload-resource-spans}/init.js (87%) rename dev-packages/browser-integration-tests/suites/tracing/metrics/{web-vitals-inp-streamed-spans => web-vitals-cls-static}/init.js (72%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/subject.js rename dev-packages/browser-integration-tests/suites/tracing/metrics/{web-vitals-cls-streamed-spans => web-vitals-cls-static}/template.html (84%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/subject.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/test.ts rename dev-packages/browser-integration-tests/suites/tracing/metrics/{pageload-resource-spans-streamed => web-vitals-fp-fcp-static}/init.js (57%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp-static/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp-static/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/init.js rename dev-packages/browser-integration-tests/suites/tracing/metrics/{web-vitals-inp-streamed-spans => web-vitals-inp-before-send-span-static}/subject.js (70%) rename dev-packages/browser-integration-tests/suites/tracing/metrics/{web-vitals-inp-streamed-spans => web-vitals-inp-before-send-span-static}/template.html (65%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/test.ts rename dev-packages/browser-integration-tests/suites/tracing/metrics/{web-vitals-lcp-streamed-spans => web-vitals-lcp-static}/assets/sentry-logo-600x179.png (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-static/init.js rename dev-packages/browser-integration-tests/suites/tracing/metrics/{web-vitals-lcp-streamed-spans => web-vitals-lcp-static}/template.html (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/init.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/test.ts rename dev-packages/browser-integration-tests/suites/tracing/metrics/{web-vitals-cls-streamed-spans => web-vitals-lcp}/init.js (65%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-static/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-static/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-streamed/test.ts rename dev-packages/browser-integration-tests/suites/tracing/metrics/{web-vitals-ttfb-streamed => web-vitals-ttfb}/init.js (66%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-static/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-static/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-streamed/subject.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-streamed/test.ts rename dev-packages/browser-integration-tests/suites/tracing/request/{xhr-streamed => fetch-static}/init.js (74%) rename dev-packages/browser-integration-tests/suites/tracing/request/{fetch-streamed => fetch-static}/subject.js (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed/test.ts rename dev-packages/browser-integration-tests/suites/tracing/request/{fetch-streamed-track-stream-performance => fetch-track-stream-performance}/init.js (62%) rename dev-packages/browser-integration-tests/suites/tracing/request/{fetch-streamed-track-stream-performance => fetch-track-stream-performance}/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/request/{fetch-streamed-track-stream-performance => fetch-track-stream-performance}/test.ts (85%) rename dev-packages/browser-integration-tests/suites/tracing/request/{fetch-streamed => fetch}/init.js (74%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-static/init.js rename dev-packages/browser-integration-tests/suites/tracing/request/{xhr-relative-url-streamed => xhr-relative-url-static}/subject.js (80%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-streamed/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-static/init.js rename dev-packages/browser-integration-tests/suites/tracing/request/{xhr-streamed => xhr-static}/subject.js (100%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-static/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-streamed/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr/init.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/interactions/disabled/init.js b/dev-packages/browser-integration-tests/suites/tracing/interactions/disabled/init.js index bb6d4918d1bf..83076460599f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/interactions/disabled/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/interactions/disabled/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/interactions/disabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/interactions/disabled/test.ts index 5a163c067969..400a19d98011 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/interactions/disabled/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/interactions/disabled/test.ts @@ -1,19 +1,19 @@ +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import type { Event as SentryEvent } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { countEnvelopes, getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest('does not capture interaction spans without the integration', async ({ getLocalTestUrl, page }) => { sentryTest.skip(shouldSkipTracingTest()); - + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - await getFirstSentryEnvelopeRequest(page); - - const countPromise = countEnvelopes(page, { envelopeType: 'transaction', timeout: 2000 }); - + await pageloadPromise; await page.locator('[data-test-id=interaction-button]').click(); + await hidePage(page); + await page.evaluate(() => (window as any).Sentry.flush()); - expect(await countPromise).toBe(0); + expect(spans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.browser.interactions')).toHaveLength(0); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/interactions/navigation-click/init.js b/dev-packages/browser-integration-tests/suites/tracing/interactions/navigation-click/init.js index 2c3e9264d882..96386e1d38d5 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/interactions/navigation-click/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/interactions/navigation-click/init.js @@ -4,7 +4,6 @@ import { interactionsIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1, integrations: [Sentry.browserTracingIntegration(), interactionsIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/tracing/interactions/navigation-click/test.ts b/dev-packages/browser-integration-tests/suites/tracing/interactions/navigation-click/test.ts index 4931aaa4b172..b9913d3a5739 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/interactions/navigation-click/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/interactions/navigation-click/test.ts @@ -1,39 +1,28 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; - -sentryTest( - 'click-triggered navigation should produce a root navigation transaction', - async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - const url = await getLocalTestUrl({ testDir: __dirname }); - - await page.goto(url); - await waitForTransactionRequest(page); // "pageload" root span - - const interactionRequestPromise = waitForTransactionRequest( - page, - evt => evt.contexts?.trace?.op === 'ui.action.click', - ); - const navigationRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation'); - - await page.locator('[data-test-id=navigate-button]').click(); - - const interactionEvent = envelopeRequestParser(await interactionRequestPromise); - const navigationEvent = envelopeRequestParser(await navigationRequestPromise); - - // Navigation is root span, not a child span on the interaction - expect(interactionEvent.contexts?.trace?.op).toBe('ui.action.click'); - expect(navigationEvent.contexts?.trace?.op).toBe('navigation'); - - expect(interactionEvent.contexts?.trace?.trace_id).not.toEqual(navigationEvent.contexts?.trace?.trace_id); - - // does not contain a child navigation span - const interactionSpans = interactionEvent.spans || []; - const hasNavigationChild = interactionSpans.some(span => span.op === 'navigation' || span.op === 'http.server'); - expect(hasNavigationChild).toBeFalsy(); - }, -); +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; + +sentryTest('starts a root navigation segment when a click triggers navigation', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + await pageloadPromise; + + const interactionPromise = waitForStreamedSpan( + page, + span => span.is_segment && getSpanOp(span) === 'ui.action.click', + ); + const navigationPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'navigation'); + await page.locator('[data-test-id=navigate-button]').click(); + const [interaction, navigation] = await Promise.all([interactionPromise, navigationPromise]); + await page.evaluate(() => (window as any).Sentry.flush()); + + expect(navigation.is_segment).toBe(true); + expect(navigation).not.toHaveProperty('parent_span_id'); + expect(navigation.trace_id).not.toBe(interaction.trace_id); + const children = spans.filter(span => span.parent_span_id === interaction.span_id); + expect(children.filter(span => ['navigation', 'http.server'].includes(getSpanOp(span) ?? ''))).toHaveLength(0); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/init.js index af4b7b3e3483..c2d6aa640c21 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/init.js @@ -5,6 +5,6 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), interactionsIntegration(), Sentry.spanStreamingIntegration()], + integrations: [Sentry.browserTracingIntegration(), interactionsIntegration()], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts index 0870974ba825..2a83365805f8 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts @@ -1,6 +1,12 @@ import { expect } from '@playwright/test'; import { SDK_VERSION } from '@sentry/core'; import { + SENTRY_IS_LOCALHOST, + CULTURE_CALENDAR, + CULTURE_LOCALE, + CULTURE_TIMEZONE, + URL_FULL, + BROWSER_WEB_VITAL_INP_TARGET, SENTRY_IDLE_SPAN_FINISH_REASON, SENTRY_SEGMENT_ID, SENTRY_SEGMENT_NAME, @@ -17,7 +23,7 @@ import { } from '@sentry/conventions/attributes'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpan, waitForStreamedSpans } from '../../../../utils/spanUtils'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest('captures streamed interaction span tree. @firefox', async ({ browserName, getLocalTestUrl, page }) => { const supportedBrowsers = ['chromium', 'firefox']; @@ -25,8 +31,10 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser sentryTest.skip(shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)); const url = await getLocalTestUrl({ testDir: __dirname }); - const interactionSpansPromise = waitForStreamedSpans(page, spans => - spans.some(span => getSpanOp(span) === 'ui.action.click'), + const spans = collectStreamedSpans(page); + const interactionPromise = waitForStreamedSpan( + page, + span => span.is_segment && getSpanOp(span) === 'ui.action.click', ); const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); @@ -39,26 +47,30 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser await page.locator('[data-test-id=interaction-button]').click(); await page.locator('.clicked[data-test-id=interaction-button]').isVisible(); - const interactionSpanTree = await interactionSpansPromise; - - const interactionSegmentSpan = interactionSpanTree.find(span => !!span.is_segment); + const interactionSegmentSpan = await interactionPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + const interactionSpanTree = spans.filter( + span => + span.span_id === interactionSegmentSpan.span_id || + span.attributes[SENTRY_SEGMENT_ID]?.value === interactionSegmentSpan.span_id, + ); expect(interactionSegmentSpan).toEqual({ attributes: { - 'sentry.is_localhost': { value: false, type: 'boolean' }, + [SENTRY_IS_LOCALHOST]: { value: false, type: 'boolean' }, [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream', }, - 'culture.calendar': { + [CULTURE_CALENDAR]: { type: 'string', value: expect.any(String), }, - 'culture.locale': { + [CULTURE_LOCALE]: { type: 'string', value: expect.any(String), }, - 'culture.timezone': { + [CULTURE_TIMEZONE]: { type: 'string', value: expect.any(String), }, @@ -66,7 +78,7 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser type: 'string', value: expect.any(String), }, - 'url.full': { + [URL_FULL]: { type: 'string', value: expect.any(String), }, @@ -130,7 +142,7 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser const interactionSpan = interactionSpanTree.find(span => getSpanOp(span) === 'ui.interaction.click'); expect(interactionSpan).toEqual({ attributes: { - 'sentry.is_localhost': { value: false, type: 'boolean' }, + [SENTRY_IS_LOCALHOST]: { value: false, type: 'boolean' }, [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream', @@ -167,7 +179,7 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser type: 'string', value: 'production', }, - 'browser.web_vital.inp.target': { + [BROWSER_WEB_VITAL_INP_TARGET]: { type: 'string', value: 'body > button.clicked', }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt-navigation/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt-navigation/init.js index 7ebcfe76faab..3e7a3d67bff0 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt-navigation/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt-navigation/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt-navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt-navigation/test.ts index c071cf089d71..ef019fa0dc56 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt-navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt-navigation/test.ts @@ -1,37 +1,24 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../../utils/spanUtils'; +import { + BROWSER_WEB_VITAL_FCP_VALUE, + BROWSER_WEB_VITAL_TTFB_VALUE, + NETWORK_CONNECTION_RTT, +} from '@sentry/conventions/attributes'; -sentryTest.beforeEach(({ browserName }) => { - if (shouldSkipTracingTest() || browserName !== 'chromium') { - sentryTest.skip(); - } -}); - -// `connection.rtt` is recorded as a measurement, which is only flushed on the pageload -// transaction. It must not leak onto navigation transactions. sentryTest( - 'records `connection.rtt` as a measurement on pageload but not on navigation transactions', - async ({ getLocalTestUrl, page }) => { - const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); + 'records connection RTT on pageload and navigation spans', + async ({ getLocalTestUrl, page, browserName }) => { + sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium'); const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - - const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise) as Event; - - const navigationRequestPromise = waitForTransactionRequest( - page, - event => event.contexts?.trace?.op === 'navigation', - ); - await page.goto(`${url}#foo`); - - const navigationRequest = envelopeRequestParser(await navigationRequestPromise) as Event; - - expect(pageloadRequest.contexts?.trace?.op).toBe('pageload'); - expect(navigationRequest.contexts?.trace?.op).toBe('navigation'); + const [pageload] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url); + const [navigation] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, `${url}#foo`); - expect(pageloadRequest.measurements?.['connection.rtt']?.value).toBeDefined(); - expect(navigationRequest.measurements?.['connection.rtt']).toBeUndefined(); + expect(pageload.attributes[NETWORK_CONNECTION_RTT]).toEqual({ type: 'integer', value: 0 }); + expect(navigation.attributes[NETWORK_CONNECTION_RTT]).toEqual(pageload.attributes[NETWORK_CONNECTION_RTT]); + expect(navigation.attributes[BROWSER_WEB_VITAL_FCP_VALUE]).toBeUndefined(); + expect(navigation.attributes[BROWSER_WEB_VITAL_TTFB_VALUE]).toBeUndefined(); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt/test.ts index 6f4b885e71d1..492027a2fead 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt/test.ts @@ -1,8 +1,9 @@ import type { Page } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../../utils/spanUtils'; +import { NETWORK_CONNECTION_RTT } from '@sentry/conventions/attributes'; sentryTest.beforeEach(({ browserName }) => { if (shouldSkipTracingTest() || browserName !== 'chromium') { @@ -22,55 +23,42 @@ async function createSessionWithLatency(page: Page, latency: number) { return session; } -sentryTest('should capture a `connection.rtt` metric. xxx', async ({ getLocalTestUrl, page }) => { +sentryTest('captures connection RTT without emulation', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const eventData = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url).then(([span]) => span); - expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.['connection.rtt']?.value).toBe(0); + expect(eventData.attributes[NETWORK_CONNECTION_RTT]?.value).toBe(0); }); -sentryTest( - 'should capture a `connection.rtt` metric with emulated value 200ms on Chromium.', - async ({ getLocalTestUrl, page }) => { - const session = await createSessionWithLatency(page, 200); +sentryTest('captures connection RTT with emulated value 200ms on Chromium.', async ({ getLocalTestUrl, page }) => { + const session = await createSessionWithLatency(page, 200); - const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const url = await getLocalTestUrl({ testDir: __dirname }); + const eventData = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url).then(([span]) => span); - await session.detach(); + await session.detach(); - expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.['connection.rtt']?.value).toBe(200); - }, -); + expect(eventData.attributes[NETWORK_CONNECTION_RTT]?.value).toBe(200); +}); -sentryTest( - 'should capture a `connection.rtt` metric with emulated value 100ms on Chromium.', - async ({ getLocalTestUrl, page }) => { - const session = await createSessionWithLatency(page, 100); +sentryTest('captures connection RTT with emulated value 100ms on Chromium.', async ({ getLocalTestUrl, page }) => { + const session = await createSessionWithLatency(page, 100); - const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const url = await getLocalTestUrl({ testDir: __dirname }); + const eventData = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url).then(([span]) => span); - await session.detach(); + await session.detach(); - expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.['connection.rtt']?.value).toBe(100); - }, -); + expect(eventData.attributes[NETWORK_CONNECTION_RTT]?.value).toBe(100); +}); -sentryTest( - 'should capture a `connection.rtt` metric with emulated value 50ms on Chromium.', - async ({ getLocalTestUrl, page }) => { - const session = await createSessionWithLatency(page, 50); +sentryTest('captures connection RTT with emulated value 50ms on Chromium.', async ({ getLocalTestUrl, page }) => { + const session = await createSessionWithLatency(page, 50); - const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const url = await getLocalTestUrl({ testDir: __dirname }); + const eventData = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url).then(([span]) => span); - await session.detach(); + await session.detach(); - expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.['connection.rtt']?.value).toBe(50); - }, -); + expect(eventData.attributes[NETWORK_CONNECTION_RTT]?.value).toBe(50); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/init.js index 8c8e5e033d47..40253c296af1 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', debug: true, dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration(), Sentry.elementTimingIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/template.html index a8ba98c05ad2..ff4754b3ebb6 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/template.html +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/template.html @@ -6,6 +6,6 @@
- + diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/test.ts index b40899e37521..be7a0585ffcf 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/test.ts @@ -1,8 +1,13 @@ +import { + BROWSER_WEB_VITAL_LCP_VALUE, + BROWSER_WEB_VITAL_LCP_ELEMENT, + BROWSER_WEB_VITAL_LCP_SIZE, +} from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest, waitForLcpCandidate } from '../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; const bundle = process.env.PW_BUNDLE || ''; @@ -20,38 +25,21 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); - const [eventData] = await Promise.all([ - getFirstSentryEnvelopeRequest(page), - page.goto(url), - page.locator('button').click(), - ]); - - expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.lcp?.value).toBeDefined(); - - // This should be body > img, but it can be flakey as sometimes it will report - // the button as LCP. - expect(eventData.contexts?.trace?.data?.['lcp.element'].startsWith('body >')).toBe(true); - - // Working around flakiness - // Only testing this when the LCP element is an image, not a button - if (eventData.contexts?.trace?.data?.['lcp.element'] === 'body > img') { - expect(eventData.contexts?.trace?.data?.['lcp.size']).toBe(107400); - - const lcp = await (await page.waitForFunction('window._LCP')).jsonValue(); - const lcp2 = await (await page.waitForFunction('window._LCP2')).jsonValue(); - const lcp3 = await page.evaluate('window._LCP3'); - - expect(lcp).toEqual(107400); - expect(lcp2).toEqual(107400); - // this has not been triggered yet - expect(lcp3).toEqual(undefined); - - // Adding a handler after LCP is completed still triggers the handler - await page.evaluate('window.ADD_HANDLER()'); - const lcp3_2 = await (await page.waitForFunction('window._LCP3')).jsonValue(); - - expect(lcp3_2).toEqual(107400); - } + const lcpPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.webvital.lcp'); + await page.goto(url); + await waitForLcpCandidate(page, 'img'); + await page.locator('button').click(); + const lcpSpan = await lcpPromise; + + expect(lcpSpan.attributes[BROWSER_WEB_VITAL_LCP_VALUE]?.value).toBeGreaterThan(0); + expect(lcpSpan.attributes[BROWSER_WEB_VITAL_LCP_ELEMENT]).toEqual({ type: 'string', value: 'body > img' }); + expect(lcpSpan.attributes[BROWSER_WEB_VITAL_LCP_SIZE]).toEqual({ type: 'integer', value: 107400 }); + expect(await page.evaluate('window._LCP')).toBe(107400); + expect(await page.evaluate('window._LCP2')).toBe(107400); + expect(await page.evaluate('window._LCP3')).toBeUndefined(); + + await page.evaluate('window.ADD_HANDLER()'); + await page.waitForFunction('window._LCP3 === 107400'); + expect(await page.evaluate('window._LCP3')).toBe(107400); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/init.js index bb6d4918d1bf..83076460599f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts index 5e85698e709d..8be9de6373d1 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts @@ -1,29 +1,31 @@ +import { PAGELOAD, BROWSER, BROWSER_LOAD_EVENT, BROWSER_REQUEST, BROWSER_RESPONSE } from '@sentry/conventions/op'; +import { URL_FULL } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; - -sentryTest('should add browser-related spans to pageload transaction', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; +sentryTest('adds browser performance spans to the pageload segment', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === PAGELOAD); const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + const pageload = await pageloadPromise; + await page.evaluate(() => (window as any).Sentry.flush()); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const browserSpans = eventData.spans?.filter(({ op }) => op?.startsWith('browser')); - - // Spans `dom_content_loaded_event`, `connect`, `cache` and `dns` are not - // always inside `pageload` transaction. - expect(browserSpans?.length).toBeGreaterThanOrEqual(4); - - ['load_event', 'request', 'response'].forEach(eventDesc => - expect(browserSpans).toContainEqual( - expect.objectContaining({ - op: `browser.${eventDesc}`, - description: page.url(), - parent_span_id: eventData.contexts?.trace?.span_id, - }), - ), - ); + const browserSpans = spans.filter(span => getSpanOp(span)?.startsWith(BROWSER)); + expect(browserSpans.length).toBeGreaterThanOrEqual(4); + [ + { op: BROWSER_LOAD_EVENT, name: 'Load event' }, + { op: BROWSER_REQUEST, name: 'Request' }, + { op: BROWSER_RESPONSE, name: 'Response' }, + ].forEach(({ op, name }) => { + const matches = browserSpans.filter(span => getSpanOp(span) === op); + expect(matches).toHaveLength(1); + expect(matches[0].name).toBe(name); + expect(matches[0].attributes[URL_FULL]).toEqual({ type: 'string', value: page.url() }); + expect(matches[0].parent_span_id).toBe(pageload.span_id); + expect(matches[0].trace_id).toBe(pageload.trace_id); + }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/image.svg b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/image.svg new file mode 100644 index 000000000000..3e8fcacea683 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/image.svg @@ -0,0 +1 @@ + diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/script.js new file mode 100644 index 000000000000..1ebec8919f7f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/script.js @@ -0,0 +1 @@ +(() => {})(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/style.css b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/style.css new file mode 100644 index 000000000000..3e9fdb9de409 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/style.css @@ -0,0 +1,4 @@ +p { + color: red; + text-align: center; +} diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/init.js similarity index 87% rename from dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/init.js index 5ab240338c8c..bb6d4918d1bf 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/init.js @@ -3,8 +3,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, - autoSessionTracking: false, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/template.html rename to dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/test.ts new file mode 100644 index 000000000000..c15ef574991a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/test.ts @@ -0,0 +1,254 @@ +import { PAGELOAD, RESOURCE, RESOURCE_SCRIPT, RESOURCE_LINK, RESOURCE_IMG } from '@sentry/conventions/op'; +import { + HTTP_REQUEST_SAME_ORIGIN, + URL_FULL, + HTTP_RESPONSE_BODY_DECODED_SIZE, + HTTP_RESPONSE_BODY_SIZE, + HTTP_RESPONSE_SIZE, + HTTP_REQUEST_CONNECT_START, + HTTP_REQUEST_CONNECTION_END, + HTTP_REQUEST_DOMAIN_LOOKUP_END, + HTTP_REQUEST_DOMAIN_LOOKUP_START, + HTTP_REQUEST_FETCH_START, + HTTP_REQUEST_REDIRECT_END, + HTTP_REQUEST_REDIRECT_START, + HTTP_REQUEST_REQUEST_START, + HTTP_REQUEST_SECURE_CONNECTION_START, + HTTP_REQUEST_WORKER_START, + HTTP_REQUEST_RESPONSE_END, + HTTP_REQUEST_RESPONSE_START, + HTTP_REQUEST_TIME_TO_FIRST_BYTE, + NETWORK_PROTOCOL_NAME, + NETWORK_PROTOCOL_VERSION, + SERVER_ADDRESS, + URL_DOMAIN, + URL_SCHEME, + HTTP_RESPONSE_STATUS_CODE, + RESOURCE_RENDER_BLOCKING_STATUS, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import { type Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestUrl, page, browserName }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const isWebkitRun = browserName === 'webkit'; + + // Intercepting asset requests to avoid network-related flakiness and random retries (on Firefox). + await page.route('https://sentry-test-site.example/path/to/image.svg', (route: Route) => + route.fulfill({ + path: `${__dirname}/assets/image.svg`, + headers: { + 'Timing-Allow-Origin': '*', + 'Content-Type': 'image/svg+xml', + }, + }), + ); + await page.route('https://sentry-test-site.example/path/to/script.js', (route: Route) => + route.fulfill({ + path: `${__dirname}/assets/script.js`, + headers: { + 'Timing-Allow-Origin': '*', + 'Content-Type': 'application/javascript', + }, + }), + ); + await page.route('https://sentry-test-site.example/path/to/style.css', (route: Route) => + route.fulfill({ + path: `${__dirname}/assets/style.css`, + headers: { + 'Timing-Allow-Origin': '*', + 'Content-Type': 'text/css', + }, + }), + ); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === PAGELOAD); + await page.goto(url); + const eventData = envelopeRequestParser(await pageloadPromise); + const resourceSpans = eventData.spans?.filter(({ op }) => op?.startsWith(RESOURCE)); + + const scriptSpans = resourceSpans?.filter(({ op }) => op === RESOURCE_SCRIPT); + const linkSpan = resourceSpans?.filter(({ op }) => op === RESOURCE_LINK)[0]; + const imgSpan = resourceSpans?.filter(({ op }) => op === RESOURCE_IMG)[0]; + + const spanId = eventData.contexts?.trace?.span_id; + const traceId = eventData.contexts?.trace?.trace_id; + + expect(spanId).toBeDefined(); + expect(traceId).toBeDefined(); + + const hasCdnBundle = (process.env.PW_BUNDLE || '').startsWith('bundle'); + + const expectedScripts = ['/init.bundle.js', 'https://sentry-test-site.example/path/to/script.js']; + if (hasCdnBundle) { + expectedScripts.unshift('/cdn.bundle.js'); + } + + expect(scriptSpans?.map(({ description }) => description).sort()).toEqual(expectedScripts); + expect(scriptSpans?.map(({ parent_span_id }) => parent_span_id)).toEqual(expectedScripts.map(() => spanId)); + + // The init bundle script is served from the test origin: its description is origin-relative, + // but `url.full` retains the full absolute URL (needed for span description inference). + const sameOriginScriptSpan = scriptSpans?.find(({ description }) => description === '/init.bundle.js'); + expect(sameOriginScriptSpan?.data?.[HTTP_REQUEST_SAME_ORIGIN]).toBe(true); + expect(sameOriginScriptSpan?.data?.[URL_FULL]).toMatch(/^https?:\/\/.+\/init\.bundle\.js$/); + + const customScriptSpan = scriptSpans?.find( + ({ description }) => description === 'https://sentry-test-site.example/path/to/script.js', + ); + + expect(imgSpan).toEqual({ + data: { + [HTTP_RESPONSE_BODY_DECODED_SIZE]: expect.any(Number), + [HTTP_RESPONSE_BODY_SIZE]: expect.any(Number), + [HTTP_RESPONSE_SIZE]: expect.any(Number), + [HTTP_REQUEST_CONNECT_START]: expect.any(Number), + [HTTP_REQUEST_CONNECTION_END]: expect.any(Number), + [HTTP_REQUEST_DOMAIN_LOOKUP_END]: expect.any(Number), + [HTTP_REQUEST_DOMAIN_LOOKUP_START]: expect.any(Number), + [HTTP_REQUEST_FETCH_START]: expect.any(Number), + [HTTP_REQUEST_REDIRECT_END]: expect.any(Number), + [HTTP_REQUEST_REDIRECT_START]: expect.any(Number), + [HTTP_REQUEST_REQUEST_START]: expect.any(Number), + [HTTP_REQUEST_SECURE_CONNECTION_START]: expect.any(Number), + [HTTP_REQUEST_WORKER_START]: expect.any(Number), + [HTTP_REQUEST_RESPONSE_END]: expect.any(Number), + [HTTP_REQUEST_RESPONSE_START]: expect.any(Number), + [HTTP_REQUEST_TIME_TO_FIRST_BYTE]: expect.any(Number), + [NETWORK_PROTOCOL_NAME]: '', + [NETWORK_PROTOCOL_VERSION]: 'unknown', + [SENTRY_OP]: RESOURCE_IMG, + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [SERVER_ADDRESS]: 'sentry-test-site.example', + [URL_DOMAIN]: 'sentry-test-site.example', + [HTTP_REQUEST_SAME_ORIGIN]: false, + [URL_SCHEME]: 'https', + [URL_FULL]: 'https://sentry-test-site.example/path/to/image.svg', + // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status + // or render blocking status. + 'http.response_delivery_type': '', + ...(!isWebkitRun && { + [HTTP_RESPONSE_STATUS_CODE]: expect.any(Number), + [RESOURCE_RENDER_BLOCKING_STATUS]: 'non-blocking', + }), + }, + description: 'https://sentry-test-site.example/path/to/image.svg', + op: RESOURCE_IMG, + origin: 'auto.resource.browser.metrics', + status: 'ok', + parent_span_id: spanId, + span_id: expect.stringMatching(/^[a-f\d]{16}$/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }); + + // range check: TTFB is reasonably <10 seconds, which is really a check that we report it in + // seconds rather than milliseconds. WebKit resolves these intercepted routes without measurable + // delay, so only the other engines are held to a non-zero value. + const imgSpanTtfb = imgSpan?.data[HTTP_REQUEST_TIME_TO_FIRST_BYTE]; + expect(imgSpanTtfb).toBeGreaterThan(isWebkitRun ? -1 : 0); + expect(imgSpanTtfb).toBeLessThan(10); + + expect(linkSpan).toEqual({ + data: { + [HTTP_RESPONSE_BODY_DECODED_SIZE]: expect.any(Number), + [HTTP_RESPONSE_BODY_SIZE]: expect.any(Number), + [HTTP_RESPONSE_SIZE]: expect.any(Number), + [HTTP_REQUEST_CONNECT_START]: expect.any(Number), + [HTTP_REQUEST_CONNECTION_END]: expect.any(Number), + [HTTP_REQUEST_DOMAIN_LOOKUP_END]: expect.any(Number), + [HTTP_REQUEST_DOMAIN_LOOKUP_START]: expect.any(Number), + [HTTP_REQUEST_FETCH_START]: expect.any(Number), + [HTTP_REQUEST_REDIRECT_END]: expect.any(Number), + [HTTP_REQUEST_REDIRECT_START]: expect.any(Number), + [HTTP_REQUEST_REQUEST_START]: expect.any(Number), + [HTTP_REQUEST_SECURE_CONNECTION_START]: expect.any(Number), + [HTTP_REQUEST_WORKER_START]: expect.any(Number), + [HTTP_REQUEST_RESPONSE_END]: expect.any(Number), + [HTTP_REQUEST_RESPONSE_START]: expect.any(Number), + [HTTP_REQUEST_TIME_TO_FIRST_BYTE]: expect.any(Number), + [NETWORK_PROTOCOL_NAME]: '', + [NETWORK_PROTOCOL_VERSION]: 'unknown', + [SENTRY_OP]: RESOURCE_LINK, + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [SERVER_ADDRESS]: 'sentry-test-site.example', + [URL_DOMAIN]: 'sentry-test-site.example', + [HTTP_REQUEST_SAME_ORIGIN]: false, + [URL_SCHEME]: 'https', + [URL_FULL]: 'https://sentry-test-site.example/path/to/style.css', + // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status + // or render blocking status. + 'http.response_delivery_type': '', + ...(!isWebkitRun && { + [HTTP_RESPONSE_STATUS_CODE]: expect.any(Number), + [RESOURCE_RENDER_BLOCKING_STATUS]: 'non-blocking', + }), + }, + description: 'https://sentry-test-site.example/path/to/style.css', + op: RESOURCE_LINK, + origin: 'auto.resource.browser.metrics', + status: 'ok', + parent_span_id: spanId, + span_id: expect.stringMatching(/^[a-f\d]{16}$/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }); + + expect(customScriptSpan).toEqual({ + data: { + [HTTP_RESPONSE_BODY_DECODED_SIZE]: expect.any(Number), + [HTTP_RESPONSE_BODY_SIZE]: expect.any(Number), + [HTTP_RESPONSE_SIZE]: expect.any(Number), + [HTTP_REQUEST_CONNECTION_END]: expect.any(Number), + [HTTP_REQUEST_CONNECT_START]: expect.any(Number), + [HTTP_REQUEST_DOMAIN_LOOKUP_END]: expect.any(Number), + [HTTP_REQUEST_DOMAIN_LOOKUP_START]: expect.any(Number), + [HTTP_REQUEST_FETCH_START]: expect.any(Number), + [HTTP_REQUEST_REDIRECT_END]: expect.any(Number), + [HTTP_REQUEST_REDIRECT_START]: expect.any(Number), + [HTTP_REQUEST_REQUEST_START]: expect.any(Number), + [HTTP_REQUEST_SECURE_CONNECTION_START]: expect.any(Number), + [HTTP_REQUEST_WORKER_START]: expect.any(Number), + [HTTP_REQUEST_RESPONSE_END]: expect.any(Number), + [HTTP_REQUEST_RESPONSE_START]: expect.any(Number), + [HTTP_REQUEST_TIME_TO_FIRST_BYTE]: expect.any(Number), + [NETWORK_PROTOCOL_NAME]: '', + [NETWORK_PROTOCOL_VERSION]: 'unknown', + [SENTRY_OP]: RESOURCE_SCRIPT, + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [SERVER_ADDRESS]: 'sentry-test-site.example', + [URL_DOMAIN]: 'sentry-test-site.example', + [HTTP_REQUEST_SAME_ORIGIN]: false, + [URL_SCHEME]: 'https', + [URL_FULL]: 'https://sentry-test-site.example/path/to/script.js', + // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status + // or render blocking status. + 'http.response_delivery_type': '', + ...(!isWebkitRun && { + [HTTP_RESPONSE_STATUS_CODE]: expect.any(Number), + [RESOURCE_RENDER_BLOCKING_STATUS]: 'non-blocking', + }), + }, + description: 'https://sentry-test-site.example/path/to/script.js', + op: RESOURCE_SCRIPT, + origin: 'auto.resource.browser.metrics', + status: 'ok', + parent_span_id: spanId, + span_id: expect.stringMatching(/^[a-f\d]{16}$/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/test.ts deleted file mode 100644 index a0a1e863137c..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { Route } from '@playwright/test'; -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, getSpansFromEnvelope, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; - -const assetsDir = `${__dirname}/../pageload-resource-spans/assets`; - -sentryTest('names streamed resource spans after the resource domain', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - // Intercepting asset requests to avoid network-related flakiness and random retries (on Firefox). - await page.route('https://sentry-test-site.example/path/to/image.svg', (route: Route) => - route.fulfill({ - path: `${assetsDir}/image.svg`, - headers: { - 'Timing-Allow-Origin': '*', - 'Content-Type': 'image/svg+xml', - }, - }), - ); - await page.route('https://sentry-test-site.example/path/to/script.js', (route: Route) => - route.fulfill({ - path: `${assetsDir}/script.js`, - headers: { - 'Timing-Allow-Origin': '*', - 'Content-Type': 'application/javascript', - }, - }), - ); - await page.route('https://sentry-test-site.example/path/to/style.css', (route: Route) => - route.fulfill({ - path: `${assetsDir}/style.css`, - headers: { - 'Timing-Allow-Origin': '*', - 'Content-Type': 'text/css', - }, - }), - ); - - const spanEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!getSpansFromEnvelope(env).find(s => getSpanOp(s) === 'resource.img'), - ); - - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - - const spans = getSpansFromEnvelope(await spanEnvelopePromise); - - const imgSpan = spans.find(s => getSpanOp(s) === 'resource.img'); - const linkSpan = spans.find(s => getSpanOp(s) === 'resource.link'); - - expect(imgSpan?.name).toBe('sentry-test-site.example'); - expect(imgSpan?.attributes['url.domain']).toEqual({ type: 'string', value: 'sentry-test-site.example' }); - expect(imgSpan?.attributes['url.full']).toEqual({ - type: 'string', - value: 'https://sentry-test-site.example/path/to/image.svg', - }); - - expect(linkSpan?.name).toBe('sentry-test-site.example'); - - // Same-origin resources used to be named by their origin-relative path, they now carry the test host. - const sameOriginScriptSpan = spans.find( - s => getSpanOp(s) === 'resource.script' && s.name !== 'sentry-test-site.example', - ); - expect(sameOriginScriptSpan?.name).toBe('sentry-test.io'); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/init.js similarity index 87% rename from dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/init.js index 5ab240338c8c..83076460599f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/init.js @@ -6,5 +6,4 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, - autoSessionTracking: false, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts index 276234385fae..f9b35fa67c6a 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts @@ -1,223 +1,291 @@ -import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { + URL_DOMAIN, + URL_FULL, + HTTP_REQUEST_SAME_ORIGIN, + HTTP_RESPONSE_BODY_DECODED_SIZE, + HTTP_RESPONSE_BODY_SIZE, + HTTP_RESPONSE_SIZE, + HTTP_REQUEST_CONNECT_START, + HTTP_REQUEST_CONNECTION_END, + HTTP_REQUEST_DOMAIN_LOOKUP_END, + HTTP_REQUEST_DOMAIN_LOOKUP_START, + HTTP_REQUEST_FETCH_START, + HTTP_REQUEST_REDIRECT_END, + HTTP_REQUEST_REDIRECT_START, + HTTP_REQUEST_REQUEST_START, + HTTP_REQUEST_SECURE_CONNECTION_START, + HTTP_REQUEST_WORKER_START, + HTTP_REQUEST_RESPONSE_END, + HTTP_REQUEST_RESPONSE_START, + HTTP_REQUEST_TIME_TO_FIRST_BYTE, + NETWORK_PROTOCOL_NAME, + NETWORK_PROTOCOL_VERSION, + SERVER_ADDRESS, + URL_SCHEME, + HTTP_RESPONSE_STATUS_CODE, + RESOURCE_RENDER_BLOCKING_STATUS, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import { type Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; -sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestUrl, page, browserName }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +const assetsDir = `${__dirname}/assets`; - const isWebkitRun = browserName === 'webkit'; +sentryTest( + 'names streamed resource spans after the resource domain', + async ({ getLocalTestUrl, page, browserName }) => { + sentryTest.skip(shouldSkipTracingTest()); - // Intercepting asset requests to avoid network-related flakiness and random retries (on Firefox). - await page.route('https://sentry-test-site.example/path/to/image.svg', (route: Route) => - route.fulfill({ - path: `${__dirname}/assets/image.svg`, - headers: { - 'Timing-Allow-Origin': '*', - 'Content-Type': 'image/svg+xml', - }, - }), - ); - await page.route('https://sentry-test-site.example/path/to/script.js', (route: Route) => - route.fulfill({ - path: `${__dirname}/assets/script.js`, - headers: { - 'Timing-Allow-Origin': '*', - 'Content-Type': 'application/javascript', - }, - }), - ); - await page.route('https://sentry-test-site.example/path/to/style.css', (route: Route) => - route.fulfill({ - path: `${__dirname}/assets/style.css`, - headers: { - 'Timing-Allow-Origin': '*', - 'Content-Type': 'text/css', - }, - }), - ); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const resourceSpans = eventData.spans?.filter(({ op }) => op?.startsWith('resource')); - - const scriptSpans = resourceSpans?.filter(({ op }) => op === 'resource.script'); - const linkSpan = resourceSpans?.filter(({ op }) => op === 'resource.link')[0]; - const imgSpan = resourceSpans?.filter(({ op }) => op === 'resource.img')[0]; - - const spanId = eventData.contexts?.trace?.span_id; - const traceId = eventData.contexts?.trace?.trace_id; - - expect(spanId).toBeDefined(); - expect(traceId).toBeDefined(); - - const hasCdnBundle = (process.env.PW_BUNDLE || '').startsWith('bundle'); - - const expectedScripts = ['/init.bundle.js', 'https://sentry-test-site.example/path/to/script.js']; - if (hasCdnBundle) { - expectedScripts.unshift('/cdn.bundle.js'); - } - - expect(scriptSpans?.map(({ description }) => description).sort()).toEqual(expectedScripts); - expect(scriptSpans?.map(({ parent_span_id }) => parent_span_id)).toEqual(expectedScripts.map(() => spanId)); + // Intercepting asset requests to avoid network-related flakiness and random retries (on Firefox). + await page.route('https://sentry-test-site.example/path/to/image.svg', (route: Route) => + route.fulfill({ + path: `${assetsDir}/image.svg`, + headers: { + 'Timing-Allow-Origin': '*', + 'Content-Type': 'image/svg+xml', + }, + }), + ); + await page.route('https://sentry-test-site.example/path/to/script.js', (route: Route) => + route.fulfill({ + path: `${assetsDir}/script.js`, + headers: { + 'Timing-Allow-Origin': '*', + 'Content-Type': 'application/javascript', + }, + }), + ); + await page.route('https://sentry-test-site.example/path/to/style.css', (route: Route) => + route.fulfill({ + path: `${assetsDir}/style.css`, + headers: { + 'Timing-Allow-Origin': '*', + 'Content-Type': 'text/css', + }, + }), + ); - // The init bundle script is served from the test origin: its description is origin-relative, - // but `url.full` retains the full absolute URL (needed for span description inference). - const sameOriginScriptSpan = scriptSpans?.find(({ description }) => description === '/init.bundle.js'); - expect(sameOriginScriptSpan?.data?.['http.request.same_origin']).toBe(true); - expect(sameOriginScriptSpan?.data?.['url.full']).toMatch(/^https?:\/\/.+\/init\.bundle\.js$/); + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + const pageload = await pageloadPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + const externalResources = spans.filter(span => span.attributes[URL_DOMAIN]?.value === 'sentry-test-site.example'); + expect(externalResources).toHaveLength(3); + externalResources.forEach(span => { + expect(span.parent_span_id).toBe(pageload.span_id); + expect(span.trace_id).toBe(pageload.trace_id); + }); - const customScriptSpan = scriptSpans?.find( - ({ description }) => description === 'https://sentry-test-site.example/path/to/script.js', - ); + const isWebkitRun = browserName === 'webkit'; + const resourceSpans = spans.filter(span => getSpanOp(span)?.startsWith('resource')); + const scriptSpans = resourceSpans.filter(span => getSpanOp(span) === 'resource.script'); + const imgSpan = resourceSpans.find(span => getSpanOp(span) === 'resource.img'); + const linkSpan = resourceSpans.find(span => getSpanOp(span) === 'resource.link'); + const customScriptSpan = scriptSpans.find( + span => span.attributes[URL_DOMAIN]?.value === 'sentry-test-site.example', + ); + const sameOriginScriptSpan = scriptSpans.find( + span => span.attributes[URL_FULL]?.value === new URL('init.bundle.js', url).href, + ); + const spanId = pageload.span_id; + const traceId = pageload.trace_id; + const expectedScripts = [new URL('init.bundle.js', url).href, 'https://sentry-test-site.example/path/to/script.js']; + if ((process.env.PW_BUNDLE || '').startsWith('bundle')) { + expectedScripts.push(new URL('cdn.bundle.js', url).href); + } + expect(scriptSpans.map(span => span.attributes[URL_FULL]?.value).sort()).toEqual(expectedScripts.sort()); + expect(scriptSpans.map(span => span.parent_span_id)).toEqual(expectedScripts.map(() => spanId)); + expect(sameOriginScriptSpan?.name).toBe('sentry-test.io'); + expect(sameOriginScriptSpan?.attributes[HTTP_REQUEST_SAME_ORIGIN]).toEqual({ type: 'boolean', value: true }); - expect(imgSpan).toEqual({ - data: { - 'http.response.body.decoded_size': expect.any(Number), - 'http.response.body.size': expect.any(Number), - 'http.response.size': expect.any(Number), - 'http.request.connect_start': expect.any(Number), - 'http.request.connection_end': expect.any(Number), - 'http.request.domain_lookup_end': expect.any(Number), - 'http.request.domain_lookup_start': expect.any(Number), - 'http.request.fetch_start': expect.any(Number), - 'http.request.redirect_end': expect.any(Number), - 'http.request.redirect_start': expect.any(Number), - 'http.request.request_start': expect.any(Number), - 'http.request.secure_connection_start': expect.any(Number), - 'http.request.worker_start': expect.any(Number), - 'http.request.response_end': expect.any(Number), - 'http.request.response_start': expect.any(Number), - 'http.request.time_to_first_byte': expect.any(Number), - 'network.protocol.name': '', - 'network.protocol.version': 'unknown', - [SENTRY_OP]: 'resource.img', - [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', - 'server.address': 'sentry-test-site.example', - 'url.domain': 'sentry-test-site.example', - 'http.request.same_origin': false, - 'url.scheme': 'https', - 'url.full': 'https://sentry-test-site.example/path/to/image.svg', - // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status - // or render blocking status. - 'http.response_delivery_type': '', - ...(!isWebkitRun && { - 'http.response.status_code': expect.any(Number), - 'resource.render_blocking_status': 'non-blocking', - }), - }, - description: 'https://sentry-test-site.example/path/to/image.svg', - op: 'resource.img', - origin: 'auto.resource.browser.metrics', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/^[a-f\d]{16}$/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, - }); + expect(imgSpan).toMatchObject({ + attributes: { + [HTTP_RESPONSE_BODY_DECODED_SIZE]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [HTTP_RESPONSE_BODY_SIZE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_RESPONSE_SIZE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_CONNECT_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_CONNECTION_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_DOMAIN_LOOKUP_END]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [HTTP_REQUEST_DOMAIN_LOOKUP_START]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [HTTP_REQUEST_FETCH_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_REDIRECT_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_REDIRECT_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_REQUEST_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_SECURE_CONNECTION_START]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [HTTP_REQUEST_WORKER_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_RESPONSE_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_RESPONSE_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_TIME_TO_FIRST_BYTE]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [NETWORK_PROTOCOL_NAME]: { type: 'string', value: '' }, + [NETWORK_PROTOCOL_VERSION]: { type: 'string', value: 'unknown' }, + [SENTRY_OP]: { type: 'string', value: 'resource.img' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.resource.browser.metrics' }, + [SERVER_ADDRESS]: { type: 'string', value: 'sentry-test-site.example' }, + [URL_DOMAIN]: { type: 'string', value: 'sentry-test-site.example' }, + [HTTP_REQUEST_SAME_ORIGIN]: { type: 'boolean', value: false }, + [URL_SCHEME]: { type: 'string', value: 'https' }, + [URL_FULL]: { type: 'string', value: 'https://sentry-test-site.example/path/to/image.svg' }, + // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status + // or render blocking status. + 'http.response_delivery_type': { type: 'string', value: '' }, + ...(!isWebkitRun && { + [HTTP_RESPONSE_STATUS_CODE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [RESOURCE_RENDER_BLOCKING_STATUS]: { type: 'string', value: 'non-blocking' }, + }), + }, + name: 'sentry-test-site.example', + status: 'ok', + parent_span_id: spanId, + span_id: expect.stringMatching(/^[a-f\d]{16}$/), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + trace_id: traceId, + }); - // range check: TTFB is reasonably <10 seconds, which is really a check that we report it in - // seconds rather than milliseconds. WebKit resolves these intercepted routes without measurable - // delay, so only the other engines are held to a non-zero value. - const imgSpanTtfb = imgSpan?.data['http.request.time_to_first_byte']; - expect(imgSpanTtfb).toBeGreaterThan(isWebkitRun ? -1 : 0); - expect(imgSpanTtfb).toBeLessThan(10); + // range check: TTFB is reasonably <10 seconds, which is really a check that we report it in + // seconds rather than milliseconds. WebKit resolves these intercepted routes without measurable + // delay, so only the other engines are held to a non-zero value. + const imgSpanTtfb = imgSpan?.attributes[HTTP_REQUEST_TIME_TO_FIRST_BYTE]?.value; + expect(imgSpanTtfb).toBeGreaterThan(isWebkitRun ? -1 : 0); + expect(imgSpanTtfb).toBeLessThan(10); - expect(linkSpan).toEqual({ - data: { - 'http.response.body.decoded_size': expect.any(Number), - 'http.response.body.size': expect.any(Number), - 'http.response.size': expect.any(Number), - 'http.request.connect_start': expect.any(Number), - 'http.request.connection_end': expect.any(Number), - 'http.request.domain_lookup_end': expect.any(Number), - 'http.request.domain_lookup_start': expect.any(Number), - 'http.request.fetch_start': expect.any(Number), - 'http.request.redirect_end': expect.any(Number), - 'http.request.redirect_start': expect.any(Number), - 'http.request.request_start': expect.any(Number), - 'http.request.secure_connection_start': expect.any(Number), - 'http.request.worker_start': expect.any(Number), - 'http.request.response_end': expect.any(Number), - 'http.request.response_start': expect.any(Number), - 'http.request.time_to_first_byte': expect.any(Number), - 'network.protocol.name': '', - 'network.protocol.version': 'unknown', - [SENTRY_OP]: 'resource.link', - [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', - 'server.address': 'sentry-test-site.example', - 'url.domain': 'sentry-test-site.example', - 'http.request.same_origin': false, - 'url.scheme': 'https', - 'url.full': 'https://sentry-test-site.example/path/to/style.css', - // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status - // or render blocking status. - 'http.response_delivery_type': '', - ...(!isWebkitRun && { - 'http.response.status_code': expect.any(Number), - 'resource.render_blocking_status': 'non-blocking', - }), - }, - description: 'https://sentry-test-site.example/path/to/style.css', - op: 'resource.link', - origin: 'auto.resource.browser.metrics', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/^[a-f\d]{16}$/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, - }); + expect(linkSpan).toMatchObject({ + attributes: { + [HTTP_RESPONSE_BODY_DECODED_SIZE]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [HTTP_RESPONSE_BODY_SIZE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_RESPONSE_SIZE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_CONNECT_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_CONNECTION_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_DOMAIN_LOOKUP_END]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [HTTP_REQUEST_DOMAIN_LOOKUP_START]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [HTTP_REQUEST_FETCH_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_REDIRECT_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_REDIRECT_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_REQUEST_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_SECURE_CONNECTION_START]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [HTTP_REQUEST_WORKER_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_RESPONSE_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_RESPONSE_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_TIME_TO_FIRST_BYTE]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [NETWORK_PROTOCOL_NAME]: { type: 'string', value: '' }, + [NETWORK_PROTOCOL_VERSION]: { type: 'string', value: 'unknown' }, + [SENTRY_OP]: { type: 'string', value: 'resource.link' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.resource.browser.metrics' }, + [SERVER_ADDRESS]: { type: 'string', value: 'sentry-test-site.example' }, + [URL_DOMAIN]: { type: 'string', value: 'sentry-test-site.example' }, + [HTTP_REQUEST_SAME_ORIGIN]: { type: 'boolean', value: false }, + [URL_SCHEME]: { type: 'string', value: 'https' }, + [URL_FULL]: { type: 'string', value: 'https://sentry-test-site.example/path/to/style.css' }, + // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status + // or render blocking status. + 'http.response_delivery_type': { type: 'string', value: '' }, + ...(!isWebkitRun && { + [HTTP_RESPONSE_STATUS_CODE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [RESOURCE_RENDER_BLOCKING_STATUS]: { type: 'string', value: 'non-blocking' }, + }), + }, + name: 'sentry-test-site.example', + status: 'ok', + parent_span_id: spanId, + span_id: expect.stringMatching(/^[a-f\d]{16}$/), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + trace_id: traceId, + }); - expect(customScriptSpan).toEqual({ - data: { - 'http.response.body.decoded_size': expect.any(Number), - 'http.response.body.size': expect.any(Number), - 'http.response.size': expect.any(Number), - 'http.request.connection_end': expect.any(Number), - 'http.request.connect_start': expect.any(Number), - 'http.request.domain_lookup_end': expect.any(Number), - 'http.request.domain_lookup_start': expect.any(Number), - 'http.request.fetch_start': expect.any(Number), - 'http.request.redirect_end': expect.any(Number), - 'http.request.redirect_start': expect.any(Number), - 'http.request.request_start': expect.any(Number), - 'http.request.secure_connection_start': expect.any(Number), - 'http.request.worker_start': expect.any(Number), - 'http.request.response_end': expect.any(Number), - 'http.request.response_start': expect.any(Number), - 'http.request.time_to_first_byte': expect.any(Number), - 'network.protocol.name': '', - 'network.protocol.version': 'unknown', - 'sentry.op': 'resource.script', - 'sentry.origin': 'auto.resource.browser.metrics', - 'server.address': 'sentry-test-site.example', - 'url.domain': 'sentry-test-site.example', - 'http.request.same_origin': false, - 'url.scheme': 'https', - 'url.full': 'https://sentry-test-site.example/path/to/script.js', - // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status - // or render blocking status. - 'http.response_delivery_type': '', - ...(!isWebkitRun && { - 'http.response.status_code': expect.any(Number), - 'resource.render_blocking_status': 'non-blocking', - }), - }, - description: 'https://sentry-test-site.example/path/to/script.js', - op: 'resource.script', - origin: 'auto.resource.browser.metrics', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/^[a-f\d]{16}$/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, - }); -}); + expect(customScriptSpan).toMatchObject({ + attributes: { + [HTTP_RESPONSE_BODY_DECODED_SIZE]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [HTTP_RESPONSE_BODY_SIZE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_RESPONSE_SIZE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_CONNECTION_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_CONNECT_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_DOMAIN_LOOKUP_END]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [HTTP_REQUEST_DOMAIN_LOOKUP_START]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [HTTP_REQUEST_FETCH_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_REDIRECT_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_REDIRECT_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_REQUEST_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_SECURE_CONNECTION_START]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [HTTP_REQUEST_WORKER_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_RESPONSE_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_RESPONSE_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_TIME_TO_FIRST_BYTE]: { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + [NETWORK_PROTOCOL_NAME]: { type: 'string', value: '' }, + [NETWORK_PROTOCOL_VERSION]: { type: 'string', value: 'unknown' }, + [SENTRY_OP]: { type: 'string', value: 'resource.script' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.resource.browser.metrics' }, + [SERVER_ADDRESS]: { type: 'string', value: 'sentry-test-site.example' }, + [URL_DOMAIN]: { type: 'string', value: 'sentry-test-site.example' }, + [HTTP_REQUEST_SAME_ORIGIN]: { type: 'boolean', value: false }, + [URL_SCHEME]: { type: 'string', value: 'https' }, + [URL_FULL]: { type: 'string', value: 'https://sentry-test-site.example/path/to/script.js' }, + // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status + // or render blocking status. + 'http.response_delivery_type': { type: 'string', value: '' }, + ...(!isWebkitRun && { + [HTTP_RESPONSE_STATUS_CODE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [RESOURCE_RENDER_BLOCKING_STATUS]: { type: 'string', value: 'non-blocking' }, + }), + }, + name: 'sentry-test-site.example', + status: 'ok', + parent_span_id: spanId, + span_id: expect.stringMatching(/^[a-f\d]{16}$/), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + trace_id: traceId, + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/init.js similarity index 72% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/init.js rename to dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/init.js index 8be8d8420f85..38429010914d 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/init.js @@ -1,15 +1,14 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -window._testBaseTimestamp = performance.timeOrigin / 1000; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ idleTimeout: 5000, }), - Sentry.spanStreamingIntegration(), ], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/subject.js new file mode 100644 index 000000000000..b33aea72d7c9 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/subject.js @@ -0,0 +1,9 @@ +import { simulateCLS } from '../../../../utils/web-vitals/cls.ts'; + +// Getting expected CLS parameter from URL hash +const expectedCLS = Number(location.hash.slice(1)); + +// CLS lands on the pageload span when it ends on the idle timeout, so nothing has to force the +// page away to finalize it. Reloading here used to do that, but it raced the envelope: on a fast +// browser the reload cancelled the in-flight send before it left the page. +simulateCLS(expectedCLS); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/template.html similarity index 84% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/template.html rename to dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/template.html index 10e2e22f7d6a..72c22be8e49a 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/template.html +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/template.html @@ -5,6 +5,5 @@
-

Some content

diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/test.ts new file mode 100644 index 000000000000..c51b0c98687c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/test.ts @@ -0,0 +1,29 @@ +import { PAGELOAD } from '@sentry/conventions/op'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest.beforeEach(async ({ browserName, page }) => { + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + await page.setViewportSize({ width: 800, height: 1200 }); +}); + +sentryTest('records CLS as a pageload measurement with its source', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + const pageloadPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === PAGELOAD); + await page.goto(`${url}#0.05`); + const eventData = envelopeRequestParser(await pageloadPromise); + + expect(eventData.measurements).toBeDefined(); + expect(eventData.measurements?.cls?.value).toBeDefined(); + + // Flakey value dependent on timings -> we check for a range + expect(eventData.measurements?.cls?.value).toBeGreaterThan(0.03); + expect(eventData.measurements?.cls?.value).toBeLessThan(0.07); + + expect(eventData.contexts?.trace?.data?.['cls.source.1']).toBe('body > div#content > p#partial'); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/subject.js deleted file mode 100644 index 9742a4a5cc29..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/subject.js +++ /dev/null @@ -1,17 +0,0 @@ -import { simulateCLS } from '../../../../utils/web-vitals/cls.ts'; - -// Simulate Layout shift right at the beginning of the page load, depending on the URL hash -// don't run if expected CLS is NaN -const expectedCLS = Number(location.hash.slice(1)); -if (expectedCLS && expectedCLS >= 0) { - simulateCLS(expectedCLS).then(() => window.dispatchEvent(new Event('cls-done'))); -} - -// Simulate layout shift whenever the trigger-cls event is dispatched -// Cannot trigger via a button click because expected layout shift after -// an interaction doesn't contribute to CLS. -window.addEventListener('trigger-cls', () => { - simulateCLS(0.1).then(() => { - window.dispatchEvent(new Event('cls-done')); - }); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/test.ts deleted file mode 100644 index 02ce7b981ed9..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { Page } from '@playwright/test'; -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; - -sentryTest.beforeEach(async ({ browserName, page }) => { - if (shouldSkipTracingTest() || browserName !== 'chromium') { - sentryTest.skip(); - } - - await page.setViewportSize({ width: 800, height: 1200 }); -}); - -function waitForLayoutShift(page: Page): Promise { - return page.evaluate(() => { - return new Promise(resolve => { - window.addEventListener('cls-done', () => resolve()); - }); - }); -} - -sentryTest('captures CLS as a streamed span with source attributes', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - - const clsSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.webvital.cls'); - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - - await page.goto(`${url}#0.15`); - await waitForLayoutShift(page); - await hidePage(page); - - const clsSpan = await clsSpanPromise; - const pageloadSpan = await pageloadSpanPromise; - - expect(clsSpan.attributes['sentry.op']).toEqual({ type: 'string', value: 'ui.webvital.cls' }); - expect(clsSpan.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.browser.cls' }); - expect(clsSpan.attributes['sentry.exclusive_time']).toEqual({ type: 'integer', value: 0 }); - expect(clsSpan.attributes['user_agent.original']?.value).toEqual(expect.stringContaining('Chrome')); - - // Check the CLS span carries the segment name it belongs to - expect(clsSpan.attributes['sentry.segment.name']).toEqual({ type: 'string', value: 'Pageload' }); - - // Check browser.web_vital.cls.source attributes - expect(clsSpan.attributes['browser.web_vital.cls.source.1']?.value).toEqual( - expect.stringContaining('body > div#content > p'), - ); - - // Check pageload span id is present - expect(clsSpan.attributes['sentry.pageload.span_id']?.value).toBe(pageloadSpan.span_id); - - // CLS is a point-in-time metric - expect(clsSpan.start_timestamp).toEqual(clsSpan.end_timestamp); - - expect(clsSpan.span_id).toMatch(/^[\da-f]{16}$/); - expect(clsSpan.trace_id).toMatch(/^[\da-f]{32}$/); - - expect(clsSpan.parent_span_id).toBe(pageloadSpan.span_id); - expect(clsSpan.trace_id).toBe(pageloadSpan.trace_id); -}); - -sentryTest('CLS streamed span has web vital value attribute', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - - const clsSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.webvital.cls'); - - await page.goto(`${url}#0.1`); - await waitForLayoutShift(page); - await hidePage(page); - - const clsSpan = await clsSpanPromise; - - // The CLS value should be set as a browser.web_vital.cls.value attribute - expect(clsSpan.attributes['browser.web_vital.cls.value']?.type).toBe('double'); - // Flakey value dependent on timings -> we check for a range - const clsValue = clsSpan.attributes['browser.web_vital.cls.value']?.value as number; - expect(clsValue).toBeGreaterThan(0.05); - expect(clsValue).toBeLessThan(0.15); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/init.js index 38429010914d..1f0b64911a75 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/init.js @@ -1,14 +1,10 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; +window._testBaseTimestamp = performance.timeOrigin / 1000; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [ - Sentry.browserTracingIntegration({ - idleTimeout: 5000, - }), - ], + integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/subject.js index b33aea72d7c9..9742a4a5cc29 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/subject.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/subject.js @@ -1,9 +1,17 @@ import { simulateCLS } from '../../../../utils/web-vitals/cls.ts'; -// Getting expected CLS parameter from URL hash +// Simulate Layout shift right at the beginning of the page load, depending on the URL hash +// don't run if expected CLS is NaN const expectedCLS = Number(location.hash.slice(1)); +if (expectedCLS && expectedCLS >= 0) { + simulateCLS(expectedCLS).then(() => window.dispatchEvent(new Event('cls-done'))); +} -// CLS lands on the pageload span when it ends on the idle timeout, so nothing has to force the -// page away to finalize it. Reloading here used to do that, but it raced the envelope: on a fast -// browser the reload cancelled the in-flight send before it left the page. -simulateCLS(expectedCLS); +// Simulate layout shift whenever the trigger-cls event is dispatched +// Cannot trigger via a button click because expected layout shift after +// an interaction doesn't contribute to CLS. +window.addEventListener('trigger-cls', () => { + simulateCLS(0.1).then(() => { + window.dispatchEvent(new Event('cls-done')); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/template.html index 72c22be8e49a..10e2e22f7d6a 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/template.html +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/template.html @@ -5,5 +5,6 @@
+

Some content

diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/test.ts index f00a2b60f100..22e494e73447 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/test.ts @@ -1,7 +1,17 @@ +import { + SENTRY_OP, + SENTRY_ORIGIN, + SENTRY_EXCLUSIVE_TIME, + USER_AGENT_ORIGINAL, + SENTRY_SEGMENT_NAME, + SENTRY_PAGELOAD_SPAN_ID, + BROWSER_WEB_VITAL_CLS_VALUE, +} from '@sentry/conventions/attributes'; +import type { Page } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest.beforeEach(async ({ browserName, page }) => { if (shouldSkipTracingTest() || browserName !== 'chromium') { @@ -11,43 +21,70 @@ sentryTest.beforeEach(async ({ browserName, page }) => { await page.setViewportSize({ width: 800, height: 1200 }); }); -sentryTest('should capture a "GOOD" CLS vital with its source(s).', async ({ getLocalTestUrl, page }) => { +function waitForLayoutShift(page: Page): Promise { + return page.evaluate(() => { + return new Promise(resolve => { + window.addEventListener('cls-done', () => resolve()); + }); + }); +} + +sentryTest('captures CLS as a streamed span with source attributes', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, `${url}#0.05`); - expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.cls?.value).toBeDefined(); + const clsSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.webvital.cls'); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - // Flakey value dependent on timings -> we check for a range - expect(eventData.measurements?.cls?.value).toBeGreaterThan(0.03); - expect(eventData.measurements?.cls?.value).toBeLessThan(0.07); + await page.goto(`${url}#0.15`); + await waitForLayoutShift(page); + await hidePage(page); - expect(eventData.contexts?.trace?.data?.['cls.source.1']).toBe('body > div#content > p#partial'); -}); + const clsSpan = await clsSpanPromise; + const pageloadSpan = await pageloadSpanPromise; -sentryTest('should capture a "MEH" CLS vital with its source(s).', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, `${url}#0.21`); + expect(clsSpan.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'ui.webvital.cls' }); + expect(clsSpan.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.http.browser.cls' }); + expect(clsSpan.attributes[SENTRY_EXCLUSIVE_TIME]).toEqual({ type: 'integer', value: 0 }); + expect(clsSpan.attributes[USER_AGENT_ORIGINAL]?.value).toEqual(expect.stringContaining('Chrome')); - expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.cls?.value).toBeDefined(); + // Check the CLS span carries the segment name it belongs to + expect(clsSpan.attributes[SENTRY_SEGMENT_NAME]).toEqual({ type: 'string', value: 'Pageload' }); - // Flakey value dependent on timings -> we check for a range - expect(eventData.measurements?.cls?.value).toBeGreaterThan(0.18); - expect(eventData.measurements?.cls?.value).toBeLessThan(0.23); + // Check browser.web_vital.cls.source attributes + expect(clsSpan.attributes['browser.web_vital.cls.source.1']?.value).toEqual( + expect.stringContaining('body > div#content > p'), + ); - expect(eventData.contexts?.trace?.data?.['cls.source.1']).toBe('body > div#content > p'); -}); + // Check pageload span id is present + expect(clsSpan.attributes[SENTRY_PAGELOAD_SPAN_ID]?.value).toBe(pageloadSpan.span_id); -sentryTest('should capture a "POOR" CLS vital with its source(s).', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, `${url}#0.35`); + // CLS is a point-in-time metric + expect(clsSpan.start_timestamp).toEqual(clsSpan.end_timestamp); + + expect(clsSpan.span_id).toMatch(/^[\da-f]{16}$/); + expect(clsSpan.trace_id).toMatch(/^[\da-f]{32}$/); + + expect(clsSpan.parent_span_id).toBe(pageloadSpan.span_id); + expect(clsSpan.trace_id).toBe(pageloadSpan.trace_id); +}); - expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.cls?.value).toBeDefined(); +[ + { cls: 0.05, min: 0.03, max: 0.07, source: 'body > div#content > p#partial' }, + { cls: 0.21, min: 0.18, max: 0.23, source: 'body > div#content > p' }, + { cls: 0.35, min: 0.34, max: 0.36, source: 'body > div#content > p' }, +].forEach(({ cls, min, max, source }) => { + sentryTest(`captures CLS ${cls} with its source`, async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + const clsPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.webvital.cls'); + await page.goto(`${url}#${cls}`); + await waitForLayoutShift(page); + await hidePage(page); + const span = await clsPromise; - // Flakey value dependent on timings -> we check for a range - expect(eventData.measurements?.cls?.value).toBeGreaterThan(0.34); - expect(eventData.measurements?.cls?.value).toBeLessThan(0.36); - expect(eventData.contexts?.trace?.data?.['cls.source.1']).toBe('body > div#content > p'); + expect(span.attributes[BROWSER_WEB_VITAL_CLS_VALUE]?.type).toBe('double'); + const value = span.attributes[BROWSER_WEB_VITAL_CLS_VALUE]?.value; + expect(value).toBeGreaterThan(min); + expect(value).toBeLessThan(max); + expect(span.attributes['browser.web_vital.cls.source.1']).toEqual({ type: 'string', value: source }); + }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp-static/init.js similarity index 57% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp-static/init.js index 4deba4c452dd..bb6d4918d1bf 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp-static/init.js @@ -3,8 +3,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ + traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()], - traceLifecycle: 'stream', + integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp-static/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp-static/template.html new file mode 100644 index 000000000000..b81f11e967e3 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp-static/template.html @@ -0,0 +1,9 @@ + + + + + + +
Rendered
+ + diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp-static/test.ts new file mode 100644 index 000000000000..6148701b53ba --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp-static/test.ts @@ -0,0 +1,46 @@ +import { PAGELOAD } from '@sentry/conventions/op'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest('should capture FP vital.', async ({ browserName, getLocalTestUrl, page }) => { + // FP is not generated on webkit or firefox + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + const pageloadPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === PAGELOAD); + await page.goto(url); + const eventData = envelopeRequestParser(await pageloadPromise); + + expect(eventData.measurements).toBeDefined(); + expect(eventData.measurements?.fp?.value).toBeDefined(); + + const fpSpan = eventData.spans?.filter(({ description }) => description === 'first-paint')[0]; + + expect(fpSpan).toBeDefined(); + expect(fpSpan?.op).toBe('browser.paint'); + expect(fpSpan?.parent_span_id).toBe(eventData.contexts?.trace?.span_id); +}); + +sentryTest('should capture FCP vital.', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + const pageloadPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === PAGELOAD); + await page.goto(url); + const eventData = envelopeRequestParser(await pageloadPromise); + + expect(eventData.measurements).toBeDefined(); + expect(eventData.measurements?.fcp?.value).toBeDefined(); + + const fcpSpan = eventData.spans?.filter(({ description }) => description === 'first-contentful-paint')[0]; + + expect(fcpSpan).toBeDefined(); + expect(fcpSpan?.op).toBe('browser.paint'); + expect(fcpSpan?.parent_span_id).toBe(eventData.contexts?.trace?.span_id); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts index cd63b699cc06..f3769c45992e 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts @@ -1,41 +1,28 @@ +import { PAGELOAD, BROWSER_PAINT } from '@sentry/conventions/op'; +import { BROWSER_WEB_VITAL_FP_VALUE, BROWSER_WEB_VITAL_FCP_VALUE } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; - -sentryTest('should capture FP vital.', async ({ browserName, getLocalTestUrl, page }) => { - // FP is not generated on webkit or firefox - if (shouldSkipTracingTest() || browserName !== 'chromium') { - sentryTest.skip(); - } - - const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.fp?.value).toBeDefined(); - - const fpSpan = eventData.spans?.filter(({ description }) => description === 'first-paint')[0]; - - expect(fpSpan).toBeDefined(); - expect(fpSpan?.op).toBe('browser.paint'); - expect(fpSpan?.parent_span_id).toBe(eventData.contexts?.trace?.span_id); -}); - -sentryTest('should capture FCP vital.', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.fcp?.value).toBeDefined(); - - const fcpSpan = eventData.spans?.filter(({ description }) => description === 'first-contentful-paint')[0]; - - expect(fcpSpan).toBeDefined(); - expect(fcpSpan?.op).toBe('browser.paint'); - expect(fcpSpan?.parent_span_id).toBe(eventData.contexts?.trace?.span_id); +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; + +[ + { vital: 'fp', attribute: BROWSER_WEB_VITAL_FP_VALUE, name: 'first-paint', chromiumOnly: true }, + { vital: 'fcp', attribute: BROWSER_WEB_VITAL_FCP_VALUE, name: 'first-contentful-paint', chromiumOnly: false }, +].forEach(({ vital, attribute, name, chromiumOnly }) => { + sentryTest(`captures ${vital.toUpperCase()} and its paint span`, async ({ getLocalTestUrl, page, browserName }) => { + sentryTest.skip(shouldSkipTracingTest() || (chromiumOnly && browserName !== 'chromium')); + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === PAGELOAD); + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + const pageload = await pageloadPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + + expect(pageload.attributes[attribute]?.value).toBeGreaterThan(0); + const paintSpans = spans.filter(span => span.name === name); + expect(paintSpans).toHaveLength(1); + expect(getSpanOp(paintSpans[0])).toBe(BROWSER_PAINT); + expect(paintSpans[0].parent_span_id).toBe(pageload.span_id); + expect(paintSpans[0].trace_id).toBe(pageload.trace_id); + }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/init.js new file mode 100644 index 000000000000..5dd891ccc2f8 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/init.js @@ -0,0 +1,39 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + idleTimeout: 4000, + enableLongTask: false, + enableInp: true, + instrumentPageLoad: false, + instrumentNavigation: false, + }), + ], + tracesSampleRate: 1, + // A plain (non-streamed) `beforeSendSpan` operates on the v1 `SpanJSON`. INP is sent as a v2 span, + // so this verifies the static callback still runs and its changes are carried into the v2 span. + beforeSendSpan: Sentry.withStaticSpan(span => { + if (span.op === 'ui.interaction.click') { + span.description = 'scrubbed'; + span.data['custom.attribute'] = 'from-before-send-span'; + } + + return span; + }), + debug: true, +}); + +const client = Sentry.getClient(); + +// Force page load transaction name to a testable value +Sentry.startBrowserTracingPageLoadSpan(client, { + name: 'test-url', + attributes: { + ['sentry.segment.name.source']: 'url', + }, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/subject.js similarity index 70% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/subject.js index f4ea1cd46d67..64524952dfa7 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/subject.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/subject.js @@ -3,17 +3,18 @@ const blockUI = e => { const startTime = Date.now(); - function getElapsed() { + function getElasped() { const time = Date.now(); return time - startTime; } - while (getElapsed() < delay) { + while (getElasped() < delay) { // } e.target.classList.add('clicked'); }; +document.querySelector('[data-test-id=not-so-slow-button]').addEventListener('click', blockUI(300)); document.querySelector('[data-test-id=slow-button]').addEventListener('click', blockUI(450)); document.querySelector('[data-test-id=normal-button]').addEventListener('click', blockUI()); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/template.html similarity index 65% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/template.html rename to dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/template.html index d5f28c7c8847..437426e9ab01 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/template.html +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/template.html @@ -4,7 +4,9 @@ +
Rendered Before Long Task
+ diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/test.ts new file mode 100644 index 000000000000..6368c5df996f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/test.ts @@ -0,0 +1,48 @@ +import { UI_INTERACTION_CLICK } from '@sentry/conventions/op'; +import { BROWSER_WEB_VITAL_INP_VALUE } from '@sentry/conventions/attributes'; +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { + collectStreamedSpans, + getSpanOp, + getSpansFromEnvelope, + waitForStreamedSpanEnvelope, +} from '../../../../utils/spanUtils'; + +sentryTest('runs a static `beforeSendSpan` for the INP span', async ({ browserName, getLocalTestUrl, page }) => { + const supportedBrowsers = ['chromium']; + + if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { + sentryTest.skip(); + } + + const spans = collectStreamedSpans(page); + const url = await getLocalTestUrl({ testDir: __dirname }); + + const spanEnvelopePromise = waitForStreamedSpanEnvelope(page, env => + getSpansFromEnvelope(env).some(span => getSpanOp(span) === UI_INTERACTION_CLICK), + ); + + await page.goto(url); + + await page.locator('[data-test-id=normal-button]').click(); + await expect(page.locator('.clicked[data-test-id=normal-button]')).toBeVisible(); + + // Page hide to trigger INP + await hidePage(page); + + await spanEnvelopePromise; + await page.evaluate(() => (window as any).Sentry.flush()); + const inpSpans = spans.filter(span => getSpanOp(span) === UI_INTERACTION_CLICK); + expect(inpSpans).toHaveLength(1); + const [inpSpan] = inpSpans; + + // The callback rewrote the name and added a custom attribute. + expect(inpSpan.name).toBe('scrubbed'); + expect(inpSpan.attributes['custom.attribute']).toEqual({ value: 'from-before-send-span', type: 'string' }); + + // The span is still a valid v2 INP span carrying its web vital value. + const inpValue = inpSpan.attributes[BROWSER_WEB_VITAL_INP_VALUE]?.value as number; + expect(inpValue).toBeGreaterThan(0); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/init.js index 5dd891ccc2f8..b320fb9f5984 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ @@ -15,16 +14,14 @@ Sentry.init({ }), ], tracesSampleRate: 1, - // A plain (non-streamed) `beforeSendSpan` operates on the v1 `SpanJSON`. INP is sent as a v2 span, - // so this verifies the static callback still runs and its changes are carried into the v2 span. - beforeSendSpan: Sentry.withStaticSpan(span => { - if (span.op === 'ui.interaction.click') { - span.description = 'scrubbed'; - span.data['custom.attribute'] = 'from-before-send-span'; + beforeSendSpan: span => { + if (span.attributes['sentry.op'] === 'ui.interaction.click') { + span.name = 'scrubbed'; + span.attributes['custom.attribute'] = 'from-before-send-span'; } return span; - }), + }, debug: true, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/test.ts index add8a0ac71c3..e65c3e9b90de 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/test.ts @@ -1,44 +1,48 @@ +import { UI_INTERACTION_CLICK } from '@sentry/conventions/op'; +import { BROWSER_WEB_VITAL_INP_VALUE } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, getSpansFromEnvelope, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; - -// This app does not enable span streaming (`traceLifecycle: 'static'`) and defines a plain, non-streamed -// `beforeSendSpan` callback (operating on the v1 `SpanJSON`). INP is still emitted as a v2 span, so this -// verifies the static callback runs for INP and its modifications are carried into the v2 span. - -sentryTest('runs a non-streamed `beforeSendSpan` for the INP span', async ({ browserName, getLocalTestUrl, page }) => { +import { + collectStreamedSpans, + getSpanOp, + getSpansFromEnvelope, + waitForStreamedSpanEnvelope, +} from '../../../../utils/spanUtils'; + +sentryTest('runs `beforeSendSpan` for the streamed INP span', async ({ browserName, getLocalTestUrl, page }) => { const supportedBrowsers = ['chromium']; if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { sentryTest.skip(); } + const spans = collectStreamedSpans(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const spanEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!getSpansFromEnvelope(env).find(s => getSpanOp(s) === 'ui.interaction.click'), + const spanEnvelopePromise = waitForStreamedSpanEnvelope(page, env => + getSpansFromEnvelope(env).some(span => getSpanOp(span) === UI_INTERACTION_CLICK), ); await page.goto(url); await page.locator('[data-test-id=normal-button]').click(); - await page.locator('.clicked[data-test-id=normal-button]').isVisible(); - - await page.waitForTimeout(500); + await expect(page.locator('.clicked[data-test-id=normal-button]')).toBeVisible(); // Page hide to trigger INP await hidePage(page); - const spanEnvelope = await spanEnvelopePromise; - const inpSpan = getSpansFromEnvelope(spanEnvelope).find(s => getSpanOp(s) === 'ui.interaction.click')!; + await spanEnvelopePromise; + await page.evaluate(() => (window as any).Sentry.flush()); + const inpSpans = spans.filter(span => getSpanOp(span) === UI_INTERACTION_CLICK); + expect(inpSpans).toHaveLength(1); + const [inpSpan] = inpSpans; // The callback rewrote the name and added a custom attribute. expect(inpSpan.name).toBe('scrubbed'); expect(inpSpan.attributes['custom.attribute']).toEqual({ value: 'from-before-send-span', type: 'string' }); // The span is still a valid v2 INP span carrying its web vital value. - const inpValue = inpSpan.attributes['browser.web_vital.inp.value']?.value as number; + const inpValue = inpSpan.attributes[BROWSER_WEB_VITAL_INP_VALUE]?.value as number; expect(inpValue).toBeGreaterThan(0); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/init.js index 398edbd14fe2..9ae2938e2969 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts index 10073a27e212..63f6f5ca17a6 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts @@ -1,8 +1,34 @@ +import { UI_INTERACTION_CLICK } from '@sentry/conventions/op'; +import { + BROWSER_WEB_VITAL_INP_VALUE, + SENTRY_IS_LOCALHOST, + SENTRY_ORIGIN, + SENTRY_OP, + UI_COMPONENT_NAME, + BROWSER_WEB_VITAL_INP_TARGET, + SENTRY_EXCLUSIVE_TIME, + BROWSER_WEB_VITAL_INP_INTERACTION_TYPE, + BROWSER_NAVIGATION_TYPE, + SENTRY_TRANSACTION, + SENTRY_SEGMENT_NAME, + USER_AGENT_ORIGINAL, + SENTRY_PAGELOAD_SPAN_ID, + SENTRY_TRACE_LIFECYCLE, + SENTRY_SEGMENT_ID, + SENTRY_SDK_NAME, + SENTRY_SDK_VERSION, + SENTRY_ENVIRONMENT, +} from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { SDK_VERSION } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, getSpansFromEnvelope, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; +import { + collectStreamedSpans, + getSpanOp, + getSpansFromEnvelope, + waitForStreamedSpanEnvelope, +} from '../../../../utils/spanUtils'; sentryTest( 'captures an INP click as a streamed span after pageload', @@ -13,19 +39,17 @@ sentryTest( sentryTest.skip(); } + const spans = collectStreamedSpans(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const spanEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!getSpansFromEnvelope(env).find(s => getSpanOp(s) === 'ui.interaction.click'), + const spanEnvelopePromise = waitForStreamedSpanEnvelope(page, env => + getSpansFromEnvelope(env).some(span => getSpanOp(span) === UI_INTERACTION_CLICK), ); await page.goto(url); await page.locator('[data-test-id=normal-button]').click(); - await page.locator('.clicked[data-test-id=normal-button]').isVisible(); - - await page.waitForTimeout(500); + await expect(page.locator('.clicked[data-test-id=normal-button]')).toBeVisible(); // Page hide to trigger INP await hidePage(page); @@ -33,7 +57,10 @@ sentryTest( const spanEnvelope = await spanEnvelopePromise; const envelopeHeader = spanEnvelope[0]; const itemHeader = spanEnvelope[1][0][0]; - const inpSpan = getSpansFromEnvelope(spanEnvelope).find(s => getSpanOp(s) === 'ui.interaction.click')!; + await page.evaluate(() => (window as any).Sentry.flush()); + const inpSpans = spans.filter(span => getSpanOp(span) === UI_INTERACTION_CLICK); + expect(inpSpans).toHaveLength(1); + const [inpSpan] = inpSpans; const traceId = envelopeHeader.trace!.trace_id; expect(traceId).toMatch(/^[\da-f]{32}$/); @@ -54,17 +81,17 @@ sentryTest( expect(itemHeader).toEqual({ type: 'span', - item_count: 1, + item_count: getSpansFromEnvelope(spanEnvelope).length, content_type: 'application/vnd.sentry.items.span.v2+json', }); - const inpValue = inpSpan.attributes['browser.web_vital.inp.value']?.value as number; + const inpValue = inpSpan.attributes[BROWSER_WEB_VITAL_INP_VALUE]?.value as number; expect(inpValue).toBeGreaterThan(0); const pageloadSpanId = inpSpan.parent_span_id; expect(inpSpan).toEqual({ - name: 'body > NormalButton', + name: 'NormalButton', span_id: expect.stringMatching(/^[\da-f]{16}$/), trace_id: traceId, parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), @@ -73,24 +100,24 @@ sentryTest( is_segment: false, status: 'ok', attributes: { - 'sentry.is_localhost': { value: false, type: 'boolean' }, - 'sentry.origin': { value: 'auto.http.browser.inp', type: 'string' }, - 'sentry.op': { value: 'ui.interaction.click', type: 'string' }, - 'ui.component_name': { value: 'NormalButton', type: 'string' }, - 'browser.web_vital.inp.target': { value: 'body > NormalButton', type: 'string' }, - 'sentry.exclusive_time': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, - 'browser.web_vital.inp.value': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, - 'browser.web_vital.inp.interaction_type': { value: 'click', type: 'string' }, - 'browser.navigation.type': { value: 'navigate', type: 'string' }, - 'sentry.transaction': { value: 'test-url', type: 'string' }, - 'sentry.segment.name': { value: 'test-url', type: 'string' }, - 'user_agent.original': { value: expect.stringContaining('Chrome'), type: 'string' }, - 'sentry.pageload.span_id': { value: pageloadSpanId, type: 'string' }, - 'sentry.trace_lifecycle': { value: 'stream', type: 'string' }, - 'sentry.segment.id': { value: pageloadSpanId, type: 'string' }, - 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, - 'sentry.sdk.version': { value: SDK_VERSION, type: 'string' }, - 'sentry.environment': { value: 'production', type: 'string' }, + [SENTRY_IS_LOCALHOST]: { value: false, type: 'boolean' }, + [SENTRY_ORIGIN]: { value: 'auto.http.browser.inp', type: 'string' }, + [SENTRY_OP]: { value: UI_INTERACTION_CLICK, type: 'string' }, + [UI_COMPONENT_NAME]: { value: 'NormalButton', type: 'string' }, + [BROWSER_WEB_VITAL_INP_TARGET]: { value: 'body > NormalButton', type: 'string' }, + [SENTRY_EXCLUSIVE_TIME]: { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, + [BROWSER_WEB_VITAL_INP_VALUE]: { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, + [BROWSER_WEB_VITAL_INP_INTERACTION_TYPE]: { value: 'click', type: 'string' }, + [BROWSER_NAVIGATION_TYPE]: { value: 'navigate', type: 'string' }, + [SENTRY_TRANSACTION]: { value: 'test-url', type: 'string' }, + [SENTRY_SEGMENT_NAME]: { value: 'test-url', type: 'string' }, + [USER_AGENT_ORIGINAL]: { value: expect.stringContaining('Chrome'), type: 'string' }, + [SENTRY_PAGELOAD_SPAN_ID]: { value: pageloadSpanId, type: 'string' }, + [SENTRY_TRACE_LIFECYCLE]: { value: 'stream', type: 'string' }, + [SENTRY_SEGMENT_ID]: { value: pageloadSpanId, type: 'string' }, + [SENTRY_SDK_NAME]: { value: 'sentry.javascript.browser', type: 'string' }, + [SENTRY_SDK_VERSION]: { value: SDK_VERSION, type: 'string' }, + [SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' }, }, }); }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/init.js index 822c40214adb..cf36be344e01 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/test.ts index c7a29f406c1e..8cdafe4870c0 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/test.ts @@ -1,8 +1,34 @@ +import { UI_INTERACTION_CLICK } from '@sentry/conventions/op'; +import { + BROWSER_WEB_VITAL_INP_VALUE, + SENTRY_IS_LOCALHOST, + SENTRY_ORIGIN, + SENTRY_OP, + UI_COMPONENT_NAME, + BROWSER_WEB_VITAL_INP_TARGET, + SENTRY_EXCLUSIVE_TIME, + BROWSER_WEB_VITAL_INP_INTERACTION_TYPE, + BROWSER_NAVIGATION_TYPE, + SENTRY_TRANSACTION, + SENTRY_SEGMENT_NAME, + USER_AGENT_ORIGINAL, + SENTRY_PAGELOAD_SPAN_ID, + SENTRY_TRACE_LIFECYCLE, + SENTRY_SEGMENT_ID, + SENTRY_SDK_NAME, + SENTRY_SDK_VERSION, + SENTRY_ENVIRONMENT, +} from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { SDK_VERSION } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, getSpansFromEnvelope, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; +import { + collectStreamedSpans, + getSpanOp, + getSpansFromEnvelope, + waitForStreamedSpanEnvelope, +} from '../../../../utils/spanUtils'; const supportedBrowsers = ['chromium']; @@ -13,20 +39,18 @@ sentryTest( sentryTest.skip(); } + const spans = collectStreamedSpans(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const spanEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!getSpansFromEnvelope(env).find(s => getSpanOp(s) === 'ui.interaction.click'), + const spanEnvelopePromise = waitForStreamedSpanEnvelope(page, env => + getSpansFromEnvelope(env).some(span => getSpanOp(span) === UI_INTERACTION_CLICK), ); await page.goto(url); // Simulating route change (keeping