-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ref(browser): Refactor sending client reports w. fetch fallback (#4008)
* ref(browser): Refactor sending client reports w. fetch fallback
- Loading branch information
1 parent
d920532
commit 0e664af
Showing
3 changed files
with
115 additions
and
84 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import { forget, getGlobalObject, isNativeFetch, logger, supportsFetch } from '@sentry/utils'; | ||
|
||
const global = getGlobalObject<Window>(); | ||
let cachedFetchImpl: FetchImpl; | ||
|
||
export type FetchImpl = typeof fetch; | ||
|
||
/** | ||
* A special usecase for incorrectly wrapped Fetch APIs in conjunction with ad-blockers. | ||
* Whenever someone wraps the Fetch API and returns the wrong promise chain, | ||
* this chain becomes orphaned and there is no possible way to capture it's rejections | ||
* other than allowing it bubble up to this very handler. eg. | ||
* | ||
* const f = window.fetch; | ||
* window.fetch = function () { | ||
* const p = f.apply(this, arguments); | ||
* | ||
* p.then(function() { | ||
* console.log('hi.'); | ||
* }); | ||
* | ||
* return p; | ||
* } | ||
* | ||
* `p.then(function () { ... })` is producing a completely separate promise chain, | ||
* however, what's returned is `p` - the result of original `fetch` call. | ||
* | ||
* This mean, that whenever we use the Fetch API to send our own requests, _and_ | ||
* some ad-blocker blocks it, this orphaned chain will _always_ reject, | ||
* effectively causing another event to be captured. | ||
* This makes a whole process become an infinite loop, which we need to somehow | ||
* deal with, and break it in one way or another. | ||
* | ||
* To deal with this issue, we are making sure that we _always_ use the real | ||
* browser Fetch API, instead of relying on what `window.fetch` exposes. | ||
* The only downside to this would be missing our own requests as breadcrumbs, | ||
* but because we are already not doing this, it should be just fine. | ||
* | ||
* Possible failed fetch error messages per-browser: | ||
* | ||
* Chrome: Failed to fetch | ||
* Edge: Failed to Fetch | ||
* Firefox: NetworkError when attempting to fetch resource | ||
* Safari: resource blocked by content blocker | ||
*/ | ||
export function getNativeFetchImplementation(): FetchImpl { | ||
if (cachedFetchImpl) { | ||
return cachedFetchImpl; | ||
} | ||
|
||
/* eslint-disable @typescript-eslint/unbound-method */ | ||
|
||
// Fast path to avoid DOM I/O | ||
if (isNativeFetch(global.fetch)) { | ||
return (cachedFetchImpl = global.fetch.bind(global)); | ||
} | ||
|
||
const document = global.document; | ||
let fetchImpl = global.fetch; | ||
// eslint-disable-next-line deprecation/deprecation | ||
if (typeof document?.createElement === `function`) { | ||
try { | ||
const sandbox = document.createElement('iframe'); | ||
sandbox.hidden = true; | ||
document.head.appendChild(sandbox); | ||
if (sandbox.contentWindow?.fetch) { | ||
fetchImpl = sandbox.contentWindow.fetch; | ||
} | ||
document.head.removeChild(sandbox); | ||
} catch (e) { | ||
logger.warn('Could not create sandbox iframe for pure fetch check, bailing to window.fetch: ', e); | ||
} | ||
} | ||
|
||
return (cachedFetchImpl = fetchImpl.bind(global)); | ||
/* eslint-enable @typescript-eslint/unbound-method */ | ||
} | ||
|
||
/** | ||
* Sends sdk client report using sendBeacon or fetch as a fallback if available | ||
* | ||
* @param url report endpoint | ||
* @param body report payload | ||
*/ | ||
export function sendReport(url: string, body: string): void { | ||
const isRealNavigator = Object.prototype.toString.call(global && global.navigator) === '[object Navigator]'; | ||
const hasSendBeacon = isRealNavigator && typeof global.navigator.sendBeacon === 'function'; | ||
|
||
if (hasSendBeacon) { | ||
// Prevent illegal invocations - https://xgwang.me/posts/you-may-not-know-beacon/#it-may-throw-error%2C-be-sure-to-catch | ||
const sendBeacon = global.navigator.sendBeacon.bind(global.navigator); | ||
return sendBeacon(url, body); | ||
} | ||
|
||
if (supportsFetch()) { | ||
const fetch = getNativeFetchImplementation(); | ||
return forget( | ||
fetch(url, { | ||
body, | ||
method: 'POST', | ||
credentials: 'omit', | ||
keepalive: true, | ||
}), | ||
); | ||
} | ||
} |