Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions packages/browser/src/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export class BrowserBackend extends BaseBackend<BrowserOptions> {
const transportOptions = {
...this._options.transportOptions,
dsn: this._options.dsn,
tunnel: this._options.tunnel,
_metadata: this._options._metadata,
};

Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/transports/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export abstract class BaseTransport implements Transport {
protected readonly _rateLimits: Record<string, Date> = {};

public constructor(public options: TransportOptions) {
this._api = new API(options.dsn, options._metadata);
this._api = new API(options.dsn, options._metadata, options.tunnel);
// eslint-disable-next-line deprecation/deprecation
this.url = this._api.getStoreEndpointWithUrlEncodedAuth();
}
Expand Down
10 changes: 10 additions & 0 deletions packages/browser/test/unit/transports/fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Event, Status, Transports } from '../../../src';

const testDsn = 'https://123@sentry.io/42';
const storeUrl = 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7';
const tunnel = 'https://hello.com/world';
const eventPayload: Event = {
event_id: '1337',
};
Expand Down Expand Up @@ -50,6 +51,15 @@ describe('FetchTransport', () => {
).equal(true);
});

it('sends a request to tunnel if configured', async () => {
transport = new Transports.FetchTransport({ dsn: testDsn, tunnel }, window.fetch);
fetch.returns(Promise.resolve({ status: 200, headers: new Headers() }));

await transport.sendEvent(eventPayload);

expect(fetch.calledWith(tunnel)).equal(true);
});

