From bed88a42c91503f42deda72450ba2ff7e939a293 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 16 Sep 2025 11:14:37 +0200 Subject: [PATCH 1/2] ref(browser): Improve `fetchTransport` error handling --- packages/browser/src/transports/fetch.ts | 36 ++++++++----------- .../browser/test/transports/fetch.test.ts | 25 ++++++++++--- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/packages/browser/src/transports/fetch.ts b/packages/browser/src/transports/fetch.ts index d21ae82486ec..f690f015f281 100644 --- a/packages/browser/src/transports/fetch.ts +++ b/packages/browser/src/transports/fetch.ts @@ -1,5 +1,5 @@ import type { Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/core'; -import { createTransport, rejectedSyncPromise } from '@sentry/core'; +import { createTransport } from '@sentry/core'; import { clearCachedImplementation, getNativeImplementation } from '@sentry-internal/browser-utils'; import type { WINDOW } from '../helpers'; import type { BrowserTransportOptions } from './types'; @@ -9,12 +9,12 @@ import type { BrowserTransportOptions } from './types'; */ export function makeFetchTransport( options: BrowserTransportOptions, - nativeFetch: typeof WINDOW.fetch | undefined = getNativeImplementation('fetch'), + nativeFetch: typeof WINDOW.fetch = getNativeImplementation('fetch'), ): Transport { let pendingBodySize = 0; let pendingCount = 0; - function makeRequest(request: TransportRequest): PromiseLike { + async function makeRequest(request: TransportRequest): Promise { const requestSize = request.body.length; pendingBodySize += requestSize; pendingCount++; @@ -39,29 +39,23 @@ export function makeFetchTransport( ...options.fetchOptions, }; - if (!nativeFetch) { - clearCachedImplementation('fetch'); - return rejectedSyncPromise('No fetch implementation available'); - } - try { - // Note: We do not need to suppress tracing here, becasue we are using the native fetch, instead of our wrapped one. - return nativeFetch(options.url, requestOptions).then(response => { - pendingBodySize -= requestSize; - pendingCount--; - return { - statusCode: response.status, - headers: { - 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), - 'retry-after': response.headers.get('Retry-After'), - }, - }; - }); + // Note: We do not need to suppress tracing here, because we are using the native fetch, instead of our wrapped one. + const response = await nativeFetch(options.url, requestOptions); + + return { + statusCode: response.status, + headers: { + 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), + 'retry-after': response.headers.get('Retry-After'), + }, + }; } catch (e) { clearCachedImplementation('fetch'); + throw e; + } finally { pendingBodySize -= requestSize; pendingCount--; - return rejectedSyncPromise(e); } } diff --git a/packages/browser/test/transports/fetch.test.ts b/packages/browser/test/transports/fetch.test.ts index cf8f8810531f..95f3f7cfe81c 100644 --- a/packages/browser/test/transports/fetch.test.ts +++ b/packages/browser/test/transports/fetch.test.ts @@ -1,6 +1,6 @@ import type { EventEnvelope, EventItem } from '@sentry/core'; import { createEnvelope, serializeEnvelope } from '@sentry/core'; -import type { Mock } from 'vitest'; +import { Mock, afterEach } from 'vitest'; import { describe, expect, it, vi } from 'vitest'; import { makeFetchTransport } from '../../src/transports/fetch'; import type { BrowserTransportOptions } from '../../src/transports/types'; @@ -29,7 +29,11 @@ class Headers { } } -describe('NewFetchTransport', () => { +describe('fetchTransport', () => { + afterEach(() => { + vi.resetAllMocks(); + }); + it('calls fetch with the given URL', async () => { const mockFetch = vi.fn(() => Promise.resolve({ @@ -102,15 +106,28 @@ describe('NewFetchTransport', () => { }); }); - it('handles when `getNativetypeof window.fetchementation` is undefined', async () => { + it('handles when native fetch implementation returns undefined', async () => { const mockFetch = vi.fn(() => undefined) as unknown as typeof window.fetch; const transport = makeFetchTransport(DEFAULT_FETCH_TRANSPORT_OPTIONS, mockFetch); expect(mockFetch).toHaveBeenCalledTimes(0); - await expect(() => transport.send(ERROR_ENVELOPE)).not.toThrow(); + await expect(() => transport.send(ERROR_ENVELOPE)).rejects.toThrow( + "Cannot read properties of undefined (reading 'status')", + ); expect(mockFetch).toHaveBeenCalledTimes(1); }); + it('handles when native fetch implementation is undefined', async () => { + vi.mock('@sentry-internal/browser-utils', async importOriginal => ({ + ...(await importOriginal()), + getNativeImplementation: () => undefined, + })); + + const transport = makeFetchTransport(DEFAULT_FETCH_TRANSPORT_OPTIONS); + + await expect(() => transport.send(ERROR_ENVELOPE)).rejects.toThrow('nativeFetch is not a function'); + }); + it('correctly sets keepalive flag', async () => { const mockFetch = vi.fn(() => Promise.resolve({ From dd26af99aa84e1ff0c5d2184dff3a3e05114fd9c Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 16 Sep 2025 11:33:12 +0200 Subject: [PATCH 2/2] fix lint --- packages/browser/test/transports/fetch.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/browser/test/transports/fetch.test.ts b/packages/browser/test/transports/fetch.test.ts index 95f3f7cfe81c..d330fe886d5f 100644 --- a/packages/browser/test/transports/fetch.test.ts +++ b/packages/browser/test/transports/fetch.test.ts @@ -1,7 +1,7 @@ import type { EventEnvelope, EventItem } from '@sentry/core'; import { createEnvelope, serializeEnvelope } from '@sentry/core'; -import { Mock, afterEach } from 'vitest'; -import { describe, expect, it, vi } from 'vitest'; +import type { Mock } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { makeFetchTransport } from '../../src/transports/fetch'; import type { BrowserTransportOptions } from '../../src/transports/types';