Skip to content

fix(core): RUM auto-instrumentation on apps with a custom JSX runtime - #15

Merged
Fiona2016 merged 4 commits into
publishfrom
fix/rum-instrumentation-nativewind
Aug 27, 2026
Merged

fix(core): RUM auto-instrumentation on apps with a custom JSX runtime#15
Fiona2016 merged 4 commits into
publishfrom
fix/rum-instrumentation-nativewind

Conversation

@Fiona2016

Copy link
Copy Markdown
Collaborator

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

enableFeatures installed the three auto-instrumentations in sequence with no protection, and _initializeFromDatadogProvider calls it before initializeNativeSDK. 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 — buffered DdRum calls 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 InternalLog and telemetry. DatadogProvider also 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.createElement and react/jsx-runtime's jsx. That is only correct when the app compiles JSX to React's own runtime. An app setting a custom jsxImportSource — anything built on react-native-css-interop — compiles to that library's runtime, which 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.

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:

import * as NativeWindJsxRuntime from 'nativewind/jsx-runtime';

config.jsxRuntimes = [NativeWindJsxRuntime];

Three related defects in the same code path are fixed with it:

  • jsxs was never patched. Under the automatic transform an element with one child compiles to jsx and one with several to jsxs, so multi-child elements went uninstrumented even without a custom runtime.
  • Factory assignments are guarded. A host can expose them as getter-only properties; the resulting TypeError used to propagate out of startTracking — which is how this met problem 1.
  • jsxDEV install/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#reportResource dropped the promise it started, so any rejection on the way to DdRum.startResource was unhandled and invisible: the resource never appeared, with nothing logged.

Tests

  • New: an injected runtime has both jsx and jsxs wrapped and restored; a read-only factory does not stop tracking; the native SDK still initializes when an instrumentation throws.
  • Full suite green (880 passed), lint and typecheck clean.
  • docs/troubleshooting_no_data.md gains 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 jsxImportSource should report views, resources and actions, with a negative control confirming each metric returns to zero when the corresponding fix is reverted.

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.
@Fiona2016
Fiona2016 merged commit fb1ba9a into publish Aug 27, 2026
1 check passed
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.

1 participant