Skip to content
Merged
13 changes: 9 additions & 4 deletions packages/browser/src/backend.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { BaseBackend } from '@sentry/core';
import { Event, EventHint, Options, Severity, Transport } from '@sentry/types';
import { BaseBackend, getEnvelopeEndpointWithUrlEncodedAuth, initAPIDetails } from '@sentry/core';
import { Event, EventHint, Options, Severity, Transport, TransportOptions } from '@sentry/types';
import { supportsFetch } from '@sentry/utils';

import { eventFromException, eventFromMessage } from './eventbuilder';
import { FetchTransport, XHRTransport } from './transports';
import { FetchTransport, makeNewFetchTransport, XHRTransport } from './transports';

/**
* Configuration options for the Sentry Browser SDK.
Expand Down Expand Up @@ -58,18 +58,23 @@ export class BrowserBackend extends BaseBackend<BrowserOptions> {
return super._setupTransport();
}

const transportOptions = {
const transportOptions: TransportOptions = {
...this._options.transportOptions,
dsn: this._options.dsn,
tunnel: this._options.tunnel,
sendClientReports: this._options.sendClientReports,
_metadata: this._options._metadata,
};

const api = initAPIDetails(transportOptions.dsn, transportOptions._metadata, transportOptions.tunnel);
const url = getEnvelopeEndpointWithUrlEncodedAuth(api.dsn, api.tunnel);

if (this._options.transport) {
return new this._options.transport(transportOptions);
}
if (supportsFetch()) {
const requestOptions: RequestInit = { ...transportOptions.fetchParameters };
this._newTransport = makeNewFetchTransport({ requestOptions, url });
return new FetchTransport(transportOptions);
}
return new XHRTransport(transportOptions);
Expand Down
2 changes: 2 additions & 0 deletions packages/browser/src/transports/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export { BaseTransport } from './base';
export { FetchTransport } from './fetch';
export { XHRTransport } from './xhr';

export { makeNewFetchTransport } from './new-fetch';
44 changes: 44 additions & 0 deletions packages/browser/src/transports/new-fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
BaseTransportOptions,
createTransport,
NewTransport,
TransportMakeRequestResponse,
TransportRequest,
} from '@sentry/core';

import { FetchImpl, getNativeFetchImplementation } from './utils';

export interface FetchTransportOptions extends BaseTransportOptions {
requestOptions?: RequestInit;
}

/**
* Creates a Transport that uses the Fetch API to send events to Sentry.
*/
export function makeNewFetchTransport(
options: FetchTransportOptions,
nativeFetch: FetchImpl = getNativeFetchImplementation(),
): NewTransport {
function makeRequest(request: TransportRequest): PromiseLike<TransportMakeRequestResponse> {
const requestOptions: RequestInit = {
body: request.body,
method: 'POST',
referrerPolicy: 'origin',
...options.requestOptions,
};

return nativeFetch(options.url, requestOptions).then(response => {
return response.text().then(body => ({
body,
headers: {
'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'),
'retry-after': response.headers.get('Retry-After'),
},
reason: response.statusText,
statusCode: response.status,
}));
});
}

return createTransport({ bufferSize: options.bufferSize }, makeRequest);
}
98 changes: 98 additions & 0 deletions packages/browser/test/unit/transports/new-fetch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { EventEnvelope, EventItem } from '@sentry/types';
import { createEnvelope, serializeEnvelope } from '@sentry/utils';

import { FetchTransportOptions, makeNewFetchTransport } from '../../../src/transports/new-fetch';
import { FetchImpl } from '../../../src/transports/utils';

const DEFAULT_FETCH_TRANSPORT_OPTIONS: FetchTransportOptions = {
url: 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7',
};

