@oneuptime/react-native-replay 13.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/OneUptimeReactNativeReplay.podspec +20 -0
  2. package/README.md +264 -0
  3. package/android/build.gradle +36 -0
  4. package/android/consumer-rules.pro +26 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/com/oneuptime/replay/OneUptimeReplayPackage.java +29 -0
  7. package/android/src/main/java/com/oneuptime/replay/OneUptimeReplayViewTreeModule.java +541 -0
  8. package/dist/index.cjs +4111 -0
  9. package/dist/index.cjs.map +7 -0
  10. package/dist/index.js +4088 -0
  11. package/dist/index.js.map +7 -0
  12. package/dist/types/ChunkBuffer.d.ts +47 -0
  13. package/dist/types/ChunkBuffer.d.ts.map +1 -0
  14. package/dist/types/Config.d.ts +59 -0
  15. package/dist/types/Config.d.ts.map +1 -0
  16. package/dist/types/Contract.d.ts +176 -0
  17. package/dist/types/Contract.d.ts.map +1 -0
  18. package/dist/types/Events.d.ts +14 -0
  19. package/dist/types/Events.d.ts.map +1 -0
  20. package/dist/types/MobileReplayRecorder.d.ts +176 -0
  21. package/dist/types/MobileReplayRecorder.d.ts.map +1 -0
  22. package/dist/types/NativeViewTree.d.ts +49 -0
  23. package/dist/types/NativeViewTree.d.ts.map +1 -0
  24. package/dist/types/Outbox.d.ts +29 -0
  25. package/dist/types/Outbox.d.ts.map +1 -0
  26. package/dist/types/ReplayProvider.d.ts +28 -0
  27. package/dist/types/ReplayProvider.d.ts.map +1 -0
  28. package/dist/types/Sanitize.d.ts +29 -0
  29. package/dist/types/Sanitize.d.ts.map +1 -0
  30. package/dist/types/Storage.d.ts +30 -0
  31. package/dist/types/Storage.d.ts.map +1 -0
  32. package/dist/types/TouchPrivacy.d.ts +15 -0
  33. package/dist/types/TouchPrivacy.d.ts.map +1 -0
  34. package/dist/types/Transport.d.ts +36 -0
  35. package/dist/types/Transport.d.ts.map +1 -0
  36. package/dist/types/ViewTreeSerializer.d.ts +29 -0
  37. package/dist/types/ViewTreeSerializer.d.ts.map +1 -0
  38. package/dist/types/index.d.ts +12 -0
  39. package/dist/types/index.d.ts.map +1 -0
  40. package/ios/OneUptimeReplayViewTreeModule.m +17 -0
  41. package/ios/OneUptimeReplayViewTreeModule.swift +239 -0
  42. package/package.json +66 -0
  43. package/react-native.config.js +15 -0
