diff --git a/CHANGELOG.md b/CHANGELOG.md
index bbc8da1e88..99da2ce40a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,7 @@
### Features
-- Add Replay Custom Masking for iOS and Android ([#4224](https://github.com/getsentry/sentry-react-native/pull/4224), [#4265](https://github.com/getsentry/sentry-react-native/pull/4265))
+- Add Replay Custom Masking for iOS, Android and Web ([#4224](https://github.com/getsentry/sentry-react-native/pull/4224), [#4265](https://github.com/getsentry/sentry-react-native/pull/4265), [#4272](https://github.com/getsentry/sentry-react-native/pull/4272))
```jsx
import * as Sentry from '@sentry/react-native';
diff --git a/packages/core/src/js/integrations/exports.ts b/packages/core/src/js/integrations/exports.ts
index 07cc3289a9..dfc9e2c3e1 100644
--- a/packages/core/src/js/integrations/exports.ts
+++ b/packages/core/src/js/integrations/exports.ts
@@ -13,6 +13,7 @@ export { viewHierarchyIntegration } from './viewhierarchy';
export { expoContextIntegration } from './expocontext';
export { spotlightIntegration } from './spotlight';
export { mobileReplayIntegration } from '../replay/mobilereplay';
+export { browserReplayIntegration } from '../replay/browserReplay';
export { appStartIntegration } from '../tracing/integrations/appStart';
export { nativeFramesIntegration, createNativeFramesIntegrations } from '../tracing/integrations/nativeFrames';
export { stallTrackingIntegration } from '../tracing/integrations/stalltracking';
@@ -30,5 +31,4 @@ export {
inboundFiltersIntegration,
linkedErrorsIntegration as browserLinkedErrorsIntegration,
rewriteFramesIntegration,
- replayIntegration as browserReplayIntegration,
} from '@sentry/react';
diff --git a/packages/core/src/js/replay/CustomMask.web.tsx b/packages/core/src/js/replay/CustomMask.web.tsx
index 87275b9986..1d89dacc59 100644
--- a/packages/core/src/js/replay/CustomMask.web.tsx
+++ b/packages/core/src/js/replay/CustomMask.web.tsx
@@ -2,17 +2,35 @@ import * as React from 'react';
import type { ViewProps } from 'react-native';
import { View } from 'react-native';
+// Wrapping children in a View and div can cause styling issues
+// but with the current implementation of react-native-web
+// we can't avoid it.
+//
+// the prop is dropped by react-native-web
+// https://github.com/necolas/react-native-web/blob/a5ba27c6226aa182979a9cff8cc23c0f5caa4d88/packages/react-native-web/src/exports/View/index.js#L47
+//
+// So we need to wrap the children in a react-dom div.
+// We are using className instead of data-attribute to
+// allow for easier CSS styling adjustments.
+
const Mask = (props: ViewProps): React.ReactElement => {
- // We have to ensure that the warning is visible even if the app is running without debug
- // eslint-disable-next-line no-console
- console.warn('[SentrySessionReplay] Mask component is not supported on web.');
- return ;
+ return (
+
+
+ {props.children}
+
+
+ );
};
+
const Unmask = (props: ViewProps): React.ReactElement => {
- // We have to ensure that the warning is visible even if the app is running without debug
- // eslint-disable-next-line no-console
- console.warn('[SentrySessionReplay] Unmask component is not supported on web.');
- return ;
+ return (
+
+
+ {props.children}
+
+
+ );
};
export { Mask, Unmask };
diff --git a/packages/core/src/js/replay/browserReplay.ts b/packages/core/src/js/replay/browserReplay.ts
new file mode 100644
index 0000000000..b72c0be69f
--- /dev/null
+++ b/packages/core/src/js/replay/browserReplay.ts
@@ -0,0 +1,13 @@
+import { replayIntegration } from '@sentry/react';
+
+const browserReplayIntegration = (
+ options: Parameters[0] = {},
+): ReturnType => {
+ return replayIntegration({
+ ...options,
+ mask: ['.sentry-react-native-mask', ...(options.mask || [])],
+ unmask: ['.sentry-react-native-unmask:not(.sentry-react-native-mask *) > *', ...(options.unmask || [])],
+ });
+};
+
+export { browserReplayIntegration };
diff --git a/packages/core/tsconfig.build.json b/packages/core/tsconfig.build.json
index 812f7eb4b7..aa8afa3015 100644
--- a/packages/core/tsconfig.build.json
+++ b/packages/core/tsconfig.build.json
@@ -2,7 +2,9 @@
"extends": "./node_modules/@sentry-internal/typescript/tsconfig.json",
"include": [
"./src/js/*.ts",
+ "./src/js/*.tsx",
"./src/js/**/*.web.ts",
+ "./src/js/**/*.web.tsx",
"./typings/react-native.d.ts"
],
"exclude": ["node_modules"],
diff --git a/samples/expo/app/(tabs)/two.tsx b/samples/expo/app/(tabs)/two.tsx
index f2ea47eebd..4482a15da3 100644
--- a/samples/expo/app/(tabs)/two.tsx
+++ b/samples/expo/app/(tabs)/two.tsx
@@ -1,4 +1,5 @@
import { StyleSheet } from 'react-native';
+import * as Sentry from '@sentry/react-native';
import EditScreenInfo from '@/components/EditScreenInfo';
import { Text, View } from '@/components/Themed';
@@ -8,6 +9,16 @@ export default function TabTwoScreen() {
Tab Two
+
+ This is unmasked because it's direct child of Sentry.Unmask (can be masked if Sentry.Masked is used higher in the hierarchy)
+
+ This is masked always because it's a child of a Sentry.Mask
+
+ {/* Sentry.Unmask does not override the Sentry.Mask from above in the hierarchy */}
+ This is masked always because it's a child of Sentry.Mask
+
+
+
);
diff --git a/samples/expo/app/_layout.tsx b/samples/expo/app/_layout.tsx
index 0145f84feb..9430d8f22f 100644
--- a/samples/expo/app/_layout.tsx
+++ b/samples/expo/app/_layout.tsx
@@ -56,6 +56,7 @@ process.env.EXPO_SKIP_DURING_EXPORT !== 'true' && Sentry.init({
}),
navigationIntegration,
Sentry.reactNativeTracingIntegration(),
+ Sentry.browserReplayIntegration(),
);
return integrations.filter(i => i.name !== 'Dedupe');
},