Skip to content

Commit b23fcd1

Browse files
authored
fix(replay): Disable mousemove sampling in rrweb for iOS browsers (#14937)
This PR updates the rrweb sampling options depending on the userAgent as this tends to block the main thread on iOS browsers. closes #14534
1 parent 74e2982 commit b23fcd1

File tree

4 files changed

+79
-1
lines changed

4 files changed

+79
-1
lines changed

packages/core/src/utils-hoist/worldwide.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import type { SdkSource } from './env';
1717

1818
/** Internal global with common properties and Sentry extensions */
1919
export type InternalGlobal = {
20-
navigator?: { userAgent?: string };
20+
navigator?: { userAgent?: string; maxTouchPoints?: number };
2121
console: Console;
2222
PerformanceObserver?: any;
2323
Sentry?: any;

packages/replay-internal/src/replay.ts

+2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { createBreadcrumb } from './util/createBreadcrumb';
4747
import { createPerformanceEntries } from './util/createPerformanceEntries';
4848
import { createPerformanceSpans } from './util/createPerformanceSpans';
4949
import { debounce } from './util/debounce';
50+
import { getRecordingSamplingOptions } from './util/getRecordingSamplingOptions';
5051
import { getHandleRecordingEmit } from './util/handleRecordingEmit';
5152
import { isExpired } from './util/isExpired';
5253
import { isSessionExpired } from './util/isSessionExpired';
@@ -452,6 +453,7 @@ export class ReplayContainer implements ReplayContainerInterface {
452453
checkoutEveryNms: Math.max(360_000, this._options._experiments.continuousCheckout),
453454
}),
454455
emit: getHandleRecordingEmit(this),
456+
...getRecordingSamplingOptions(),
455457
onMutation: this._onMutationHandler.bind(this),
456458
...(canvasOptions
457459
? {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { GLOBAL_OBJ } from '@sentry/core';
2+
3+
const NAVIGATOR = GLOBAL_OBJ.navigator;
4+
5+
/**
6+
* Disable sampling mousemove events on iOS browsers as this can cause blocking the main thread
7+
* https://github.com/getsentry/sentry-javascript/issues/14534
8+
*/
9+
export function getRecordingSamplingOptions(): Partial<{ sampling: { mousemove: boolean } }> {
10+
if (
11+
/iPhone|iPad|iPod/i.test(NAVIGATOR?.userAgent ?? '') ||
12+
(/Macintosh/i.test(NAVIGATOR?.userAgent ?? '') && NAVIGATOR?.maxTouchPoints && NAVIGATOR?.maxTouchPoints > 1)
13+
) {
14+
return {
15+
sampling: {
16+
mousemove: false,
17+
},
18+
};
19+
}
20+
21+
return {};
22+
}

packages/replay-internal/test/integration/rrweb.test.ts

+54
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,58 @@ describe('Integration | rrweb', () => {
8686
}
8787
`);
8888
});
89+
90+
it('calls rrweb.record with updated sampling options on iOS', async () => {
91+
// Mock iOS user agent
92+
const originalNavigator = global.navigator;
93+
Object.defineProperty(global, 'navigator', {
94+
value: {
95+
userAgent:
96+
'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1',
97+
},
98+
configurable: true,
99+
});
100+
101+
const { mockRecord } = await resetSdkMock({
102+
replayOptions: {},
103+
sentryOptions: {
104+
replaysOnErrorSampleRate: 1.0,
105+
replaysSessionSampleRate: 1.0,
106+
},
107+
});
108+
109+
// Restore original navigator
110+
Object.defineProperty(global, 'navigator', {
111+
value: originalNavigator,
112+
configurable: true,
113+
});
114+
115+
expect(mockRecord.mock.calls[0]?.[0]).toMatchInlineSnapshot(`
116+
{
117+
"blockSelector": ".sentry-block,[data-sentry-block],base,iframe[srcdoc]:not([src]),img,image,svg,video,object,picture,embed,map,audio,link[rel="icon"],link[rel="apple-touch-icon"]",
118+
"collectFonts": true,
119+
"emit": [Function],
120+
"errorHandler": [Function],
121+
"ignoreSelector": ".sentry-ignore,[data-sentry-ignore],input[type="file"]",
122+
"inlineImages": false,
123+
"inlineStylesheet": true,
124+
"maskAllInputs": true,
125+
"maskAllText": true,
126+
"maskAttributeFn": [Function],
127+
"maskInputFn": undefined,
128+
"maskInputOptions": {
129+
"password": true,
130+
},
131+
"maskTextFn": undefined,
132+
"maskTextSelector": ".sentry-mask,[data-sentry-mask]",
133+
"onMutation": [Function],
134+
"sampling": {
135+
"mousemove": false,
136+
},
137+
"slimDOMOptions": "all",
138+
"unblockSelector": "",
139+
"unmaskTextSelector": "",
140+
}
141+
`);
142+
});
89143
});

0 commit comments

Comments
 (0)