Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
review suggestions
  • Loading branch information
s1gr1d committed Oct 2, 2026
commit 40141bf58da41bba519ad2d0a782ef39866e75de
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ test('links a cached layout hit to the trace that filled the layout entry', asyn
const putSpan = findCacheSpan(missSpans, 'cache.put');
expect(putSpan).toBeDefined();

expect(putSpan!.attributes['code.file.path']?.value).toBe('app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx');
if (!isWebpackBuild) {
expect(putSpan!.attributes['code.file.path']?.value).toBe(
'app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx',
);
}

const hitGetSpan = findCacheSpan(hitSpans, 'cache.get', true);
expect(hitGetSpan).toBeDefined();
Expand Down
21 changes: 12 additions & 9 deletions packages/nextjs/src/server/useCacheInstrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,12 @@ function shouldRecordCacheSpan(): boolean {
return !!activeSpan && spanIsSampled(activeSpan);
}

function startCacheSpan<T>(op: typeof CACHE_GET | typeof CACHE_PUT, digest: string, callback: (span: Span) => T): T {
function startCacheSpan<T>(
op: typeof CACHE_GET | typeof CACHE_PUT,
digest: string,
extraAttributes: Record<string, string>,
callback: (span: Span) => T,
): T {
const client = getClient();

return startSpan(
Expand All @@ -116,6 +121,7 @@ function startCacheSpan<T>(op: typeof CACHE_GET | typeof CACHE_PUT, digest: stri
[SENTRY_ORIGIN]: CACHE_SPAN_ORIGIN,
[CACHE_KEY]: [digest],
[CACHE_OPERATION]: CACHE_OPERATION_NAMES[op],
...extraAttributes,
},
},
callback,
Expand Down Expand Up @@ -252,7 +258,7 @@ function instrumentHandler(handler: unknown): void {
return originalGet.call(this, cacheKey, softTags);
}
const digest = keyDigest(cacheKey);
return startCacheSpan(CACHE_GET, digest, span =>
return startCacheSpan(CACHE_GET, digest, {}, span =>
// `Promise.resolve` because custom handlers may return the entry synchronously.
Promise.resolve(originalGet.call(this, cacheKey, softTags)).then(entry => {
try {
Expand All @@ -279,20 +285,17 @@ function instrumentHandler(handler: unknown): void {

// The handler drains `pendingEntry` (the still-streaming entry) before storing, so this
// span covers producing and storing the entry, not just the write.
return startCacheSpan(CACHE_PUT, digest, span => {
if (sourceFile) {
span.setAttribute(CODE_FILE_PATH, sourceFile);
}
return startCacheSpan(CACHE_PUT, digest, sourceFile ? { [CODE_FILE_PATH]: sourceFile } : {}, span =>
// Only a successful write becomes a fill origin: a failed write leaves no entry or the
// previous one (whose origin still stands). A dropped span (`ignoreSpans`) never
// reaches Sentry, so a link to it would be broken.
return Promise.resolve(originalSet.call(this, cacheKey, pendingEntry)).then(result => {
Promise.resolve(originalSet.call(this, cacheKey, pendingEntry)).then(result => {
if (span.isRecording()) {
rememberCacheOrigin(originKeyPrefix + digest, span, pendingEntry);
}
return result;
});
});
}),
);
};
});
} catch (error) {
Expand Down
24 changes: 19 additions & 5 deletions packages/nextjs/test/server/useCacheInstrumentation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,24 +496,34 @@ describe('instrumentUseCacheHandlers', () => {
});
}

it('sets `code.file.path` when the key parses and the manifest knows the function', async () => {
it('starts the `cache.put` span with `code.file.path` when the key parses and the manifest knows the function', async () => {
setManifest('app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx');
const handler = installWithDefaultHandler();

await handler.set(jsonCacheKey, Promise.resolve({}));

expect(mocks.setAttribute).toHaveBeenCalledWith(
'code.file.path',
'app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx',
// In the start options (not set afterwards), so samplers and span processors see it.
expect(mocks.startSpan).toHaveBeenCalledWith(
expect.objectContaining({
op: 'cache.put',
attributes: expect.objectContaining({
'code.file.path': 'app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx',
}),
}),
expect.any(Function),
);
});

it('does not set `code.file.path` on `cache.get` spans', async () => {
it('does not put `code.file.path` on `cache.get` spans', async () => {
setManifest('app/page.tsx');
const handler = installWithDefaultHandler({ timestamp: nowMs() });

await handler.get(jsonCacheKey);

expect(mocks.startSpan).not.toHaveBeenCalledWith(
expect.objectContaining({ attributes: expect.objectContaining({ 'code.file.path': expect.anything() }) }),
expect.any(Function),
);
expect(mocks.setAttribute).not.toHaveBeenCalledWith('code.file.path', expect.anything());
});

Expand All @@ -524,6 +534,10 @@ describe('instrumentUseCacheHandlers', () => {

await expect(handler.set('multipart-encoded-key', Promise.resolve({}))).resolves.toBeUndefined();

expect(mocks.startSpan).not.toHaveBeenCalledWith(
expect.objectContaining({ attributes: expect.objectContaining({ 'code.file.path': expect.anything() }) }),
expect.any(Function),
);
expect(mocks.setAttribute).not.toHaveBeenCalledWith('code.file.path', expect.anything());
});
});
Expand Down
Loading