-
Notifications
You must be signed in to change notification settings - Fork 50
[RUM-11630] Support SVG in Session Replay #985
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
[RUM-11630] Support SVG in Session Replay #985
Conversation
9a73098
to
1a4f6ec
Compare
b539a74
to
7228edb
Compare
packages/react-native-babel-plugin/src/cli/generate-sr-assets.ts
Outdated
Show resolved
Hide resolved
packages/react-native-babel-plugin/src/cli/generate-sr-assets.ts
Outdated
Show resolved
Hide resolved
packages/react-native-babel-plugin/src/libraries/react-native-svg/constants.ts
Outdated
Show resolved
Hide resolved
packages/react-native-babel-plugin/src/libraries/react-native-svg/constants.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this 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.
packages/react-native-babel-plugin/src/cli/generate-sr-assets.ts
Outdated
Show resolved
Hide resolved
packages/react-native-babel-plugin/src/cli/generate-sr-assets.ts
Outdated
Show resolved
Hide resolved
packages/react-native-babel-plugin/src/cli/generate-sr-assets.ts
Outdated
Show resolved
Hide resolved
packages/react-native-babel-plugin/src/cli/generate-sr-assets.ts
Outdated
Show resolved
Hide resolved
packages/react-native-babel-plugin/src/cli/generate-sr-assets.ts
Outdated
Show resolved
Hide resolved
4e46b59
to
80c0caf
Compare
There was a problem hiding this 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!
3877d16
to
1786b82
Compare
in context: SessionReplayViewTreeRecordingContext | ||
) -> SessionReplayNodeSemantics? { | ||
|
||
if (view.accessibilityIdentifier != nil) { |
There was a problem hiding this comment.
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.
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] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) |
There was a problem hiding this comment.
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.
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) |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
struct SVGData: Codable { | |
internal struct SVGData: Codable { |
imagePrivacyLevel: context.recorder.imagePrivacy | ||
) | ||
|
||
let element = SessionReplaySpecificElement(subtreeStrategy: .ignore, nodes: [ |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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"
}
guard let svgInfo = svgMap[hash] else { | ||
return nil | ||
} |
There was a problem hiding this comment.
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
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 | ||
} | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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
- iOS 2.30.2 - Android 2.26.2
- 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.
…es for clarity and consistency
…nsform properties from style objects
8a7ad2e
to
507b67c
Compare
There was a problem hiding this 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 💯
packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift
Outdated
Show resolved
Hide resolved
packages/react-native-session-replay/ios/Sources/SvgViewRecorder.swift
Outdated
Show resolved
Hide resolved
try { | ||
mergeSvgAssets(assetsDir); | ||
} catch (error) { | ||
console.warn('[SessionReplayAggregator] merge failed:', error); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ultra-nit:
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
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:
@datadog/mobile-react-native-babel-plugin
package)@datadog/mobile-react-native-session-replay
package)@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:
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:
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:
3. External SVGs (Remote)
SVGs loaded from external servers using the SvgUri component from react-native-svg.
Example:
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 ofreact-native-svg
, such as: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:
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:
assets.bin
) with an accompanying JSON index (assets.json
) for efficient native lookupbundle_build_done
andtransformer_load_done
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 togetherassets.json
- An index mapping SVG IDs to their byte offset and length in the binary fileThis 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:
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:
.js
,.jsx
,.ts
, and.tsx
files (excludingnode_modules
, test files, etc.)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:2. Setup the Metro Plugin
Configure the Metro plugin in your
metro.config.js
to enable asset bundling: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:
react-native-svg
This ensures that SVGs are seamlessly captured in Session Replay without requiring manual asset management.