const ERROR_ENVELOPE = createEnvelope<EventEnvelope>({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [
[{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem,
]);

class Headers {
headers: { [key: string]: string } = {};
get(key: string) {
return this.headers[key] || null;
}
set(key: string, value: string) {
this.headers[key] = value;
}
}

describe('NewFetchTransport', () => {
it('calls fetch with the given URL', async () => {
const mockFetch = jest.fn(() =>
Promise.resolve({
headers: new Headers(),
status: 200,
text: () => Promise.resolve({}),
}),
) as unknown as FetchImpl;
const transport = makeNewFetchTransport(DEFAULT_FETCH_TRANSPORT_OPTIONS, mockFetch);

expect(mockFetch).toHaveBeenCalledTimes(0);
const res = await transport.send(ERROR_ENVELOPE);
expect(mockFetch).toHaveBeenCalledTimes(1);

expect(res.status).toBe('success');

expect(mockFetch).toHaveBeenLastCalledWith(DEFAULT_FETCH_TRANSPORT_OPTIONS.url, {
body: serializeEnvelope(ERROR_ENVELOPE),
method: 'POST',
referrerPolicy: 'origin',
});
});

it('sets rate limit headers', async () => {
const headers = {
get: jest.fn(),
};

const mockFetch = jest.fn(() =>
Promise.resolve({
headers,
status: 200,
text: () => Promise.resolve({}),
}),
) as unknown as FetchImpl;
const transport = makeNewFetchTransport(DEFAULT_FETCH_TRANSPORT_OPTIONS, mockFetch);

expect(headers.get).toHaveBeenCalledTimes(0);
await transport.send(ERROR_ENVELOPE);

expect(headers.get).toHaveBeenCalledTimes(2);
expect(headers.get).toHaveBeenCalledWith('X-Sentry-Rate-Limits');
expect(headers.get).toHaveBeenCalledWith('Retry-After');
});

it('allows for custom options to be passed in', async () => {
const mockFetch = jest.fn(() =>
Promise.resolve({
headers: new Headers(),
status: 200,
text: () => Promise.resolve({}),
}),
) as unknown as FetchImpl;

const REQUEST_OPTIONS: RequestInit = {
referrerPolicy: 'strict-origin',
keepalive: true,
referrer: 'http://example.org',
};

const transport = makeNewFetchTransport(
{ ...DEFAULT_FETCH_TRANSPORT_OPTIONS, requestOptions: REQUEST_OPTIONS },
mockFetch,
);

await transport.send(ERROR_ENVELOPE);
expect(mockFetch).toHaveBeenLastCalledWith(DEFAULT_FETCH_TRANSPORT_OPTIONS.url, {
body: serializeEnvelope(ERROR_ENVELOPE),
method: 'POST',
...REQUEST_OPTIONS,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Sentry.captureException(new Error('this is an error'));
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { expect } from '@playwright/test';
import { Event } from '@sentry/types';

import { sentryTest } from '../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers';

sentryTest('should capture an error with the new fetch transport', async ({ getLocalTestPath, page }) => {
const url = await getLocalTestPath({ testDir: __dirname });

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);

expect(eventData.exception?.values).toHaveLength(1);
expect(eventData.exception?.values?.[0]).toMatchObject({
type: 'Error',
value: 'this is an error',
mechanism: {
type: 'generic',
handled: true,
},
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const transaction = Sentry.startTransaction({ name: 'test_transaction_1' });
transaction.finish();
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { expect } from '@playwright/test';
import { Event } from '@sentry/types';

import { sentryTest } from '../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers';

sentryTest('should report a transaction with the new fetch transport', async ({ getLocalTestPath, page }) => {
const url = await getLocalTestPath({ testDir: __dirname });
const transaction = await getFirstSentryEnvelopeRequest<Event>(page, url);

expect(transaction.transaction).toBe('test_transaction_1');
expect(transaction.spans).toBeDefined();
});
13 changes: 13 additions & 0 deletions packages/integration-tests/suites/new-transports/init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as Sentry from '@sentry/browser';
// eslint-disable-next-line no-unused-vars
import * as _ from '@sentry/tracing';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://[email protected]/1337',
_experiments: {
newTransport: true,
},
tracesSampleRate: 1.0,
});