Skip to content

Commit de3421f

Browse files
author
Luca Forstner
committed
Merge remote-tracking branch 'origin/master' into lforst-node-transports
2 parents 22b0dba + 249302a commit de3421f

File tree

27 files changed

+1008
-36
lines changed

27 files changed

+1008
-36
lines changed

packages/browser/src/backend.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { BaseBackend } from '@sentry/core';
2-
import { Event, EventHint, Options, Severity, Transport } from '@sentry/types';
1+
import { BaseBackend, getEnvelopeEndpointWithUrlEncodedAuth, initAPIDetails } from '@sentry/core';
2+
import { Event, EventHint, Options, Severity, Transport, TransportOptions } from '@sentry/types';
33
import { supportsFetch } from '@sentry/utils';
44

55
import { eventFromException, eventFromMessage } from './eventbuilder';
6-
import { FetchTransport, XHRTransport } from './transports';
6+
import { FetchTransport, makeNewFetchTransport, XHRTransport } from './transports';
77

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

61-
const transportOptions = {
61+
const transportOptions: TransportOptions = {
6262
...this._options.transportOptions,
6363
dsn: this._options.dsn,
6464
tunnel: this._options.tunnel,
6565
sendClientReports: this._options.sendClientReports,
6666
_metadata: this._options._metadata,
6767
};
6868

69+
const api = initAPIDetails(transportOptions.dsn, transportOptions._metadata, transportOptions.tunnel);
70+
const url = getEnvelopeEndpointWithUrlEncodedAuth(api.dsn, api.tunnel);
71+
6972
if (this._options.transport) {
7073
return new this._options.transport(transportOptions);
7174
}
7275
if (supportsFetch()) {
76+
const requestOptions: RequestInit = { ...transportOptions.fetchParameters };
77+
this._newTransport = makeNewFetchTransport({ requestOptions, url });
7378
return new FetchTransport(transportOptions);
7479
}
7580
return new XHRTransport(transportOptions);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export { BaseTransport } from './base';
22
export { FetchTransport } from './fetch';
33
export { XHRTransport } from './xhr';
4+
5+
export { makeNewFetchTransport } from './new-fetch';
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {
2+
BaseTransportOptions,
3+
createTransport,
4+
NewTransport,
5+
TransportMakeRequestResponse,
6+
TransportRequest,
7+
} from '@sentry/core';
8+
9+
import { FetchImpl, getNativeFetchImplementation } from './utils';
10+
11+
export interface FetchTransportOptions extends BaseTransportOptions {
12+
requestOptions?: RequestInit;
13+
}
14+
15+
/**
16+
* Creates a Transport that uses the Fetch API to send events to Sentry.
17+
*/
18+
export function makeNewFetchTransport(
19+
options: FetchTransportOptions,
20+
nativeFetch: FetchImpl = getNativeFetchImplementation(),
21+
): NewTransport {
22+
function makeRequest(request: TransportRequest): PromiseLike<TransportMakeRequestResponse> {
23+
const requestOptions: RequestInit = {
24+
body: request.body,
25+
method: 'POST',
26+
referrerPolicy: 'origin',
27+
...options.requestOptions,
28+
};
29+
30+
return nativeFetch(options.url, requestOptions).then(response => {
31+
return response.text().then(body => ({
32+
body,
33+
headers: {
34+
'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'),
35+
'retry-after': response.headers.get('Retry-After'),
36+
},
37+
reason: response.statusText,
38+
statusCode: response.status,
39+
}));
40+
});
41+
}
42+
43+
return createTransport({ bufferSize: options.bufferSize }, makeRequest);
44+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { EventEnvelope, EventItem } from '@sentry/types';
2+
import { createEnvelope, serializeEnvelope } from '@sentry/utils';
3+
4+
import { FetchTransportOptions, makeNewFetchTransport } from '../../../src/transports/new-fetch';
5+
import { FetchImpl } from '../../../src/transports/utils';
6+
7+
const DEFAULT_FETCH_TRANSPORT_OPTIONS: FetchTransportOptions = {
8+
url: 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7',
9+
};
10+
11+
const ERROR_ENVELOPE = createEnvelope<EventEnvelope>({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [
12+
[{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem,
13+
]);
14+
15+
class Headers {
16+
headers: { [key: string]: string } = {};
17+
get(key: string) {
18+
return this.headers[key] || null;
19+
}
20+
set(key: string, value: string) {
21+
this.headers[key] = value;
22+
}
23+
}
24+
25+
describe('NewFetchTransport', () => {
26+
it('calls fetch with the given URL', async () => {
27+
const mockFetch = jest.fn(() =>
28+
Promise.resolve({
29+
headers: new Headers(),
30+
status: 200,
31+
text: () => Promise.resolve({}),
32+
}),
33+
) as unknown as FetchImpl;
34+
const transport = makeNewFetchTransport(DEFAULT_FETCH_TRANSPORT_OPTIONS, mockFetch);
35+
36+
expect(mockFetch).toHaveBeenCalledTimes(0);
37+
const res = await transport.send(ERROR_ENVELOPE);
38+
expect(mockFetch).toHaveBeenCalledTimes(1);
39+
40+
expect(res.status).toBe('success');
41+
42+
expect(mockFetch).toHaveBeenLastCalledWith(DEFAULT_FETCH_TRANSPORT_OPTIONS.url, {
43+
body: serializeEnvelope(ERROR_ENVELOPE),
44+
method: 'POST',
45+
referrerPolicy: 'origin',
46+
});
47+
});
48+
49+
it('sets rate limit headers', async () => {
50+
const headers = {
51+
get: jest.fn(),
52+
};
53+
54+
const mockFetch = jest.fn(() =>
55+
Promise.resolve({
56+
headers,
57+
status: 200,
58+
text: () => Promise.resolve({}),
59+
}),
60+
) as unknown as FetchImpl;
61+
const transport = makeNewFetchTransport(DEFAULT_FETCH_TRANSPORT_OPTIONS, mockFetch);
62+
63+
expect(headers.get).toHaveBeenCalledTimes(0);
64+
await transport.send(ERROR_ENVELOPE);
65+
66+
expect(headers.get).toHaveBeenCalledTimes(2);
67+
expect(headers.get).toHaveBeenCalledWith('X-Sentry-Rate-Limits');
68+
expect(headers.get).toHaveBeenCalledWith('Retry-After');
69+
});
70+
71+
it('allows for custom options to be passed in', async () => {
72+
const mockFetch = jest.fn(() =>
73+
Promise.resolve({
74+
headers: new Headers(),
75+
status: 200,
76+
text: () => Promise.resolve({}),
77+
}),
78+
) as unknown as FetchImpl;
79+
80+
const REQUEST_OPTIONS: RequestInit = {
81+
referrerPolicy: 'strict-origin',
82+
keepalive: true,
83+
referrer: 'http://example.org',
84+
};
85+
86+
const transport = makeNewFetchTransport(
87+
{ ...DEFAULT_FETCH_TRANSPORT_OPTIONS, requestOptions: REQUEST_OPTIONS },
88+
mockFetch,
89+
);
90+
91+
await transport.send(ERROR_ENVELOPE);
92+
expect(mockFetch).toHaveBeenLastCalledWith(DEFAULT_FETCH_TRANSPORT_OPTIONS.url, {
93+
body: serializeEnvelope(ERROR_ENVELOPE),
94+
method: 'POST',
95+
...REQUEST_OPTIONS,
96+
});
97+
});
98+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Sentry.captureException(new Error('this is an error'));
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { expect } from '@playwright/test';
2+
import { Event } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers';
6+
7+
sentryTest('should capture an error with the new fetch transport', async ({ getLocalTestPath, page }) => {
8+
const url = await getLocalTestPath({ testDir: __dirname });
9+
10+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
11+
12+
expect(eventData.exception?.values).toHaveLength(1);
13+
expect(eventData.exception?.values?.[0]).toMatchObject({
14+
type: 'Error',
15+
value: 'this is an error',
16+
mechanism: {
17+
type: 'generic',
18+
handled: true,
19+
},
20+
});
21+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const transaction = Sentry.startTransaction({ name: 'test_transaction_1' });
2+
transaction.finish();
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { expect } from '@playwright/test';
2+
import { Event } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers';
6+
7+
sentryTest('should report a transaction with the new fetch transport', async ({ getLocalTestPath, page }) => {
8+
const url = await getLocalTestPath({ testDir: __dirname });
9+
const transaction = await getFirstSentryEnvelopeRequest<Event>(page, url);
10+
11+
expect(transaction.transaction).toBe('test_transaction_1');
12+
expect(transaction.spans).toBeDefined();
13+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as Sentry from '@sentry/browser';
2+
// eslint-disable-next-line no-unused-vars
3+
import * as _ from '@sentry/tracing';
4+
5+
window.Sentry = Sentry;
6+
7+
Sentry.init({
8+
dsn: 'https://[email protected]/1337',
9+
_experiments: {
10+
newTransport: true,
11+
},
12+
tracesSampleRate: 1.0,
13+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Integration Tests for Sentry Node.JS SDK
2+
3+
## Structure
4+
5+
```
6+
suites/
7+
|---- public-api/
8+
|---- captureMessage/
9+
|---- test.ts [assertions]
10+
|---- scenario.ts [Sentry initialization and test subject]
11+
|---- customTest/
12+
|---- test.ts [assertions]
13+
|---- scenario_1.ts [optional extra test scenario]
14+
|---- scenario_2.ts [optional extra test scenario]
15+
|---- server_with_mongo.ts [optional custom server]
16+
|---- server_with_postgres.ts [optional custom server]
17+
utils/
18+
|---- defaults/
19+
|---- server.ts [default Express server configuration]
20+
```
21+
22+
The tests are grouped by their scopes, such as `public-api` or `tracing`. In every group of tests, there are multiple folders containing test scenarios and assertions.
23+
24+
Tests run on Express servers (a server instance per test). By default, a simple server template inside `utils/defaults/server.ts` is used. Every server instance runs on a different port.
25+
26+
A custom server configuration can be used, supplying a script that exports a valid express server instance as default. `runServer` utility function accepts an optional `serverPath` argument for this purpose.
27+
28+
`scenario.ts` contains the initialization logic and the test subject. By default, `{TEST_DIR}/scenario.ts` is used, but `runServer` also accepts an optional `scenarioPath` argument for non-standard usage.
29+
30+
`test.ts` is required for each test case, and contains the server runner logic, request interceptors for Sentry requests, and assertions. Test server, interceptors and assertions are all run on the same Jest thread.
31+
32+
### Utilities
33+
34+
`utils/` contains helpers and Sentry-specific assertions that can be used in (`test.ts`).
35+
36+
## Running Tests Locally
37+
38+
Tests can be run locally with:
39+
40+
`yarn test`
41+
42+
To run tests with Jest's watch mode:
43+
44+
`yarn test:jest`
45+
46+
To filter tests by their title:
47+
48+
`yarn test -t "set different properties of a scope"`
49+
50+
You can refer to [Jest documentation](https://jestjs.io/docs/cli) for other CLI options.

packages/node-integration-tests/package.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,19 @@
1111
"lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish",
1212
"lint:prettier": "prettier --check \"{suites,utils}/**/*.ts\"",
1313
"type-check": "tsc",
14-
"test": "jest --detectOpenHandles --runInBand --forceExit"
14+
"test": "jest --detectOpenHandles --runInBand --forceExit",
15+
"test:watch": "yarn test --watch"
1516
},
1617
"dependencies": {
18+
"@types/mongodb": "^3.6.20",
19+
"@types/mysql": "^2.15.21",
20+
"@types/pg": "^8.6.5",
1721
"express": "^4.17.3",
22+
"mysql": "^2.18.1",
23+
"mongodb": "^3.7.3",
24+
"mongodb-memory-server": "^8.4.1",
1825
"nock": "^13.1.0",
26+
"pg": "^8.7.3",
1927
"portfinder": "^1.0.28"
2028
}
2129
}

packages/node-integration-tests/suites/tracing/manual-tracing-unnamed-import/test.ts renamed to packages/node-integration-tests/suites/public-api/startTransaction/basic-usage/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { assertSentryTransaction, getEnvelopeRequest, runServer } from '../../../utils';
1+
import { assertSentryTransaction, getEnvelopeRequest, runServer } from '../../../../utils';
22

33
test('should send a manually started transaction when @sentry/tracing is imported using unnamed import.', async () => {
44
const url = await runServer(__dirname);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import '@sentry/tracing';
2+
3+
import * as Sentry from '@sentry/node';
4+
5+
Sentry.init({
6+
dsn: 'https://[email protected]/1337',
7+
release: '1.0',
8+
tracesSampleRate: 1.0,
9+
});
10+
11+
const transaction = Sentry.startTransaction({ name: 'test_transaction_1' });
12+
const span_1 = transaction.startChild({
13+
op: 'span_1',
14+
data: {
15+
foo: 'bar',
16+
baz: [1, 2, 3],
17+
},
18+
});
19+
for (let i = 0; i < 2000; i++);
20+
21+
// span_1 finishes
22+
span_1.finish();
23+
24+
// span_2 doesn't finish
25+
transaction.startChild({ op: 'span_2' });
26+
for (let i = 0; i < 4000; i++);
27+
28+
const span_3 = transaction.startChild({ op: 'span_3' });
29+
for (let i = 0; i < 4000; i++);
30+
31+
// span_4 is the child of span_3 but doesn't finish.
32+
span_3.startChild({ op: 'span_4', data: { qux: 'quux' } });
33+
34+
// span_5 is another child of span_3 but finishes.
35+
span_3.startChild({ op: 'span_5' }).finish();
36+
37+
// span_3 also finishes
38+
span_3.finish();
39+
40+
transaction.finish();
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { assertSentryTransaction, getEnvelopeRequest, runServer } from '../../../../utils';
2+
3+
test('should report finished spans as children of the root transaction.', async () => {
4+
const url = await runServer(__dirname);
5+
const envelope = await getEnvelopeRequest(url);
6+
7+
expect(envelope).toHaveLength(3);
8+
9+
const rootSpanId = (envelope?.[2] as any)?.contexts?.trace?.span_id;
10+
const span3Id = (envelope?.[2] as any)?.spans?.[1].span_id;
11+
12+
expect(rootSpanId).toEqual(expect.any(String));
13+
expect(span3Id).toEqual(expect.any(String));
14+
15+
assertSentryTransaction(envelope[2], {
16+
transaction: 'test_transaction_1',
17+
spans: [
18+
{
19+
op: 'span_1',
20+
data: {
21+
foo: 'bar',
22+
baz: [1, 2, 3],
23+
},
24+
parent_span_id: rootSpanId,
25+
},
26+
{
27+
op: 'span_3',
28+
parent_span_id: rootSpanId,
29+
},
30+
{
31+
op: 'span_5',
32+
parent_span_id: span3Id,
33+
},
34+
],
35+
});
36+
});

0 commit comments

Comments
 (0)