Skip to content

Commit 862359b

Browse files
nicohrubecclaude
andauthored
fix(replay): Capture aborted/errored fetch requests in replay network tab (#20722)
Fetch requests that are aborted or fail before receiving response headers never appear in the replay network tab. Reproducible by calling `fetch()` with an `AbortController` that fires before headers arrive. With this setup the request shows up in Sentry breadcrumbs (on the error objects) but is missing from the replay network tab. `_isFetchHint()` gates on `hint.response` existing, but when a fetch errors or is aborted the breadcrumb handler in the breadcrumb integration creates a hint with the error as `data` and no `response` (since none was received). Changed the guard to check `hint.input` instead, which is always present. Closes #20714 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e9791d3 commit 862359b

2 files changed

Lines changed: 52 additions & 2 deletions

File tree

packages/replay-internal/src/coreHandlers/handleNetworkBreadcrumbs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,6 @@ function _isXhrHint(hint?: BreadcrumbHint): hint is XhrHint {
9595
return hint?.xhr;
9696
}
9797

98-
function _isFetchHint(hint?: BreadcrumbHint): hint is FetchHint {
99-
return hint?.response;
98+
function _isFetchHint(hint?: BreadcrumbHint): hint is Partial<FetchHint> {
99+
return hint?.input !== undefined;
100100
}

packages/replay-internal/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,56 @@ other-header: test`;
359359
]);
360360
});
361361

362+
it('handles fetch breadcrumb for aborted request (no response)', async () => {
363+
const breadcrumb: Breadcrumb = {
364+
category: 'fetch',
365+
level: 'error',
366+
data: {
367+
method: 'GET',
368+
url: 'https://example.com',
369+
},
370+
};
371+
372+
const hint: FetchBreadcrumbHint = {
373+
data: new Error('The operation was aborted'),
374+
input: ['GET', {}],
375+
startTimestamp: BASE_TIMESTAMP + 1000,
376+
endTimestamp: BASE_TIMESTAMP + 2000,
377+
};
378+
beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
379+
380+
expect(breadcrumb).toEqual({
381+
category: 'fetch',
382+
level: 'error',
383+
data: {
384+
method: 'GET',
385+
url: 'https://example.com',
386+
},
387+
});
388+
389+
await waitForReplayEventBuffer();
390+
391+
expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([
392+
{
393+
type: 5,
394+
timestamp: (BASE_TIMESTAMP + 1000) / 1000,
395+
data: {
396+
tag: 'performanceSpan',
397+
payload: {
398+
data: {
399+
method: 'GET',
400+
statusCode: 0,
401+
},
402+
description: 'https://example.com',
403+
endTimestamp: (BASE_TIMESTAMP + 2000) / 1000,
404+
op: 'resource.fetch',
405+
startTimestamp: (BASE_TIMESTAMP + 1000) / 1000,
406+
},
407+
},
408+
},
409+
]);
410+
});
411+
362412
it('parses fetch response body if necessary', async () => {
363413
const breadcrumb: Breadcrumb = {
364414
category: 'fetch',

0 commit comments

Comments
 (0)