Skip to content

Conversation

cdn34dd
Copy link
Contributor

@cdn34dd cdn34dd commented Sep 15, 2025

What does this PR do?

This PR adds support for tracking SVG images in React Native Session Replay.

This PR is composed of 3 main components that work together to create a workflow that enables SVGs to be captured in Session Replay:

  • New Babel Plugin Feature (@datadog/mobile-react-native-babel-plugin package)
  • New Metro Plugin (@datadog/mobile-react-native-session-replay package)
  • New CLI Utility (@datadog/mobile-react-native-babel-plugin package)

Babel Plugin

The Babel plugin feature is the core of this PR, containing most of the complexity. Its main responsibilities are to:

  • Extract SVG nodes from the user's codebase during the build process
  • Convert SVG nodes into a web-compliant format, which involves:
    • Updating tag and property names (e.g., strokeWidth → stroke-width)
    • Removing React Native-specific metadata
    • Converting styling information from React Native style objects into proper web SVG properties
    • Extracting transform properties from style objects
  • Save extracted SVGs as optimized assets on the file system using SVGO optimization
  • Properly identify SVG code nodes so they can be tracked by Session Replay on the native side

What kind of SVGs does the plugin handle?

To correctly identify and extract SVGs in a user's codebase, the plugin differentiates between three types of SVGs and handles each appropriately:

1. Inline SVGs

SVGs built directly in the codebase using elements from the react-native-svg library (e.g., <Svg>, <Path>, <Circle>). These require the most complex logic for full extraction and transformation.

Example:

<Svg width="100" height="100">
   <Circle cx="50" cy="50" r="45" fill="blue" />
</Svg>

2. External SVGs (Local)

SVGs loaded from the user's filesystem. These are typically already in web-compliant format, but the plugin needs to properly identify them in the user's code since they can be named anything.

Example:

import MyIllustration from './assets/illustration.svg';

<MyIllustration width="100" height="100" />

3. External SVGs (Remote)

SVGs loaded from external servers using the SvgUri component from react-native-svg.

Example:

 <SvgUri uri="https://example.com/icon.svg" />

What kind of SVGs do we still need to support?

The plugin currently handles direct usage of react-native-svg elements and loading SVG files directly. The main area for future expansion is adding support for popular SVG libraries built on top of react-native-svg, such as:

  • react-native-feather
  • iconoir-react-native
  • @fluentui/react-native-icons

The groundwork to support these libraries is already in place, as the logic should be similar to handling local SVGs (treating library-generated SVGs as specialized local SVG cases).

Areas of Improvement

Some areas for future enhancement that didn't make it into this first release:

  • Improved tracking of local SVGs when using aliased paths (e.g., @assets/icon.svg)
  • Enhanced variable tracking for dynamically referenced SVG sources
  • Extended style-to-SVG property conversion for edge cases where users set style properties directly on SVG elements
  • Support for SVG data URLs. While the plugin itself supports it, the native Session Replay SDKs on Android and iOS still need to add support for it.
  • Better error handling and reporting during the transformation process

All of these improvements are feasible with sufficient time allocation.

Metro Plugin

The Metro plugin (withSessionReplayAssetBundler) integrates with React Native's Metro bundler to automatically aggregate SVG assets during development and production builds.

Key responsibilities:

  • Monitors the assets directory for new SVG files created by the Babel plugin during development
  • Merges individual SVG files into a single optimized binary format (assets.bin) with an accompanying JSON index (assets.json) for efficient native lookup
  • Automatically triggers asset aggregation on Metro events such as bundle_build_done and transformer_load_done
  • Uses file watching with debouncing to efficiently handle rapid asset changes during hot reload

How it works:

The Metro plugin wraps the Metro config's reporter and watches for new .svg files in the
packages/react-native-session-replay/assets/ directory. When new SVG assets are detected (either from the Babel plugin or the CLI utility), it packs them into:

  • assets.bin - A binary file containing all SVG data concatenated together
  • assets.json - An index mapping SVG IDs to their byte offset and length in the binary file

This approach allows the native SDKs to efficiently load SVG assets by reading specific byte ranges from a single file rather than managing hundreds of individual files.

CLI Utility

The CLI utility (npx datadog-generate-sr-assets) provides a way to pre-generate all Session Replay SVG assets before native builds.

Key responsibilities:

  • Scans the entire codebase for React components containing SVG elements
  • Processes files through the Babel plugin with SVG tracking enabled
  • Extracts and saves SVG assets to the Session Replay package's assets directory
  • Generates the final assets.bin and assets.json files for native platform consumption

When to use it:

The CLI utility should be run after installing dependencies and before building iOS/Android apps:

After installing dependencies
yarn install

Generate SVG assets
npx datadog-generate-sr-assets

Then build native apps
cd ios && pod install && cd ..

