Skip to content

Commit d88b929

Browse files
mydeabillyvg
authored andcommitted
feat(replay): Ensure to use unwrapped setTimeout method
This moves some code around in `browser-utils` so we can-reuse the logic for getting the unwrapped fetch implementation to also get the unwrapped `setTimeout` implementation. E.g. Angular wraps this for change detection, which can lead to performance degration.
1 parent a42012e commit d88b929

File tree

28 files changed

+153
-118
lines changed

28 files changed

+153
-118
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { logger } from '@sentry/utils';
2+
import { DEBUG_BUILD } from './debug-build';
3+
import { WINDOW } from './types';
4+
5+
/**
6+
* We generally want to use window.fetch / window.setTimeout.
7+
* However, in some cases this may be wrapped (e.g. by Zone.js for Angular),
8+
* so we try to get an unpatched version of this from a sandboxed iframe.
9+
*/
10+
11+
interface CacheableImplementations {
12+
setTimeout: typeof WINDOW.setTimeout;
13+
fetch: typeof WINDOW.fetch;
14+
}
15+
16+
const cachedImplementations: Partial<CacheableImplementations> = {};
17+
18+
/**
19+
* Get the native implementation of a browser function.
20+
*
21+
* This can be used to ensure we get an unwrapped version of a function, in cases where a wrapped function can lead to problems.
22+
*
23+
* The following methods can be retrieved:
24+
* - `setTimeout`: This can be wrapped by e.g. Angular, causing change detection to be triggered.
25+
* - `fetch`: This can be wrapped by e.g. ad-blockers, causing an infinite loop when a request is blocked.
26+
*/
27+
export function getNativeImplementation<T extends keyof CacheableImplementations>(
28+
name: T,
29+
): CacheableImplementations[T] {
30+
const cached = cachedImplementations[name];
31+
if (cached) {
32+
return cached;
33+
}
34+
35+
const document = WINDOW.document;
36+
let impl = WINDOW[name] as CacheableImplementations[T];
37+
// eslint-disable-next-line deprecation/deprecation
38+
if (document && typeof document.createElement === 'function') {
39+
try {
40+
const sandbox = document.createElement('iframe');
41+
sandbox.hidden = true;
42+
document.head.appendChild(sandbox);
43+
const contentWindow = sandbox.contentWindow;
44+
if (contentWindow && contentWindow[name]) {
45+
impl = contentWindow[name] as CacheableImplementations[T];
46+
}
47+
document.head.removeChild(sandbox);
48+
} catch (e) {
49+
// Could not create sandbox iframe, just use window.xxx
50+
DEBUG_BUILD && logger.warn(`Could not create sandbox iframe for ${name} check, bailing to window.${name}: `, e);
51+
}
52+
}
53+
54+
// Sanity check: This _should_ not happen, but if it does, we just skip caching...
55+
// This can happen e.g. in tests where fetch may not be available in the env, or similar.
56+
if (!impl) {
57+
return impl;
58+
}
59+
60+
return (cachedImplementations[name] = impl.bind(WINDOW) as CacheableImplementations[T]);
61+
}
62+
63+
/** Clear a cached implementation. */
64+
export function clearCachedImplementation(name: keyof CacheableImplementations): void {
65+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
66+
delete cachedImplementations[name];
67+
}
68+
69+
/**
70+
* A special usecase for incorrectly wrapped Fetch APIs in conjunction with ad-blockers.
71+
* Whenever someone wraps the Fetch API and returns the wrong promise chain,
72+
* this chain becomes orphaned and there is no possible way to capture it's rejections
73+
* other than allowing it bubble up to this very handler. eg.
74+
*
75+
* const f = window.fetch;
76+
* window.fetch = function () {
77+
* const p = f.apply(this, arguments);
78+
*
79+
* p.then(function() {
80+
* console.log('hi.');
81+
* });
82+
*
83+
* return p;
84+
* }
85+
*
86+
* `p.then(function () { ... })` is producing a completely separate promise chain,
87+
* however, what's returned is `p` - the result of original `fetch` call.
88+
*
89+
* This mean, that whenever we use the Fetch API to send our own requests, _and_
90+
* some ad-blocker blocks it, this orphaned chain will _always_ reject,
91+
* effectively causing another event to be captured.
92+
* This makes a whole process become an infinite loop, which we need to somehow
93+
* deal with, and break it in one way or another.
94+
*
95+
* To deal with this issue, we are making sure that we _always_ use the real
96+
* browser Fetch API, instead of relying on what `window.fetch` exposes.
97+
* The only downside to this would be missing our own requests as breadcrumbs,
98+
* but because we are already not doing this, it should be just fine.
99+
*
100+
* Possible failed fetch error messages per-browser:
101+
*
102+
* Chrome: Failed to fetch
103+
* Edge: Failed to Fetch
104+
* Firefox: NetworkError when attempting to fetch resource
105+
* Safari: resource blocked by content blocker
106+
*/
107+
export function fetch(...rest: Parameters<typeof WINDOW.fetch>): ReturnType<typeof WINDOW.fetch> {
108+
return getNativeImplementation('fetch')(...rest);
109+
}
110+
111+
/**
112+
* Get an unwrapped `setTimeout` method.
113+
* This ensures that even if e.g. Angular wraps `setTimeout`, we get the native implementation,
114+
* avoiding triggering change detection.
115+
*/
116+
export function setTimeout(...rest: Parameters<typeof WINDOW.setTimeout>): ReturnType<typeof WINDOW.setTimeout> {
117+
return getNativeImplementation('setTimeout')(...rest);
118+
}

