Skip to content

feat(core): Add base64 polyfill carrier hook for envelope encoding#6312

Closed
alwx wants to merge 3 commits into
mainfrom
alwx/feature/base64-polyfill-hook
Closed

feat(core): Add base64 polyfill carrier hook for envelope encoding#6312
alwx wants to merge 3 commits into
mainfrom
alwx/feature/base64-polyfill-hook

Conversation

@alwx

@alwx alwx commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

📢 Type of change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring

📜 Description

Adds a carrier-level base64 polyfill hook (carrier.base64Polyfill) that mirrors the existing encodePolyfill indirection used for UTF-8 encoding.

  • New packages/core/src/js/transports/base64Polyfill.ts — exposes useBase64Polyfill() and the default base64Polyfill implementation (delegates to the existing JS base64StringFromByteArray from vendor/base64-js).
  • New packages/core/src/js/utils/base64.tsencodeToBase64() helper that lazy-installs the polyfill and dispatches through the carrier.
  • wrapper.ts (captureEnvelope) now calls encodeToBase64(envelopeBytes) instead of base64StringFromByteArray(envelopeBytes) directly.
  • sdk.tsx installs the polyfill at init alongside useEncodePolyfill().
  • The React Native carrier type is extended locally with an optional base64Polyfill?: (input: Uint8Array | number[]) => string field. Upstream @sentry/core is not touched.

Behavior is unchanged — the same JS encoder runs by default. This PR is plumbing only.

💡 Motivation and Context

Stage 1 of #4884. The pure-JS base64 encoder is a measurable bottleneck on the envelope hot path (RNSentry.captureEnvelope) for larger payloads (profiles, attachments, replays). Putting the carrier hook in place now means a future native (JSI / TurboModule) encoder can be dropped in by swapping a single carrier assignment, without touching any call sites in event-processing code.

Refs #4884.

💚 How did you test it?

  • New test: packages/core/test/transports/base64Polyfill.test.ts — verifies the polyfill is installed on the carrier and that the default encoder produces correct base64 output.
  • yarn test — all 1619 tests pass.
  • yarn build — passes.
  • yarn lint:lerna — 0 warnings, 0 errors.
  • yarn circularDepCheck — no circular dependencies.
  • yarn api-report:check — API report up to date (no public API changes).

📝 Checklist

  • I added tests to verify changes
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • All tests passing
  • No breaking changes

🔮 Next steps

Stage 2 of #4884: implement the native fast path behind the same carrier hook. Two candidates to evaluate:

  1. Skip base64 entirely — pass envelope bytes across the bridge as ArrayBuffer via TurboModule (new-arch only; fall back to the JS polyfill on old arch).
  2. Vendor a JSI base64 encoder (e.g. react-native-quick-base64-style) and install it as the polyfill.

alwx added 3 commits June 17, 2026 14:37
Install a Sentry-owned `facebook::react::NativeModulePerfLogger` on
both platforms so the SDK observes every TurboModule lifecycle event \u2014
`moduleDataCreate*`, `moduleCreate*`, sync/async method call
`start`/`end`/`fail`, async dispatch and execution `start`/`end`/`fail`
\u2014 for follow-up features (crash attribution, per-module spans,
aggregated stats) to plug into.

The implementation is split into:

- **Shared C++** (`packages/core/cpp/`): a single
  `SentryTurboModulePerfController` singleton owns the installed logger
  and an atomic `enabled` flag. When disabled, every callback hits one
  atomic load and returns. When enabled, callbacks are forwarded to a
  swappable `ISentryTurboModulePerfSink` \u2014 follow-up issues ship the
  sinks; this PR just exposes the hook.

- **iOS**: the perf logger is installed from a dedicated installer
  class's `+load` so it fires before `RCTBridge` / `RCTHost` create
  their first TurboModule. (`RNSentry`'s own `+load` is reserved by
  `RCT_EXPORT_MODULE()`.) The cpp/ directory is added to the podspec
  sources; files are guarded with `RCT_NEW_ARCH_ENABLED` so Old Arch
  builds compile to empty TUs.

