From d648afaf8e0a77899839298973fa436873bff693 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:24:40 +0200 Subject: [PATCH 1/2] test(nextjs): Add tests for low-cardinality span names --- .../tests/useCacheSpans.spec.ts | 31 +++++++++++++++ .../server/useCacheInstrumentation.test.ts | 38 ++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts new file mode 100644 index 000000000000..5c8a32606378 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts @@ -0,0 +1,31 @@ +import { expect, test } from '@playwright/test'; +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; +import { findCacheSpan } from './cacheOriginLinks-utils'; + +test('uses low-cardinality names for `use cache` spans', async ({ request }) => { + // A fresh id makes the request a guaranteed cache miss, so the trace contains both a `cache.get` and a `cache.put` span. + const id = crypto.randomUUID(); + + const spansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /api/use-cache' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'cache.get') && + spansOfTrace.some(span => getSpanOp(span) === 'cache.put') + ); + }); + + await request.get(`/api/use-cache?id=${id}`); + const spans = await spansPromise; + + // With span streaming the span name is the low-cardinality op + const getSpan = findCacheSpan(spans, 'cache.get'); + expect(getSpan).toBeDefined(); + expect(getSpan!.name).toBe('cache.get'); + const cacheKeyDigest = getSpan!.attributes['cache.key']?.value; + expect(cacheKeyDigest).toEqual([expect.stringMatching(/^[0-9a-f]{12}$/)]); + + const putSpan = findCacheSpan(spans, 'cache.put'); + expect(putSpan).toBeDefined(); + expect(putSpan!.name).toBe('cache.put'); + expect(putSpan!.attributes['cache.key']?.value).toEqual(cacheKeyDigest); +}); diff --git a/packages/nextjs/test/server/useCacheInstrumentation.test.ts b/packages/nextjs/test/server/useCacheInstrumentation.test.ts index 0534725ab05b..29763cdaadb9 100644 --- a/packages/nextjs/test/server/useCacheInstrumentation.test.ts +++ b/packages/nextjs/test/server/useCacheInstrumentation.test.ts @@ -8,12 +8,13 @@ const mocks = vi.hoisted(() => { startSpan: vi.fn((_options: unknown, callback: (span: unknown) => unknown) => callback({ setAttribute })), activeSpan: undefined as object | undefined, sampled: true, + client: undefined as { getOptions: () => { traceLifecycle?: 'static' | 'stream' } } | undefined, }; }); vi.mock('@sentry/core', async importOriginal => ({ ...(await importOriginal()), - getClient: () => undefined, + getClient: () => mocks.client, getActiveSpan: () => mocks.activeSpan, spanIsSampled: () => mocks.sampled, startSpan: mocks.startSpan, @@ -53,6 +54,7 @@ describe('instrumentUseCacheHandlers', () => { beforeEach(() => { mocks.activeSpan = {}; mocks.sampled = true; + mocks.client = undefined; }); afterEach(() => { @@ -280,6 +282,40 @@ describe('instrumentUseCacheHandlers', () => { }); }); + it('names spans with the low-cardinality op when span streaming is enabled', async () => { + mocks.client = { getOptions: () => ({ traceLifecycle: 'stream' }) }; + const handler = installWithDefaultHandler(); + + await handler.get('cache-key'); + await handler.set('cache-key', Promise.resolve({})); + + // The key digest stays on `cache.key`, where Relay reads the span description from. + expect(mocks.startSpan).toHaveBeenCalledWith( + { + op: 'cache.get', + name: 'cache.get', + attributes: { + 'sentry.origin': 'auto.cache.nextjs', + 'cache.key': [expect.stringMatching(/^[0-9a-f]{12}$/)], + 'cache.operation': 'get', + }, + }, + expect.any(Function), + ); + expect(mocks.startSpan).toHaveBeenCalledWith( + { + op: 'cache.put', + name: 'cache.put', + attributes: { + 'sentry.origin': 'auto.cache.nextjs', + 'cache.key': [expect.stringMatching(/^[0-9a-f]{12}$/)], + 'cache.operation': 'put', + }, + }, + expect.any(Function), + ); + }); + it('creates a `cache.put` span around handler writes', async () => { const handler = installWithDefaultHandler(); From 462ed1a7dda5bb77b5e695fcf46e8d0596802a8f Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:44:11 +0200 Subject: [PATCH 2/2] comment --- .../tests/useCacheSpans.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts index 5c8a32606378..2704de1a11f6 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts @@ -3,7 +3,8 @@ import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; import { findCacheSpan } from './cacheOriginLinks-utils'; test('uses low-cardinality names for `use cache` spans', async ({ request }) => { - // A fresh id makes the request a guaranteed cache miss, so the trace contains both a `cache.get` and a `cache.put` span. + // A fresh id makes the request a guaranteed cache miss (the id is part of the cache key), so the + // trace contains both a `cache.get` and a `cache.put` span. const id = crypto.randomUUID(); const spansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { @@ -17,7 +18,8 @@ test('uses low-cardinality names for `use cache` spans', async ({ request }) => await request.get(`/api/use-cache?id=${id}`); const spans = await spansPromise; - // With span streaming the span name is the low-cardinality op + // With span streaming the span name is the low-cardinality op; the key digest stays on + // `cache.key`, where Relay reads the span description from. const getSpan = findCacheSpan(spans, 'cache.get'); expect(getSpan).toBeDefined(); expect(getSpan!.name).toBe('cache.get');