fix(core): RUM auto-instrumentation on apps with a custom JSX runtime - #15
Merged
Conversation
Interaction tracking wrapped `React.createElement` and `react/jsx-runtime`'s `jsx`, which is only correct when the app compiles JSX to React's own runtime. An app that sets a custom `jsxImportSource` - anything built on react-native-css-interop does - compiles to that library's runtime instead, and that runtime captures React's factories while the bundle is evaluated, long before the SDK starts. Patching `react/jsx-runtime` afterwards can no longer reach the app's elements, so no onPress was wrapped and no action was ever recorded. Such a runtime cannot be required from here: Metro resolves requires statically, so a hard-coded require would break bundling for every app that does not depend on it. The app passes its runtimes in through the new `jsxRuntimes` configuration instead, and each one is patched the same way React's is. This drops the assumption that the JSX runtime is necessarily React's rather than special-casing one library. Three related defects, all in the same code path: - `jsxs` was never patched at all. Under the automatic transform an element with a single child compiles to `jsx` and one with several to `jsxs`, so every multi-child element went uninstrumented even without a custom runtime. Both keys, and `jsxDEV`, are now handled per runtime, which also makes install and uninstall symmetric - `jsxDEV` used to be written to the production runtime and restored from the dev one. - Assignments to the element factories are guarded: a host can expose them as getter-only properties, and the resulting TypeError propagated out of `startTracking`. Failing to patch one factory now logs and moves on. - `telemetryDebug` and `sendTelemetryLog` are called through optional call syntax. The object check alone still threw when the native module exposed no such method, which in the catch block meant the handler itself raised a second error.
`enableFeatures` installed the three auto-instrumentations in sequence with no protection, and `_initializeFromDatadogProvider` calls it *before* `initializeNativeSDK`. So an exception while installing the first one did not merely cost that feature's events: it skipped the other two and aborted the native initialization that followed, and the app got no RUM data whatsoever - not even the buffered `DdRum` calls, which never flushed because the SDK never came up. The legacy `initialize()` path installs the features after the native SDK, so the same exception there left views and errors working while silently dropping all three instrumentations. One cause, two very different-looking symptoms, neither of them pointing at the instrumentation that actually failed. Each feature now installs independently and reports its own failure through InternalLog and telemetry. Instrumentation is best-effort; it must never decide whether the SDK starts. `DatadogProvider` also catches the initialization promise it starts during render. Nothing awaited it, so a rejection left no trace at all - and in a release build that strips console calls, not even that.
`ResourceReporter#reportResource` called the module-level reporter and dropped the promise it returned. Any rejection on the way to `DdRum.startResource` was therefore unhandled and invisible: the resource simply never appeared, with nothing logged and no way for an integrator to tell a swallowed error from a request the proxy never saw. Return the promise and log the failure.
…n it fails Checking `writable` before assigning was too narrow. A module namespace can expose its element factories through accessors rather than plain properties - which of the two you get depends on how the module was built and how the bundler models the import - and assignment to one silently does nothing in sloppy mode. Verify the result instead of trusting the descriptor, then fall back to redefining the property, which still works as long as it is configurable. When even that fails, the log said only that a property could not be replaced, leaving the integrator to work out what it cost them. Say the consequence instead: no RUM action will be recorded for anything that runtime renders, and no configuration recovers from it. Report it through telemetry too, so it is visible without a debug build. Documents the dead end rather than leaving it to be discovered, and covers both the accessor and the non-configurable case with tests.
This was referenced Aug 27, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three fixes for RUM not reporting. They were found together but are independent, and the commits are separated accordingly.
1. A failing instrumentation aborted SDK startup
enableFeaturesinstalled the three auto-instrumentations in sequence with no protection, and_initializeFromDatadogProvidercalls it beforeinitializeNativeSDK. An exception while installing the first one skipped the other two and aborted the native initialization, so the app got no RUM data at all — bufferedDdRumcalls never flushed, because the SDK never came up.The legacy
initialize()path installs the features after the native SDK, so the same exception there left views working while silently dropping all three instrumentations. One cause, two very different symptoms, neither pointing at the instrumentation that failed.Each feature now installs independently and reports its own failure through
InternalLogand telemetry.DatadogProvideralso catches the initialization promise it starts during render — nothing awaited it, so a rejection left no trace at all, and none whatsoever in a release build that strips console calls.2. Actions were never recorded when the app uses a custom JSX runtime
Interaction tracking wrapped
React.createElementandreact/jsx-runtime'sjsx. That is only correct when the app compiles JSX to React's own runtime. An app setting a customjsxImportSource— anything built onreact-native-css-interop— compiles to that library's runtime, which captures React's factories while the bundle is evaluated, long before the SDK starts. Patchingreact/jsx-runtimeafterwards can no longer reach the app's elements, so noonPresswas wrapped.Those modules cannot be required from the SDK: Metro resolves requires statically, so a hard-coded require would break bundling for every app that does not depend on them. The app passes its runtimes in instead:
Three related defects in the same code path are fixed with it:
jsxswas never patched. Under the automatic transform an element with one child compiles tojsxand one with several tojsxs, so multi-child elements went uninstrumented even without a custom runtime.TypeErrorused to propagate out ofstartTracking— which is how this met problem 1.jsxDEVinstall/uninstall were asymmetric (written to the production runtime, restored from the dev one), and the telemetry calls in the catch path lacked optional-call syntax, so the error handler could raise a second error.3. The resource reporting promise was discarded
ResourceReporter#reportResourcedropped the promise it started, so any rejection on the way toDdRum.startResourcewas unhandled and invisible: the resource never appeared, with nothing logged.Tests
jsxandjsxswrapped and restored; a read-only factory does not stop tracking; the native SDK still initializes when an instrumentation throws.docs/troubleshooting_no_data.mdgains a section for the missing-actions case.Not yet verified
The device-level check is still outstanding: a release build of an app using a custom
jsxImportSourceshould report views, resources and actions, with a negative control confirming each metric returns to zero when the corresponding fix is reverted.