- **Android**: a new `libsentry-tm-perf-logger.so` shared library is
  built via CMake under New Architecture only and exposes `JNI_OnLoad`
  + a tiny `nativeSetEnabled` JNI hook. It links against React
  Native's `reactnative` prefab; the missing
  `<reactperflogger/NativeModulePerfLogger.h>` header is plugged by
  pointing the include path at the source tree (mirroring how
  react-native-reanimated resolves react-native via the standard
  `REACT_NATIVE_NODE_MODULES_DIR` / `require.resolve` fallback).
  `RNSentryPackage`'s static initializer `System.loadLibrary`s the
  perf-logger lib \u2014 host apps do NOT need to touch their own
  `OnLoad.cpp`. A guarded `try { \u2026 } catch (UnsatisfiedLinkError)`
  keeps Old Architecture (and any host that strips the lib) working
  as before.

Runtime gate: new `enableTurboModuleTracking` option on `Sentry.init`,
default `false` for this first release so the foundation lands without
behavioral change. The native logger is always installed (we never want
to miss early lifecycle events), the flag only decides whether
forwarded callbacks reach the Sentry sink. The option is plumbed
through `initNativeSdk` on both platforms.

Foundation only \u2014 no sink is installed in this PR. Follow-up issues
ship the actual instrumentation.

Closes #6162
Address Warden's medium-severity finding on PR #6307: the new
`SentryTurboModulePerfController` and `RNSentryTurboModulePerfTracker`
shipped without unit coverage. Add focused tests that exercise the
state machines independently of React Native's runtime.

- **iOS** (`RNSentryCocoaTester/.../RNSentryTurboModulePerfControllerTests.mm`):
  default `isEnabled() == false`, `setEnabled` toggle, the C-linkage
  `Sentry_SetTurboModuleTrackingEnabled` entry point matches the typed
  setter, `setSink`/`sink` round-trips including `nullptr` detach,
  and `Sentry_InstallTurboModulePerfLogger` idempotency under repeated
  calls. End-to-end forwarding through `facebook::react::TurboModulePerfLogger`
  is intentionally not covered here \u2014 it requires `+load` ordering and
  process-wide singletons that the follow-up sink PRs will integration-test.

- **Android** (`RNSentryAndroidTester/.../RNSentryTurboModulePerfTrackerTest.kt`):
  the JVM-side latch around the JNI symbol. In the test JVM the
  underlying `.so` is not loaded, so the first `setEnabled` call must
  catch `UnsatisfiedLinkError` and flip `nativeUnavailable`; subsequent
  calls must short-circuit. Uses Robolectric so the `android.util.Log.i`
  call inside the catch branch resolves instead of throwing the
  not-mocked stub. A small `@TestOnly` window on the tracker exposes
  the latch state to assertions.

