@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,541 @@
1
+ package com.oneuptime.replay;
2
+
3
+ import android.graphics.Color;
4
+ import android.graphics.drawable.ColorDrawable;
5
+ import android.os.Build;
6
+ import android.view.SurfaceView;
7
+ import android.view.TextureView;
8
+ import android.view.View;
9
+ import android.view.ViewGroup;
10
+ import android.webkit.WebView;
11
+ import android.widget.Button;
12
+ import android.widget.EditText;
13
+ import android.widget.ImageView;
14
+ import android.widget.ScrollView;
15
+ import android.widget.TextView;
16
+
17
+ import androidx.annotation.NonNull;
18
+
19
+ import com.facebook.react.bridge.Arguments;
20
+ import com.facebook.react.bridge.Promise;
21
+ import com.facebook.react.bridge.ReactApplicationContext;
22
+ import com.facebook.react.bridge.ReactContextBaseJavaModule;
23
+ import com.facebook.react.bridge.UIManager;
24
+ import com.facebook.react.bridge.ReactMethod;
25
+ import com.facebook.react.bridge.UiThreadUtil;
26
+ import com.facebook.react.bridge.WritableArray;
27
+ import com.facebook.react.bridge.WritableMap;
28
+ import com.facebook.react.uimanager.RootView;
29
+ import com.facebook.react.uimanager.UIManagerHelper;
30
+
31
+ import java.lang.reflect.Method;
32
+ import java.util.Locale;
33
+
34
+ public final class OneUptimeReplayViewTreeModule extends ReactContextBaseJavaModule {
35
+ private static final String REPLAY_MASK_TEST_ID = "oneuptime-replay-mask";
36
+ private static final int MAX_TREE_DEPTH = 64;
37
+ private static final int MAX_TREE_NODES = 5000;
38
+
39
+ private static final class TraversalState {
40
+ int visited = 0;
41
+ int truncated = 0;
42
+ }
43
+
44
+ OneUptimeReplayViewTreeModule(ReactApplicationContext reactContext) {
45
+ super(reactContext);
46
+ }
47
+
48
+ @NonNull
49
+ @Override
50
+ public String getName() {
51
+ return "OneUptimeReplayViewTree";
52
+ }
53
+
54
+ @ReactMethod
55
+ public void captureViewTree(double rootTagValue, Promise promise) {
56
+ final int rootTag = (int) rootTagValue;
57
+ UiThreadUtil.runOnUiThread(() -> {
58
+ try {
59
+ UIManager uiManager = UIManagerHelper.getUIManagerForReactTag(
60
+ getReactApplicationContext(),
61
+ rootTag
62
+ );
63
+ if (uiManager == null) {
64
+ promise.reject("E_UI_MANAGER", "React Native UIManager is unavailable.");
65
+ return;
66
+ }
67
+
68
+ View root = uiManager.resolveView(rootTag);
69
+ if (root == null) {
70
+ promise.reject("E_ROOT_VIEW", "Replay root view was not found.");
71
+ return;
72
+ }
73
+
74
+ int[] rootLocation = new int[2];
75
+ root.getLocationOnScreen(rootLocation);
76
+ TraversalState state = new TraversalState();
77
+ WritableMap serialized = serializeView(
78
+ root,
79
+ rootLocation[0],
80
+ rootLocation[1],
81
+ 0,
82
+ true,
83
+ state
84
+ );
85
+ if (serialized == null) {
86
+ promise.reject("E_EMPTY_ROOT", "Replay root view could not be serialized.");
87
+ return;
88
+ }
89
+ serialized.putInt("truncatedNodes", state.truncated);
90
+ promise.resolve(serialized);
91
+ } catch (Throwable exception) {
92
+ promise.reject("E_CAPTURE", "Unable to serialize the native view tree.", exception);
93
+ }
94
+ });
95
+ }
96
+
97
+ @ReactMethod
98
+ public void getAppMetadata(Promise promise) {
99
+ try {
100
+ android.content.pm.PackageInfo packageInfo = getReactApplicationContext()
101
+ .getPackageManager()
102
+ .getPackageInfo(getReactApplicationContext().getPackageName(), 0);
103
+ android.content.pm.ApplicationInfo applicationInfo =
104
+ getReactApplicationContext().getApplicationInfo();
105
+ WritableMap metadata = Arguments.createMap();
106
+ metadata.putString(
107
+ "appName",
108
+ getReactApplicationContext().getPackageManager()
109
+ .getApplicationLabel(applicationInfo).toString()
110
+ );
111
+ metadata.putString(
112
+ "appVersion",
113
+ packageInfo.versionName == null ? "unknown" : packageInfo.versionName
114
+ );
115
+ metadata.putString("osName", "android");
116
+ metadata.putString("osVersion", Build.VERSION.RELEASE);
117
+ promise.resolve(metadata);
118
+ } catch (Throwable exception) {
119
+ promise.reject("E_METADATA", "Unable to read application metadata.", exception);
120
+ }
121
+ }
122
+
123
+ @ReactMethod
124
+ public void isTouchTargetPrivate(
125
+ double targetTagValue,
126
+ double replayRootTagValue,
127
+ Promise promise
128
+ ) {
129
+ if (!isReactTag(targetTagValue) || !isReactTag(replayRootTagValue)) {
130
+ promise.resolve(true);
131
+ return;
132
+ }
133
+
134
+ final int targetTag = (int) targetTagValue;
135
+ final int replayRootTag = (int) replayRootTagValue;
136
+ UiThreadUtil.runOnUiThread(() -> {
137
+ try {
138
+ View target = resolveView(targetTag);
139
+ View replayRoot = resolveView(replayRootTag);
140
+ if (target == null || replayRoot == null) {
141
+ promise.resolve(true);
142
+ return;
143
+ }
144
+
145
+ View current = target;
146
+ while (current != null) {
147
+ if (isReplayMask(current) || isOpaqueView(current)) {
148
+ promise.resolve(true);
149
+ return;
150
+ }
151
+ if (current == replayRoot) {
152
+ promise.resolve(false);
153
+ return;
154
+ }
155
+
156
+ android.view.ViewParent parent = current.getParent();
157
+ current = parent instanceof View ? (View) parent : null;
158
+ }
159
+
160
+ /* The event target was not a descendant of this replay root. */
161
+ promise.resolve(true);
162
+ } catch (Throwable ignored) {
163
+ /* Privacy decisions fail closed on stale tags or renderer errors. */
164
+ promise.resolve(true);
165
+ }
166
+ });
167
+ }
168
+
169
+ private WritableMap serializeView(
170
+ View view,
171
+ int rootX,
172
+ int rootY,
173
+ int depth,
174
+ boolean isRoot,
175
+ TraversalState state
176
+ ) {
177
+ if (depth > MAX_TREE_DEPTH || state.visited >= MAX_TREE_NODES) {
178
+ state.truncated += 1;
179
+ return null;
180
+ }
181
+ if (!isRoot && (view.getVisibility() != View.VISIBLE
182
+ || view.getWidth() <= 0 || view.getHeight() <= 0)) {
183
+ return null;
184
+ }
185
+ state.visited += 1;
186
+ int[] location = new int[2];
187
+ view.getLocationOnScreen(location);
188
+ boolean masked = isReplayMask(view);
189
+ String kind = classify(view, masked);
190
+ boolean opaque = masked || "image".equals(kind) || "webview".equals(kind)
191
+ || "canvas".equals(kind);
192
+
193
+ WritableMap node = Arguments.createMap();
194
+ node.putInt(
195
+ "nativeId",
196
+ view.getId() == View.NO_ID ? System.identityHashCode(view) : view.getId()
197
+ );
198
+ node.putString("kind", kind);
199
+ node.putDouble("x", toDp(view, location[0] - rootX));
200
+ node.putDouble("y", toDp(view, location[1] - rootY));
201
+ node.putDouble("width", toDp(view, view.getWidth()));
202
+ node.putDouble("height", toDp(view, view.getHeight()));
203
+ node.putBoolean("masked", masked);
204
+ node.putBoolean("opaque", opaque);
205
+ if (isRoot) {
206
+ View reactTouchRoot = findReactTouchRoot(view);
207
+ if (reactTouchRoot != null) {
208
+ int[] touchRootLocation = new int[2];
209
+ reactTouchRoot.getLocationOnScreen(touchRootLocation);
210
+ node.putDouble(
211
+ "touchOriginX",
212
+ toDp(view, location[0] - touchRootLocation[0])
213
+ );
214
+ node.putDouble(
215
+ "touchOriginY",
216
+ toDp(view, location[1] - touchRootLocation[1])
217
+ );
218
+ }
219
+ }
220
+ node.putDouble("opacity", view.getAlpha());
221
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
222
+ node.putDouble("zIndex", view.getZ());
223
+ }
224
+ putVisualStyles(view, node);
225
+
226
+ WritableArray children = Arguments.createArray();
227
+ if (!opaque && view instanceof ViewGroup) {
228
+ ViewGroup group = (ViewGroup) view;
229
+ for (int index = 0; index < group.getChildCount(); index += 1) {
230
+ WritableMap child = serializeView(
231
+ group.getChildAt(index),
232
+ location[0],
233
+ location[1],
234
+ depth + 1,
235
+ false,
236
+ state
237
+ );
238
+ if (child != null) {
239
+ children.pushMap(child);
240
+ }
241
+ }
242
+ }
243
+ node.putArray("children", children);
244
+ return node;
245
+ }
246
+
247
+ private boolean isReplayMask(View view) {
248
+ Object testId = view.getTag(com.facebook.react.R.id.react_test_id);
249
+ return REPLAY_MASK_TEST_ID.equals(testId);
250
+ }
251
+
252
+ private boolean isOpaqueView(View view) {
253
+ String kind = classify(view, false);
254
+ return "image".equals(kind) || "webview".equals(kind)
255
+ || "canvas".equals(kind);
256
+ }
257
+
258
+ private View resolveView(int reactTag) {
259
+ UIManager uiManager = UIManagerHelper.getUIManagerForReactTag(
260
+ getReactApplicationContext(),
261
+ reactTag
262
+ );
263
+ return uiManager == null ? null : uiManager.resolveView(reactTag);
264
+ }
265
+
266
+ private boolean isReactTag(double value) {
267
+ return Double.isFinite(value) && value > 0 && value <= Integer.MAX_VALUE
268
+ && value == Math.rint(value);
269
+ }
270
+
271
+ private View findReactTouchRoot(View view) {
272
+ View current = view;
273
+ while (current != null) {
274
+ if (current instanceof RootView) {
275
+ return current;
276
+ }
277
+ android.view.ViewParent parent = current.getParent();
278
+ current = parent instanceof View ? (View) parent : null;
279
+ }
280
+ return null;
281
+ }
282
+
283
+ private String classify(View view, boolean masked) {
284
+ if (masked) {
285
+ return "masked";
286
+ }
287
+ if (view instanceof EditText) {
288
+ return "input";
289
+ }
290
+ if (view instanceof Button) {
291
+ return "button";
292
+ }
293
+ if (view instanceof TextView) {
294
+ return "text";
295
+ }
296
+ String className = view.getClass().getName().toLowerCase(Locale.US);
297
+ if (view instanceof ImageView || className.contains("reactimageview")
298
+ || className.contains("draweeview")) {
299
+ return "image";
300
+ }
301
+ if (view instanceof WebView || view.getClass().getName().contains("WebView")) {
302
+ return "webview";
303
+ }
304
+
305
+ if (view instanceof SurfaceView || view instanceof TextureView
306
+ || isNamedOpaqueSurface(className)) {
307
+ return "canvas";
308
+ }
309
+ if (view instanceof ScrollView) {
310
+ return "scroll";
311
+ }
312
+ if (!(view instanceof ViewGroup)) {
313
+ /* Unknown leaf views may draw arbitrary private pixels themselves. */
314
+ return view.getClass() == View.class ? "view" : "canvas";
315
+ }
316
+ if (((ViewGroup) view).getChildCount() == 0
317
+ && !isKnownStructuralContainer(view)) {
318
+ return "canvas";
319
+ }
320
+ return "view";
321
+ }
322
+
323
+ private boolean isNamedOpaqueSurface(String className) {
324
+ return className.contains("skia") || className.contains("opengl")
325
+ || className.contains("canvas") || className.contains("svg")
326
+ || className.contains("mapview") || className.contains("camera")
327
+ || className.contains("video") || className.contains("player")
328
+ || className.contains("pdf") || className.contains("signature")
329
+ || className.contains("drawing") || className.contains("paint")
330
+ || className.contains("chart");
331
+ }
332
+
333
+ private boolean isKnownStructuralContainer(View view) {
334
+ if (view instanceof RootView) {
335
+ return true;
336
+ }
337
+ String className = view.getClass().getName();
338
+ if (className.startsWith("android.") || className.startsWith("androidx.")) {
339
+ return true;
340
+ }
341
+
342
+ Class<?> current = view.getClass();
343
+ while (current != null) {
344
+ String currentName = current.getName();
345
+ if ("com.facebook.react.views.view.ReactViewGroup".equals(currentName)
346
+ || "com.facebook.react.ReactRootView".equals(currentName)) {
347
+ return true;
348
+ }
349
+ current = current.getSuperclass();
350
+ }
351
+ return false;
352
+ }
353
+
354
+ private String cssColor(int color) {
355
+ return String.format(
356
+ Locale.US,
357
+ "#%02x%02x%02x%02x",
358
+ Color.red(color),
359
+ Color.green(color),
360
+ Color.blue(color),
361
+ Color.alpha(color)
362
+ );
363
+ }
364
+
365
+ /*
366
+ * RN 0.81 moved backgrounds and borders out of ColorDrawable. Reflection
367
+ * keeps this library binary-compatible with 0.73-0.80 while using the
368
+ * public 0.81+ BackgroundStyleApplicator getters when they are present.
369
+ * Only visual paint values are requested; no props or content are read.
370
+ */
371
+ private void putVisualStyles(View view, WritableMap node) {
372
+ Integer backgroundColor = null;
373
+ if (view.getBackground() instanceof ColorDrawable) {
374
+ backgroundColor = ((ColorDrawable) view.getBackground()).getColor();
375
+ }
376
+
377
+ Integer legacyBackgroundColor = readLegacyReactBackground(view, node);
378
+ if (legacyBackgroundColor != null) {
379
+ backgroundColor = legacyBackgroundColor;
380
+ }
381
+
382
+ try {
383
+ Class<?> applicator = Class.forName(
384
+ "com.facebook.react.uimanager.BackgroundStyleApplicator"
385
+ );
386
+ Method getBackgroundColor = applicator.getMethod(
387
+ "getBackgroundColor",
388
+ View.class
389
+ );
390
+ Object reflectedBackground = getBackgroundColor.invoke(null, view);
391
+ if (reflectedBackground instanceof Number) {
392
+ backgroundColor = ((Number) reflectedBackground).intValue();
393
+ }
394
+
395
+ Class<?> logicalEdge = Class.forName(
396
+ "com.facebook.react.uimanager.style.LogicalEdge"
397
+ );
398
+ Object allEdges = enumConstant(logicalEdge, "ALL");
399
+ if (allEdges != null) {
400
+ Object reflectedWidth = applicator
401
+ .getMethod("getBorderWidth", View.class, logicalEdge)
402
+ .invoke(null, view, allEdges);
403
+ if (reflectedWidth instanceof Number) {
404
+ double width = ((Number) reflectedWidth).doubleValue();
405
+ if (Double.isFinite(width) && width > 0) {
406
+ node.putDouble("borderWidth", width);
407
+ Object reflectedColor = applicator
408
+ .getMethod("getBorderColor", View.class, logicalEdge)
409
+ .invoke(null, view, allEdges);
410
+ if (reflectedColor instanceof Number) {
411
+ node.putString(
412
+ "borderColor",
413
+ cssColor(((Number) reflectedColor).intValue())
414
+ );
415
+ }
416
+ }
417
+ }
418
+ }
419
+
420
+ Class<?> radiusProperty = Class.forName(
421
+ "com.facebook.react.uimanager.style.BorderRadiusProp"
422
+ );
423
+ Object allCorners = enumConstant(radiusProperty, "BORDER_RADIUS");
424
+ if (allCorners != null) {
425
+ Object radius = applicator
426
+ .getMethod("getBorderRadius", View.class, radiusProperty)
427
+ .invoke(null, view, allCorners);
428
+ if (radius != null) {
429
+ Object reflectedRadius = radius
430
+ .getClass()
431
+ .getMethod("resolve", float.class)
432
+ .invoke(
433
+ radius,
434
+ (float) toDp(
435
+ view,
436
+ Math.min(view.getWidth(), view.getHeight())
437
+ )
438
+ );
439
+ if (reflectedRadius instanceof Number) {
440
+ double radiusPx = ((Number) reflectedRadius).doubleValue();
441
+ if (Double.isFinite(radiusPx) && radiusPx > 0) {
442
+ node.putDouble("borderRadius", radiusPx);
443
+ }
444
+ }
445
+ }
446
+ }
447
+ } catch (ReflectiveOperationException | LinkageError | RuntimeException ignored) {
448
+ /* RN before the style getter API uses the legacy drawable fallback. */
449
+ }
450
+
451
+ if (backgroundColor != null) {
452
+ node.putString("backgroundColor", cssColor(backgroundColor));
453
+ }
454
+ }
455
+
456
+ /*
457
+ * RN 0.73-0.80 stores normal React view paint in
458
+ * ReactViewBackgroundDrawable. Referencing that class directly would make
459
+ * this package fail to load once RN removes it, so the compatibility path
460
+ * checks the exact class name and calls only its public visual getters.
461
+ */
462
+ private Integer readLegacyReactBackground(View view, WritableMap node) {
463
+ Object background = view.getBackground();
464
+ if (background == null || !(
465
+ "com.facebook.react.views.view.ReactViewBackgroundDrawable"
466
+ ).equals(background.getClass().getName())) {
467
+ return null;
468
+ }
469
+
470
+ try {
471
+ Class<?> legacyDrawable = background.getClass();
472
+ Object reflectedBackground = legacyDrawable
473
+ .getMethod("getColor")
474
+ .invoke(background);
475
+
476
+ Object reflectedWidth = legacyDrawable
477
+ .getMethod("getFullBorderWidth")
478
+ .invoke(background);
479
+ if (reflectedWidth instanceof Number) {
480
+ double width = toDp(
481
+ view,
482
+ ((Number) reflectedWidth).doubleValue()
483
+ );
484
+ if (Double.isFinite(width) && width > 0) {
485
+ node.putDouble("borderWidth", width);
486
+
487
+ Class<?> spacing = Class.forName(
488
+ "com.facebook.react.uimanager.Spacing"
489
+ );
490
+ int allEdges = spacing.getField("ALL").getInt(null);
491
+ Object reflectedColor = legacyDrawable
492
+ .getMethod("getBorderColor", int.class)
493
+ .invoke(background, allEdges);
494
+ if (reflectedColor instanceof Number) {
495
+ node.putString(
496
+ "borderColor",
497
+ cssColor(((Number) reflectedColor).intValue())
498
+ );
499
+ }
500
+ }
501
+ }
502
+
503
+ Object reflectedRadius = legacyDrawable
504
+ .getMethod("getFullBorderRadius")
505
+ .invoke(background);
506
+ if (reflectedRadius instanceof Number) {
507
+ double radius = toDp(
508
+ view,
509
+ ((Number) reflectedRadius).doubleValue()
510
+ );
511
+ if (Double.isFinite(radius) && radius > 0) {
512
+ node.putDouble("borderRadius", radius);
513
+ }
514
+ }
515
+
516
+ return reflectedBackground instanceof Number
517
+ ? ((Number) reflectedBackground).intValue()
518
+ : null;
519
+ } catch (ReflectiveOperationException | LinkageError | RuntimeException ignored) {
520
+ return null;
521
+ }
522
+ }
523
+
524
+ private Object enumConstant(Class<?> enumClass, String name) {
525
+ Object[] constants = enumClass.getEnumConstants();
526
+ if (constants == null) {
527
+ return null;
528
+ }
529
+ for (Object constant : constants) {
530
+ if (name.equals(constant.toString())) {
531
+ return constant;
532
+ }
533
+ }
534
+ return null;
535
+ }
536
+
537
+ private double toDp(View view, double pixels) {
538
+ float density = view.getResources().getDisplayMetrics().density;
539
+ return density > 0 ? pixels / density : pixels;
540
+ }
541
+ }