From f514f67ebb08372d7499be650215c9ce01bbfe83 Mon Sep 17 00:00:00 2001 From: jonathanmos <48201295+jonathanmos@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:11:07 +0300 Subject: [PATCH 1/2] Add svg testing screens to the example subprojects --- example-new-architecture/App.tsx | 35 +- example-new-architecture/SvgScreen.tsx | 706 +++++++++++++++++++++ example-new-architecture/assets/heart.svg | 3 + example-new-architecture/assets/icons.ts | 11 + example-new-architecture/assets/shield.svg | 4 + example-new-architecture/assets/star.svg | 3 + example-new-architecture/assets/svg.d.ts | 13 + example-new-architecture/babel.config.js | 7 + example-new-architecture/ios/Podfile.lock | 86 ++- example-new-architecture/metro.config.js | 14 +- example-new-architecture/package.json | 6 +- example/babel.config.js | 7 + example/index.tsx | 10 +- example/ios/Podfile | 46 +- example/ios/Podfile.lock | 12 +- example/metro.config.js | 20 +- example/package.json | 5 +- example/src/App.tsx | 56 +- example/src/assets/heart.svg | 3 + example/src/assets/icons.ts | 11 + example/src/assets/shield.svg | 4 + example/src/assets/star.svg | 3 + example/src/assets/svg.d.ts | 13 + example/src/ddUtils.tsx | 18 +- example/src/screens/MainScreen.tsx | 5 + example/src/screens/SvgScreen.tsx | 689 ++++++++++++++++++++ yarn.lock | 307 ++++++++- 27 files changed, 2050 insertions(+), 47 deletions(-) create mode 100644 example-new-architecture/SvgScreen.tsx create mode 100644 example-new-architecture/assets/heart.svg create mode 100644 example-new-architecture/assets/icons.ts create mode 100644 example-new-architecture/assets/shield.svg create mode 100644 example-new-architecture/assets/star.svg create mode 100644 example-new-architecture/assets/svg.d.ts create mode 100644 example/src/assets/heart.svg create mode 100644 example/src/assets/icons.ts create mode 100644 example/src/assets/shield.svg create mode 100644 example/src/assets/star.svg create mode 100644 example/src/assets/svg.d.ts create mode 100644 example/src/screens/SvgScreen.tsx diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index 190667007..42f7fb286 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -13,15 +13,22 @@ import { PropagatorType, } from '@datadog/mobile-react-native'; import {DatadogOpenFeatureProvider} from '@datadog/mobile-react-native-openfeature'; +import { + SessionReplay, + ImagePrivacyLevel, + TextAndInputPrivacyLevel, + TouchPrivacyLevel, +} from '@datadog/mobile-react-native-session-replay'; import { OpenFeature, OpenFeatureProvider, useObjectFlagDetails, } from '@openfeature/react-sdk'; -import React, {Suspense} from 'react'; +import React, {Suspense, useState} from 'react'; import type {PropsWithChildren} from 'react'; import { ActivityIndicator, + Button, SafeAreaView, ScrollView, StatusBar, @@ -40,6 +47,7 @@ import { } from 'react-native/Libraries/NewAppScreen'; // @ts-ignore import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; +import SvgScreen from './SvgScreen'; (async () => { const config = new CoreConfiguration( @@ -72,6 +80,16 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; // Initialize the Datadog SDK. await DdSdkReactNative.initialize(config); + // Enable Session Replay so the SVG test screen can be verified in the Datadog portal. + // Using MASK_NON_BUNDLED_ONLY to mirror a real customer configuration — bundled SVGs + // from assets.bin should appear; remote/network images are masked. + await SessionReplay.enable({ + replaySampleRate: 100, + imagePrivacyLevel: ImagePrivacyLevel.MASK_NON_BUNDLED_ONLY, + textAndInputPrivacyLevel: TextAndInputPrivacyLevel.MASK_SENSITIVE_INPUTS, + touchPrivacyLevel: TouchPrivacyLevel.SHOW, + }); + // Enable Datadog Flags feature. await DdFlags.enable(); @@ -120,6 +138,10 @@ function AppWithProviders() { } function App(): React.JSX.Element { + // example-new-architecture has no navigation library — this app has a single screen + // (App.tsx) plus the SVG test screen, toggled via local state instead of a route. + const [showSvgScreen, setShowSvgScreen] = useState(false); + const greetingFlag = useObjectFlagDetails('rn-sdk-test-json-flag', { greeting: 'Default greeting', }); @@ -129,6 +151,10 @@ function App(): React.JSX.Element { backgroundColor: isDarkMode ? Colors.darker : Colors.lighter, }; + if (showSvgScreen) { + return setShowSvgScreen(false)} />; + } + return ( +
+
+
Edit App.tsx to change this screen and then come back to see your edits. diff --git a/example-new-architecture/SvgScreen.tsx b/example-new-architecture/SvgScreen.tsx new file mode 100644 index 000000000..fff0526db --- /dev/null +++ b/example-new-architecture/SvgScreen.tsx @@ -0,0 +1,706 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import React from 'react'; +// Alias react-native's Text so the SVG Text component keeps its real name. +// The Babel plugin converts JSX element names via convertAttributeCasing — 'Text' → 'text' +// which is in svgElements, but 'SvgText' → 'svgText' which is not, producing a +// mismatched opening/closing tag pair that breaks svgo. +import { ScrollView, SafeAreaView, Text as RNText, View, StyleSheet, Animated, Button } from 'react-native'; +import { + Svg, Circle, Rect, Ellipse, Polygon, Path, Line, Polyline, + G, Defs, LinearGradient, RadialGradient, Stop, ClipPath, + Text +} from 'react-native-svg'; + +import { + SessionReplayView, + ImagePrivacyLevel +} from '@datadog/mobile-react-native-session-replay'; + +import StarSvg from './assets/star.svg'; +import { HeartIcon, ShieldIcon } from './assets/icons'; + +// Module-level const used in Case D1 to test findIdentifierInScope +const BADGE_SIZE = 72; + +// Used in Group I to reproduce the customer's `AnimatedPath` case without pulling in +// react-native-reanimated. `Animated.createAnimatedComponent` is core React Native and +// produces the exact same shape that breaks the Babel plugin: a capitalized custom +// component whose first-letter-lowercased tag name ('AnimatedPath' -> 'animatedPath') +// is not in svgElements. +const AnimatedPath = Animated.createAnimatedComponent(Path); + +// ───────────────────────────────────────────────────────────── +// GROUP A — Basic static shapes +// All attributes are string literals. All should appear in replay. +// ───────────────────────────────────────────────────────────── + +/** A1: Simple filled circle */ +function BlueCircle() { + return ( + + + + ); +} + +/** A2: Rounded rectangle */ +function RedRoundedRect() { + return ( + + + + ); +} + +/** A3: Ellipse */ +function GreenEllipse() { + return ( + + + + ); +} + +/** A4: Polygon — 5-pointed star (10 vertices: 5 outer + 5 inner) */ +function OrangeStar() { + return ( + + + + ); +} + +/** A5: Path — chevron/arrow */ +function PurpleArrow() { + return ( + + + + ); +} + +/** A6: Line and Polyline */ +function TealLines() { + return ( + + + + + ); +} + +// ───────────────────────────────────────────────────────────── +// GROUP B — SVG features (defs, gradients, groups, text, clip) +// All static. All should appear in replay. +// ───────────────────────────────────────────────────────────── + +/** B1: Linear gradient via Defs */ +function GradientRect() { + return ( + + + + + + + + + + ); +} + +/** B2: Radial gradient */ +function RadialGlow() { + return ( + + + + + + + + + + ); +} + +/** B3: Group — traffic light */ +function TrafficLight() { + return ( + + + + + + + ); +} + +/** B4: ClipPath — clipped circle */ +function ClippedCircle() { + return ( + + + + + + + + + + ); +} + +/** B5: SVG Text element */ +function SvgLabel() { + return ( + + + + DD + + + ); +} + +// ───────────────────────────────────────────────────────────── +// GROUP C — Transform attributes +// translateX/Y, rotate are converted from RN props to SVG transform. +// ───────────────────────────────────────────────────────────── + +/** C1: Group with translateX/Y (RN-style transform props) */ +function TranslatedGroup() { + return ( + + + {/* G with RN transform props — converted to SVG transform="translate(40,40)" */} + + + + + ); +} + +/** C2: Rotated shape via rotate prop */ +function RotatedDiamond() { + return ( + + + + + + ); +} + +/** C3: Scaled element */ +function ScaledStar() { + return ( + + + + + + ); +} + +// ───────────────────────────────────────────────────────────── +// GROUP D — Dimension handling +// ───────────────────────────────────────────────────────────── + +/** D1: Module-level const dimensions — resolved by findIdentifierInScope */ +function ConstSizedBadge() { + return ( + + + + + ); +} + +/** D2: ViewBox only — no explicit width/height. Native layer injects them from view bounds. */ +function ViewBoxOnlyIcon() { + return ( + + + + + ); +} + +// ───────────────────────────────────────────────────────────── +// GROUP E — Dynamic props +// Non-resolvable props are stripped before svgo runs (Fix 1). +// Shape is captured; dynamic attribute is absent in replay. +// ───────────────────────────────────────────────────────────── + +/** E1: Dynamic fill on root element — fill dropped, container captured */ +function DynamicRootFill({ accentColor }: { accentColor: string }) { + return ( + + + + ); +} + +/** E2: Dynamic fill on child — child fill dropped, shape still captured */ +function DynamicChildFill({ iconColor }: { iconColor: string }) { + return ( + + + + + ); +} + +/** E3: Dynamic stroke and strokeWidth — both dropped, base shape preserved */ +function DynamicStrokeRect({ borderColor, borderWidth }: { borderColor: string; borderWidth: number }) { + return ( + + + + ); +} + +/** E4: Mix of static fill + dynamic opacity — fill kept, opacity dropped */ +function DynamicOpacityCircle({ fadeLevel }: { fadeLevel: number }) { + return ( + + + + ); +} + +// ───────────────────────────────────────────────────────────── +// GROUP F — File imports +// LocalSvgHandler reads the SVG file from disk (Fix 2, 3, 4). +// ───────────────────────────────────────────────────────────── + +/** F1: Default import of a local .svg file */ +function LocalStarImport() { + return ; +} + +/** F2: Named import from barrel file (export { default as HeartIcon }) + * Tests Fix 6: ExportNamedDeclaration uses spec.exported.name, not spec.local.name */ +function BarrelHeartImport() { + return ; +} + +/** F3: Second barrel export — ShieldIcon */ +function BarrelShieldImport() { + return ; +} + +// ───────────────────────────────────────────────────────────── +// GROUP H — Unsupported nested elements (customer repro) +// AnimatedPath isn't a recognized SVG tag, so it's now spliced out of the tree +// instead of breaking generation for the whole parent (see RNSvgHandler). +// ───────────────────────────────────────────────────────────── + +/** + * I1: The checkmark stroke uses AnimatedPath instead of Path. SHOULD appear in replay + * as the green circle background WITHOUT the white checkmark — the unsupported + * AnimatedPath element is removed, but the rest of the SVG is still captured. Compare + * against I2: same circle, but I2 additionally shows the checkmark since it uses a + * plain, supported . + */ +function BrokenAnimatedCheckmark() { + const opacity = React.useRef(new Animated.Value(1)).current; + return ( + + + + + ); +} + +/** + * I2: Control — identical icon to I1, but the checkmark uses plain . SHOULD + * appear in replay. Compare directly against I1: same shape, only the wrapper + * component differs. + */ +function ControlCheckmark() { + return ( + + + + + ); +} + +// ───────────────────────────────────────────────────────────── +// Screen +// ───────────────────────────────────────────────────────────── + +type CaseProps = { + label: string; + sublabel?: string; + children: React.ReactNode; +}; + +function Case({ label, sublabel, children }: CaseProps) { + return ( + + {children} + + {label} + {sublabel ? {sublabel} : null} + + + ); +} + +function Section({ title, children }: { title: string; children: React.ReactNode }) { + return ( + + {title} + {children} + + ); +} + +type SvgScreenProps = { + // example-new-architecture has no navigation library — this screen is shown/hidden + // via a simple boolean state toggle in App.tsx instead of a Stack.Navigator route. + onBack: () => void; +}; + +export default function SvgScreen({ onBack }: SvgScreenProps) { + return ( + // example-new-architecture has no navigation library, so this screen has no native + // header to supply a safely-positioned back button (unlike example/src/App.tsx's + // Stack.Screen with headerShown: true). Without SafeAreaView, the manual back + // button below would render flush at y=0, underneath the status bar / Dynamic Island. + + + + + Session Replay SVG Test Screen + + All cases in Groups A–D should appear in replay.{'\n'} + Group E: shape appears, dynamic attr is absent.{'\n'} + Group F: appears after buildSvgMap fixes.{'\n'} + Group G: privacy overrides — verify masking behavior in replay.{'\n'} + Group I: I1 shows circle only (checkmark removed), I2 shows circle + checkmark. + + +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ + {/* ─── GROUP G — Privacy interaction ─── */} + {/* + These cases test whether the native SDK's view-level privacy mechanism + (DdPrivacyView view tags / SessionReplayPrivacyOverrides) correctly + interacts with SVG wireframes produced by the custom mapper/recorder. + + The Babel plugin wraps each SVG in SessionReplayView.Privacy (no imagePrivacy + set — inherits session level). The outer wrappers here set explicit per-view + overrides. Whether those cascade to the inner SVG wireframe depends on the + native SDK's privacy propagation behaviour. + + Expected results are annotated on each case so you can verify in replay. + */} +
+ {/* G1: MaskNone override. EXPECTED: SVG content VISIBLE. */} + + + + + + + {/* + G2: MaskAll override. EXPECTED: SVG content MASKED. If it still shows, + the custom mapper bypasses the view-level privacy override (known gap: + SvgViewMapper hardcodes isEmpty:false on Android; SvgViewRecorder never + checks imagePrivacy on iOS). + */} + + + + + + + {/* G3: hide=true. EXPECTED: entire view ABSENT from replay (no bounding box). */} + + + + + + + {/* + G4: imagePrivacy = MASK_NON_BUNDLED_ONLY, matching the customer's session + setting. SVGs come from assets.bin (bundled), so EXPECTED: VISIBLE. + */} + + + + + + + {/* + G5: nested privacy, inner MaskNone inside outer MaskAll. EXPECTED: MASKED + (outer MaskAll wins — inner MaskNone can't "unlock" it). + */} + + + + + + + +
+ +
+ + + + + + +
+ + + J — Known gaps (will NOT appear in replay) + + • SVG components rendered by node_modules design-system packages are excluded + from the Babel transform by design (index.ts:81,128). No DdPrivacyView wrapper + is ever created around them.{'\n\n'} + • SvgUri is listed in svgSupportedNames but its handler is commented out — + <SvgUri uri="..." /> is silently skipped. + + +
+
+ ); +} + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + }, + container: { + padding: 16, + paddingBottom: 48, + gap: 24, + }, + heading: { + fontSize: 20, + fontWeight: 'bold', + }, + subtitle: { + fontSize: 12, + color: '#555', + lineHeight: 18, + }, + section: { + gap: 8, + }, + sectionTitle: { + fontSize: 14, + fontWeight: '700', + color: '#2c3e50', + borderBottomWidth: 1, + borderBottomColor: '#bdc3c7', + paddingBottom: 4, + }, + sectionGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 12, + }, + case: { + alignItems: 'center', + gap: 4, + minWidth: 80, + }, + caseContent: { + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1, + borderColor: '#ecf0f1', + borderRadius: 6, + padding: 4, + backgroundColor: '#fafafa', + }, + caseMeta: { + alignItems: 'center', + maxWidth: 100, + }, + caseLabel: { + fontSize: 11, + fontWeight: '600', + textAlign: 'center', + color: '#2c3e50', + }, + caseSublabel: { + fontSize: 10, + color: '#7f8c8d', + textAlign: 'center', + }, + gapNote: { + backgroundColor: '#fef9e7', + borderLeftWidth: 4, + borderLeftColor: '#f39c12', + padding: 12, + borderRadius: 4, + gap: 8, + }, + gapTitle: { + fontSize: 13, + fontWeight: '700', + color: '#8e6300', + }, + gapBody: { + fontSize: 12, + color: '#7d6608', + lineHeight: 18, + }, +}); diff --git a/example-new-architecture/assets/heart.svg b/example-new-architecture/assets/heart.svg new file mode 100644 index 000000000..e095d98d0 --- /dev/null +++ b/example-new-architecture/assets/heart.svg @@ -0,0 +1,3 @@ + + + diff --git a/example-new-architecture/assets/icons.ts b/example-new-architecture/assets/icons.ts new file mode 100644 index 000000000..5662e288f --- /dev/null +++ b/example-new-architecture/assets/icons.ts @@ -0,0 +1,11 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +// Barrel file used to test Fix 6: ExportNamedDeclaration must use spec.exported.name +// not spec.local.name, otherwise 'export { default as HeartIcon }' stores 'default' +// as the key in localSvgMap instead of 'HeartIcon', and is never found. +export { default as HeartIcon } from './heart.svg'; +export { default as ShieldIcon } from './shield.svg'; diff --git a/example-new-architecture/assets/shield.svg b/example-new-architecture/assets/shield.svg new file mode 100644 index 000000000..0171eebd0 --- /dev/null +++ b/example-new-architecture/assets/shield.svg @@ -0,0 +1,4 @@ + + + + diff --git a/example-new-architecture/assets/star.svg b/example-new-architecture/assets/star.svg new file mode 100644 index 000000000..39d06c8cd --- /dev/null +++ b/example-new-architecture/assets/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/example-new-architecture/assets/svg.d.ts b/example-new-architecture/assets/svg.d.ts new file mode 100644 index 000000000..eb2ad40ff --- /dev/null +++ b/example-new-architecture/assets/svg.d.ts @@ -0,0 +1,13 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import React from 'react'; +import { SvgProps } from 'react-native-svg'; + +declare module '*.svg' { + const content: React.FC; + export default content; +} diff --git a/example-new-architecture/babel.config.js b/example-new-architecture/babel.config.js index f7b3da3b3..d79333729 100644 --- a/example-new-architecture/babel.config.js +++ b/example-new-architecture/babel.config.js @@ -1,3 +1,10 @@ module.exports = { presets: ['module:@react-native/babel-preset'], + plugins: [ + ['@datadog/mobile-react-native-babel-plugin', { + sessionReplay: { + svgTracking: true + } + }] + ] }; diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index cc0c0b9dd..4a4aa0e2b 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -69,6 +69,31 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - DatadogSDKReactNativeSessionReplay (3.5.2): + - DatadogSDKReactNative + - DatadogSessionReplay (= 3.11.0) + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - DatadogSessionReplay (3.11.0): + - DatadogInternal (= 3.11.0) - DatadogTrace (3.11.0): - DatadogInternal (= 3.11.0) - OpenTelemetry-Swift-Api (~> 2.3.0) @@ -1644,6 +1669,49 @@ PODS: - React-logger - React-perflogger - React-utils (= 0.76.9) + - RNSVG (15.12.1): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNSVG/common (= 15.12.1) + - Yoga + - RNSVG/common (15.12.1): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - SocketRocket (0.7.1) - Yoga (0.0.0) @@ -1651,6 +1719,7 @@ DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - DatadogSDKReactNative (from `../../packages/core/DatadogSDKReactNative.podspec`) - DatadogSDKReactNative/Tests (from `../../packages/core/DatadogSDKReactNative.podspec`) + - "DatadogSDKReactNativeSessionReplay (from `../node_modules/@datadog/mobile-react-native-session-replay`)" - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -1715,6 +1784,7 @@ DEPENDENCIES: - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) - ReactCodegen (from `build/generated/ios`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - RNSVG (from `../node_modules/react-native-svg`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: @@ -1725,6 +1795,7 @@ SPEC REPOS: - DatadogInternal - DatadogLogs - DatadogRUM + - DatadogSessionReplay - DatadogTrace - DatadogWebViewTracking - KSCrash @@ -1736,6 +1807,8 @@ EXTERNAL SOURCES: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" DatadogSDKReactNative: :path: "../../packages/core/DatadogSDKReactNative.podspec" + DatadogSDKReactNativeSessionReplay: + :path: "../node_modules/@datadog/mobile-react-native-session-replay" DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" fast_float: @@ -1861,6 +1934,8 @@ EXTERNAL SOURCES: :path: build/generated/ios ReactCommon: :path: "../node_modules/react-native/ReactCommon" + RNSVG: + :path: "../node_modules/react-native-svg" Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" @@ -1873,17 +1948,19 @@ SPEC CHECKSUMS: DatadogLogs: ef98708261f8f7ba82d15360f9951d43f578163b DatadogRUM: 67d130164f4a7663e05a3d02803be9e9dd1abbb7 DatadogSDKReactNative: c2877e376b00655cf51363ab6cdc0fd013eb7e3b + DatadogSDKReactNativeSessionReplay: 6325ba357ef0cd25e7b455dbaa6777126b0e1ccd + DatadogSessionReplay: 23107da4c61d0c84435efc6111929d562988045b DatadogTrace: 17f50647107755fba79fc2298bf4cb282e0efb1f DatadogWebViewTracking: 1290fce6010bf65b5e0e0ffa9048b448782e469c - DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 + DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45 fmt: 01b82d4ca6470831d1cc0852a1af644be019e8f6 - glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a + glog: fdfdfe5479092de0c4bdbebedd9056951f092c4f hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11 KSCrash: 80e1e24eaefbe5134934ae11ca8d7746586bc2ed OpenTelemetry-Swift-Api: 3d77582ab6837a63b65bf7d2eacc57d8f2595edd - RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17 + RCT-Folly: 7b4f73a92ad9571b9dbdb05bb30fad927fa971e1 RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83 RCTRequired: ca91e5dd26b64f577b528044c962baf171c6b716 RCTTypeSafety: e7678bd60850ca5a41df9b8dc7154638cb66871f @@ -1939,8 +2016,9 @@ SPEC CHECKSUMS: React-utils: 54df9ada708578c8ad40d92895d6fed03e0e8a9e ReactCodegen: 21a52ccddc6479448fc91903a437dd23ddc7366c ReactCommon: bfd3600989d79bc3acbe7704161b171a1480b9fd + RNSVG: 0347d0157d326d757e810e3d0ac4660b3f3fbbbd SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a + Yoga: 40f19fff64dce86773bf8b602c7070796c007970 PODFILE CHECKSUM: 85124514d3acbe8b60389d46d3c38133e7c8e8de diff --git a/example-new-architecture/metro.config.js b/example-new-architecture/metro.config.js index 1f6822840..2163e82d0 100644 --- a/example-new-architecture/metro.config.js +++ b/example-new-architecture/metro.config.js @@ -16,16 +16,28 @@ const modules = Object.keys({ * * @type {import('metro-config').MetroConfig} */ +const defaultConfig = getDefaultConfig(__dirname); + const config = { projectRoot: __dirname, watchFolders: [root], resetCache: true, + // Route .svg files through react-native-svg-transformer so they are importable + // as React components (used by Session Replay SVG file-import test cases). + transformer: { + babelTransformerPath: require.resolve('react-native-svg-transformer'), + }, // We need to make sure that only one version is loaded for peerDependencies // So we blacklist them at the root, and alias them to the versions in example's node_modules // This block is very important, because otherwise things like React can be packed multiple times // while it should be only one React instance in the runtime. exclusionList relies on the modules which are // declared as peer dependencies in the core package. resolver: { + // Remove svg from asset extensions and add it to source extensions so Metro + // sends it through the transformer rather than copying it as a static asset. + assetExts: defaultConfig.resolver.assetExts.filter(ext => ext !== 'svg'), + sourceExts: [...defaultConfig.resolver.sourceExts, 'svg'], + blacklistRE: exclusionList( modules.map( m => new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`), @@ -42,4 +54,4 @@ const config = { }, }; -module.exports = mergeConfig(getDefaultConfig(__dirname), config); +module.exports = mergeConfig(defaultConfig, config); diff --git a/example-new-architecture/package.json b/example-new-architecture/package.json index 83a990a29..25a18605b 100644 --- a/example-new-architecture/package.json +++ b/example-new-architecture/package.json @@ -10,14 +10,17 @@ "dependencies": { "@datadog/mobile-react-native": "workspace:packages/core", "@datadog/mobile-react-native-openfeature": "workspace:packages/react-native-openfeature", + "@datadog/mobile-react-native-session-replay": "workspace:packages/react-native-session-replay", "@openfeature/react-sdk": "^1.1.0", "react": "18.3.1", - "react-native": "0.76.9" + "react-native": "0.76.9", + "react-native-svg": "15.12.1" }, "devDependencies": { "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.3", "@babel/runtime": "^7.26.10", + "@datadog/mobile-react-native-babel-plugin": "workspace:packages/react-native-babel-plugin", "@react-native-community/cli": "15.0.1", "@react-native-community/cli-platform-android": "15.0.1", "@react-native-community/cli-platform-ios": "15.0.1", @@ -31,6 +34,7 @@ "eslint": "^8.19.0", "jest": "^29.6.3", "prettier": "^2.8.8", + "react-native-svg-transformer": "^1.5.0", "react-test-renderer": "18.3.1", "typescript": "5.0.4" }, diff --git a/example/babel.config.js b/example/babel.config.js index cafccd5f2..bc1ebcd62 100644 --- a/example/babel.config.js +++ b/example/babel.config.js @@ -1,3 +1,10 @@ module.exports = { presets: ['module:@react-native/babel-preset'], + plugins: [ + ['@datadog/mobile-react-native-babel-plugin', { + sessionReplay: { + svgTracking: true + } + }] + ] }; diff --git a/example/index.tsx b/example/index.tsx index 077a3f11c..6b924a2b4 100644 --- a/example/index.tsx +++ b/example/index.tsx @@ -12,10 +12,18 @@ import {navigation as navigationLib} from './app.json'; import {initializeDatadog} from './src/ddUtils'; import {TrackingConsent} from '@datadog/mobile-react-native'; import {Navigation} from 'react-native-navigation'; +import {gestureHandlerRootHOC} from 'react-native-gesture-handler'; console.log('Starting Application with navigation library: ' + navigationLib); if (navigationLib == 'react-navigation') { - AppRegistry.registerComponent(appName, () => App); + // Wrap with gestureHandlerRootHOC so that @react-navigation/stack's gesture + // handling works correctly inside the react-native-navigation native container. + const AppWithGestureHandler = gestureHandlerRootHOC(App); + AppRegistry.registerComponent(appName, () => AppWithGestureHandler); + // RNN v8+ requires the component to be in Navigation's own registry as well, + // not just AppRegistry. Without this, Navigation.setRoot() cannot find the + // component by name and shows a blank screen. + Navigation.registerComponent(appName, () => AppWithGestureHandler); // this is a hack. we need to set root of `react-native-navigation`, // because native screen belongs to it. otherwise we will have blank screen Navigation.events().registerAppLaunchedListener(() => { diff --git a/example/ios/Podfile b/example/ios/Podfile index 0c9182f55..95aca17e7 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -10,6 +10,17 @@ require Pod::Executable.execute_command('node', ['-p', platform :ios, min_ios_version_supported prepare_react_native_project! +# This example app uses react-native-navigation, whose AppDelegate integration (manual +# RCTBridge creation + [ReactNativeNavigation bootstrapWithBridge:]) predates New +# Architecture and doesn't wire up Fabric/TurboModules. `:new_arch_enabled => false` passed +# to `use_react_native!` below only controls React Native's own podspecs though — +# third-party pods (react-native-navigation, react-native-screens, react-native-svg) read +# RCT_NEW_ARCH_ENABLED directly in their own podspecs and ignore that Podfile option, so it +# must also be forced off here (before use_native_modules! resolves them) or they still +# compile new-arch and fail with errors like "use of undeclared identifier +# 'RCTAppSetupDefaultJsExecutorFactory'". +ENV['RCT_NEW_ARCH_ENABLED'] = '0' + linkage = ENV['USE_FRAMEWORKS'] if linkage != nil Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green @@ -26,7 +37,11 @@ target 'ddSdkReactnativeExample' do use_react_native!( :path => config[:reactNativePath], # An absolute path to your application root. - :app_path => "#{Pod::Config.instance.installation_root}/.." + :app_path => "#{Pod::Config.instance.installation_root}/..", + # example-new-architecture is the project intended for testing New Architecture; this + # app matches what its AppDelegate actually supports (see ENV['RCT_NEW_ARCH_ENABLED'] + # above for why old arch is also forced for third-party pods). + :new_arch_enabled => false ) # pod 'DatadogSDKReactNative', :path => '../../packages/core/DatadogSDKReactNative.podspec', :testspecs => ['Tests'] @@ -50,6 +65,35 @@ target 'ddSdkReactnativeExample' do end end + # These pods use non-modular includes that Xcode 26 / iOS 26.5 SDK now treats as hard + # errors (via -Werror=non-modular-include-in-framework-module). This was not a problem + # before Xcode 26. Downgrade to a warning only for the affected pods so it can compile, + # rather than silencing the diagnostic project-wide (including our own Datadog pods). + non_modular_include_pods = %w[ + ReactNativeNavigation + React-RCTFabric + React-RCTAppDelegate + RNScreens + RNSVG + ] + installer.pods_project.targets.each do |target| + next unless non_modular_include_pods.include?(target.name) + + target.build_configurations.each do |config| + config.build_settings['CLANG_WARN_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES'] = 'NO' + end + end + + # Xcode 26 / iOS 26.5 SDK changed how C system library modules (CommonCrypto, zlib) + # are resolved from Swift. DatadogSessionReplay uses `import CommonCrypto` and + # `import zlib` which fail unless the SDK's usr/include is on SWIFT_INCLUDE_PATHS. + # Apply this broadly — it is harmless for pods that don't use these imports. + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['SWIFT_INCLUDE_PATHS'] = '$(inherited) $(SDKROOT)/usr/include' + end + end + # Modify RCT-Folly Portability.h to disable coroutines Dir.glob(installer.sandbox.root + "RCT-Folly/folly/Portability.h") do |file| begin diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 2d42b191a..c7d97375b 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1713,6 +1713,8 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - RNSVG (15.12.1): + - React-Core - SocketRocket (0.7.1) - Yoga (0.0.0) @@ -1795,6 +1797,7 @@ DEPENDENCIES: - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNScreens (from `../node_modules/react-native-screens`) + - RNSVG (from `../node_modules/react-native-svg`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: @@ -1961,6 +1964,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-gesture-handler" RNScreens: :path: "../node_modules/react-native-screens" + RNSVG: + :path: "../node_modules/react-native-svg" Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" @@ -1987,7 +1992,7 @@ SPEC CHECKSUMS: HMSegmentedControl: 34c1f54d822d8308e7b24f5d901ec674dfa31352 KSCrash: 80e1e24eaefbe5134934ae11ca8d7746586bc2ed OpenTelemetry-Swift-Api: 3d77582ab6837a63b65bf7d2eacc57d8f2595edd - RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17 + RCT-Folly: 7b4f73a92ad9571b9dbdb05bb30fad927fa971e1 RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83 RCTRequired: ca91e5dd26b64f577b528044c962baf171c6b716 RCTTypeSafety: e7678bd60850ca5a41df9b8dc7154638cb66871f @@ -2050,9 +2055,10 @@ SPEC CHECKSUMS: RNCAsyncStorage: 23e56519cc41d3bade3c8d4479f7760cb1c11996 RNGestureHandler: 950dfa674dbf481460ca389c65b9036ac4ab8ada RNScreens: 606ab1cf68162f7ba0d049a31f2a84089a6fffb4 + RNSVG: 61b5de449d8daa7b75da111990fee37baeb4a6f2 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a + Yoga: 40f19fff64dce86773bf8b602c7070796c007970 -PODFILE CHECKSUM: a2729cbf6c0a559932ef8f49d27110873b77ff7d +PODFILE CHECKSUM: 077c13134c2f7f0a3d790748bbf6e621f27e21dd COCOAPODS: 1.16.2 diff --git a/example/metro.config.js b/example/metro.config.js index a2df59826..62bf95170 100644 --- a/example/metro.config.js +++ b/example/metro.config.js @@ -4,6 +4,12 @@ const escape = require('escape-string-regexp'); const pakCore = require('../packages/core/package.json'); const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); const { withDatadogMetroConfig } = require('@datadog/mobile-react-native/metro'); +// withSessionReplayAssetBundler is temporarily excluded here: as of `develop`, +// react-native-session-replay still depends on chokidar ^5.0.0, which is ESM-only +// and cannot be require()'d from a CommonJS Metro config (see the chokidar revert +// to ^4.0.3 in a sibling branch/PR). Once that lands, switch back to wrapping this +// config with withSessionReplayAssetBundler. Until then, assets are pre-generated +// via `npx datadog-generate-sr-assets` before the native build. const root = path.resolve(__dirname, '..'); @@ -17,16 +23,28 @@ const modules = Object.keys({ * * @type {import('metro-config').MetroConfig} */ +const defaultConfig = getDefaultConfig(__dirname); + const config = { projectRoot: __dirname, watchFolders: [root], resetCache: true, + // Route .svg files through react-native-svg-transformer so they are importable + // as React components (used by Session Replay SVG file-import test cases). + transformer: { + babelTransformerPath: require.resolve('react-native-svg-transformer') + }, // We need to make sure that only one version is loaded for peerDependencies // So we blacklist them at the root, and alias them to the versions in example's node_modules // This block is very important, because otherwise things like React can be packed multiple times // while it should be only one React instance in the runtime. exclusionList relies on the modules which are // declared as peer dependencies in the core package. resolver: { + // Remove svg from asset extensions and add it to source extensions so Metro + // sends it through the transformer rather than copying it as a static asset. + assetExts: defaultConfig.resolver.assetExts.filter(ext => ext !== 'svg'), + sourceExts: [...defaultConfig.resolver.sourceExts, 'svg'], + blacklistRE: exclusionList( modules.map( m => @@ -46,4 +64,4 @@ const config = { } }; -module.exports = withDatadogMetroConfig(mergeConfig(getDefaultConfig(__dirname), config)); +module.exports = withDatadogMetroConfig(mergeConfig(defaultConfig, config)); diff --git a/example/package.json b/example/package.json index 2ea88f482..9106101d9 100644 --- a/example/package.json +++ b/example/package.json @@ -32,6 +32,7 @@ "react-native-navigation": "8.0.0-snapshot.1658", "react-native-safe-area-context": "5.1.0", "react-native-screens": "4.5.0", + "react-native-svg": "15.12.1", "react-native-webview": "13.14.2" }, "devDependencies": { @@ -39,6 +40,7 @@ "@babel/preset-env": "^7.25.9", "@babel/runtime": "^7.29.2", "@datadog/datadog-ci": "5.13.1", + "@datadog/mobile-react-native-babel-plugin": "workspace:packages/react-native-babel-plugin", "@react-native/babel-preset": "0.76.9", "@react-native/eslint-config": "0.76.9", "@react-native/metro-config": "0.76.9", @@ -46,7 +48,8 @@ "@rnx-kit/metro-resolver-symlinks": "^0.1.36", "babel-plugin-module-resolver": "^4.0.0", "patch-package": "^8.0.1", - "postinstall-postinstall": "^2.1.0" + "postinstall-postinstall": "^2.1.0", + "react-native-svg-transformer": "^1.5.0" }, "engines": { "node": ">=18" diff --git a/example/src/App.tsx b/example/src/App.tsx index e6e4ac694..42608f9b9 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { NavigationContainer } from '@react-navigation/native'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import { createStackNavigator } from '@react-navigation/stack'; import MainScreen from './screens/MainScreen'; import ErrorScreen from './screens/ErrorScreen'; import AboutScreen from './screens/AboutScreen'; @@ -13,9 +14,11 @@ import { DatadogOpenFeatureProvider } from '@datadog/mobile-react-native-openfea import { OpenFeature, OpenFeatureProvider } from '@openfeature/react-sdk'; import { Route } from "@react-navigation/native"; import { NestedNavigator } from './screens/NestedNavigator/NestedNavigator'; +import SvgScreen from './screens/SvgScreen'; import { getDatadogConfig, onDatadogInitialization } from './ddUtils'; const Tab = createBottomTabNavigator(); +const Stack = createStackNavigator(); // === Navigation Tracking custom predicates const viewNamePredicate: ViewNamePredicate = function customViewNamePredicate(route: Route, trackedName: string) { @@ -48,28 +51,14 @@ const navigationTrackingOptions: NavigationTrackingOptions = { viewTrackingPredicate, paramsTrackingPredicate, } + // === Datadog Provider Configuration schemes === // 1.- Direct configuration const configuration = getDatadogConfig(TrackingConsent.GRANTED) -// 2.- File based configuration from .json -// const configuration = new FileBasedConfiguration(require("../datadog-configuration.json")); - -// 3.- File based configuration from .json and custom mapper setup -// const configuration = new FileBasedConfiguration( { -// configuration: require("../datadog-configuration.json").configuration, -// errorEventMapper: (event) => event, -// resourceEventMapper: (event) => event, -// actionEventMapper: (event) => event}); - -// 4.- File based configuration from the native side (using initFromNative) -// see https://docs.datadoghq.com/real_user_monitoring/guide/initialize-your-native-sdk-before-react-native-starts - -// const configuration = new DatadogProviderConfiguration("fake_value", "fake_value"); - const handleDatadogInitialization = async () => { - onDatadogInitialization(); + await onDatadogInitialization(); // Enable Datadog Flags feature. await DdFlags.enable(); @@ -79,6 +68,22 @@ const handleDatadogInitialization = async () => { OpenFeature.setProvider(provider); } +function TabNavigator() { + return ( + null + }}> + + + + + + + ); +} + export default function App() { return ( @@ -88,17 +93,14 @@ export default function App() { navigationRef.current, navigationTrackingOptions) }}> - null - }}> - - - - - - + + + + diff --git a/example/src/assets/heart.svg b/example/src/assets/heart.svg new file mode 100644 index 000000000..e095d98d0 --- /dev/null +++ b/example/src/assets/heart.svg @@ -0,0 +1,3 @@ + + + diff --git a/example/src/assets/icons.ts b/example/src/assets/icons.ts new file mode 100644 index 000000000..5662e288f --- /dev/null +++ b/example/src/assets/icons.ts @@ -0,0 +1,11 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +// Barrel file used to test Fix 6: ExportNamedDeclaration must use spec.exported.name +// not spec.local.name, otherwise 'export { default as HeartIcon }' stores 'default' +// as the key in localSvgMap instead of 'HeartIcon', and is never found. +export { default as HeartIcon } from './heart.svg'; +export { default as ShieldIcon } from './shield.svg'; diff --git a/example/src/assets/shield.svg b/example/src/assets/shield.svg new file mode 100644 index 000000000..0171eebd0 --- /dev/null +++ b/example/src/assets/shield.svg @@ -0,0 +1,4 @@ + + + + diff --git a/example/src/assets/star.svg b/example/src/assets/star.svg new file mode 100644 index 000000000..39d06c8cd --- /dev/null +++ b/example/src/assets/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/example/src/assets/svg.d.ts b/example/src/assets/svg.d.ts new file mode 100644 index 000000000..eb2ad40ff --- /dev/null +++ b/example/src/assets/svg.d.ts @@ -0,0 +1,13 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import React from 'react'; +import { SvgProps } from 'react-native-svg'; + +declare module '*.svg' { + const content: React.FC; + export default content; +} diff --git a/example/src/ddUtils.tsx b/example/src/ddUtils.tsx index 190aba093..0fa7decdf 100644 --- a/example/src/ddUtils.tsx +++ b/example/src/ddUtils.tsx @@ -14,6 +14,12 @@ import { DatadogOpenFeatureProvider } from '@datadog/mobile-react-native-openfea import { OpenFeature } from '@openfeature/react-sdk'; import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; +import { + SessionReplay, + ImagePrivacyLevel, + TextAndInputPrivacyLevel, + TouchPrivacyLevel +} from '@datadog/mobile-react-native-session-replay'; import { BatchProcessingLevel } from '@datadog/mobile-react-native/src/config/types'; // New SDK Setup - not available for react-native-navigation @@ -57,10 +63,20 @@ export function getDatadogConfig(trackingConsent: TrackingConsent) { return config } - export function onDatadogInitialization() { + export async function onDatadogInitialization() { DdLogs.info('The RN Sdk was properly initialized') DdSdkReactNative.setUserInfo({id: "1337", name: "Xavier", email: "xg@example.com", extraInfo: { type: "premium" } }) DdSdkReactNative.addAttributes({campaign: "ad-network"}) + + // Enable Session Replay so the SVG test screen can be verified in the Datadog portal. + // Using MASK_NON_BUNDLED_ONLY to mirror the customer's configuration — bundled SVGs + // from assets.bin should appear; remote/network images are masked. + await SessionReplay.enable({ + replaySampleRate: 100, + imagePrivacyLevel: ImagePrivacyLevel.MASK_NON_BUNDLED_ONLY, + textAndInputPrivacyLevel: TextAndInputPrivacyLevel.MASK_SENSITIVE_INPUTS, + touchPrivacyLevel: TouchPrivacyLevel.SHOW, + }); } // Legacy SDK Setup diff --git a/example/src/screens/MainScreen.tsx b/example/src/screens/MainScreen.tsx index 1616e953d..f9bc0bb28 100644 --- a/example/src/screens/MainScreen.tsx +++ b/example/src/screens/MainScreen.tsx @@ -210,6 +210,11 @@ export default class MainScreen extends Component { Click me (error) +