Skip to content

Commit 0e664af

Browse files
authored
ref(browser): Refactor sending client reports w. fetch fallback (#4008)
* ref(browser): Refactor sending client reports w. fetch fallback
1 parent d920532 commit 0e664af

File tree

3 files changed

+115
-84
lines changed

3 files changed

+115
-84
lines changed

packages/browser/src/transports/base.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
SentryError,
1818
} from '@sentry/utils';
1919

20+
import { sendReport } from './utils';
21+
2022
const CATEGORY_MAPPING: {
2123
[key in SentryRequestType]: string;
2224
} = {
@@ -99,11 +101,6 @@ export abstract class BaseTransport implements Transport {
99101
return;
100102
}
101103

102-
if (!global.navigator || typeof global.navigator.sendBeacon !== 'function') {
103-
logger.warn('Beacon API not available, skipping sending outcomes.');
104-
return;
105-
}
106-
107104
const outcomes = this._outcomes;
108105
this._outcomes = {};
109106

@@ -134,7 +131,11 @@ export abstract class BaseTransport implements Transport {
134131
});
135132
const envelope = `${envelopeHeader}\n${itemHeaders}\n${item}`;
136133

137-
global.navigator.sendBeacon(url, envelope);
134+
try {
135+
sendReport(url, envelope);
136+
} catch (e) {
137+
logger.error(e);
138+
}
138139
}
139140

140141
/**

packages/browser/src/transports/fetch.ts

Lines changed: 2 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,9 @@
11
import { eventToSentryRequest, sessionToSentryRequest } from '@sentry/core';
22
import { Event, Outcome, Response, SentryRequest, Session, TransportOptions } from '@sentry/types';
3-
import {
4-
getGlobalObject,
5-
isNativeFetch,
6-
logger,
7-
SentryError,
8-
supportsReferrerPolicy,
9-
SyncPromise,
10-
} from '@sentry/utils';
3+
import { SentryError, supportsReferrerPolicy, SyncPromise } from '@sentry/utils';
114

125
import { BaseTransport } from './base';
13-
14-
type FetchImpl = typeof fetch;
15-
16-
/**
17-
* A special usecase for incorrectly wrapped Fetch APIs in conjunction with ad-blockers.
18-
* Whenever someone wraps the Fetch API and returns the wrong promise chain,
19-
* this chain becomes orphaned and there is no possible way to capture it's rejections
20-
* other than allowing it bubble up to this very handler. eg.
21-
*
22-
* const f = window.fetch;
23-
* window.fetch = function () {
24-
* const p = f.apply(this, arguments);
25-
*
26-
* p.then(function() {
27-
* console.log('hi.');
28-
* });
29-
*
30-
* return p;
31-
* }
32-
*
33-
* `p.then(function () { ... })` is producing a completely separate promise chain,
34-
* however, what's returned is `p` - the result of original `fetch` call.
35-
*
36-
* This mean, that whenever we use the Fetch API to send our own requests, _and_
37-
* some ad-blocker blocks it, this orphaned chain will _always_ reject,
38-
* effectively causing another event to be captured.
39-
* This makes a whole process become an infinite loop, which we need to somehow
40-
* deal with, and break it in one way or another.
41-
*
42-
* To deal with this issue, we are making sure that we _always_ use the real
43-
* browser Fetch API, instead of relying on what `window.fetch` exposes.
44-
* The only downside to this would be missing our own requests as breadcrumbs,
45-
* but because we are already not doing this, it should be just fine.
46-
*
47-
* Possible failed fetch error messages per-browser:
48-
*
49-
* Chrome: Failed to fetch
50-
* Edge: Failed to Fetch
51-
* Firefox: NetworkError when attempting to fetch resource
52-
* Safari: resource blocked by content blocker
53-
*/
54-
function getNativeFetchImplementation(): FetchImpl {
55-
/* eslint-disable @typescript-eslint/unbound-method */
56-
57-
// Fast path to avoid DOM I/O
58-
const global = getGlobalObject<Window>();
59-
if (isNativeFetch(global.fetch)) {
60-
return global.fetch.bind(global);
61-
}
62-
63-
const document = global.document;
64-
let fetchImpl = global.fetch;
65-
// eslint-disable-next-line deprecation/deprecation
66-
if (typeof document?.createElement === `function`) {
67-
try {
68-
const sandbox = document.createElement('iframe');
69-
sandbox.hidden = true;
70-
document.head.appendChild(sandbox);
71-
if (sandbox.contentWindow?.fetch) {
72-
fetchImpl = sandbox.contentWindow.fetch;
73-
}
74-
document.head.removeChild(sandbox);
75-
} catch (e) {
76-
logger.warn('Could not create sandbox iframe for pure fetch check, bailing to window.fetch: ', e);
77-
}
78-
}
79-
80-
return fetchImpl.bind(global);
81-
/* eslint-enable @typescript-eslint/unbound-method */
82-
}
6+
import { FetchImpl, getNativeFetchImplementation } from './utils';
837