packages/browser-utils/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export { addClickKeypressInstrumentationHandler } from './instrument/dom';
1818

1919
export { addHistoryInstrumentationHandler } from './instrument/history';
2020

21+
export { fetch, setTimeout, clearCachedImplementation, getNativeImplementation } from './getNativeImplementation';
22+
2123
export {
2224
addXhrInstrumentationHandler,
2325
SENTRY_XHR_DATA_KEY,

packages/browser-utils/src/instrument/dom.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { HandlerDataDom } from '@sentry/types';
22

33
import { addHandler, addNonEnumerableProperty, fill, maybeInstrument, triggerHandlers, uuid4 } from '@sentry/utils';
4-
import { WINDOW } from '../metrics/types';
4+
import { WINDOW } from '../types';
55

66
type SentryWrappedTarget = HTMLElement & { _sentryId?: string };
77

packages/browser-utils/src/instrument/history.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { HandlerDataHistory } from '@sentry/types';
22
import { addHandler, fill, maybeInstrument, supportsHistory, triggerHandlers } from '@sentry/utils';
3-
import { WINDOW } from '../metrics/types';
3+
import { WINDOW } from '../types';
44

55
let lastHref: string | undefined;
66

packages/browser-utils/src/instrument/xhr.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { HandlerDataXhr, SentryWrappedXMLHttpRequest, WrappedFunction } from '@sentry/types';
22

33
import { addHandler, fill, isString, maybeInstrument, triggerHandlers } from '@sentry/utils';
4-
import { WINDOW } from '../metrics/types';
4+
import { WINDOW } from '../types';
55

66
export const SENTRY_XHR_DATA_KEY = '__sentry_xhr_v3__';
77

packages/browser-utils/src/metrics/browserMetrics.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import { browserPerformanceTimeOrigin, getComponentName, htmlTreeAsString, logge
66

77
import { spanToJSON } from '@sentry/core';
88
import { DEBUG_BUILD } from '../debug-build';
9+
import { WINDOW } from './../types';
910
import {
1011
addClsInstrumentationHandler,
1112
addFidInstrumentationHandler,
1213
addLcpInstrumentationHandler,
1314
addPerformanceInstrumentationHandler,
1415
addTtfbInstrumentationHandler,
1516
} from './instrument';
16-
import { WINDOW } from './types';
1717
import { getBrowserPerformanceAPI, isMeasurementValue, msToSec, startAndEndSpan } from './utils';
1818
import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry';
1919
import { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher';

packages/browser-utils/src/metrics/web-vitals/getINP.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { WINDOW } from '../types';
17+
import { WINDOW } from '../../types';
1818
import { bindReporter } from './lib/bindReporter';
1919
import { initMetric } from './lib/initMetric';
2020
import { observe } from './lib/observe';

packages/browser-utils/src/metrics/web-vitals/getLCP.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { WINDOW } from '../types';
17+
import { WINDOW } from '../../types';
1818
import { bindReporter } from './lib/bindReporter';
1919
import { getActivationStart } from './lib/getActivationStart';
2020
import { getVisibilityWatcher } from './lib/getVisibilityWatcher';

packages/browser-utils/src/metrics/web-vitals/lib/getNavigationEntry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { WINDOW } from '../../types';
17+
import { WINDOW } from '../../../types';
1818
import type { NavigationTimingPolyfillEntry } from '../types';
1919

2020
export const getNavigationEntry = (): PerformanceNavigationTiming | NavigationTimingPolyfillEntry | undefined => {

packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { WINDOW } from '../../types';
17+
import { WINDOW } from '../../../types';
1818

1919
let firstHiddenTime = -1;
2020

packages/browser-utils/src/metrics/web-vitals/lib/initMetric.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { WINDOW } from '../../types';
17+
import { WINDOW } from '../../../types';
1818
import type { MetricType } from '../types';
1919
import { generateUniqueID } from './generateUniqueID';
2020
import { getActivationStart } from './getActivationStart';

packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { WINDOW } from '../../types';
17+
import { WINDOW } from '../../../types';
1818

1919
export interface OnHiddenCallback {
2020
(event: Event): void;

packages/browser-utils/src/metrics/web-vitals/lib/whenActivated.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { WINDOW } from '../../types';
17+
import { WINDOW } from '../../../types';
1818

1919
export const whenActivated = (callback: () => void) => {
2020
if (WINDOW.document && WINDOW.document.prerendering) {

packages/browser-utils/src/metrics/web-vitals/onTTFB.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { WINDOW } from '../types';
17+
import { WINDOW } from '../../types';
1818
import { bindReporter } from './lib/bindReporter';
1919
import { getActivationStart } from './lib/getActivationStart';
2020
import { getNavigationEntry } from './lib/getNavigationEntry';

packages/browser-utils/test/browser/browserMetrics.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
import type { Span } from '@sentry/types';
1212
import type { ResourceEntry } from '../../src/metrics/browserMetrics';
1313
import { _addMeasureSpans, _addResourceSpans } from '../../src/metrics/browserMetrics';
14-
import { WINDOW } from '../../src/metrics/types';
14+
import { WINDOW } from '../../src/types';
1515
import { TestClient, getDefaultClientOptions } from '../utils/TestClient';
1616

1717
const mockWindowLocation = {

packages/browser/.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ module.exports = {
22
env: {
33
browser: true,
44
},
5-
ignorePatterns: ['test/integration/**', 'src/loader.js'],
5+
ignorePatterns: ['test/integration/**', 'test/loader.js'],
66
extends: ['../../.eslintrc.js'],
77
};

packages/browser/src/transports/fetch.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1+
import { clearCachedImplementation, getNativeImplementation } from '@sentry-internal/browser-utils';
12
import { createTransport } from '@sentry/core';
23
import type { Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types';
34
import { rejectedSyncPromise } from '@sentry/utils';
5+
import type { WINDOW } from '../helpers';
46

57
import type { BrowserTransportOptions } from './types';
6-
import type { FetchImpl } from './utils';
7-
import { clearCachedFetchImplementation, getNativeFetchImplementation } from './utils';
88

99
/**
1010
* Creates a Transport that uses the Fetch API to send events to Sentry.
1111
*/
1212
export function makeFetchTransport(
1313
options: BrowserTransportOptions,
14-
nativeFetch: FetchImpl | undefined = getNativeFetchImplementation(),
14+
nativeFetch: typeof WINDOW.fetch | undefined = getNativeImplementation('fetch'),
1515
): Transport {
1616
let pendingBodySize = 0;
1717
let pendingCount = 0;
@@ -42,7 +42,7 @@ export function makeFetchTransport(
4242
};
4343

4444
if (!nativeFetch) {
45-
clearCachedFetchImplementation();
45+
clearCachedImplementation('fetch');
4646
return rejectedSyncPromise('No fetch implementation available');
4747
}
4848

@@ -59,7 +59,7 @@ export function makeFetchTransport(
5959
};
6060
});
6161
} catch (e) {
62-
clearCachedFetchImplementation();
62+
clearCachedImplementation('fetch');
6363
pendingBodySize -= requestSize;
6464
pendingCount--;
6565
return rejectedSyncPromise(e);

packages/browser/src/transports/utils.ts

Lines changed: 0 additions & 91 deletions
This file was deleted.
File renamed without changes.

0 commit comments

Comments
 (0)