This ensures that native asset references are available during the build process, particularly important for iOS where the
assets need to be included in the Xcode project.

What it does:

  1. Clears any existing assets to ensure a fresh state
  2. Scans all .js, .jsx, .ts, and .tsx files (excluding node_modules, test files, etc.)
  3. Transforms each file using the Babel plugin to extract SVG assets
  4. Merges all extracted SVGs into the binary format
  5. Reports the number of assets generated

How does this all work together?

Here's the complete workflow for integrating SVG support in your React Native app:

1. Setup the Babel Plugin

Configure the Babel plugin in your babel.config.js to enable SVG tracking:

  module.exports = {
    presets: ['module:@react-native/babel-preset'],
    plugins: [
      [
        '@datadog/mobile-react-native-babel-plugin',
        {
          sessionReplay: {
             // This is enabled by default, no need to include it
             // but it controls the SVG asset extraction activation
             // if set to false, no assets are extracted
            svgTracking: true 
          }
        }
      ]
    ]
  };

2. Setup the Metro Plugin

Configure the Metro plugin in your metro.config.js to enable asset bundling:

const { withSessionReplayAssetBundler } = require('@datadog/mobile-react-native-session-replay/metro');

module.exports = withSessionReplayAssetBundler({ /* your existing Metro config */ });

3. Generate Assets Before Native Builds

Run the CLI utility after installing dependencies and before building native apps:

Install dependencies
yarn install

Generate Session Replay SVG assets
npx datadog-generate-sr-assets

Build iOS (if needed)
cd ios && pod install && cd ..

Now you can run your app
yarn ios
or
yarn android

4. Development Workflow

During development, the Metro plugin automatically handles new SVG assets created by the Babel plugin:

  • Write your components with SVG elements from react-native-svg
  • Metro bundler detects the new assets via the Babel plugin transformation
  • The Metro plugin watches for new .svg files and automatically merges them
  • Assets are immediately available for Session Replay recording

This ensures that SVGs are seamlessly captured in Session Replay without requiring manual asset management.

@cdn34dd cdn34dd force-pushed the carlosnogueira/RUM-11630/react-native-sr-svg-support branch from 9a73098 to 1a4f6ec Compare September 16, 2025 11:21
@cdn34dd cdn34dd force-pushed the carlosnogueira/RUM-11630/react-native-sr-svg-support branch 6 times, most recently from b539a74 to 7228edb Compare October 6, 2025 15:00
@marco-saia-datadog marco-saia-datadog self-requested a review October 13, 2025 09:27
Copy link
Member

@marco-saia-datadog marco-saia-datadog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing work 💯 the code looks good to me, and is very well documented, thank you! 🙌

I've left a few comments, some are minor nits and a couple are more important points to consider.

@cdn34dd cdn34dd force-pushed the carlosnogueira/RUM-11630/react-native-sr-svg-support branch 2 times, most recently from 4e46b59 to 80c0caf Compare October 15, 2025 11:05
@cdn34dd cdn34dd marked this pull request as ready for review October 15, 2025 11:07
@cdn34dd cdn34dd requested review from a team as code owners October 15, 2025 11:07
OliviaShoup
OliviaShoup previously approved these changes Oct 15, 2025
Copy link

@OliviaShoup OliviaShoup left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks great. thanks for the update!