@@ -0,0 +1,20 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |spec|
6
+ spec.name = "OneUptimeReactNativeReplay"
7
+ spec.version = package["version"]
8
+ spec.summary = package["description"]
9
+ spec.homepage = "https://oneuptime.com"
10
+ spec.license = package["license"]
11
+ spec.author = package["author"]
12
+ spec.source = {
13
+ :git => "https://github.com/OneUptime/oneuptime.git",
14
+ :tag => "#{spec.version}"
15
+ }
16
+ spec.platforms = { :ios => "13.0" }
17
+ spec.source_files = "ios/**/*.{h,m,mm,swift}"
18
+ spec.swift_version = "5.7"
19
+ spec.dependency "React-Core"
20
+ end
package/README.md ADDED
@@ -0,0 +1,264 @@
1
+ # `@oneuptime/react-native-replay`
2
+
3
+ Privacy-first session replay for React Native. The SDK samples the native view
4
+ hierarchy into synthetic rrweb events and sends them through OneUptime's normal
5
+ session-replay ingest pipeline. It records layout, tightly allowlisted visual
6
+ styles, navigation, touches, custom events, and JavaScript errors without
7
+ capturing screenshots or readable native text.
8
+
9
+ ## Requirements
10
+
11
+ - React Native 0.73 or newer.
12
+ - `@react-native-async-storage/async-storage` installed in the application.
13
+ - A OneUptime RUM application with session replay enabled.
14
+ - The Android application ID or iOS bundle ID added to that application's
15
+ replay origin allowlist as `app://com.example.yourapp`.
16
+
17
+ This package contains native modules. **Expo Go is not supported.** Expo apps
18
+ must use a development build or EAS build after prebuild. Bare iOS apps must
19
+ run `pod install` (or `npx pod-install`) after installation. Android and iOS
20
+ modules autolink on supported React Native versions.
21
+
22
+ ```sh
23
+ npm install @oneuptime/react-native-replay \
24
+ @react-native-async-storage/async-storage
25
+ npx pod-install
26
+ ```
27
+
28
+ ## Start recording
29
+
30
+ Wrap the application root so the SDK can register one non-collapsable native
31
+ root and observe bubbling touch events. The provider can start the default
32
+ client for you:
33
+
34
+ ```tsx
35
+ import {
36
+ OneUptimeReplayProvider,
37
+ ReplayMask,
38
+ } from "@oneuptime/react-native-replay";
39
+
40
+ export default function App() {
41
+ return (
42
+ <OneUptimeReplayProvider
43
+ options={{
44
+ host: "https://oneuptime.example.com",
45
+ token: "YOUR_TELEMETRY_INGESTION_KEY",
46
+ appIdentifier: "YOUR_RUM_APPLICATION_IDENTIFIER",
47
+ mobileAppIdentifier: "com.example.checkout",
48
+ userRef: "support-user-42", // optional record-next-session target
49
+ }}
50
+ >
51
+ <RootNavigator />
52
+ </OneUptimeReplayProvider>
53
+ );
54
+ }
55
+ ```
56
+
57
+ `mobileAppIdentifier` is the installed binary's Android `applicationId` or
58
+ iOS bundle identifier. It is canonicalized to lowercase and must contain at
59
+ least two reverse-DNS labels. `appIdentifier` is the OneUptime RUM application
60
+ identifier; the two identifiers are intentionally different.
61
+
62
+ For manual lifecycle control, omit `options` from the provider and use the
63
+ singleton:
64
+
65
+ ```ts
66
+ import OneUptimeReplay from "@oneuptime/react-native-replay";
67
+
68
+ await OneUptimeReplay.start({
69
+ host: "https://oneuptime.example.com",
70
+ token: "YOUR_TELEMETRY_INGESTION_KEY",
71
+ appIdentifier: "YOUR_RUM_APPLICATION_IDENTIFIER",
72
+ mobileAppIdentifier: "com.example.checkout",
73
+ appName: "Checkout", // optional metadata override
74
+ appVersion: "2.4.1", // optional metadata override
75
+ userRef: "support-user-42", // optional targeting reference
76
+ });
77
+
78
+ await OneUptimeReplay.stop();
79
+ ```
80
+
81
+ `host` must be an HTTP(S) origin, not a URL with credentials, a path, query,
82
+ or fragment. The SDK fetches policy before capturing and fails closed if the
83
+ policy request or native bridge is unavailable.
84
+
85
+ ## Privacy model
86
+
87
+ Mobile replay is deliberately stricter than web replay:
88
+
89
+ - `<Text>` content and `<TextInput>` values are never read by native code and
90
+ never appear in an event, regardless of the web masking policy.
91
+ - `<ReplayMask>` is a hard traversal boundary. The wrapper's frame becomes one
92
+ opaque placeholder and no descendant is visited.
93
+ - Images, WebViews, Skia, Metal/OpenGL, Canvas, SurfaceView, and TextureView
94
+ content is opaque. Only the containing frame is recorded.
95
+ - Native output is allowlisted to parent-relative geometry, canonical hex
96
+ background/border colors, bounded border width/radius, opacity, and z-order.
97
+ Accessibility content, arbitrary props, pixels, and developer test IDs are
98
+ not serialized.
99
+ - Error messages and stacks can quote user input, so v1 records the JavaScript
100
+ error type only when it is a built-in safe name and a fixed `[masked]`
101
+ marker, never a custom name, original message, or stack.
102
+
103
+ Use `ReplayMask` for any subtree whose mere shape or presence should not be
104
+ recorded:
105
+
106
+ ```tsx
107
+ <ReplayMask>
108
+ <PaymentCard />
109
+ </ReplayMask>
110
+ ```
111
+
112
+ ## Consent
113
+
114
+ When policy requires explicit consent, capture stays only in the bounded
115
+ in-memory pre-roll until the host grants consent. Nothing is persisted or sent
116
+ before that call.
117
+
118
+ The first policy request is always anonymous. `grantConsent()` always refreshes
119
+ the current policy before anything is persisted or sent. If `userRef` is
120
+ supplied, the SDK only includes it in a targeting refresh after the anonymous
121
+ policy says consent is not required, or during that post-consent refresh. A
122
+ failed or disabled fresh policy stops recording rather than falling back to the
123
+ older anonymous policy.
124
+
125
+ ```ts
126
+ await OneUptimeReplay.grantConsent();
127
+ await OneUptimeReplay.revokeConsent();
128
+ ```
129
+
130
+ Revocation stops capture and clears the in-memory buffer, session identifier,
131
+ anonymous visitor identifier, and AsyncStorage outbox. A later grant starts a
132
+ new visit.
133
+
134
+ ## Identity, tags, events, and routes
135
+
136
+ ```ts
137
+ OneUptimeReplay.identify("support-user-42", {
138
+ plan: "enterprise",
139
+ role: "admin",
140
+ });
141
+
142
+ OneUptimeReplay.setTags({ build: "2026.09.13", experiment: "checkout-b" });
143
+ OneUptimeReplay.addTag("region", "eu-west");
144
+ OneUptimeReplay.track("checkout_opened", { cartSize: 3 });
145
+
146
+ // Call from the navigation container's state-change callback.
147
+ await OneUptimeReplay.setRoute("/checkout?payment_token=discarded");
148
+ ```
149
+
150
+ Calling `identify()` before `start()` preserves that reference for the
151
+ consent-safe targeting refresh. A non-empty `userRef` passed to `start()` takes
152
+ precedence over an earlier pre-start `identify()` call. Changing or clearing
153
+ the normalized reference while recording seals the previous user's replay and
154
+ starts a new session before applying the new identity. The SDK immediately
155
+ revalidates targeting for the new reference; a target or manual trigger from
156
+ the old user never carries into the new session.
157
+
158
+ Identity and traits are sent only when the RUM application's policy enables
159
+ user-identity capture. Under mobile's mandatory `MaskAllText`, trait values,
160
+ custom-event property values, and manual-capture reasons use the same coarse
161
+ text masks as the browser recorder. Event names and the explicitly supplied
162
+ user reference stay readable. Tag values deliberately stay readable and
163
+ searchable, matching the browser contract; do not use tags for secrets. All
164
+ maps remain count/length bounded. Routes always drop query strings and
165
+ fragments and are stored as `app://com.example.checkout/checkout`. The
166
+ remaining path stays readable, so pass stable screen templates rather than
167
+ account names, order numbers, or other sensitive path values.
168
+
169
+ ## Manual and error capture
170
+
171
+ ```ts
172
+ await OneUptimeReplay.captureSession("support-request");
173
+
174
+ try {
175
+ await submitOrder();
176
+ } catch (error) {
177
+ await OneUptimeReplay.captureError(error);
178
+ throw error;
179
+ }
180
+ ```
181
+
182
+ React Native's existing global JavaScript error handler is chained only when
183
+ it can be safely restored. The SDK never takes ownership of an application
184
+ handler it cannot restore. In error-triggered mode a bounded 60-second/2 MiB
185
+ pre-roll is uploaded after a manual or JavaScript error trigger.
186
+
187
+ ## Diagnostics
188
+
189
+ ```ts
190
+ const diagnostics = OneUptimeReplay.getDiagnostics();
191
+ // status, sessionId, sampled, triggered, consentState, pendingEvents, events
192
+ ```
193
+
194
+ Diagnostics contain recorder decisions and fixed reason codes, not view text.
195
+ Pass `debug: true` at startup to mirror those codes to the development console.
196
+
197
+ Use the current consented session ID to correlate the application's own traces
198
+ and logs with replay. The listener fires immediately when a session exists,
199
+ again on every rotation, and with `null` when capture stops or consent is
200
+ withdrawn:
201
+
202
+ ```ts
203
+ const unsubscribe = OneUptimeReplay.onSessionChange((sessionId) => {
204
+ updateOpenTelemetryResource({ "session.id": sessionId });
205
+ });
206
+
207
+ const sessionId = OneUptimeReplay.getSessionId();
208
+ unsubscribe();
209
+ ```
210
+
211
+ For independent clients or tests, instantiate `new MobileReplayRecorder()` and
212
+ pass it to `<OneUptimeReplayProvider recorder={client}>`.
213
+
214
+ ## Durability and lifecycle
215
+
216
+ The SDK takes a view-tree sample every 500 ms, creates a full checkout at
217
+ least every 60 seconds, and closes a chunk at 15 seconds or 256 KiB. Chunks are
218
+ gzip-compressed with `fflate.gzipSync`. Each chunk is placed in an ordered,
219
+ bounded AsyncStorage outbox before POST and removed only after a terminal
220
+ server response. AppState backgrounding records visibility, closes the current
221
+ chunk, drains the outbox only when policy and consent permit uploading, and
222
+ pauses sampling; foregrounding resumes with a fresh full snapshot.
223
+
224
+ Policy is revalidated without HTTP-cache reuse before every foreground resume,
225
+ after identity changes, and at five-minute intervals while the app remains
226
+ active. An explicit disable takes effect before more capture; a transient
227
+ network failure outside the consent transition keeps the last valid policy and
228
+ retries on the next bounded refresh cadence. Consent grants always fail closed
229
+ when a fresh policy cannot be obtained.
230
+
231
+ The SDK never sets a browser `Origin` header. It sends
232
+ `x-oneuptime-replay-recorder-kind: rn-view-tree` and the mobile app identifier;
233
+ the server validates the identifier and synthesizes its exact `app://` origin.
234
+
235
+ ## Fidelity limitations
236
+
237
+ - Reanimated/UI-thread animations are sampled, not frame-accurate.
238
+ - Image pixels, WebView content, Skia, Metal/OpenGL, and other drawing surfaces
239
+ replay as disclosed opaque rectangles.
240
+ - JavaScript errors are captured reliably. Hard native crashes may terminate
241
+ the process before JavaScript can close or upload a chunk and are not a
242
+ reliable session-replay trigger.
243
+ - Mobile operating systems may suspend networking immediately after an app
244
+ enters the background. Unacknowledged chunks remain in AsyncStorage for the
245
+ next launch.
246
+ - This is a wireframe replay, not a screenshot or video recording.
247
+ - Touches beginning in a `ReplayMask`, image, WebView, Skia/canvas, or other
248
+ opaque surface are discarded for the entire pointer sequence. If a gesture
249
+ crosses into one later, that touch and the remainder of its pointer sequence
250
+ are discarded. If current native target ancestry or the coordinate basis
251
+ cannot be verified, touch telemetry fails closed and is discarded.
252
+
253
+ ## Package development
254
+
255
+ ```sh
256
+ npm run compile
257
+ npm test
258
+ npm run build
259
+ npm pack --dry-run
260
+ ```
261
+
262
+ `prepack` and `prepublishOnly` both build ESM, CommonJS, source maps, and TypeScript
263
+ declarations. The published package contains those artifacts plus Android and
264
+ iOS native sources, the podspec, autolinking configuration, and this README.
@@ -0,0 +1,36 @@
1
+ buildscript {
2
+ repositories {
3
+ google()
4
+ mavenCentral()
5
+ }
6
+ }
7
+
8
+ apply plugin: "com.android.library"
9
+
10
+ def safeExtGet(prop, fallback) {
11
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
12
+ }
13
+
14
+ android {
15
+ namespace "com.oneuptime.replay"
16
+ compileSdkVersion safeExtGet("compileSdkVersion", 35)
17
+
18
+ defaultConfig {
19
+ minSdkVersion safeExtGet("minSdkVersion", 23)
20
+ targetSdkVersion safeExtGet("targetSdkVersion", 35)
21
+ consumerProguardFiles "consumer-rules.pro"
22
+ }
23
+
24
+ lintOptions {
25
+ abortOnError false
26
+ }
27
+ }
28
+
29
+ repositories {
30
+ google()
31
+ mavenCentral()
32
+ }
33
+
34
+ dependencies {
35
+ implementation "com.facebook.react:react-android"
36
+ }
@@ -0,0 +1,26 @@
1
+ # These React Native APIs are accessed reflectively so the same SDK binary can
2
+ # support both the RN 0.73 legacy drawable and newer background implementations.
3
+ -keepnames class com.facebook.react.views.view.ReactViewBackgroundDrawable
4
+ -keepclassmembers class com.facebook.react.views.view.ReactViewBackgroundDrawable {
5
+ public int getColor();
6
+ public float getFullBorderWidth();
7
+ public int getBorderColor(int);
8
+ public float getFullBorderRadius();
9
+ }
10
+
11
+ -keepnames class com.facebook.react.uimanager.BackgroundStyleApplicator
12
+ -keepclassmembers class com.facebook.react.uimanager.BackgroundStyleApplicator {
13
+ public static *** getBackgroundColor(...);
14
+ public static *** getBorderWidth(...);
15
+ public static *** getBorderColor(...);
16
+ public static *** getBorderRadius(...);
17
+ }
18
+
19
+ -keep class com.facebook.react.uimanager.Spacing {
20
+ public static int ALL;
21
+ }
22
+ -keep class com.facebook.react.uimanager.style.LogicalEdge { *; }
23
+ -keep class com.facebook.react.uimanager.style.BorderRadiusProp { *; }
24
+ -keepclassmembers class com.facebook.react.uimanager.LengthPercentage {
25
+ public float resolve(float);
26
+ }
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,29 @@
1
+ package com.oneuptime.replay;
2
+
3
+ import androidx.annotation.NonNull;
4
+
5
+ import com.facebook.react.ReactPackage;
6
+ import com.facebook.react.bridge.NativeModule;
7
+ import com.facebook.react.bridge.ReactApplicationContext;
8
+ import com.facebook.react.uimanager.ViewManager;
9
+
10
+ import java.util.Collections;
11
+ import java.util.List;
12
+
13
+ public final class OneUptimeReplayPackage implements ReactPackage {
14
+ @NonNull
15
+ @Override
16
+ public List<NativeModule> createNativeModules(
17
+ @NonNull ReactApplicationContext reactContext
18
+ ) {
19
+ return Collections.singletonList(new OneUptimeReplayViewTreeModule(reactContext));
20
+ }
21
+
22
+ @NonNull
23
+ @Override
24
+ public List<ViewManager> createViewManagers(
25
+ @NonNull ReactApplicationContext reactContext
26
+ ) {
27
+ return Collections.emptyList();
28
+ }
29
+ }