diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/instrument.server.mjs b/dev-packages/e2e-tests/test-applications/tanstackstart-react/instrument.server.mjs index af26fd68fc57..8bc20de7578b 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/instrument.server.mjs +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/instrument.server.mjs @@ -5,7 +5,6 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, - traceLifecycle: process.env.E2E_TEST_STREAMED_SPANS === '1' ? 'stream' : 'static', transportOptions: { // We expect the app to send a lot of events in a short time bufferSize: 1000, diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json b/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json index 724d0508cf37..dbb48b537ec4 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json @@ -18,7 +18,6 @@ "test:assert:proxy": "pnpm test && TEST_ENV=development pnpm playwright test db-drivers", "test:assert": "pnpm test:assert:proxy", "test:assert:tunnel-generated": "E2E_TEST_TUNNEL_ROUTE_MODE=dynamic E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm test", - "test:assert:tunnel-streamed": "E2E_TEST_TUNNEL_ROUTE_MODE=dynamic E2E_TEST_DSN=http://public@localhost:3031/1337 E2E_TEST_STREAMED_SPANS=1 pnpm test", "test:assert:tunnel-static": "E2E_TEST_TUNNEL_ROUTE_MODE=static E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm test", "test:assert:tunnel-custom": "E2E_TEST_CUSTOM_TUNNEL_ROUTE=1 pnpm test", "test:assert:tunnel-object": "E2E_TEST_TUNNEL_ROUTE_MODE=object E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm test" @@ -57,11 +56,6 @@ "build-command": "pnpm test:build:tunnel-generated", "assert-command": "pnpm test:assert:tunnel-generated" }, - { - "label": "tanstackstart-react (tunnel-streamed)", - "build-command": "pnpm test:build:tunnel-generated", - "assert-command": "pnpm test:assert:tunnel-streamed" - }, { "label": "tanstackstart-react (tunnel-static)", "build-command": "pnpm test:build:tunnel-static", diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/instrument.client.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/instrument.client.ts index 95351c60f9ca..ad7d0b23a8fa 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/instrument.client.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/instrument.client.ts @@ -1,7 +1,6 @@ import * as Sentry from '@sentry/tanstackstart-react'; Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: __APP_DSN__, tracesSampleRate: 1.0, diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/db-drivers.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/db-drivers.test.ts index a8cac8f5e3b6..f4d2e029b601 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/db-drivers.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/db-drivers.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; const usesManagedTunnelRoute = (process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1'; @@ -10,89 +10,104 @@ test.skip(usesManagedTunnelRoute, 'Default e2e suites run only in the proxy vari // `sentryTanstackStart()` auto-wires into the server bundle, and the runtime hook in `vite dev`, // where the drivers stay external on Node's own loader. test('Instruments ioredis automatically', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => { - return ( - transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.transaction === 'GET /api/db-ioredis' - ); - }); + const spansPromise = collectStreamedSpans( + 'tanstackstart-react', + spans => + spans.some( + span => + span.is_segment && + getSpanOp(span) === 'http.server' && + String(span.attributes['url.path']?.value ?? '').includes('db-ioredis'), + ) && + spans.some(span => span.attributes['db.query.text']?.value === 'set test-key [1 other arguments]') && + spans.some(span => span.attributes['db.query.text']?.value === 'get test-key'), + ); await fetch(`${baseURL}/api/db-ioredis`); - const transactionEvent = await transactionEventPromise; - - const spans = transactionEvent.spans || []; + const spans = await spansPromise; + // ioredis also emits handshake commands (SETINFO, INFO) as db.query spans. + const redisSpans = spans.filter( + span => + getSpanOp(span) === 'db.query' && + (span.attributes['db.operation.name']?.value === 'set' || span.attributes['db.operation.name']?.value === 'get'), + ); - expect(spans).toContainEqual( + expect(redisSpans).toHaveLength(2); + expect(redisSpans).toContainEqual( expect.objectContaining({ - op: 'db.query', - origin: 'auto.db.redis', - description: 'set test-key [1 other arguments]', + name: 'set localhost:6379', status: 'ok', - data: expect.objectContaining({ - 'db.system.name': 'redis', - 'db.operation.name': 'set', - 'db.query.text': 'set test-key [1 other arguments]', + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'db.query' }, + 'sentry.origin': { type: 'string', value: 'auto.db.redis' }, + 'db.system.name': { type: 'string', value: 'redis' }, + 'db.operation.name': { type: 'string', value: 'set' }, + 'db.query.text': { type: 'string', value: 'set test-key [1 other arguments]' }, }), }), ); - expect(spans).toContainEqual( + expect(redisSpans).toContainEqual( expect.objectContaining({ - op: 'db.query', - origin: 'auto.db.redis', - description: 'get test-key', + name: 'get localhost:6379', status: 'ok', - data: expect.objectContaining({ - 'db.system.name': 'redis', - 'db.operation.name': 'get', - 'db.query.text': 'get test-key', + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'db.query' }, + 'sentry.origin': { type: 'string', value: 'auto.db.redis' }, + 'db.system.name': { type: 'string', value: 'redis' }, + 'db.operation.name': { type: 'string', value: 'get' }, + 'db.query.text': { type: 'string', value: 'get test-key' }, }), }), ); }); test('Instruments mysql automatically', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => { - return ( - transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.transaction === 'GET /api/db-mysql' - ); - }); + const spansPromise = collectStreamedSpans( + 'tanstackstart-react', + spans => + spans.some( + span => + span.is_segment && + getSpanOp(span) === 'http.server' && + String(span.attributes['url.path']?.value ?? '').includes('db-mysql'), + ) && + spans.some(span => span.attributes['db.query.text']?.value === 'SELECT ? + ? AS solution') && + spans.some(span => span.attributes['db.query.text']?.value === 'SELECT NOW()'), + ); await fetch(`${baseURL}/api/db-mysql`); - const transactionEvent = await transactionEventPromise; + const spans = await spansPromise; + const mysqlSpans = spans.filter(span => span.attributes['sentry.origin']?.value === 'auto.db.mysql'); - const spans = transactionEvent.spans || []; + const firstQuery = mysqlSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT ? + ? AS solution'); + expect(firstQuery).toBeDefined(); + expect(firstQuery!.name).toBe('SELECT'); + expect(firstQuery!.status).toBe('ok'); + expect(firstQuery!.attributes).toMatchObject({ + 'sentry.op': { type: 'string', value: 'db' }, + 'sentry.origin': { type: 'string', value: 'auto.db.mysql' }, + 'db.system.name': { type: 'string', value: 'mysql' }, + 'db.query.text': { type: 'string', value: 'SELECT ? + ? AS solution' }, + 'db.user': { type: 'string', value: 'root' }, + 'db.connection_string': { type: 'string', value: expect.any(String) }, + 'server.address': { type: 'string', value: expect.any(String) }, + 'server.port': { type: 'integer', value: 3306 }, + }); - expect(spans).toContainEqual( - expect.objectContaining({ - op: 'db', - origin: 'auto.db.mysql', - description: 'SELECT ? + ? AS solution', - status: 'ok', - data: expect.objectContaining({ - 'db.system.name': 'mysql', - 'db.query.text': 'SELECT ? + ? AS solution', - 'db.user': 'root', - 'db.connection_string': expect.any(String), - 'server.address': expect.any(String), - 'server.port': 3306, - }), - }), - ); - expect(spans).toContainEqual( - expect.objectContaining({ - op: 'db', - origin: 'auto.db.mysql', - description: 'SELECT NOW()', - status: 'ok', - data: expect.objectContaining({ - 'db.system.name': 'mysql', - 'db.query.text': 'SELECT NOW()', - 'db.user': 'root', - 'db.connection_string': expect.any(String), - 'server.address': expect.any(String), - 'server.port': 3306, - }), - }), - ); + const secondQuery = mysqlSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT NOW()'); + expect(secondQuery).toBeDefined(); + expect(secondQuery!.name).toBe('SELECT'); + expect(secondQuery!.status).toBe('ok'); + expect(secondQuery!.attributes).toMatchObject({ + 'sentry.op': { type: 'string', value: 'db' }, + 'sentry.origin': { type: 'string', value: 'auto.db.mysql' }, + 'db.system.name': { type: 'string', value: 'mysql' }, + 'db.query.text': { type: 'string', value: 'SELECT NOW()' }, + 'db.user': { type: 'string', value: 'root' }, + 'db.connection_string': { type: 'string', value: expect.any(String) }, + 'server.address': { type: 'string', value: expect.any(String) }, + 'server.port': { type: 'integer', value: 3306 }, + }); }); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/errors.test.ts index 9447409b6cf9..f262bcd57e66 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/errors.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils'; const usesManagedTunnelRoute = (process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1'; @@ -123,16 +123,16 @@ test('Does not send SSR loader error to Sentry', async ({ baseURL, page }) => { if (!event.type && event.exception?.values?.[0]?.value === 'Sentry SSR Test Error') { errorEventOccurred = true; } - return event?.transaction === 'GET /ssr-error'; + return false; }); - const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => { - return transactionEvent?.transaction === 'GET /ssr-error'; + const serverSpanPromise = waitForStreamedSpan('tanstackstart-react', span => { + return span.is_segment && getSpanOp(span) === 'http.server' && span.attributes['url.path']?.value === '/ssr-error'; }); await page.goto('/ssr-error'); - await transactionEventPromise; + await serverSpanPromise; await (await fetch(`${baseURL}/api/flush`)).text(); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/middleware.test.ts index c5c1961c0413..dbf616fde5af 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/middleware.test.ts @@ -1,18 +1,27 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; const usesManagedTunnelRoute = (process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1'; test.skip(usesManagedTunnelRoute, 'Default e2e suites run only in the proxy variant'); +function isHttpServerPath(span: Parameters[0], path: string | ((value: string) => boolean)): boolean { + if (!span.is_segment || getSpanOp(span) !== 'http.server') { + return false; + } + const urlPath = String(span.attributes['url.path']?.value ?? ''); + return typeof path === 'function' ? path(urlPath) : urlPath === path; +} + test('Sends spans for multiple middlewares and verifies they are siblings under the same parent span', async ({ page, }) => { - const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => { + const spansPromise = collectStreamedSpans('tanstackstart-react', spans => { return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - !!transactionEvent?.transaction?.startsWith('GET /_serverFn') + spans.some(span => isHttpServerPath(span, p => p.startsWith('/_serverFn'))) && + spans.some(span => span.name === 'serverFnMiddleware') && + spans.some(span => span.name === 'globalFunctionMiddleware') ); }); @@ -20,47 +29,41 @@ test('Sends spans for multiple middlewares and verifies they are siblings under await expect(page.locator('#server-fn-middleware-btn')).toBeVisible(); await page.locator('#server-fn-middleware-btn').click(); - const transactionEvent = await transactionEventPromise; - - expect(Array.isArray(transactionEvent?.spans)).toBe(true); + const spans = await spansPromise; - // Find both middleware spans - const serverFnMiddlewareSpan = transactionEvent?.spans?.find( - (span: { description?: string; origin?: string }) => - span.description === 'serverFnMiddleware' && span.origin === 'auto.middleware.tanstackstart', + const serverFnMiddlewareSpan = spans.find( + span => + span.name === 'serverFnMiddleware' && span.attributes['sentry.origin']?.value === 'auto.middleware.tanstackstart', ); - const globalFunctionMiddlewareSpan = transactionEvent?.spans?.find( - (span: { description?: string; origin?: string }) => - span.description === 'globalFunctionMiddleware' && span.origin === 'auto.middleware.tanstackstart', + const globalFunctionMiddlewareSpan = spans.find( + span => + span.name === 'globalFunctionMiddleware' && + span.attributes['sentry.origin']?.value === 'auto.middleware.tanstackstart', ); - // Verify both middleware spans exist with expected properties - expect(serverFnMiddlewareSpan).toEqual( - expect.objectContaining({ - description: 'serverFnMiddleware', - op: 'middleware', - origin: 'auto.middleware.tanstackstart', - status: 'ok', + expect(serverFnMiddlewareSpan).toMatchObject({ + name: 'serverFnMiddleware', + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'middleware' }, + 'sentry.origin': { type: 'string', value: 'auto.middleware.tanstackstart' }, }), - ); - expect(globalFunctionMiddlewareSpan).toEqual( - expect.objectContaining({ - description: 'globalFunctionMiddleware', - op: 'middleware', - origin: 'auto.middleware.tanstackstart', - status: 'ok', + }); + expect(globalFunctionMiddlewareSpan).toMatchObject({ + name: 'globalFunctionMiddleware', + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'middleware' }, + 'sentry.origin': { type: 'string', value: 'auto.middleware.tanstackstart' }, }), - ); + }); - // Both middleware spans should be siblings under the same parent expect(serverFnMiddlewareSpan?.parent_span_id).toBe(globalFunctionMiddlewareSpan?.parent_span_id); }); test('Sends spans for global function middleware', async ({ page }) => { - const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => { + const spansPromise = collectStreamedSpans('tanstackstart-react', spans => { return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - !!transactionEvent?.transaction?.startsWith('GET /_serverFn') + spans.some(span => isHttpServerPath(span, p => p.startsWith('/_serverFn'))) && + spans.some(span => span.name === 'globalFunctionMiddleware') ); }); @@ -68,82 +71,76 @@ test('Sends spans for global function middleware', async ({ page }) => { await expect(page.locator('#server-fn-global-only-btn')).toBeVisible(); await page.locator('#server-fn-global-only-btn').click(); - const transactionEvent = await transactionEventPromise; + const spans = await spansPromise; - expect(Array.isArray(transactionEvent?.spans)).toBe(true); - - // Check for the global function middleware span - expect(transactionEvent?.spans).toEqual( + expect(spans).toEqual( expect.arrayContaining([ expect.objectContaining({ - description: 'globalFunctionMiddleware', - op: 'middleware', - origin: 'auto.middleware.tanstackstart', - status: 'ok', + name: 'globalFunctionMiddleware', + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'middleware' }, + 'sentry.origin': { type: 'string', value: 'auto.middleware.tanstackstart' }, + }), }), ]), ); }); test('Sends spans for global request middleware', async ({ page }) => { - const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => { + const spansPromise = collectStreamedSpans('tanstackstart-react', spans => { return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-middleware' + spans.some(span => isHttpServerPath(span, '/test-middleware')) && + spans.some(span => span.name === 'globalRequestMiddleware') ); }); await page.goto('/test-middleware'); - const transactionEvent = await transactionEventPromise; - - expect(Array.isArray(transactionEvent?.spans)).toBe(true); + const spans = await spansPromise; - // Check for the global request middleware span - expect(transactionEvent?.spans).toEqual( + expect(spans).toEqual( expect.arrayContaining([ expect.objectContaining({ - description: 'globalRequestMiddleware', - op: 'middleware', - origin: 'auto.middleware.tanstackstart', - status: 'ok', + name: 'globalRequestMiddleware', + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'middleware' }, + 'sentry.origin': { type: 'string', value: 'auto.middleware.tanstackstart' }, + }), }), ]), ); }); test('Sends spans for server route request middleware', async ({ page }) => { - const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => { + const spansPromise = collectStreamedSpans('tanstackstart-react', spans => { return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /api/test-middleware' + spans.some(span => isHttpServerPath(span, '/api/test-middleware')) && + spans.some(span => span.name === 'serverRouteRequestMiddleware') ); }); await page.goto('/api/test-middleware'); - const transactionEvent = await transactionEventPromise; - - expect(Array.isArray(transactionEvent?.spans)).toBe(true); + const spans = await spansPromise; - // Check for the server route request middleware span - expect(transactionEvent?.spans).toEqual( + expect(spans).toEqual( expect.arrayContaining([ expect.objectContaining({ - description: 'serverRouteRequestMiddleware', - op: 'middleware', - origin: 'auto.middleware.tanstackstart', - status: 'ok', + name: 'serverRouteRequestMiddleware', + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'middleware' }, + 'sentry.origin': { type: 'string', value: 'auto.middleware.tanstackstart' }, + }), }), ]), ); }); test('Sends span for middleware that returns early without calling next()', async ({ page }) => { - const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => { + const spansPromise = collectStreamedSpans('tanstackstart-react', spans => { return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - !!transactionEvent?.transaction?.startsWith('GET /_serverFn') + spans.some(span => isHttpServerPath(span, p => p.startsWith('/_serverFn'))) && + spans.some(span => span.name === 'earlyReturnMiddleware') ); }); @@ -151,28 +148,26 @@ test('Sends span for middleware that returns early without calling next()', asyn await expect(page.locator('#server-fn-early-return-btn')).toBeVisible(); await page.locator('#server-fn-early-return-btn').click(); - const transactionEvent = await transactionEventPromise; + const spans = await spansPromise; - expect(Array.isArray(transactionEvent?.spans)).toBe(true); - - // Check for the early return middleware span - expect(transactionEvent?.spans).toEqual( + expect(spans).toEqual( expect.arrayContaining([ expect.objectContaining({ - description: 'earlyReturnMiddleware', - op: 'middleware', - origin: 'auto.middleware.tanstackstart', - status: 'ok', + name: 'earlyReturnMiddleware', + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'middleware' }, + 'sentry.origin': { type: 'string', value: 'auto.middleware.tanstackstart' }, + }), }), ]), ); }); test('Sends span for middleware that throws an error', async ({ page }) => { - const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => { + const spansPromise = collectStreamedSpans('tanstackstart-react', spans => { return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - !!transactionEvent?.transaction?.startsWith('GET /_serverFn') + spans.some(span => isHttpServerPath(span, p => p.startsWith('/_serverFn'))) && + spans.some(span => span.name === 'errorMiddleware') ); }); @@ -180,17 +175,15 @@ test('Sends span for middleware that throws an error', async ({ page }) => { await expect(page.locator('#server-fn-error-btn')).toBeVisible(); await page.locator('#server-fn-error-btn').click(); - const transactionEvent = await transactionEventPromise; - - expect(Array.isArray(transactionEvent?.spans)).toBe(true); + const spans = await spansPromise; - // Check for the error middleware span - expect(transactionEvent?.spans).toEqual( + expect(spans).toEqual( expect.arrayContaining([ expect.objectContaining({ - description: 'errorMiddleware', - op: 'middleware', - origin: 'auto.middleware.tanstackstart', + name: 'errorMiddleware', + attributes: expect.objectContaining({ + 'sentry.origin': { type: 'string', value: 'auto.middleware.tanstackstart' }, + }), }), ]), ); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/route-parametrization.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/route-parametrization.test.ts index 39058a45bf29..a1e63e1d9ec2 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/route-parametrization.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/route-parametrization.test.ts @@ -1,82 +1,74 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; const usesManagedTunnelRoute = (process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1'; test.skip(usesManagedTunnelRoute, 'Default e2e suites run only in the proxy variant'); -test('should parametrize server and client transaction names for dynamic routes', async ({ page }) => { - const serverTxPromise = waitForTransaction('tanstackstart-react', transactionEvent => { +test('should parametrize server and client span names for dynamic routes', async ({ page }) => { + const serverSpanPromise = waitForStreamedSpan('tanstackstart-react', span => { return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - typeof transactionEvent?.transaction === 'string' && - transactionEvent.transaction.includes('/param/') + span.is_segment && + getSpanOp(span) === 'http.server' && + String(span.attributes['url.path']?.value ?? '').includes('/param/') ); }); - const clientTxPromise = waitForTransaction('tanstackstart-react', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'pageload' && - typeof transactionEvent?.transaction === 'string' && - transactionEvent.transaction.includes('/param/') - ); + const clientSpanPromise = waitForStreamedSpan('tanstackstart-react', span => { + return span.is_segment && getSpanOp(span) === 'pageload' && String(span.name ?? '').includes('/param/'); }); await page.goto('/param/42'); - const serverTx = await serverTxPromise; - const clientTx = await clientTxPromise; + const serverSpan = await serverSpanPromise; + const clientSpan = await clientSpanPromise; - expect(serverTx.transaction).toBe('GET /param/$id'); - expect(serverTx.transaction_info?.source).toBe('route'); + expect(serverSpan.name).toBe('GET /param/$id'); + expect(serverSpan.attributes['sentry.segment.name.source']).toEqual({ type: 'string', value: 'route' }); - expect(clientTx.transaction).toBe('/param/$id'); - expect(clientTx.transaction_info?.source).toBe('route'); + expect(clientSpan.name).toBe('/param/$id'); + expect(clientSpan.attributes['sentry.segment.name.source']).toEqual({ type: 'string', value: 'route' }); }); -test('should parametrize server and client transaction names for nested dynamic routes', async ({ page }) => { - const serverTxPromise = waitForTransaction('tanstackstart-react', transactionEvent => { +test('should parametrize server and client span names for nested dynamic routes', async ({ page }) => { + const serverSpanPromise = waitForStreamedSpan('tanstackstart-react', span => { return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - typeof transactionEvent?.transaction === 'string' && - transactionEvent.transaction.includes('/users/') + span.is_segment && + getSpanOp(span) === 'http.server' && + String(span.attributes['url.path']?.value ?? '').includes('/users/') ); }); - const clientTxPromise = waitForTransaction('tanstackstart-react', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'pageload' && - typeof transactionEvent?.transaction === 'string' && - transactionEvent.transaction.includes('/users/') - ); + const clientSpanPromise = waitForStreamedSpan('tanstackstart-react', span => { + return span.is_segment && getSpanOp(span) === 'pageload' && String(span.name ?? '').includes('/users/'); }); await page.goto('/users/123'); - const serverTx = await serverTxPromise; - const clientTx = await clientTxPromise; + const serverSpan = await serverSpanPromise; + const clientSpan = await clientSpanPromise; - expect(serverTx.transaction).toBe('GET /users/$userId'); - expect(serverTx.transaction_info?.source).toBe('route'); + expect(serverSpan.name).toBe('GET /users/$userId'); + expect(serverSpan.attributes['sentry.segment.name.source']).toEqual({ type: 'string', value: 'route' }); - expect(clientTx.transaction).toBe('/users/$userId'); - expect(clientTx.transaction_info?.source).toBe('route'); + expect(clientSpan.name).toBe('/users/$userId'); + expect(clientSpan.attributes['sentry.segment.name.source']).toEqual({ type: 'string', value: 'route' }); }); -test('should parametrize API route transaction names', async ({ baseURL }) => { - const serverTxPromise = waitForTransaction('tanstackstart-react', transactionEvent => { +test('should parametrize API route span names', async ({ baseURL }) => { + const serverSpanPromise = waitForStreamedSpan('tanstackstart-react', span => { return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - typeof transactionEvent?.transaction === 'string' && - transactionEvent.transaction.includes('/api/user/') + span.is_segment && + getSpanOp(span) === 'http.server' && + String(span.attributes['url.path']?.value ?? '').includes('/api/user/') ); }); await fetch(`${baseURL}/api/user/456`); - const serverTx = await serverTxPromise; + const serverSpan = await serverSpanPromise; - expect(serverTx.transaction).toBe('GET /api/user/$id'); - expect(serverTx.transaction_info?.source).toBe('route'); + expect(serverSpan.name).toBe('GET /api/user/$id'); + expect(serverSpan.attributes['sentry.segment.name.source']).toEqual({ type: 'string', value: 'route' }); }); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/trace-propagation.test.ts index 88c4be62c120..3f7310ffcd66 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/trace-propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/trace-propagation.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; const usesManagedTunnelRoute = (process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1'; @@ -23,19 +23,30 @@ test.describe('Trace propagation', () => { }); test('should have trace connection between server and client', async ({ page }) => { - const serverTxPromise = waitForTransaction('tanstackstart-react', transactionEvent => { - return transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /'; - }); - - const clientTxPromise = waitForTransaction('tanstackstart-react', transactionEvent => { - return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/'; + const spansPromise = collectStreamedSpans('tanstackstart-react', spans => { + return ( + spans.some( + span => span.is_segment && getSpanOp(span) === 'http.server' && span.attributes['url.path']?.value === '/', + ) && + spans.some( + span => + span.is_segment && + getSpanOp(span) === 'pageload' && + (span.name === '/' || span.attributes['url.path']?.value === '/'), + ) + ); }); await page.goto('/'); - const serverTx = await serverTxPromise; - const clientTx = await clientTxPromise; + const spans = await spansPromise; + const serverSpan = spans.find( + span => span.is_segment && getSpanOp(span) === 'http.server' && span.attributes['url.path']?.value === '/', + ); + const clientSpan = spans.find(span => span.is_segment && getSpanOp(span) === 'pageload'); - expect(clientTx.contexts?.trace?.trace_id).toBe(serverTx.contexts?.trace?.trace_id); + expect(serverSpan).toBeDefined(); + expect(clientSpan).toBeDefined(); + expect(clientSpan?.trace_id).toBe(serverSpan?.trace_id); }); }); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/transaction.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/transaction.test.ts index 65b15b9ff42a..8b4adf5f2ecc 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/transaction.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/transaction.test.ts @@ -1,18 +1,24 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; const usesManagedTunnelRoute = (process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1'; test.skip(usesManagedTunnelRoute, 'Default e2e suites run only in the proxy variant'); -test('Sends a server function transaction with auto-instrumentation', async ({ page }) => { - const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - !!transactionEvent?.transaction?.startsWith('GET /_serverFn') - ); - }); +function isServerFnSegment(span: Parameters[0]): boolean { + return ( + !!span.is_segment && + getSpanOp(span) === 'http.server' && + String(span.attributes['url.path']?.value ?? '').startsWith('/_serverFn') + ); +} + +test('Sends a server function span with auto-instrumentation', async ({ page }) => { + const spansPromise = collectStreamedSpans( + 'tanstackstart-react', + spans => spans.some(isServerFnSegment) && spans.some(span => span.name === 'GET /_serverFn/testLog'), + ); await page.goto('/test-serverFn'); @@ -20,37 +26,34 @@ test('Sends a server function transaction with auto-instrumentation', async ({ p await page.getByText('Call server function', { exact: true }).click(); - const transactionEvent = await transactionEventPromise; + const spans = await spansPromise; - // Check for the auto-instrumented server function span - expect(Array.isArray(transactionEvent?.spans)).toBe(true); - expect(transactionEvent?.spans).toEqual( + expect(spans).toEqual( expect.arrayContaining([ expect.objectContaining({ - description: 'GET /_serverFn/testLog', - op: 'function', - origin: 'auto.function.tanstackstart.server', - data: { - 'sentry.op': 'function', - 'sentry.origin': 'auto.function.tanstackstart.server', - 'tanstackstart.function.id': expect.any(String), - 'tanstackstart.function.filename': 'src/routes/test-serverFn.tsx', - }, + name: 'GET /_serverFn/testLog', status: 'ok', + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'function' }, + 'sentry.origin': { type: 'string', value: 'auto.function.tanstackstart.server' }, + 'tanstackstart.function.filename': { type: 'string', value: 'src/routes/test-serverFn.tsx' }, + }), }), ]), ); }); -test('Sends a server function transaction for a nested server function only if it is manually instrumented', async ({ +test('Sends a server function span for a nested server function only if it is manually instrumented', async ({ page, }) => { - const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - !!transactionEvent?.transaction?.startsWith('GET /_serverFn') - ); - }); + const spansPromise = collectStreamedSpans( + 'tanstackstart-react', + spans => + spans.some(isServerFnSegment) && + spans.some(span => span.name === 'GET /_serverFn/testNestedLog') && + spans.some(span => span.name === 'testNestedLog') && + spans.some(span => span.name === 'globalFunctionMiddleware'), + ); await page.goto('/test-serverFn'); @@ -58,52 +61,44 @@ test('Sends a server function transaction for a nested server function only if i await page.getByText('Call server function nested').click(); - const transactionEvent = await transactionEventPromise; - - expect(Array.isArray(transactionEvent?.spans)).toBe(true); + const spans = await spansPromise; - // Check for the auto-instrumented server function span - expect(transactionEvent?.spans).toEqual( + expect(spans).toEqual( expect.arrayContaining([ expect.objectContaining({ - description: 'GET /_serverFn/testNestedLog', - op: 'function', - origin: 'auto.function.tanstackstart.server', - data: { - 'sentry.op': 'function', - 'sentry.origin': 'auto.function.tanstackstart.server', - 'tanstackstart.function.id': expect.any(String), - 'tanstackstart.function.filename': 'src/routes/test-serverFn.tsx', - }, + name: 'GET /_serverFn/testNestedLog', status: 'ok', + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'function' }, + 'sentry.origin': { type: 'string', value: 'auto.function.tanstackstart.server' }, + 'tanstackstart.function.filename': { type: 'string', value: 'src/routes/test-serverFn.tsx' }, + }), }), ]), ); - // Check for the manually instrumented nested span - expect(transactionEvent?.spans).toEqual( + expect(spans).toEqual( expect.arrayContaining([ expect.objectContaining({ - description: 'testNestedLog', - origin: 'manual', + name: 'testNestedLog', status: 'ok', + attributes: expect.objectContaining({ + 'sentry.origin': { type: 'string', value: 'manual' }, + }), }), ]), ); - // Verify that globalFunctionMiddleware and testNestedLog are sibling spans under the root - const functionMiddlewareSpan = transactionEvent?.spans?.find( - (span: { description?: string; origin?: string }) => - span.description === 'globalFunctionMiddleware' && span.origin === 'auto.middleware.tanstackstart', + const functionMiddlewareSpan = spans.find( + span => + span.name === 'globalFunctionMiddleware' && + span.attributes['sentry.origin']?.value === 'auto.middleware.tanstackstart', ); - const nestedSpan = transactionEvent?.spans?.find( - (span: { description?: string; origin?: string }) => - span.description === 'testNestedLog' && span.origin === 'manual', + const nestedSpan = spans.find( + span => span.name === 'testNestedLog' && span.attributes['sentry.origin']?.value === 'manual', ); expect(functionMiddlewareSpan).toBeDefined(); expect(nestedSpan).toBeDefined(); - - // Both spans should be siblings under the same parent (root transaction) expect(nestedSpan?.parent_span_id).toBe(functionMiddlewareSpan?.parent_span_id); }); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts index d3f9694a6d22..e6b9dfab05e0 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts @@ -1,9 +1,8 @@ import { expect, test } from '@playwright/test'; -import { getSpanOp, waitForError, waitForStreamedSpan, waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils'; const tunnelRouteMode = process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? (process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1' ? 'custom' : 'off'); -const useStreamedSpans = process.env.E2E_TEST_STREAMED_SPANS === '1'; const expectedTunnelPathMatcher = tunnelRouteMode === 'static' ? '/monitor' @@ -68,28 +67,30 @@ function pathnameMatchesTunnelRoute(pathname: string): boolean { : expectedTunnelPathMatcher.test(pathname); } -// A server `http.server` transaction can arrive either as a classic transaction event (static trace -// lifecycle) or as a Span v2 segment span (`traceLifecycle: 'stream'`). These helpers wait on whichever -// format the current run produces, so the assertion below covers both lifecycles with one test body. -function waitForServerHttpEvent(matchesPathname: (pathname: string) => boolean): Promise { - if (useStreamedSpans) { - return waitForStreamedSpan('tanstackstart-react', span => { - return getSpanOp(span) === 'http.server' && matchesPathname((span.name ?? '').split(' ')[1] ?? ''); - }); - } - - return waitForTransaction('tanstackstart-react', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'http.server' && - matchesPathname((transactionEvent.transaction ?? '').split(' ')[1] ?? '') - ); - }); +function waitForServerHttpSpan(matchesPathname: (pathname: string) => boolean, since?: number): Promise { + return waitForStreamedSpan( + 'tanstackstart-react', + span => { + return getSpanOp(span) === 'http.server' && matchesPathname(String(span.attributes['url.path']?.value ?? '')); + }, + since, + ); } test('Does not create a server transaction for the tunnel route', async ({ page }) => { + // Custom routes self-register `ignoreSpans` on the first POST; that first streamed + // span still leaks (see `createSentryTunnelRoute`). The previous test already made + // that POST — wait it out so it cannot win the race below. Managed routes register + // at startup and never emit this span. The leaked span may already have arrived, so + // this wait looks back over everything the proxy recorded instead of only what comes + // in from here on. + if (tunnelRouteMode === 'custom') { + await waitForServerHttpSpan(pathnameMatchesTunnelRoute, 0); + } + // The incoming POST to the tunnel route must not be turned into an `http.server` // transaction by the server SDK — tunnel traffic is plumbing, not application requests. - const tunnelServerEventPromise = waitForServerHttpEvent(pathnameMatchesTunnelRoute); + const tunnelServerEventPromise = waitForServerHttpSpan(pathnameMatchesTunnelRoute); await page.goto('/'); const pageOrigin = new URL(page.url()).origin; @@ -115,7 +116,7 @@ test('Does not create a server transaction for the tunnel route', async ({ page // Anchor on a regular server transaction issued *after* the tunnel POST. The Node transport flushes // transactions/spans in FIFO order, so a (buggy) tunnel-route transaction would always arrive before // this anchor. Racing the two lets us assert the absence of a tunnel transaction without idling on a timeout. - const anchorServerEventPromise = waitForServerHttpEvent(pathname => pathname.includes('/api/user/')); + const anchorServerEventPromise = waitForServerHttpSpan(pathname => pathname.includes('/api/user/')); await page.request.get('/api/user/456');