Skip to content

Missing platform (web) conditioning for withDatadogMetroConfig and @datadog/mobile-react-native-babel-plugin #1204

@leggomuhgreggo

Description

@leggomuhgreggo

Description

For cross-platform React Native apps that also target web (e.g., Expo with web support), the v3 SDK's build tooling plugins assume a native-only build pipeline.

Both withDatadogMetroConfig and @datadog/mobile-react-native-babel-plugin run unconditionally for all platforms, requiring consumers to manually add platform guards in their build configs.

Ideally the tools could add built-in platform awareness, so cross-platform apps don't need custom conditioning boilerplate.

Current Workarounds

babel.config.js — skip babel plugin for web

module.exports = function (api) {
  const isWeb = api.caller((caller) => caller && caller.platform === 'web');
  api.cache.using(() => `platform:${isWeb ? 'web' : 'native'}`); // api.cache.using() lets Babel cache per-platform instead just a single case

  const plugins = [ 'react-native-reanimated/plugin', /* ...other plugins... */ ];

  // Only add for native builds
  if (!isWeb) plugins.push('@datadog/mobile-react-native-babel-plugin');

  return {
    presets: ['babel-preset-expo'],
    plugins,
  };
};

metro.config.js — skip metro plugin for web

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

// ...config setup...

const isWebBuild = process.env.EXPO_PUBLIC_PLATFORM === 'web';
module.exports = isWebBuild ? baseConfig : withDatadogMetroConfig(baseConfig);

These work, but every cross-platform consumer has to independently figure out and maintain this conditioning.

🦙 Analysis + Recommendations

withDatadogMetroConfig

Analysis — no platform check

The custom serializer in metroSerializer.js only checks graph.transformOptions.hot (to skip hot reload). It never checks graph.transformOptions.platform, which Metro provides for every build and will be "web" for web bundles.

What happens without conditioning:

  • The serializer injects a debug ID virtual module and rewrites the bundle + sourcemap for every platform, including web.
  • Web builds bundled through Metro (Expo web) will get unnecessary debug ID injection. The injected globalThis.__dd_debug_id code is dead weight — web sourcemaps are handled by @datadog/browser-rum via a completely separate mechanism (Datadog CI sourcemaps upload or browser SDK inline).
  • More critically, the serializer calls into Metro internals (baseJSBundle, bundleToString, sourceMapString) which may behave differently or break for web bundles depending on Metro version and Expo's web pipeline config.

For comparison: Sentry's withSentryConfig (which DD's metro code is adapted from) does have platform-aware logic — it uses graph.transformOptions.platform to conditionally exclude native-only packages from web builds and apply web-specific features like injectReleaseForWeb.

Proposed solution

Option A — Check platform in the serializer (preferred):

The serializer receives graph.transformOptions.platform for every build. It could skip debug ID injection for web:

if (graph.transformOptions.hot || graph.transformOptions.platform === 'web') {
  return serializer(entryPoint, preModules, graph, options);
}

Option B — Accept a platforms option:

withDatadogMetroConfig(baseConfig, { platforms: ['ios', 'android'] });

Option A is better because it requires no consumer changes and "just works."

@datadog/mobile-react-native-babel-plugin

Analysis — platform-aware but doesn't skip web

The plugin already reads caller.platform via PluginState.getPlatform():

getPlatform() {
  return this.state?.file?.opts?.caller?.platform || 'unknown';
}

But it only uses this for per-platform deduplication of the globalThis.__DD_RN_BABEL_PLUGIN_ENABLED__ flag (so it initializes once per platform). It does not use it to skip web transforms entirely.

What happens without conditioning:

  • The plugin instruments every JSX component with RUM action tracking wrappers (DdBabelInteractionTracking) — even in web bundles.
  • It injects imports from @datadog/mobile-react-native/interaction-tracking into instrumented files. On web, @datadog/mobile-react-native may pull in native modules (expo-datadog, React Native bridge code) that don't exist in a browser context. Tree-shaking may or may not save you depending on side effects.
  • It sets globalThis.__DD_RN_BABEL_PLUGIN_ENABLED__ = true in web bundles. While @datadog/browser-rum doesn't check this flag (it's only consumed by @datadog/mobile-react-native), it's still dead code in the wrong context.
  • The injected __ddExtractText helper and jsx/jsxs runtime wrappers add bundle size overhead for zero benefit on web.
Proposed solution

The plugin already has caller.platform available via PluginState.getPlatform(). It could skip all transforms when platform === 'web':

// In the Program.enter visitor:
if (pluginState.getPlatform() === 'web') {
  return; // No-op for web builds
}

This is a minimal, backwards-compatible change. The platform detection pattern is well-established — react-native-reanimated/plugin and others use api.caller() the same way.

Environment

  • @datadog/mobile-react-native: 3.1.2
  • @datadog/mobile-react-native-babel-plugin: 3.1.2
  • @datadog/browser-rum: 6.30.1 (used for web RUM)
  • Expo SDK: 54
  • Metro (via @expo/metro-config)
  • Build setup: Nx monorepo with shared Metro/Babel configs serving both native and web targets

Reproduction steps

Steps to reproduce:

Create an Expo app that targets both native (iOS/Android) and web
Install @datadog/mobile-react-native and @datadog/mobile-react-native-babel-plugin
Add withDatadogMetroConfig(config) to metro.config.js without platform conditioning
Add '@datadog/mobile-react-native-babel-plugin' to babel.config.js plugins without platform conditioning
Run npx expo start --web or npx expo export --platform web
Expected: Build tooling is a no-op for web bundles (debug ID injection and RUM action instrumentation are native-only concerns)

Actual:

Metro serializer injects debug IDs into web bundles unnecessarily
Babel plugin instruments web JSX with native RUM action wrappers and injects imports from @datadog/mobile-react-native/interaction-tracking
globalThis.DD_RN_BABEL_PLUGIN_ENABLED is set in web context

SDK logs

No response

Expected behavior

No response

Affected SDK versions

v3

Latest working SDK version

n/a (new features)

Did you confirm if the latest SDK version fixes the bug?

Yes

Integration Methods

NPM

React Native Version

0.81.5

Package.json Contents

No response

iOS Setup

No response

Android Setup

No response

Device Information

No response

Other relevant information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions