Skip to content

Commit 9325e24

Browse files
committed
Merge remote-tracking branch 'upstream/7.x' into feat/attachments
2 parents dbd70d1 + 1c5bff0 commit 9325e24

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+701
-436
lines changed

MIGRATION.md

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,110 @@ const storeEndpoint = api.getStoreEndpointWithUrlEncodedAuth();
102102
const envelopeEndpoint = api.getEnvelopeEndpointWithUrlEncodedAuth();
103103
```
104104

105-
## Enum changes
105+
## Transport Changes
106+
107+
The `Transport` API was simplified and some functionality (e.g. APIDetails and client reports) was refactored and moved
108+
to the Client. To send data to Sentry, we switched from the previously used [Store endpoint](https://develop.sentry.dev/sdk/store/) to the [Envelopes endpoint](https://develop.sentry.dev/sdk/envelopes/).
109+
110+
This example shows the new v7 and the v6 Transport API:
111+
112+
```js
113+
// New in v7:
114+
export interface Transport {
115+
/* Sends an envelope to the Envelope endpoint in Sentry */
116+
send(request: Envelope): PromiseLike<void>;
117+
/* Waits for all events to be sent or the timeout to expire, whichever comes first */
118+
flush(timeout?: number): PromiseLike<boolean>;
119+
}
120+
121+
// Before:
122+
export interface Transport {
123+
/* Sends the event to the Store endpoint in Sentry */
124+
sendEvent(event: Event): PromiseLike<Response>;
125+
/* Sends the session to the Envelope endpoint in Sentry */
126+
sendSession?(session: Session | SessionAggregates): PromiseLike<Response>;
127+
/* Waits for all events to be sent or the timeout to expire, whichever comes first */
128+
close(timeout?: number): PromiseLike<boolean>;
129+
/* Increment the counter for the specific client outcome */
130+
recordLostEvent?(type: Outcome, category: SentryRequestType): void;
131+
}
132+
```
133+
134+
### Custom Transports
135+
If you rely on a custom transport, you will need to make some adjustments to how it is created when migrating
136+
to v7. Note that we changed our transports from a class-based to a functional approach, meaning that
137+
the previously class-based transports are now created via functions. This also means that custom transports
138+
are now passed by specifying a factory function in the `Sentry.init` options object instead passing the custom
139+
transport's class.
140+
141+
The following example shows how to create a custom transport in v7 vs. how it was done in v6:
142+
143+
```js
144+
// New in v7:
145+
import { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types';
146+
import { createTransport } from '@sentry/core';
147+
148+
export function makeMyCustomTransport(options: BaseTransportOptions): Transport {
149+
function makeRequest(request: TransportRequest): PromiseLike<TransportMakeRequestResponse> {
150+
// this is where your sending logic goes
151+
const myCustomRequest = {
152+
body: request.body,
153+
url: options.url
154+
};
155+
// you define how `sendMyCustomRequest` works
156+
return sendMyCustomRequest(myCustomRequest).then(response => ({
157+
headers: {
158+
'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'),
159+
'retry-after': response.headers.get('Retry-After'),
160+
},
161+
}));
162+
}
163+
164+
// `createTransport` takes care of rate limiting and flushing
165+
return createTransport({ bufferSize: options.bufferSize }, makeRequest);
166+
}
167+
168+
Sentry.init({
169+
dsn: '...',
170+
transport: makeMyCustomTransport, // this function will be called when the client is initialized
171+
...
172+
})
173+
174+
175+
176+
// Before:
177+
class MyCustomTransport extends BaseTransport {
178+
constructor(options: TransportOptions) {
179+
// initialize your transport here
180+
super(options);
181+
}
182+
183+
public sendEvent(event: Event): PromiseLike<Response> {
184+
// this is where your sending logic goes
185+
// `url` is decoded from dsn in BaseTransport
186+
const myCustomRequest = createMyCustomRequestFromEvent(event, this.url);
187+
return sendMyCustomRequest(myCustomRequest).then(() => resolve({status: 'success'}));
188+
}
189+
190+
public sendSession(session: Session): PromiseLike<Response> {...}
191+
// ...
192+
}
193+
194+
Sentry.init({
195+
dsn: '...',
196+
transport: MyCustomTransport, // the constructor was called when the client was initialized
197+
...
198+
})
199+
```
200+
201+
Overall, the new way of transport creation allows you to create your custom sending implementation
202+
without having to deal with the conversion of events or sessions to envelopes.
203+
We recommend calling using the `createTransport` function from `@sentry/core` as demonstrated in the example above which, besides creating the `Transport`
204+
object with your custom logic, will also take care of rate limiting and flushing.
205+
206+
For a complete v7 transport implementation, take a look at our [browser fetch transport](https://github.com/getsentry/sentry-javascript/blob/ebc938a03d6efe7d0c4bbcb47714e84c9a566a9c/packages/browser/src/transports/fetch.ts#L1-L34).
207+
208+
## Enum Changes
106209

107210
Given that enums have a high bundle-size impact, our long term goal is to eventually remove all enums from the SDK in
108211
favor of string literals.

packages/browser/src/client.ts

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { BaseClient, Scope, SDK_VERSION } from '@sentry/core';
22
import { AttachmentItem, ClientOptions, Event, EventHint, Options, Severity, SeverityLevel } from '@sentry/types';
3-
import { getGlobalObject, logger } from '@sentry/utils';
43

54
import { eventFromException, eventFromMessage } from './eventbuilder';
6-
import { IS_DEBUG_BUILD } from './flags';
7-
import { injectReportDialog, ReportDialogOptions } from './helpers';
85
import { Breadcrumbs } from './integrations';
96

107
export interface BaseBrowserOptions {
@@ -62,29 +59,6 @@ export class BrowserClient extends BaseClient<BrowserClientOptions> {
6259
super(options);
6360
}
6461

65-
/**
66-
* Show a report dialog to the user to send feedback to a specific event.
67-
*
68-
* @param options Set individual options for the dialog
69-
*/
70-
public showReportDialog(options: ReportDialogOptions = {}): void {
71-
// doesn't work without a document (React Native)
72-
const document = getGlobalObject<Window>().document;
73-
if (!document) {
74-
return;
75-
}
76-
77-
if (!this._isEnabled()) {
78-
IS_DEBUG_BUILD && logger.error('Trying to call showReportDialog with Sentry Client disabled');
79-
return;
80-
}
81-
82-
injectReportDialog({
83-
...options,
84-
dsn: options.dsn || this.getDsn(),
85-
});
86-
}
87-
8862
/**
8963
* @inheritDoc
9064
*/

packages/browser/src/exports.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,5 @@ export {
5252
opera11StackParser,
5353
winjsStackParser,
5454
} from './stack-parsers';
55-
export { injectReportDialog } from './helpers';
5655
export { defaultIntegrations, forceLoad, init, lastEventId, onLoad, showReportDialog, flush, close, wrap } from './sdk';
5756
export { SDK_NAME } from './version';

packages/browser/src/helpers.ts

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
1-
import { captureException, getReportDialogEndpoint, withScope } from '@sentry/core';
1+
import { captureException, withScope } from '@sentry/core';
22
import { DsnLike, Event as SentryEvent, Mechanism, Scope, WrappedFunction } from '@sentry/types';
33
import {
44
addExceptionMechanism,
55
addExceptionTypeValue,
66
addNonEnumerableProperty,
7-
getGlobalObject,
87
getOriginalFunction,
9-
logger,
108
markFunctionWrapped,
119
} from '@sentry/utils';
1210

13-
import { IS_DEBUG_BUILD } from './flags';
14-
15-
const global = getGlobalObject<Window>();
1611
let ignoreOnError: number = 0;
1712

1813
/**
@@ -182,38 +177,3 @@ export interface ReportDialogOptions {
182177
/** Callback after reportDialog showed up */
183178
onLoad?(): void;
184179
}
185-
186-
/**
187-
* Injects the Report Dialog script
188-
* @hidden
189-
*/
190-
export function injectReportDialog(options: ReportDialogOptions = {}): void {
191-
if (!global.document) {
192-
return;
193-
}
194-
195-
if (!options.eventId) {
196-
IS_DEBUG_BUILD && logger.error('Missing eventId option in showReportDialog call');
197-
return;
198-
}
199-
200-
if (!options.dsn) {
201-
IS_DEBUG_BUILD && logger.error('Missing dsn option in showReportDialog call');
202-
return;
203-
}
204-
205-
const script = global.document.createElement('script');
206-
script.async = true;
207-
script.src = getReportDialogEndpoint(options.dsn, options);
208-
209-
if (options.onLoad) {
210-
// eslint-disable-next-line @typescript-eslint/unbound-method
211-
script.onload = options.onLoad;
212-
}
213-
214-
const injectionPoint = global.document.head || global.document.body;
215-
216-
if (injectionPoint) {
217-
injectionPoint.appendChild(script);
218-
}
219-
}

packages/browser/src/sdk.ts

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import { getCurrentHub, getIntegrationsToSetup, initAndBind, Integrations as CoreIntegrations } from '@sentry/core';
2-
import { Hub } from '@sentry/types';
1+
import {
2+
getCurrentHub,
3+
getIntegrationsToSetup,
4+
getReportDialogEndpoint,
5+
Hub,
6+
initAndBind,
7+
Integrations as CoreIntegrations,
8+
} from '@sentry/core';
39
import {
410
addInstrumentationHandler,
511
getGlobalObject,
@@ -121,9 +127,21 @@ export function init(options: BrowserOptions = {}): void {
121127
*
122128
* @param options Everything is optional, we try to fetch all info need from the global scope.
123129
*/
124-
export function showReportDialog(options: ReportDialogOptions = {}): void {
125-
const hub = getCurrentHub();
126-
const scope = hub.getScope();
130+
export function showReportDialog(options: ReportDialogOptions = {}, hub: Hub = getCurrentHub()): void {
131+
// doesn't work without a document (React Native)
132+
const global = getGlobalObject<Window>();
133+
if (!global.document) {
134+
IS_DEBUG_BUILD && logger.error('Global document not defined in showReportDialog call');
135+
return;
136+
}
137+
138+
const { client, scope } = hub.getStackTop();
139+
const dsn = options.dsn || (client && client.getDsn());
140+
if (!dsn) {
141+
IS_DEBUG_BUILD && logger.error('DSN not configured for showReportDialog call');
142+
return;
143+
}
144+
127145
if (scope) {
128146
options.user = {
129147
...scope.getUser(),
@@ -134,9 +152,21 @@ export function showReportDialog(options: ReportDialogOptions = {}): void {
134152
if (!options.eventId) {
135153
options.eventId = hub.lastEventId();
136154
}
137-
const client = hub.getClient<BrowserClient>();
138-
if (client) {
139-
client.showReportDialog(options);
155+
156+
const script = global.document.createElement('script');
157+
script.async = true;
158+
script.src = getReportDialogEndpoint(dsn, options);
159+
160+
if (options.onLoad) {
161+
// eslint-disable-next-line @typescript-eslint/unbound-method
162+
script.onload = options.onLoad;
163+
}
164+
165+
const injectionPoint = global.document.head || global.document.body;
166+
if (injectionPoint) {
167+
injectionPoint.appendChild(script);
168+
} else {
169+
IS_DEBUG_BUILD && logger.error('Not injecting report dialog. No injection point found in HTML');
140170
}
141171
}
142172

packages/browser/src/transports/fetch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,5 @@ export function makeFetchTransport(
3030
}));
3131
}
3232

33-
return createTransport({ bufferSize: options.bufferSize }, makeRequest);
33+
return createTransport(options, makeRequest);
3434
}

packages/browser/src/transports/xhr.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,20 @@ export interface XHRTransportOptions extends BaseTransportOptions {
2121
*/
2222
export function makeXHRTransport(options: XHRTransportOptions): Transport {
2323
function makeRequest(request: TransportRequest): PromiseLike<TransportMakeRequestResponse> {
24-
return new SyncPromise<TransportMakeRequestResponse>((resolve, _reject) => {
24+
return new SyncPromise((resolve, reject) => {
2525
const xhr = new XMLHttpRequest();
2626

27+
xhr.onerror = reject;
28+
2729
xhr.onreadystatechange = (): void => {
2830
if (xhr.readyState === XHR_READYSTATE_DONE) {
29-
resolve({
31+
const response = {
3032
headers: {
3133
'x-sentry-rate-limits': xhr.getResponseHeader('X-Sentry-Rate-Limits'),
3234
'retry-after': xhr.getResponseHeader('Retry-After'),
3335
},
34-
});
36+
};
37+
resolve(response);
3538
}
3639
};
3740

@@ -47,5 +50,5 @@ export function makeXHRTransport(options: XHRTransportOptions): Transport {
4750
});
4851
}
4952

50-
return createTransport({ bufferSize: options.bufferSize }, makeRequest);
53+
return createTransport(options, makeRequest);
5154
}

packages/browser/test/unit/helper/browser-client-options.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { BrowserClientOptions } from '../../../src/client';
66
export function getDefaultBrowserClientOptions(options: Partial<BrowserClientOptions> = {}): BrowserClientOptions {
77
return {
88
integrations: [],
9-
transport: () => createTransport({}, _ => resolvedSyncPromise({})),
9+
transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})),
1010
stackParser: () => [],
1111
...options,
1212
};

packages/browser/test/unit/index.test.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SDK_VERSION } from '@sentry/core';
1+
import { getReportDialogEndpoint, SDK_VERSION } from '@sentry/core';
22

33
import {
44
addBreadcrumb,
@@ -24,6 +24,14 @@ const dsn = 'https://[email protected]/4291';
2424
// eslint-disable-next-line no-var
2525
declare var global: any;
2626

27+
jest.mock('@sentry/core', () => {
28+
const original = jest.requireActual('@sentry/core');
29+
return {
30+
...original,
31+
getReportDialogEndpoint: jest.fn(),
32+
};
33+
});
34+
2735
describe('SentryBrowser', () => {
2836
const beforeSend = jest.fn();
2937

@@ -74,16 +82,14 @@ describe('SentryBrowser', () => {
7482
});
7583

7684
describe('showReportDialog', () => {
85+
beforeEach(() => {
86+
(getReportDialogEndpoint as jest.Mock).mockReset();
87+
});
88+
7789
describe('user', () => {
7890
const EX_USER = { email: '[email protected]' };
7991
const options = getDefaultBrowserClientOptions({ dsn });
8092
const client = new BrowserClient(options);
81-
const reportDialogSpy = jest.spyOn(client, 'showReportDialog');
82-
83-
beforeEach(() => {
84-
reportDialogSpy.mockReset();
85-
});
86-
8793
it('uses the user on the scope', () => {
8894
configureScope(scope => {
8995
scope.setUser(EX_USER);
@@ -92,8 +98,11 @@ describe('SentryBrowser', () => {
9298

9399
showReportDialog();
94100

95-
expect(reportDialogSpy).toBeCalled();
96-
expect(reportDialogSpy.mock.calls[0][0]!.user!.email).toBe(EX_USER.email);
101+
expect(getReportDialogEndpoint).toHaveBeenCalledTimes(1);
102+
expect(getReportDialogEndpoint).toHaveBeenCalledWith(
103+
expect.any(Object),
104+
expect.objectContaining({ user: { email: EX_USER.email } }),
105+
);
97106
});
98107

99108
it('prioritizes options user over scope user', () => {
@@ -105,8 +114,11 @@ describe('SentryBrowser', () => {
105114
const DIALOG_OPTION_USER = { email: '[email protected]' };
106115
showReportDialog({ user: DIALOG_OPTION_USER });
107116

108-
expect(reportDialogSpy).toBeCalled();
109-
expect(reportDialogSpy.mock.calls[0][0]!.user!.email).toBe(DIALOG_OPTION_USER.email);
117+
expect(getReportDialogEndpoint).toHaveBeenCalledTimes(1);
118+
expect(getReportDialogEndpoint).toHaveBeenCalledWith(
119+
expect.any(Object),
120+
expect.objectContaining({ user: { email: DIALOG_OPTION_USER.email } }),
121+
);
110122
});
111123
});
112124
});

0 commit comments

Comments
 (0)