@cdn34dd cdn34dd changed the title [RUM-11630] Support SVG in Session Replay though Babel Plugin [RUM-11630] Support SVG in Session Replay Oct 15, 2025
@cdn34dd cdn34dd force-pushed the carlosnogueira/RUM-11630/react-native-sr-svg-support branch 2 times, most recently from 3877d16 to 1786b82 Compare October 15, 2025 22:52
sbarrio
sbarrio previously approved these changes Oct 16, 2025
in context: SessionReplayViewTreeRecordingContext
) -> SessionReplayNodeSemantics? {

if (view.accessibilityIdentifier != nil) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's the SDK that sets the accessibilityIdentifier, we could use a prefix (e.g. dd-sr-svg-) so that we can identify which ones to look into, with something like this:

guard let identifier = view.accessibilityIdentifier,
          identifier.hasPrefix("dd-sr-svg-") else {
        return nil
    }

Otherwise, given the multiple levels of nesting below, I'd recommend to use guard let statements instead to exit early and make it more readable.

Suggested change
if (view.accessibilityIdentifier != nil) {
guard view.accessibilityIdentifier != nil else {
return nil
}

let viewId = context.ids.nodeID(view: view, nodeRecorder: self)
let subView = view.subviews[0]

if let attrs = view.value(forKey: "attributes") as? [String: String] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if let attrs = view.value(forKey: "attributes") as? [String: String] {
guard let attrs = view.value(forKey: "attributes") as? [String: String] else {
return nil
}

}

let bundle = Bundle(for: SvgViewRecorder.self)
if let url = bundle.url(forResource: "assets", withExtension: "bin") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if let url = bundle.url(forResource: "assets", withExtension: "bin") {
guard let url = bundle.url(forResource: "assets", withExtension: "bin") {
return nil
}

let decoder = JSONDecoder()
svgMap = try decoder.decode([String: SVGData].self, from: data)
} catch {
consolePrint("Failed to load or decode assets.json", .debug)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we stop here when it fails to load thge assets? We can also add the error to help troubleshooting.

Suggested change
consolePrint("Failed to load or decode assets.json", .debug)
consolePrint("Failed to load or decode assets.json: \(error)", .debug)
return

)


let bundle = Bundle(for: DdSessionReplayImplementation.self)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will the assets always be loaded from this bundle?

import DatadogSDKReactNative
import React

struct SVGData: Codable {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: if only used internally + you could also add some doc

Suggested change
struct SVGData: Codable {
internal struct SVGData: Codable {

imagePrivacyLevel: context.recorder.imagePrivacy
)

let element = SessionReplaySpecificElement(subtreeStrategy: .ignore, nodes: [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could add a comment explaining why we ignore the children

let subView = view.subviews[0]

if let attrs = view.value(forKey: "attributes") as? [String: String] {
let svgId = context.ids.nodeID(view: subView, nodeRecorder: self)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: since we rely on multiple hardcoded strings here, we could extract them to make it safer

private enum DdSessionReplayConstants {
    static let attributesKey = "attributes"
    static let widthKey = "width"
    static let heightKey = "height"
    static let hashKey = "hash"
    static let typeKey = "type"
    static let svgTypeValue = "svg"
}

Comment on lines 79 to 81
guard let svgInfo = svgMap[hash] else {
return nil
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can move this before the do catch statement, right after getting the hash or after checking the svg type; no need to open the file in this case

Comment on lines 106 to 113
if let svgStart = svg.range(of: "<svg"),
let tagEnd = svg.range(of: ">", range: svgStart.upperBound..<svg.endIndex) {

let dimensions = " " + svgAttributes.joined(separator: " ")
svg.replaceSubrange(tagEnd, with: dimensions + ">")
svgData = svg
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is brittle and prone to break. It could also use some more comments/doc. Could we use a regex instead?

Copy link
Contributor Author

@cdn34dd cdn34dd Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is made to handle a very specific scenario, where the user passes dynamic properties to their SVG code in the JS side, but since babel is analyzing these files statically, we can't really resolve the properties if they come from outside the file being parsed.

So what we do instead is simply remove the properties from the SVG at the babel plugin level and signal that the native layer, which has the actual data, should handle it instead. Native will check the signal, which in this case is if either width or height is null, and append the values from the wireframes to the end of the SVG tag.

But is this brittle, in theory yes, but in practice is extremely unlikely to break, first because we sanitize all broken/invalid data form the SVG before hand, and the SVG data goes through SVGO which ensures it's validity. We are also setting these at the root tag of the svg which does accept that many attributes and finally, if something is broken in the SVG it probably won't render in the user's code anyways.

In any case I'll try it with a regex.

@cdn34dd cdn34dd changed the base branch from carlosnogueira/RUM-11710/session-replay-svg to develop October 16, 2025 17:56
@cdn34dd cdn34dd dismissed stale reviews from sbarrio and OliviaShoup October 16, 2025 17:56

The base branch was changed.

- Wrap identified SVG nodes with a `SessionReplayView.Privacy` view, so
we can pass extra information to the native side (e.g., `width`,
`height`, `hash`, ....)
- Add `sessionReplay` option to the plugin, allowing SVG tracking to be
enabled or disabled.
- Add a new session replay mapper targeting SVG views
- Add new session replay node recorder targeting SVG views
- The code now searches within the file itself for the variables used
for width and height and sets those as static values.
- If the variables come from outside, this work is then handled in the
native layer by using the wireframe's data to fill those values.
@cdn34dd cdn34dd force-pushed the carlosnogueira/RUM-11630/react-native-sr-svg-support branch from 8a7ad2e to 507b67c Compare October 16, 2025 22:04
Copy link
Member

@marco-saia-datadog marco-saia-datadog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me! Thanks for addressing my comments. From my side, the only important change left is to list the 3rd party libraries we have added with these changes. Other than that I think we are good to go 💯

try {
mergeSvgAssets(assetsDir);
} catch (error) {
console.warn('[SessionReplayAggregator] merge failed:', error);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ultra-nit:

Suggested change
console.warn('[SessionReplayAggregator] merge failed:', error);
console.warn('[SessionReplayAssetBundler] SVGs merge failed:', error);

- Ensure assets directory in Session Replay package is cleaned up when
`yarn prepare` is ran so no unwanted files get packed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants