diff --git a/CHANGELOG.md b/CHANGELOG.md
index 592d58402e..4037478b71 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,21 @@
> make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first.
+## Unreleased
+
+### Features
+
+- Add `pauseTouchTracking()` and `resumeTouchTracking()` to the Session Replay API ([#7030](https://github.com/getsentry/sentry-react-native/pull/7030))
+ - Use them to stop recording touch positions on sensitive screens, for example a custom PIN pad. Screenshots and breadcrumbs continue to be recorded.
+ - They are independent of `pause()` and `resume()`. Touch tracking stays paused across new replays until you call `resumeTouchTracking()`.
+ - Requires sentry-android 8.65.0 and sentry-cocoa 9.35.0 or newer. On older native SDKs and on Web, the calls do nothing.
+
+ ```js
+ Sentry.getReplay()?.pauseTouchTracking();
+ // Show the PIN pad.
+ Sentry.getReplay()?.resumeTouchTracking();
+ ```
+
## 8.35.0
### Features
diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayControlTests.m b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayControlTests.m
index afafba5b41..c4b517d4ee 100644
--- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayControlTests.m
+++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayControlTests.m
@@ -4,7 +4,8 @@
/**
* Coverage for the Session Replay runtime controls bridged through
* `RNSentryInternal` (`startReplay`, `startReplayBuffering`, `stopReplay`,
- * `pauseReplay`, `resumeReplay`, `flushReplay`).
+ * `pauseReplay`, `resumeReplay`, `pauseReplayTouchTracking`,
+ * `resumeReplayTouchTracking`, `flushReplay`).
*
* These forward to `SentrySDK.internal.replay.*`. Without a started SDK there is
* no active replay, so every control must be a safe no-op that does not crash,
@@ -47,6 +48,18 @@ - (void)testResumeReplayDoesNotCrashWhenNotRunning
XCTAssertNil(RNSentryInternal.replayId);
}
+- (void)testPauseReplayTouchTrackingDoesNotCrashWhenNotRunning
+{
+ [RNSentryInternal pauseReplayTouchTracking];
+ XCTAssertNil(RNSentryInternal.replayId);
+}
+
+- (void)testResumeReplayTouchTrackingDoesNotCrashWhenNotRunning
+{
+ [RNSentryInternal resumeReplayTouchTracking];
+ XCTAssertNil(RNSentryInternal.replayId);
+}
+
- (void)testFlushReplayDoesNotCrashWhenNotRunning
{
[RNSentryInternal flushReplay];
diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java
index 3c0beea631..cafd576add 100644
--- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java
+++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java
@@ -565,6 +565,26 @@ public void resumeReplay(Promise promise) {
}
}
+ public void pauseReplayTouchTracking(Promise promise) {
+ try {
+ Sentry.replay().pauseTouchTracking();
+ promise.resolve(null);
+ } catch (Throwable e) { // NOPMD - degrade at the bridge boundary, never crash the host app
+ logger.log(SentryLevel.ERROR, "Failed to pause replay touch tracking", e);
+ promise.reject("SentryReactNative", e.getMessage(), e);
+ }
+ }
+
+ public void resumeReplayTouchTracking(Promise promise) {
+ try {
+ Sentry.replay().resumeTouchTracking();
+ promise.resolve(null);
+ } catch (Throwable e) { // NOPMD - degrade at the bridge boundary, never crash the host app
+ logger.log(SentryLevel.ERROR, "Failed to resume replay touch tracking", e);
+ promise.reject("SentryReactNative", e.getMessage(), e);
+ }
+ }
+
public void flushReplay(Promise promise) {
try {
Sentry.replay().flush();
diff --git a/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java b/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java
index e9d3874bd9..c822e23c85 100644
--- a/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java
+++ b/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java
@@ -230,6 +230,16 @@ public void resumeReplay(Promise promise) {
this.impl.resumeReplay(promise);
}
+ @Override
+ public void pauseReplayTouchTracking(Promise promise) {
+ this.impl.pauseReplayTouchTracking(promise);
+ }
+
+ @Override
+ public void resumeReplayTouchTracking(Promise promise) {
+ this.impl.resumeReplayTouchTracking(promise);
+ }
+
@Override
public void flushReplay(Promise promise) {
this.impl.flushReplay(promise);
diff --git a/packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java b/packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java
index 6c87070c4f..07589558d8 100644
--- a/packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java
+++ b/packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java
@@ -230,6 +230,16 @@ public void resumeReplay(Promise promise) {
this.impl.resumeReplay(promise);
}
+ @ReactMethod
+ public void pauseReplayTouchTracking(Promise promise) {
+ this.impl.pauseReplayTouchTracking(promise);
+ }
+
+ @ReactMethod
+ public void resumeReplayTouchTracking(Promise promise) {
+ this.impl.resumeReplayTouchTracking(promise);
+ }
+
@ReactMethod
public void flushReplay(Promise promise) {
this.impl.flushReplay(promise);
diff --git a/packages/core/android/src/test/java/io/sentry/react/RNSentryReplayControlTest.java b/packages/core/android/src/test/java/io/sentry/react/RNSentryReplayControlTest.java
index 2d6ab0d1c1..4ff171d191 100644
--- a/packages/core/android/src/test/java/io/sentry/react/RNSentryReplayControlTest.java
+++ b/packages/core/android/src/test/java/io/sentry/react/RNSentryReplayControlTest.java
@@ -24,7 +24,8 @@
/**
* Coverage for the Session Replay runtime controls exposed on {@link RNSentryModuleImpl} ({@code
* startReplay}, {@code startReplayBuffering}, {@code stopReplay}, {@code pauseReplay}, {@code
- * resumeReplay}, {@code flushReplay}).
+ * resumeReplay}, {@code pauseReplayTouchTracking}, {@code resumeReplayTouchTracking}, {@code
+ * flushReplay}).
*
*
Each control forwards to the corresponding {@link IReplayApi} method returned by {@code
* Sentry.replay()} and then resolves the promise with {@code null}.
@@ -113,6 +114,34 @@ public void resumeReplayCallsSdkAndResolvesNull() {
}
}
+ @Test
+ public void pauseReplayTouchTrackingCallsSdkAndResolvesNull() {
+ try (MockedStatic sentry = mockStatic(Sentry.class)) {
+ final IReplayApi replay = mock(IReplayApi.class);
+ sentry.when(Sentry::replay).thenReturn(replay);
+
+ final Promise promise = mock(Promise.class);
+ module.pauseReplayTouchTracking(promise);
+
+ verify(replay).pauseTouchTracking();
+ verify(promise).resolve(null);
+ }
+ }
+
+ @Test
+ public void resumeReplayTouchTrackingCallsSdkAndResolvesNull() {
+ try (MockedStatic sentry = mockStatic(Sentry.class)) {
+ final IReplayApi replay = mock(IReplayApi.class);
+ sentry.when(Sentry::replay).thenReturn(replay);
+
+ final Promise promise = mock(Promise.class);
+ module.resumeReplayTouchTracking(promise);
+
+ verify(replay).resumeTouchTracking();
+ verify(promise).resolve(null);
+ }
+ }
+
@Test
public void flushReplayCallsSdkAndResolvesNull() {
try (MockedStatic sentry = mockStatic(Sentry.class)) {
diff --git a/packages/core/etc/sentry-react-native.api.md b/packages/core/etc/sentry-react-native.api.md
index 9e90bdfe9f..b67d925c72 100644
--- a/packages/core/etc/sentry-react-native.api.md
+++ b/packages/core/etc/sentry-react-native.api.md
@@ -967,7 +967,9 @@ export interface Replay extends Integration {
getRecordingMode?(): ReplayRecordingMode | undefined;
getReplayId(): string | undefined | null;
pause(): void;
+ pauseTouchTracking(): void;
resume(): void;
+ resumeTouchTracking(): void;
start(): void;
startBuffering(): void;
stop(): Promise;
diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm
index 55a906648a..0f04e8cad9 100644
--- a/packages/core/ios/RNSentry.mm
+++ b/packages/core/ios/RNSentry.mm
@@ -1020,6 +1020,38 @@ + (BOOL)captureReplayWithReturnValue
#endif
}
+RCT_EXPORT_METHOD(pauseReplayTouchTracking : (RCTPromiseResolveBlock)resolve rejecter : (
+ RCTPromiseRejectBlock)reject)
+{
+#if SENTRY_TARGET_REPLAY_SUPPORTED
+ @try {
+ [RNSentryInternalWrapper pauseReplayTouchTracking];
+ resolve(nil);
+ } @catch (NSException *exception) {
+ NSLog(@"[RNSentry] Failed to call pauseReplayTouchTracking: %@", exception);
+ reject(@"SentryReactNative", exception.reason, nil);
+ }
+#else
+ resolve(nil);
+#endif
+}
+
+RCT_EXPORT_METHOD(resumeReplayTouchTracking : (RCTPromiseResolveBlock)resolve rejecter : (
+ RCTPromiseRejectBlock)reject)
+{
+#if SENTRY_TARGET_REPLAY_SUPPORTED
+ @try {
+ [RNSentryInternalWrapper resumeReplayTouchTracking];
+ resolve(nil);
+ } @catch (NSException *exception) {
+ NSLog(@"[RNSentry] Failed to call resumeReplayTouchTracking: %@", exception);
+ reject(@"SentryReactNative", exception.reason, nil);
+ }
+#else
+ resolve(nil);
+#endif
+}
+
RCT_EXPORT_METHOD(
flushReplay : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
{
diff --git a/packages/core/ios/RNSentryInternalWrapper.h b/packages/core/ios/RNSentryInternalWrapper.h
index 9bb94c3c82..5b7b60ec3f 100644
--- a/packages/core/ios/RNSentryInternalWrapper.h
+++ b/packages/core/ios/RNSentryInternalWrapper.h
@@ -82,6 +82,10 @@ NS_ASSUME_NONNULL_BEGIN
+ (void)resumeReplay;
++ (void)pauseReplayTouchTracking;
+
++ (void)resumeReplayTouchTracking;
+
+ (void)flushReplay;
+ (void)registerReplayTraceId:(NSString *)traceId;
diff --git a/packages/core/ios/RNSentryInternalWrapper.m b/packages/core/ios/RNSentryInternalWrapper.m
index d1b40c0714..1bee43c281 100644
--- a/packages/core/ios/RNSentryInternalWrapper.m
+++ b/packages/core/ios/RNSentryInternalWrapper.m
@@ -121,6 +121,16 @@ + (void)resumeReplay
[RNSentryInternal resumeReplay];
}
++ (void)pauseReplayTouchTracking
+{
+ [RNSentryInternal pauseReplayTouchTracking];
+}
+
++ (void)resumeReplayTouchTracking
+{
+ [RNSentryInternal resumeReplayTouchTracking];
+}
+
+ (void)flushReplay
{
[RNSentryInternal flushReplay];
diff --git a/packages/core/ios/Swift/RNSentryInternal.swift b/packages/core/ios/Swift/RNSentryInternal.swift
index f8a4750321..7c73fc6aa5 100644
--- a/packages/core/ios/Swift/RNSentryInternal.swift
+++ b/packages/core/ios/Swift/RNSentryInternal.swift
@@ -172,6 +172,14 @@ import Foundation
SentrySDK.internal.replay.resume()
}
+ @_spi(Private) @objc public static func pauseReplayTouchTracking() {
+ SentrySDK.internal.replay.pauseTouchTracking()
+ }
+
+ @_spi(Private) @objc public static func resumeReplayTouchTracking() {
+ SentrySDK.internal.replay.resumeTouchTracking()
+ }
+
@_spi(Private) @objc public static func flushReplay() {
SentrySDK.internal.replay.flush()
}
@@ -207,6 +215,8 @@ import Foundation
@_spi(Private) @objc public static func stopReplay() {}
@_spi(Private) @objc public static func pauseReplay() {}
@_spi(Private) @objc public static func resumeReplay() {}
+ @_spi(Private) @objc public static func pauseReplayTouchTracking() {}
+ @_spi(Private) @objc public static func resumeReplayTouchTracking() {}
@_spi(Private) @objc public static func flushReplay() {}
@_spi(Private) @objc public static var replayId: String? { nil }
@_spi(Private) @objc public static func setReplayRedactContainerClass(_ containerClass: AnyClass) {}
diff --git a/packages/core/src/js/NativeRNSentry.ts b/packages/core/src/js/NativeRNSentry.ts
index 7710b3432f..b4dfe3806a 100644
--- a/packages/core/src/js/NativeRNSentry.ts
+++ b/packages/core/src/js/NativeRNSentry.ts
@@ -63,6 +63,8 @@ export interface Spec extends TurboModule {
stopReplay(): Promise;
pauseReplay(): Promise;
resumeReplay(): Promise;
+ pauseReplayTouchTracking(): Promise;
+ resumeReplayTouchTracking(): Promise;
flushReplay(): Promise;
registerReplayTraceId(traceId: string): void;
crashedLastRun(): Promise;
diff --git a/packages/core/src/js/replay/browserReplay.ts b/packages/core/src/js/replay/browserReplay.ts
index a2fd9286c5..c2ab9d0eee 100644
--- a/packages/core/src/js/replay/browserReplay.ts
+++ b/packages/core/src/js/replay/browserReplay.ts
@@ -33,18 +33,21 @@ const browserReplayIntegration = (options: ReplayConfiguration = {}): Replay =>
// `stop`, `flush`, …) and lifecycle hooks (`afterAllSetup`, `processSpan`)
// live on the prototype. It must NOT be spread — a spread would drop every
// prototype method and break the integration. We attach `pause`/`resume`
- // directly on the instance instead.
+ // (and the touch tracking controls) directly on the instance instead.
const integration = replayIntegration({
...options,
mask: ['.sentry-react-native-mask', ...(options.mask || [])],
unmask: ['.sentry-react-native-unmask:not(.sentry-react-native-mask *) > *', ...(options.unmask || [])],
}) as unknown as Replay;
- // `pause`/`resume` are part of the shared `Replay` interface (native-backed on
- // mobile) but the browser Session Replay SDK does not expose them, so they are
- // no-ops that log on Web to keep the cross-platform API interchangeable.
+ // `pause`/`resume` and `pauseTouchTracking`/`resumeTouchTracking` are part of
+ // the shared `Replay` interface (native-backed on mobile) but the browser
+ // Session Replay SDK does not expose them, so they are no-ops that log on Web
+ // to keep the cross-platform API interchangeable.
integration.pause = pauseNoop;
integration.resume = resumeNoop;
+ integration.pauseTouchTracking = pauseTouchTrackingNoop;
+ integration.resumeTouchTracking = resumeTouchTrackingNoop;
return integration;
};
@@ -56,6 +59,14 @@ const resumeNoop = (): void => {
debug.log(`[${INTEGRATION_NAME}] \`resume()\` is not supported on Web. No-op.`);
};
+const pauseTouchTrackingNoop = (): void => {
+ debug.log(`[${INTEGRATION_NAME}] \`pauseTouchTracking()\` is not supported on Web. No-op.`);
+};
+
+const resumeTouchTrackingNoop = (): void => {
+ debug.log(`[${INTEGRATION_NAME}] \`resumeTouchTracking()\` is not supported on Web. No-op.`);
+};
+
const browserReplayIntegrationNoop = (): Replay => {
return {
name: INTEGRATION_NAME,
@@ -64,6 +75,8 @@ const browserReplayIntegrationNoop = (): Replay => {
stop: () => Promise.resolve(),
pause: () => {},
resume: () => {},
+ pauseTouchTracking: () => {},
+ resumeTouchTracking: () => {},
flush: () => Promise.resolve(),
getReplayId: () => undefined,
};
diff --git a/packages/core/src/js/replay/mobilereplay.ts b/packages/core/src/js/replay/mobilereplay.ts
index 0275957452..3835986a54 100644
--- a/packages/core/src/js/replay/mobilereplay.ts
+++ b/packages/core/src/js/replay/mobilereplay.ts
@@ -754,6 +754,8 @@ export const mobileReplayIntegration = (initOptions: MobileReplayOptions = defau
stop: () => settleAndInvalidateReplayId(NATIVE.stopReplay()),
pause: () => fireReplayControl(NATIVE.pauseReplay(), 'pause'),
resume: () => fireReplayControl(NATIVE.resumeReplay(), 'resume'),
+ pauseTouchTracking: () => fireReplayControl(NATIVE.pauseReplayTouchTracking(), 'pauseTouchTracking'),
+ resumeTouchTracking: () => fireReplayControl(NATIVE.resumeReplayTouchTracking(), 'resumeTouchTracking'),
flush: (options?: { continueRecording?: boolean }) => {
// The native `flushReplay()` always keeps recording after the flush (a
// buffered replay is converted to a session and continues), which matches
@@ -768,8 +770,8 @@ export const mobileReplayIntegration = (initOptions: MobileReplayOptions = defau
/**
* Runs a fire-and-forget replay control (`start`/`startBuffering`/`pause`/
- * `resume`) whose public signature is synchronous (`void`) to match the web
- * Replay API. The underlying native call is async, so we swallow and log any
+ * `resume`/`pauseTouchTracking`/`resumeTouchTracking`) whose public signature
+ * is synchronous (`void`) to match the web Replay API. The underlying native call is async, so we swallow and log any
* rejection here to avoid an unhandled promise rejection.
*/
function fireReplayControl(promise: Promise, method: string): void {
@@ -788,6 +790,8 @@ const mobileReplayIntegrationNoop = (): MobileReplayIntegration => {
stop: () => Promise.resolve(),
pause: () => {},
resume: () => {},
+ pauseTouchTracking: () => {},
+ resumeTouchTracking: () => {},
flush: () => Promise.resolve(),
};
};
diff --git a/packages/core/src/js/replay/replayInterface.ts b/packages/core/src/js/replay/replayInterface.ts
index 5c748fb00f..607ae05a5a 100644
--- a/packages/core/src/js/replay/replayInterface.ts
+++ b/packages/core/src/js/replay/replayInterface.ts
@@ -9,9 +9,10 @@ import type { Integration, ReplayRecordingMode } from '@sentry/core';
* can control the replay with the same API regardless of platform.
*
* The shape mirrors the native (iOS/Android) Session Replay runtime controls.
- * On Web, `pause()` and `resume()` are no-ops (logged) because the browser
- * Session Replay SDK does not expose them; every other method is fully
- * supported on all platforms.
+ * On Web, `pause()`, `resume()`, `pauseTouchTracking()` and
+ * `resumeTouchTracking()` are no-ops (logged) because the browser Session
+ * Replay SDK does not expose them; every other method is fully supported on
+ * all platforms.
*/
export interface Replay extends Integration {
/**
@@ -49,6 +50,34 @@ export interface Replay extends Integration {
*/
resume(): void;
+ /**
+ * Stop recording touch positions in the replay. Screenshots continue to be
+ * recorded, so a masked screen (for example a PIN pad) stays visible without
+ * revealing where the user tapped. Breadcrumbs are not affected.
+ *
+ * Independent of `pause()`/`resume()`: `resume()` does not resume touch
+ * tracking. The state persists across new replays and `stop()`/`start()`
+ * until `resumeTouchTracking()` is called, and it can be set before a replay
+ * starts.
+ *
+ * @note Requires a native SDK version that supports it (sentry-android
+ * 8.65.0, sentry-cocoa 9.35.0); no-op on older native SDKs.
+ * @note No-op on Web (logged) - the browser Session Replay SDK does not
+ * expose touch tracking controls.
+ */
+ pauseTouchTracking(): void;
+
+ /**
+ * Resume recording touch positions after `pauseTouchTracking()`. Touches are
+ * still not recorded while the replay itself is paused with `pause()`.
+ *
+ * @note Requires a native SDK version that supports it (sentry-android
+ * 8.65.0, sentry-cocoa 9.35.0); no-op on older native SDKs.
+ * @note No-op on Web (logged) - the browser Session Replay SDK does not
+ * expose touch tracking controls.
+ */
+ resumeTouchTracking(): void;
+
/**
* Flush the current replay data to Sentry, or start a full-session replay if
* recording is stopped.
diff --git a/packages/core/src/js/wrapper.ts b/packages/core/src/js/wrapper.ts
index 778fa003d4..816151eae3 100644
--- a/packages/core/src/js/wrapper.ts
+++ b/packages/core/src/js/wrapper.ts
@@ -83,7 +83,15 @@ interface SentryNativeWrapper {
_serializeObject(data: { [key: string]: unknown }): { [key: string]: string };
_isModuleLoaded(module: Spec | undefined): module is Spec;
_callReplayControl(
- method: 'startReplay' | 'startReplayBuffering' | 'stopReplay' | 'pauseReplay' | 'resumeReplay' | 'flushReplay',
+ method:
+ | 'startReplay'
+ | 'startReplayBuffering'
+ | 'stopReplay'
+ | 'pauseReplay'
+ | 'resumeReplay'
+ | 'pauseReplayTouchTracking'
+ | 'resumeReplayTouchTracking'
+ | 'flushReplay',
): Promise;
isNativeAvailable(): boolean;
@@ -159,6 +167,8 @@ interface SentryNativeWrapper {
stopReplay(): Promise;
pauseReplay(): Promise;
resumeReplay(): Promise;
+ pauseReplayTouchTracking(): Promise;
+ resumeReplayTouchTracking(): Promise;
flushReplay(): Promise;
registerReplayTraceId(traceId: string): void;
@@ -1025,6 +1035,14 @@ export const NATIVE: SentryNativeWrapper = {
return this._callReplayControl('resumeReplay');
},
+ pauseReplayTouchTracking(): Promise {
+ return this._callReplayControl('pauseReplayTouchTracking');
+ },
+
+ resumeReplayTouchTracking(): Promise {
+ return this._callReplayControl('resumeReplayTouchTracking');
+ },
+
flushReplay(): Promise {
return this._callReplayControl('flushReplay');
},
@@ -1035,7 +1053,15 @@ export const NATIVE: SentryNativeWrapper = {
* cached, older) native binary predates the method - it never throws.
*/
_callReplayControl(
- method: 'startReplay' | 'startReplayBuffering' | 'stopReplay' | 'pauseReplay' | 'resumeReplay' | 'flushReplay',
+ method:
+ | 'startReplay'
+ | 'startReplayBuffering'
+ | 'stopReplay'
+ | 'pauseReplay'
+ | 'resumeReplay'
+ | 'pauseReplayTouchTracking'
+ | 'resumeReplayTouchTracking'
+ | 'flushReplay',
): Promise {
if (!this.enableNative) {
debug.warn(`[NATIVE] \`${method}\` is not available when native is disabled.`);
diff --git a/packages/core/test/mockWrapper.ts b/packages/core/test/mockWrapper.ts
index dabc4555f9..e00f5f8488 100644
--- a/packages/core/test/mockWrapper.ts
+++ b/packages/core/test/mockWrapper.ts
@@ -75,6 +75,8 @@ const NATIVE: MockInterface = {
stopReplay: jest.fn(),
pauseReplay: jest.fn(),
resumeReplay: jest.fn(),
+ pauseReplayTouchTracking: jest.fn(),
+ resumeReplayTouchTracking: jest.fn(),
flushReplay: jest.fn(),
registerReplayTraceId: jest.fn(),
@@ -118,6 +120,8 @@ NATIVE.startReplayBuffering.mockResolvedValue(undefined);
NATIVE.stopReplay.mockResolvedValue(undefined);
NATIVE.pauseReplay.mockResolvedValue(undefined);
NATIVE.resumeReplay.mockResolvedValue(undefined);
+NATIVE.pauseReplayTouchTracking.mockResolvedValue(undefined);
+NATIVE.resumeReplayTouchTracking.mockResolvedValue(undefined);
NATIVE.flushReplay.mockResolvedValue(undefined);
NATIVE.crashedLastRun.mockResolvedValue(false);
NATIVE.popTimeToDisplayFor.mockResolvedValue(null);
diff --git a/packages/core/test/replay/browserReplay.test.ts b/packages/core/test/replay/browserReplay.test.ts
index a22449c0ef..3ecacca372 100644
--- a/packages/core/test/replay/browserReplay.test.ts
+++ b/packages/core/test/replay/browserReplay.test.ts
@@ -23,7 +23,7 @@ describe('Browser Replay', () => {
expect(SentryReact.replayIntegration).not.toHaveBeenCalled();
});
- describe('pause/resume no-op on Web', () => {
+ describe('pause/resume and touch tracking no-op on Web', () => {
const mockUpstreamIntegration = (): void => {
spyOn(environment, 'notWeb').mockReturnValue(false);
spyOn(SentryReact, 'replayIntegration').mockReturnValue({
@@ -48,5 +48,23 @@ describe('Browser Replay', () => {
expect(() => integration.resume()).not.toThrow();
expect(debugLog).toHaveBeenCalledWith(expect.stringContaining('`resume()` is not supported on Web'));
});
+
+ test('pauseTouchTracking() is a no-op that logs', () => {
+ mockUpstreamIntegration();
+ const debugLog = spyOn(debug, 'log').mockImplementation(() => {});
+
+ const integration = browserReplayIntegration();
+ expect(() => integration.pauseTouchTracking()).not.toThrow();
+ expect(debugLog).toHaveBeenCalledWith(expect.stringContaining('`pauseTouchTracking()` is not supported on Web'));
+ });
+
+ test('resumeTouchTracking() is a no-op that logs', () => {
+ mockUpstreamIntegration();
+ const debugLog = spyOn(debug, 'log').mockImplementation(() => {});
+
+ const integration = browserReplayIntegration();
+ expect(() => integration.resumeTouchTracking()).not.toThrow();
+ expect(debugLog).toHaveBeenCalledWith(expect.stringContaining('`resumeTouchTracking()` is not supported on Web'));
+ });
});
});
diff --git a/packages/core/test/replay/mobilereplay.test.ts b/packages/core/test/replay/mobilereplay.test.ts
index 0c73a0f0ac..360c5c7666 100644
--- a/packages/core/test/replay/mobilereplay.test.ts
+++ b/packages/core/test/replay/mobilereplay.test.ts
@@ -717,6 +717,8 @@ describe('Mobile Replay Integration', () => {
(NATIVE.stopReplay as jest.Mock).mockResolvedValue(undefined as never);
(NATIVE.pauseReplay as jest.Mock).mockResolvedValue(undefined as never);
(NATIVE.resumeReplay as jest.Mock).mockResolvedValue(undefined as never);
+ (NATIVE.pauseReplayTouchTracking as jest.Mock).mockResolvedValue(undefined as never);
+ (NATIVE.resumeReplayTouchTracking as jest.Mock).mockResolvedValue(undefined as never);
(NATIVE.flushReplay as jest.Mock).mockResolvedValue(undefined as never);
});
@@ -750,6 +752,32 @@ describe('Mobile Replay Integration', () => {
expect(NATIVE.resumeReplay).toHaveBeenCalledTimes(1);
});
+ it('pauseTouchTracking() calls the native pauseReplayTouchTracking control', () => {
+ const integration = mobileReplayIntegration();
+ integration.pauseTouchTracking();
+ expect(NATIVE.pauseReplayTouchTracking).toHaveBeenCalledTimes(1);
+ expect(NATIVE.pauseReplay).not.toHaveBeenCalled();
+ });
+
+ it('resumeTouchTracking() calls the native resumeReplayTouchTracking control', () => {
+ const integration = mobileReplayIntegration();
+ integration.resumeTouchTracking();
+ expect(NATIVE.resumeReplayTouchTracking).toHaveBeenCalledTimes(1);
+ expect(NATIVE.resumeReplay).not.toHaveBeenCalled();
+ });
+
+ it('swallows and logs a rejected pauseTouchTracking()', async () => {
+ const error = new Error('native boom');
+ (NATIVE.pauseReplayTouchTracking as jest.Mock).mockRejectedValue(error as never);
+ const debugError = jest.spyOn(debug, 'error').mockImplementation(() => {});
+
+ const integration = mobileReplayIntegration();
+ expect(() => integration.pauseTouchTracking()).not.toThrow();
+
+ await new Promise(resolve => setImmediate(resolve));
+ expect(debugError).toHaveBeenCalledWith(expect.stringContaining('Failed to pauseTouchTracking replay'), error);
+ });
+
it('flush() calls the native flushReplay control and resolves', async () => {
const integration = mobileReplayIntegration();
await integration.flush();
diff --git a/packages/core/test/wrapper.test.ts b/packages/core/test/wrapper.test.ts
index 24a0b60edf..41267ff090 100644
--- a/packages/core/test/wrapper.test.ts
+++ b/packages/core/test/wrapper.test.ts
@@ -70,6 +70,8 @@ jest.mock('react-native', () => {
stopReplay: jest.fn(() => Promise.resolve()),
pauseReplay: jest.fn(() => Promise.resolve()),
resumeReplay: jest.fn(() => Promise.resolve()),
+ pauseReplayTouchTracking: jest.fn(() => Promise.resolve()),
+ resumeReplayTouchTracking: jest.fn(() => Promise.resolve()),
flushReplay: jest.fn(() => Promise.resolve()),
registerReplayTraceId: jest.fn(),
startProfiler: jest.fn(() => Promise.resolve('profiler-id')),
@@ -1712,7 +1714,15 @@ describe('Tests Native Wrapper', () => {
describe('replay controls', () => {
const controls: {
- name: 'start' | 'startBuffering' | 'stop' | 'pause' | 'resume' | 'flush';
+ name:
+ | 'start'
+ | 'startBuffering'
+ | 'stop'
+ | 'pause'
+ | 'resume'
+ | 'pauseTouchTracking'
+ | 'resumeTouchTracking'
+ | 'flush';
call: () => Promise;
native: jest.Mock;
}[] = [
@@ -1725,6 +1735,16 @@ describe('Tests Native Wrapper', () => {
{ name: 'stop', call: () => NATIVE.stopReplay(), native: RNSentry.stopReplay as jest.Mock },
{ name: 'pause', call: () => NATIVE.pauseReplay(), native: RNSentry.pauseReplay as jest.Mock },
{ name: 'resume', call: () => NATIVE.resumeReplay(), native: RNSentry.resumeReplay as jest.Mock },
+ {
+ name: 'pauseTouchTracking',
+ call: () => NATIVE.pauseReplayTouchTracking(),
+ native: RNSentry.pauseReplayTouchTracking as jest.Mock,
+ },
+ {
+ name: 'resumeTouchTracking',
+ call: () => NATIVE.resumeReplayTouchTracking(),
+ native: RNSentry.resumeReplayTouchTracking as jest.Mock,
+ },
{ name: 'flush', call: () => NATIVE.flushReplay(), native: RNSentry.flushReplay as jest.Mock },
];
@@ -1769,6 +1789,8 @@ describe('Tests Native Wrapper', () => {
stop: 'stopReplay',
pause: 'pauseReplay',
resume: 'resumeReplay',
+ pauseTouchTracking: 'pauseReplayTouchTracking',
+ resumeTouchTracking: 'resumeReplayTouchTracking',
flush: 'flushReplay',
} as const
)[name];
diff --git a/samples/react-native/src/tabs/PlaygroundTab.tsx b/samples/react-native/src/tabs/PlaygroundTab.tsx
index 6bd49544a6..ca6fd5b45a 100644
--- a/samples/react-native/src/tabs/PlaygroundTab.tsx
+++ b/samples/react-native/src/tabs/PlaygroundTab.tsx
@@ -157,6 +157,22 @@ export default function getPlaygroundTab() {
}}
/>
+