it('rejects with non-200 status code', async () => {
const response = { status: 403, headers: new Headers() };

Expand Down
10 changes: 10 additions & 0 deletions packages/browser/test/unit/transports/xhr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Event, Status, Transports } from '../../../src';
const testDsn = 'https://123@sentry.io/42';
const storeUrl = 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7';
const envelopeUrl = 'https://sentry.io/api/42/envelope/?sentry_key=123&sentry_version=7';
const tunnel = 'https://hello.com/world';
const eventPayload: Event = {
event_id: '1337',
};
Expand Down Expand Up @@ -46,6 +47,15 @@ describe('XHRTransport', () => {
expect(JSON.parse(request.requestBody)).deep.equal(eventPayload);
});

it('sends a request to tunnel if configured', async () => {
transport = new Transports.XHRTransport({ dsn: testDsn, tunnel });
server.respondWith('POST', tunnel, [200, {}, '']);

await transport.sendEvent(eventPayload);

expect(server.requests[0].url).equal(tunnel);
});

it('rejects with non-200 status code', async () => {
server.respondWith('POST', storeUrl, [403, {}, '']);

Expand Down
30 changes: 23 additions & 7 deletions packages/core/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,30 @@ export class API {
/** The internally used Dsn object. */
private readonly _dsnObject: Dsn;

/** The envelope tunnel to use. */
private readonly _tunnel?: string;

/** Create a new instance of API */
public constructor(dsn: DsnLike, metadata: SdkMetadata = {}) {
public constructor(dsn: DsnLike, metadata: SdkMetadata = {}, tunnel?: string) {
this.dsn = dsn;
this._dsnObject = new Dsn(dsn);
this.metadata = metadata;
this._tunnel = tunnel;
}

/** Returns the Dsn object. */
public getDsn(): Dsn {
return this._dsnObject;
}

/** Does this transport force envelopes? */
public forceEnvelope(): boolean {
return !!this._tunnel;
}

/** Returns the prefix to construct Sentry ingestion API endpoints. */
public getBaseApiEndpoint(): string {
const dsn = this._dsnObject;
const dsn = this.getDsn();
const protocol = dsn.protocol ? `${dsn.protocol}:` : '';
const port = dsn.port ? `:${dsn.port}` : '';
return `${protocol}//${dsn.host}${port}${dsn.path ? `/${dsn.path}` : ''}/api/`;
Expand All @@ -58,12 +67,16 @@ export class API {
* Sending auth as part of the query string and not as custom HTTP headers avoids CORS preflight requests.
*/
public getEnvelopeEndpointWithUrlEncodedAuth(): string {
if (this.forceEnvelope()) {
return this._tunnel as string;
}

return `${this._getEnvelopeEndpoint()}?${this._encodedAuth()}`;
}

/** Returns only the path component for the store endpoint. */
public getStoreEndpointPath(): string {
const dsn = this._dsnObject;
const dsn = this.getDsn();
return `${dsn.path ? `/${dsn.path}` : ''}/api/${dsn.projectId}/store/`;
}

Expand All @@ -73,7 +86,7 @@ export class API {
*/
public getRequestHeaders(clientName: string, clientVersion: string): { [key: string]: string } {
// CHANGE THIS to use metadata but keep clientName and clientVersion compatible
const dsn = this._dsnObject;
const dsn = this.getDsn();
const header = [`Sentry sentry_version=${SENTRY_API_VERSION}`];
header.push(`sentry_client=${clientName}/${clientVersion}`);
header.push(`sentry_key=${dsn.publicKey}`);
Expand All @@ -94,7 +107,7 @@ export class API {
user?: { name?: string; email?: string };
} = {},
): string {
const dsn = this._dsnObject;
const dsn = this.getDsn();
const endpoint = `${this.getBaseApiEndpoint()}embed/error-page/`;

const encodedOptions = [];
Expand Down Expand Up @@ -132,14 +145,17 @@ export class API {

/** Returns the ingest API endpoint for target. */
private _getIngestEndpoint(target: 'store' | 'envelope'): string {
if (this._tunnel) {
return this._tunnel;
}
const base = this.getBaseApiEndpoint();
const dsn = this._dsnObject;
const dsn = this.getDsn();
return `${base}${dsn.projectId}/${target}/`;
}

/** Returns a URL-encoded string with auth config suitable for a query string. */
private _encodedAuth(): string {
const dsn = this._dsnObject;
const dsn = this.getDsn();
const auth = {
// We send only the minimum set of required information. See
// https://github.com/getsentry/sentry-javascript/issues/2572.
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function sessionToSentryRequest(session: Session | SessionAggregates, api
export function eventToSentryRequest(event: Event, api: API): SentryRequest {
const sdkInfo = getSdkMetadataForEnvelopeHeader(api);
const eventType = event.type || 'event';
const useEnvelope = eventType === 'transaction';
const useEnvelope = eventType === 'transaction' || api.forceEnvelope();

const { transactionSampling, ...metadata } = event.debug_meta || {};
const { method: samplingMethod, rate: sampleRate } = transactionSampling || {};
Expand All @@ -78,6 +78,7 @@ export function eventToSentryRequest(event: Event, api: API): SentryRequest {
event_id: event.event_id,
sent_at: new Date().toISOString(),
...(sdkInfo && { sdk: sdkInfo }),
...(api.forceEnvelope() && { dsn: api.getDsn().toString() }),
});
const itemHeaders = JSON.stringify({
type: event.type,
Expand Down
8 changes: 8 additions & 0 deletions packages/core/test/lib/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { API } from '../../src/api';
const ingestDsn = 'https://abc@xxxx.ingest.sentry.io:1234/subpath/123';
const dsnPublic = 'https://abc@sentry.io:1234/subpath/123';
const legacyDsn = 'https://abc:123@sentry.io:1234/subpath/123';
const tunnel = 'https://hello.com/world';

describe('API', () => {
test('getStoreEndpoint', () => {
Expand All @@ -15,6 +16,13 @@ describe('API', () => {
expect(new API(ingestDsn).getStoreEndpoint()).toEqual('https://xxxx.ingest.sentry.io:1234/subpath/api/123/store/');
});

test('getEnvelopeEndpoint', () => {
expect(new API(dsnPublic).getEnvelopeEndpointWithUrlEncodedAuth()).toEqual(
'https://sentry.io:1234/subpath/api/123/envelope/?sentry_key=abc&sentry_version=7',
);
expect(new API(dsnPublic, {}, tunnel).getEnvelopeEndpointWithUrlEncodedAuth()).toEqual(tunnel);
});

test('getRequestHeaders', () => {
expect(new API(dsnPublic).getRequestHeaders('a', '1.0')).toMatchObject({
'Content-Type': 'application/json',
Expand Down
55 changes: 45 additions & 10 deletions packages/core/test/lib/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,33 @@ const api = new API('https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.s
},
});

const ingestDsn = 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012';
const tunnel = 'https://hello.com/world';

function parseEnvelopeRequest(request: SentryRequest): any {
const [envelopeHeaderString, itemHeaderString, eventString] = request.body.split('\n');

return {
envelopeHeader: JSON.parse(envelopeHeaderString),
itemHeader: JSON.parse(itemHeaderString),
event: JSON.parse(eventString),
};
}

describe('eventToSentryRequest', () => {
let api: API;
let event: Event;
function parseEnvelopeRequest(request: SentryRequest): any {
const [envelopeHeaderString, itemHeaderString, eventString] = request.body.split('\n');

return {
envelopeHeader: JSON.parse(envelopeHeaderString),
itemHeader: JSON.parse(itemHeaderString),
event: JSON.parse(eventString),
};
}

beforeEach(() => {
api = new API(ingestDsn, {
sdk: {
integrations: ['AWSLambda'],
name: 'sentry.javascript.browser',
version: `12.31.12`,
packages: [{ name: 'npm:@sentry/browser', version: `12.31.12` }],
},
});

event = {
contexts: { trace: { trace_id: '1231201211212012', span_id: '12261980', op: 'pageload' } },
environment: 'dogpark',
Expand All @@ -37,7 +51,7 @@ describe('eventToSentryRequest', () => {
};
});

it(`adds transaction sampling information to item header`, () => {
it('adds transaction sampling information to item header', () => {
event.debug_meta = { transactionSampling: { method: TransactionSamplingMethod.Rate, rate: 0.1121 } };

const result = eventToSentryRequest(event, api);
Expand Down Expand Up @@ -124,6 +138,27 @@ describe('eventToSentryRequest', () => {
}),
);
});

it('uses tunnel as the url if it is configured', () => {
api = new API(ingestDsn, {}, tunnel);

const result = eventToSentryRequest(event, api);

expect(result.url).toEqual(tunnel);
});

it('adds dsn to envelope header if tunnel is configured', () => {
api = new API(ingestDsn, {}, tunnel);

const result = eventToSentryRequest(event, api);
const envelope = parseEnvelopeRequest(result);

expect(envelope.envelopeHeader).toEqual(
expect.objectContaining({
dsn: ingestDsn,
}),
);
});
});

describe('sessionToSentryRequest', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/node/src/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export class NodeBackend extends BaseBackend<NodeOptions> {
...(this._options.httpsProxy && { httpsProxy: this._options.httpsProxy }),
...(this._options.caCerts && { caCerts: this._options.caCerts }),
dsn: this._options.dsn,
tunnel: this._options.tunnel,
_metadata: this._options._metadata,
};

Expand Down
2 changes: 1 addition & 1 deletion packages/node/src/transports/base/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export abstract class BaseTransport implements Transport {

/** Create instance and set this.dsn */
public constructor(public options: TransportOptions) {
this._api = new API(options.dsn, options._metadata);
this._api = new API(options.dsn, options._metadata, options.tunnel);
}

/** Default function used to parse URLs */
Expand Down
14 changes: 14 additions & 0 deletions packages/node/test/transports/http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const mockSetEncoding = jest.fn();
const dsn = 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622';
const storePath = '/mysubpath/api/50622/store/';
const envelopePath = '/mysubpath/api/50622/envelope/';
const tunnel = 'https://hello.com/world';
const eventPayload: Event = {
event_id: '1337',
};
Expand Down Expand Up @@ -159,6 +160,19 @@ describe('HTTPTransport', () => {
}
});

test('sends a request to tunnel if configured', async () => {
const transport = createTransport({ dsn, tunnel });

await transport.sendEvent({
message: 'test',
});

const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0];
expect(requestOptions.protocol).toEqual('https:');
expect(requestOptions.hostname).toEqual('hello.com');
expect(requestOptions.path).toEqual('/world');
});

test('back-off using retry-after header', async () => {
const retryAfterSeconds = 10;
mockReturnCode = 429;
Expand Down
14 changes: 14 additions & 0 deletions packages/node/test/transports/https.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const mockSetEncoding = jest.fn();
const dsn = 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622';
const storePath = '/mysubpath/api/50622/store/';
const envelopePath = '/mysubpath/api/50622/envelope/';
const tunnel = 'https://hello.com/world';
const sessionsPayload: SessionAggregates = {
attrs: { environment: 'test', release: '1.0' },
aggregates: [{ started: '2021-03-17T16:00:00.000Z', exited: 1 }],
Expand Down Expand Up @@ -144,6 +145,19 @@ describe('HTTPSTransport', () => {
}
});

test('sends a request to tunnel if configured', async () => {
const transport = createTransport({ dsn, tunnel });

await transport.sendEvent({
message: 'test',
});

const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0];
expect(requestOptions.protocol).toEqual('https:');
expect(requestOptions.hostname).toEqual('hello.com');
expect(requestOptions.path).toEqual('/world');
});

test('back-off using retry-after header', async () => {
const retryAfterSeconds = 10;
mockReturnCode = 429;
Expand Down
7 changes: 7 additions & 0 deletions packages/types/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ export interface Options {
*/
transportOptions?: TransportOptions;

/**
* A URL to an envelope tunnel endpoint. An envelope tunnel is an HTTP endpoint
* that accepts Sentry envelopes for forwarding. This can be used to force data
* through a custom server independent of the type of data.
*/
tunnel?: string;

/**
* The release identifier used when uploading respective source maps. Specify
* this value to allow Sentry to resolve the correct source maps when
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export interface TransportOptions {
caCerts?: string;
/** Fetch API init parameters */
fetchParameters?: { [key: string]: string };
/** The envelope tunnel to use. */
tunnel?: string;
/**
* Set of metadata about the SDK that can be internally used to enhance envelopes and events,
* and provide additional data about every request.
Expand Down