Also fix the changelog entry to reference the PR (#6307) rather than
the issue (#6162) so danger stops nagging.
Introduce a carrier-level base64 polyfill (`carrier.base64Polyfill`) that
mirrors the existing `encodePolyfill` indirection. `captureEnvelope` in the
native wrapper now goes through `encodeToBase64`, which lazy-installs and
dispatches via the carrier, instead of calling the vendored JS base64
encoder directly.

The default implementation is unchanged — it still delegates to the
JS `base64StringFromByteArray` from `vendor/base64-js`, so there is no
behavior change in this commit. The point is to put the indirection in
place so a future change can swap in a native (JSI/TurboModule) base64
encoder without touching any call sites — addressing the >10x perf
opportunity tracked in #4884.

The React Native carrier type is extended locally with an optional
`base64Polyfill` field; upstream `@sentry/core` is untouched.
@github-actions

Copy link
Copy Markdown
Contributor

Semver Impact of This PR

None (no version bump detected)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


  • feat(core): Add base64 polyfill carrier hook for envelope encoding by alwx in #6312
  • chore(deps): update Android SDK to v8.44.0 by github-actions in #6310
  • chore(deps): update CLI to v3.5.1 by github-actions in #6305
  • chore(deps): update JavaScript SDK to v10.58.0 by github-actions in #6296
  • chore: Fix lint issues by antonis in #6300
  • chore: Bump sample and perf test apps to React Native 0.86.0 by antonis in #6287
  • fix(deps): bump form-data from 4.0.5 to 4.0.6 by antonis in #6297
  • fix(ci): Handle @sentry-internal/* package renames in JS updater by antonis in #6295
  • Record network request/response bodies in Session Replay by alwx in #6288
  • chore(deps): bump tar from 7.5.11 to 7.5.16 by dependabot in #6293
  • fix(ci): Update renamed @sentry-internal/* packages in JS updater script by antonis in #6294
  • chore(deps): bump launch-editor from 2.11.1 to 2.14.1 by dependabot in #6291
  • chore(deps-dev): bump @babel/core from 7.26.7 to 7.29.6 by dependabot in #6292
  • fix(deps): Resolve shell-quote to >=1.8.4 (Dependabot RNSentryModule.captureEvent is ignoring environment #547) by antonis in #6286
  • fix(ci): Support version catalog in android SDK version check by antonis in #6280
  • test(e2e): Bump E2E tests to React Native 0.86.0 by antonis in #6268
  • feat(android): Add nativeStackAndroid support to NativeLinkedErrors by lucas-zimerman in #6278
  • chore(deps): bump ruby/setup-ruby from 1.310.0 to 1.313.0 by dependabot in #6282
  • chore(deps): update Maestro to v2.6.1 by github-actions in #6277
  • chore(deps): bump gradle/actions from 6.1.0 to 6.2.0 by dependabot in #6284
  • chore(deps): bump getsentry/craft from 2.26.8 to 2.26.10 by dependabot in #6283
  • chore(deps): bump getsentry/craft/.github/workflows/changelog-preview.yml from 2.26.8 to 2.26.10 by dependabot in #6281
  • chore(deps): update Sentry Android Gradle Plugin to v6.11.0 by github-actions in #6275
  • chore(deps): update Android SDK to v8.43.2 by github-actions in #6273

Plus 4 more


🤖 This preview updates automatically when you update the PR.

@github-actions

Copy link
Copy Markdown
Contributor
Fails
🚫 Pull request is not ready for merge, please add the "ready-to-merge" label to the pull request
🚫 Please consider adding a changelog entry for the next release.
Warnings
⚠️

⚠️ Android SDK Version Mismatch

Component Version
sentry-android in build.gradle 8.44.0
sentry-android bundled by gradle plugin 6.11.0 8.43.2

If a user enables AGP autoInstallation (default: true), this mismatch causes:

IllegalStateException: Sentry SDK has detected a mix of versions

Our docs instruct React Native users to set autoInstallation.enabled = false, so users following the guide are unaffected. Consider either updating packages/core/android/build.gradle to 8.43.2 or waiting for a gradle plugin release that bundles 8.44.0.

Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Instructions and example for changelog

Please add an entry to CHANGELOG.md to the "Unreleased" section. Make sure the entry includes this PR's number.

Example:

## Unreleased

### Features

- Add base64 polyfill carrier hook for envelope encoding ([#6312](https://github.com/getsentry/sentry-react-native/pull/6312))

If none of the above apply, you can opt out of this check by adding #skip-changelog to the PR description or adding a skip-changelog label.

Generated by 🚫 dangerJS against f17dd96

@alwx alwx closed this Jun 18, 2026
@alwx alwx deleted the alwx/feature/base64-polyfill-hook branch June 18, 2026 09:11
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