848
/** `fetch` based transport */
859
export class FetchTransport extends BaseTransport {
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { forget, getGlobalObject, isNativeFetch, logger, supportsFetch } from '@sentry/utils';
2+
3+
const global = getGlobalObject<Window>();
4+
let cachedFetchImpl: FetchImpl;
5+
6+
export type FetchImpl = typeof fetch;
7+
8+
/**
9+
* A special usecase for incorrectly wrapped Fetch APIs in conjunction with ad-blockers.
10+
* Whenever someone wraps the Fetch API and returns the wrong promise chain,
11+
* this chain becomes orphaned and there is no possible way to capture it's rejections
12+
* other than allowing it bubble up to this very handler. eg.
13+
*
14+
* const f = window.fetch;
15+
* window.fetch = function () {
16+
* const p = f.apply(this, arguments);
17+
*
18+
* p.then(function() {
19+
* console.log('hi.');
20+
* });
21+
*
22+
* return p;
23+
* }
24+
*
25+
* `p.then(function () { ... })` is producing a completely separate promise chain,
26+
* however, what's returned is `p` - the result of original `fetch` call.
27+
*
28+
* This mean, that whenever we use the Fetch API to send our own requests, _and_
29+
* some ad-blocker blocks it, this orphaned chain will _always_ reject,
30+
* effectively causing another event to be captured.
31+
* This makes a whole process become an infinite loop, which we need to somehow
32+
* deal with, and break it in one way or another.
33+
*
34+
* To deal with this issue, we are making sure that we _always_ use the real
35+
* browser Fetch API, instead of relying on what `window.fetch` exposes.
36+
* The only downside to this would be missing our own requests as breadcrumbs,
37+
* but because we are already not doing this, it should be just fine.
38+
*
39+
* Possible failed fetch error messages per-browser:
40+
*
41+
* Chrome: Failed to fetch
42+
* Edge: Failed to Fetch
43+
* Firefox: NetworkError when attempting to fetch resource
44+
* Safari: resource blocked by content blocker
45+
*/
46+
export function getNativeFetchImplementation(): FetchImpl {
47+
if (cachedFetchImpl) {
48+
return cachedFetchImpl;
49+
}
50+
51+
/* eslint-disable @typescript-eslint/unbound-method */
52+
53+
// Fast path to avoid DOM I/O
54+
if (isNativeFetch(global.fetch)) {
55+
return (cachedFetchImpl = global.fetch.bind(global));
56+
}
57+
58+
const document = global.document;
59+
let fetchImpl = global.fetch;
60+
// eslint-disable-next-line deprecation/deprecation
61+
if (typeof document?.createElement === `function`) {
62+
try {
63+
const sandbox = document.createElement('iframe');
64+
sandbox.hidden = true;
65+
document.head.appendChild(sandbox);
66+
if (sandbox.contentWindow?.fetch) {
67+
fetchImpl = sandbox.contentWindow.fetch;
68+
}
69+
document.head.removeChild(sandbox);
70+
} catch (e) {
71+
logger.warn('Could not create sandbox iframe for pure fetch check, bailing to window.fetch: ', e);
72+
}
73+
}
74+
75+
return (cachedFetchImpl = fetchImpl.bind(global));
76+
/* eslint-enable @typescript-eslint/unbound-method */
77+
}
78+
79+
/**
80+
* Sends sdk client report using sendBeacon or fetch as a fallback if available
81+
*
82+
* @param url report endpoint
83+
* @param body report payload
84+
*/
85+
export function sendReport(url: string, body: string): void {
86+
const isRealNavigator = Object.prototype.toString.call(global && global.navigator) === '[object Navigator]';
87+
const hasSendBeacon = isRealNavigator && typeof global.navigator.sendBeacon === 'function';
88+
89+
if (hasSendBeacon) {
90+
// Prevent illegal invocations - https://xgwang.me/posts/you-may-not-know-beacon/#it-may-throw-error%2C-be-sure-to-catch
91+
const sendBeacon = global.navigator.sendBeacon.bind(global.navigator);
92+
return sendBeacon(url, body);
93+
}
94+
95+
if (supportsFetch()) {
96+
const fetch = getNativeFetchImplementation();
97+
return forget(
98+
fetch(url, {
99+
body,
100+
method: 'POST',
101+
credentials: 'omit',
102+
keepalive: true,
103+
}),
104+
);
105+
}
106+
}

0 commit comments

Comments
 (0)