From 2ddb7d20277191856520f333959e39e4b6b05cc4 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 18 Jun 2026 11:18:27 +0200 Subject: [PATCH 1/2] feat(core): Use react-native-quick-base64 for envelope encoding when available MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `react-native-quick-base64` as an optional peer dependency. When the package is installed by the consumer, envelope payloads are base64-encoded via its native JSI implementation (~10x faster than the bundled JS encoder). When it is absent, the SDK transparently falls back to the existing `base64-js`-derived encoder in `vendor/base64-js` — no behavior change for users who do not opt in. The encoder is resolved once at first use and cached, so the optional `require` runs at most once per session. Base64 encoding is on the hot path of `RNSentry.captureEnvelope` and is most impactful for large envelopes (profiles, attachments, replays). Closes #4884. --- CHANGELOG.md | 1 + packages/core/package.json | 6 ++- packages/core/src/js/utils/base64.ts | 56 +++++++++++++++++++++++++ packages/core/src/js/wrapper.ts | 4 +- packages/core/test/utils/base64.test.ts | 43 +++++++++++++++++++ yarn.lock | 3 ++ 6 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 packages/core/src/js/utils/base64.ts create mode 100644 packages/core/test/utils/base64.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 47145a41d4..5118a8b039 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Add `nativeStackAndroid` support to `NativeLinkedErrors`, capturing the JVM stack trace of rejected native module promises as a linked exception ([#6278](https://github.com/getsentry/sentry-react-native/pull/6278)) - Record XHR request/response headers and (optionally) bodies in Mobile Session Replay. Opt in via `mobileReplayIntegration` with `networkDetailAllowUrls` to capture headers; set `networkCaptureBodies: true` to also capture bodies. Other options: `networkDetailDenyUrls`, `networkRequestHeaders`, `networkResponseHeaders`. Authorization-like headers are always stripped, bodies are capped at ~150 KB. Covers XHR-based clients like `axios`; fetch will follow. See [Network Details](https://docs.sentry.io/platforms/react-native/session-replay/#network-details) for details. ([#6288](https://github.com/getsentry/sentry-react-native/pull/6288)) +- Use `react-native-quick-base64` for envelope encoding when installed, providing a ~10x speedup over the bundled JS encoder. Install it alongside `@sentry/react-native` to opt in; the SDK falls back to the JS encoder if the package is absent ([#4884](https://github.com/getsentry/sentry-react-native/issues/4884)) - Warn during dev builds when multiple versions of Sentry JS SDK are detected ([#6269](https://github.com/getsentry/sentry-react-native/pull/6269)) ### Fixes diff --git a/packages/core/package.json b/packages/core/package.json index 34626fe6b4..4ad5f9bfac 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -70,7 +70,8 @@ "peerDependencies": { "expo": ">=49.0.0", "react": ">=17.0.0", - "react-native": ">=0.65.0" + "react-native": ">=0.65.0", + "react-native-quick-base64": ">=3.0.0" }, "dependencies": { "@sentry/babel-plugin-component-annotate": "5.3.0", @@ -130,6 +131,9 @@ "peerDependenciesMeta": { "expo": { "optional": true + }, + "react-native-quick-base64": { + "optional": true } } } diff --git a/packages/core/src/js/utils/base64.ts b/packages/core/src/js/utils/base64.ts new file mode 100644 index 0000000000..e4c55456bf --- /dev/null +++ b/packages/core/src/js/utils/base64.ts @@ -0,0 +1,56 @@ +import { debug } from '@sentry/core'; + +import { base64StringFromByteArray } from '../vendor'; + +type FromByteArray = (bytes: Uint8Array, urlSafe?: boolean) => string; + +let cachedEncoder: FromByteArray | null = null; +let resolved = false; + +/** + * Resolves the base64 encoder once. If the optional peer dependency + * `react-native-quick-base64` is installed, its native JSI encoder is used + * (~10x faster than the pure-JS fallback). Otherwise the bundled JS encoder + * from `vendor/base64-js` is used. + * + * The resolution is cached so the require cost is paid at most once. + */ +function resolveEncoder(): FromByteArray { + if (resolved) { + return cachedEncoder ?? base64StringFromByteArray; + } + resolved = true; + + try { + // Optional peer dependency — only loaded if the consumer installed it. + // eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-extraneous-dependencies + const quickBase64 = require('react-native-quick-base64') as { fromByteArray?: FromByteArray }; + if (quickBase64 && typeof quickBase64.fromByteArray === 'function') { + cachedEncoder = quickBase64.fromByteArray; + debug.log('Using react-native-quick-base64 for envelope encoding.'); + return cachedEncoder; + } + } catch (_e) { + // Not installed — fall through to JS encoder. + } + + cachedEncoder = base64StringFromByteArray; + return cachedEncoder; +} + +/** + * Encode a byte array to a base64 string. Prefers the native + * `react-native-quick-base64` encoder when available, otherwise uses the + * bundled JS implementation. + */ +export function encodeToBase64(input: Uint8Array): string { + return resolveEncoder()(input); +} + +/** + * @internal Test helper. Resets the cached encoder so the next call re-resolves. + */ +export function _resetBase64EncoderForTesting(): void { + cachedEncoder = null; + resolved = false; +} diff --git a/packages/core/src/js/wrapper.ts b/packages/core/src/js/wrapper.ts index d4225740d6..f53cfa5726 100644 --- a/packages/core/src/js/wrapper.ts +++ b/packages/core/src/js/wrapper.ts @@ -30,11 +30,11 @@ import type { MobileReplayOptions } from './replay/mobilereplay'; import type { RequiredKeysUser } from './user'; import { isHardCrash } from './misc'; +import { encodeToBase64 } from './utils/base64'; import { encodeUTF8 } from './utils/encode'; import { isTurboModuleEnabled } from './utils/environment'; import { convertToNormalizedObject } from './utils/normalize'; import { ReactNativeLibraries } from './utils/rnlibraries'; -import { base64StringFromByteArray } from './vendor'; import { SDK_VERSION } from './version'; /** @@ -231,7 +231,7 @@ export const NATIVE: SentryNativeWrapper = { envelopeBytes = newBytes; } - await RNSentry.captureEnvelope(base64StringFromByteArray(envelopeBytes), { hardCrashed }); + await RNSentry.captureEnvelope(encodeToBase64(envelopeBytes), { hardCrashed }); }, /** diff --git a/packages/core/test/utils/base64.test.ts b/packages/core/test/utils/base64.test.ts new file mode 100644 index 0000000000..9305f546aa --- /dev/null +++ b/packages/core/test/utils/base64.test.ts @@ -0,0 +1,43 @@ +import { _resetBase64EncoderForTesting, encodeToBase64 } from '../../src/js/utils/base64'; + +jest.mock( + 'react-native-quick-base64', + () => ({ + __esModule: true, + fromByteArray: jest.fn((bytes: Uint8Array) => `quick:${bytes.length}`), + }), + { virtual: true }, +); + +describe('encodeToBase64', () => { + beforeEach(() => { + _resetBase64EncoderForTesting(); + jest.resetModules(); + }); + + test('uses react-native-quick-base64 when available', () => { + // The module mock above provides a fake `fromByteArray` returning a sentinel. + const result = encodeToBase64(new Uint8Array([0x73, 0x65, 0x6e, 0x74, 0x72, 0x79])); + expect(result).toBe('quick:6'); + }); + + test('falls back to the JS encoder when react-native-quick-base64 is not installed', () => { + jest.isolateModules(() => { + jest.doMock( + 'react-native-quick-base64', + () => { + throw new Error('Cannot find module'); + }, + { virtual: true }, + ); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { + encodeToBase64: isolatedEncode, + _resetBase64EncoderForTesting: isolatedReset, + } = require('../../src/js/utils/base64'); + isolatedReset(); + // "sentry" => "c2VudHJ5" + expect(isolatedEncode(new Uint8Array([0x73, 0x65, 0x6e, 0x74, 0x72, 0x79]))).toBe('c2VudHJ5'); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index a5beec674d..9d57f24b17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10931,9 +10931,12 @@ __metadata: expo: ">=49.0.0" react: ">=17.0.0" react-native: ">=0.65.0" + react-native-quick-base64: ">=3.0.0" peerDependenciesMeta: expo: optional: true + react-native-quick-base64: + optional: true bin: sentry-eas-build-on-complete: scripts/eas-build-hook.js sentry-eas-build-on-error: scripts/eas-build-hook.js From 383df08ac3d7b0c79da24b438e9ed46fba94c917 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Thu, 18 Jun 2026 11:22:11 +0200 Subject: [PATCH 2/2] docs(changelog): Match house style for base64 entry Link to the PR (#6314) instead of the issue, drop the unverified "~10x" figure (we have not benchmarked locally), and match the terser phrasing used by the closest analogue (#4874, the TextEncoder envelope entry). --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5118a8b039..42420eae55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ - Add `nativeStackAndroid` support to `NativeLinkedErrors`, capturing the JVM stack trace of rejected native module promises as a linked exception ([#6278](https://github.com/getsentry/sentry-react-native/pull/6278)) - Record XHR request/response headers and (optionally) bodies in Mobile Session Replay. Opt in via `mobileReplayIntegration` with `networkDetailAllowUrls` to capture headers; set `networkCaptureBodies: true` to also capture bodies. Other options: `networkDetailDenyUrls`, `networkRequestHeaders`, `networkResponseHeaders`. Authorization-like headers are always stripped, bodies are capped at ~150 KB. Covers XHR-based clients like `axios`; fetch will follow. See [Network Details](https://docs.sentry.io/platforms/react-native/session-replay/#network-details) for details. ([#6288](https://github.com/getsentry/sentry-react-native/pull/6288)) -- Use `react-native-quick-base64` for envelope encoding when installed, providing a ~10x speedup over the bundled JS encoder. Install it alongside `@sentry/react-native` to opt in; the SDK falls back to the JS encoder if the package is absent ([#4884](https://github.com/getsentry/sentry-react-native/issues/4884)) +- Use the optional `react-native-quick-base64` peer dependency for envelope base64 encoding when installed, to improve `captureEnvelope` performance. Falls back to the bundled JS encoder if the package is absent. ([#6314](https://github.com/getsentry/sentry-react-native/pull/6314)) - Warn during dev builds when multiple versions of Sentry JS SDK are detected ([#6269](https://github.com/getsentry/sentry-react-native/pull/6269)) ### Fixes