@ansight/react-native 1.3.0-preview.1 → 1.3.0-preview.10

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.
package/README.md CHANGED
@@ -77,8 +77,8 @@ bundle; its API requires the native Ansight module.
77
77
 
78
78
  This package version expects matching native SDK packages:
79
79
 
80
- - CocoaPods: `Ansight`, `AnsightObjC` version `1.3.0-preview.1`
81
- - Maven: `ai.ansight:ansight-android:1.3.0-preview.1`
80
+ - CocoaPods: `Ansight`, `AnsightObjC` version `1.3.0-preview.10`
81
+ - Maven: `ai.ansight:ansight-android:1.3.0-preview.10`
82
82
 
83
83
  ## Quickstart
84
84
 
@@ -136,10 +136,12 @@ The TypeScript `AnsightOptions` surface mirrors Android `AnsightOptions`, iOS
136
136
  | `retentionPeriodSeconds` | Local metric/event retention window. |
137
137
  | `enableFramesPerSecond` | Enables native FPS sampling. |
138
138
  | `enableBatteryLevel` | Enables battery sampling where available. |
139
+ | `enableOpenFileHandleTracking` | Enables native process open-file-handle sampling. Disabled by default. |
140
+ | `enableJniReferenceCountTracking` | Enables JNI reference-count tracking where the runtime can supply it. Disabled by default. |
139
141
  | `defaultMemoryChannels` | Selects built-in memory channels. Prefer `managedHeap`, `nativeHeap`, `residentSetSize`, and `physicalFootprint`; `javaHeap` and `rss` are accepted as Android/RN compatibility aliases. |
140
142
  | `reactNativeMemory` | Controls native React Native runtime memory channels. Enabled by default; set to `false` to disable, or use `{ jsHeapUsed, jsHeapTotal }`. |
141
143
  | `additionalChannels` | Registers custom metric channels. |
142
- | `sessionJpegCapture` | Object to enable/configure capture, or `false` to disable. Supports `captureGpuBackedSurfaces`; on iOS it defaults to `true` so Metal, SceneKit, and similar GPU-backed views are included. |
144
+ | `sessionJpegCapture` | Object to enable/configure capture, or `false` to disable. `mode` accepts `screenshotOnly`, `screenshotAndVisualTree`, or `screenshotWithVisualTreeOnTouch`. Supports `captureGpuBackedSurfaces`; on iOS it defaults to `true` so Metal, SceneKit, and similar GPU-backed views are included. |
143
145
  | `touchCapture` | Object to enable/configure capture, or `false` to disable. |
144
146
  | `lifecycleCapture` | Native lifecycle and screen-view capture options. |
145
147
  | `toolGuard` | `"disabled"`, `"readOnly"`, `"readWrite"`, or `"fullAccess"`. |
@@ -150,6 +152,11 @@ The TypeScript `AnsightOptions` surface mirrors Android `AnsightOptions`, iOS
150
152
  | `remoteTools` | Native visual tree, file, database, preferences, reflection, and secure-storage tool options. |
151
153
  | `lifecycle` | JS AppState tracking toggle. Defaults to true. |
152
154
 
155
+ Use `withOpenFileHandleTracking()` and `withJniReferenceCountTracking()` to
156
+ opt in through the builder. Matching `without...` methods disable the channels
157
+ again. Open handles are sampled by the native Android/iOS runtime. JNI counts
158
+ are available on Android only when the host integration can supply them.
159
+
153
160
  Cellular host connections are disabled by default. The restriction applies to
154
161
  enrollment and reconnect requests. Opt in only for a trusted development host
155
162
  or personal hotspot:
@@ -175,6 +182,7 @@ await Ansight.initializeAndActivate({
175
182
  quality: 60,
176
183
  maxWidth: 480,
177
184
  captureGpuBackedSurfaces: true,
185
+ mode: "screenshotWithVisualTreeOnTouch",
178
186
  },
179
187
  touchCapture: {
180
188
  captureMoveEvents: true,
@@ -192,6 +200,10 @@ await Ansight.initializeAndActivate({
192
200
  });
193
201
  ```
194
202
 
203
+ The touch mode captures native visual trees only on touch down and touch up.
204
+ Move and cancel events do not trigger capture. Native touch capture and
205
+ visual-tree tools/providers must remain enabled.
206
+
195
207
  On iOS, `captureGpuBackedSurfaces` defaults to `true` so Metal, SceneKit, and
196
208
  similar GPU-backed views are included. Set it to `false` to use a lower-overhead
197
209
  capture path when those surfaces are not needed.
@@ -67,6 +67,6 @@ repositories {
67
67
  dependencies {
68
68
  implementation("com.facebook.react:react-android")
69
69
  def ansightAndroidVersion =
70
- findProperty("ansightAndroidVersion") ?: "1.3.0-preview.1"
71
- implementation("ai.ansight:ansight-android:1.3.0-preview.1")
70
+ findProperty("ansightAndroidVersion") ?: "1.3.0-preview.10"
71
+ implementation("ai.ansight:ansight-android:1.3.0-preview.10")
72
72
  }
@@ -6,6 +6,7 @@ import ai.ansight.runtime.AndroidToolExecutionContext
6
6
  import ai.ansight.runtime.AndroidToolResult
7
7
  import ai.ansight.runtime.AnsightChannel
8
8
  import ai.ansight.runtime.AnsightChannels
9
+ import ai.ansight.runtime.AnsightCrashCaptureOptions
9
10
  import ai.ansight.runtime.AnsightHostAutoProbeOptions
10
11
  import ai.ansight.runtime.AnsightHostConnectionOptions
11
12
  import ai.ansight.runtime.AnsightLogCallback
@@ -225,6 +226,21 @@ class AnsightReactNativeModule(
225
226
  }.resolve(promise)
226
227
  }
227
228
 
229
+ @ReactMethod
230
+ fun recordCrashCandidate(input: ReadableMap, promise: Promise) {
231
+ runCatching {
232
+ val candidateId = AnsightRuntime.recordCrashCandidate(
233
+ runtime = input.stringValue("runtime") ?: "react-native-javascript",
234
+ kind = input.stringValue("kind") ?: "unhandled_javascript_error",
235
+ message = input.stringValue("message"),
236
+ stack = input.stringValue("stack"),
237
+ fatal = input.booleanValue("fatal", false),
238
+ metadataJson = input.stringValue("metadata"),
239
+ )
240
+ Arguments.createMap().apply { putString("candidateId", candidateId) }
241
+ }.resolve(promise)
242
+ }
243
+
228
244
  @ReactMethod
229
245
  fun screenViewed(name: String, details: ReadableMap?, promise: Promise) {
230
246
  runCatching {
@@ -693,6 +709,22 @@ class AnsightReactNativeModule(
693
709
  if (map.hasBoolean("enableBatteryLevel")) {
694
710
  options = options.copy(enableBatteryLevel = map.booleanValue("enableBatteryLevel", options.enableBatteryLevel))
695
711
  }
712
+ if (map.hasBoolean("enableOpenFileHandleTracking")) {
713
+ options = options.copy(
714
+ enableOpenFileHandleTracking = map.booleanValue(
715
+ "enableOpenFileHandleTracking",
716
+ options.enableOpenFileHandleTracking,
717
+ ),
718
+ )
719
+ }
720
+ if (map.hasBoolean("enableJniReferenceCountTracking")) {
721
+ options = options.copy(
722
+ enableJniReferenceCountTracking = map.booleanValue(
723
+ "enableJniReferenceCountTracking",
724
+ options.enableJniReferenceCountTracking,
725
+ ),
726
+ )
727
+ }
696
728
  if (map.hasMap("defaultMemoryChannels")) {
697
729
  val memory = map.getMapOrNull("defaultMemoryChannels")
698
730
  val managedHeap = if (memory.hasBoolean("managedHeap")) {
@@ -751,6 +783,10 @@ class AnsightReactNativeModule(
751
783
  AnsightSessionJpegCaptureOptions.DefaultCaptureGpuBackedSurfaces,
752
784
  ),
753
785
  mode = sessionJpegCaptureMode(jpeg.stringValue("mode")),
786
+ captureKeyboardPresence = jpeg.booleanValue(
787
+ "captureKeyboardPresence",
788
+ AnsightSessionJpegCaptureOptions.DefaultCaptureKeyboardPresence,
789
+ ),
754
790
  )
755
791
  },
756
792
  )
@@ -768,6 +804,24 @@ class AnsightReactNativeModule(
768
804
  },
769
805
  )
770
806
  }
807
+ if (map.hasKey("crashCapture")) {
808
+ options = options.copy(
809
+ crashCapture = if (map.isFalse("crashCapture")) {
810
+ options.crashCapture.copy(enabled = false)
811
+ } else {
812
+ val crash = map.getMapOrNull("crashCapture")
813
+ AnsightCrashCaptureOptions(
814
+ enabled = crash.booleanValue("enabled", true),
815
+ studioHandoffEnabled = crash.booleanValue("studioHandoffEnabled", true),
816
+ offlineCaptureAttachmentEnabled = crash.booleanValue("offlineCaptureAttachmentEnabled", true),
817
+ maximumPendingReports = crash.intValue("maximumPendingReports", 8),
818
+ retentionDays = crash.intValue("retentionDays", 7),
819
+ maximumBreadcrumbs = crash.intValue("maximumBreadcrumbs", 64),
820
+ maximumTraceBytes = crash.intValue("maximumTraceBytes", 1_048_576),
821
+ )
822
+ },
823
+ )
824
+ }
771
825
  if (map.hasString("toolGuard")) {
772
826
  options = options.copy(toolGuard = toolGuard(map.stringValue("toolGuard")))
773
827
  }
@@ -931,14 +985,18 @@ class AnsightReactNativeModule(
931
985
  AnsightSessionJpegCaptureOptions.DefaultCaptureGpuBackedSurfaces,
932
986
  ),
933
987
  mode = sessionJpegCaptureMode(map.stringValue("mode")),
988
+ captureKeyboardPresence = map.booleanValue(
989
+ "captureKeyboardPresence",
990
+ AnsightSessionJpegCaptureOptions.DefaultCaptureKeyboardPresence,
991
+ ),
934
992
  )
935
993
  }
936
994
 
937
995
  private fun sessionJpegCaptureMode(value: String?): AnsightSessionJpegCaptureMode =
938
- if (value == "screenshotAndVisualTree") {
939
- AnsightSessionJpegCaptureMode.ScreenshotAndVisualTree
940
- } else {
941
- AnsightSessionJpegCaptureMode.ScreenshotOnly
996
+ when (value) {
997
+ "screenshotAndVisualTree" -> AnsightSessionJpegCaptureMode.ScreenshotAndVisualTree
998
+ "screenshotWithVisualTreeOnTouch" -> AnsightSessionJpegCaptureMode.ScreenshotWithVisualTreeOnTouch
999
+ else -> AnsightSessionJpegCaptureMode.ScreenshotOnly
942
1000
  }
943
1001
 
944
1002
  private fun toolDefinition(map: ReadableMap): ToolDefinition =
@@ -1111,6 +1169,8 @@ class AnsightReactNativeModule(
1111
1169
  putInt("retentionPeriodSeconds", options.retentionPeriodSeconds)
1112
1170
  putBoolean("enableFramesPerSecond", options.enableFramesPerSecond)
1113
1171
  putBoolean("enableBatteryLevel", options.enableBatteryLevel)
1172
+ putBoolean("enableOpenFileHandleTracking", options.enableOpenFileHandleTracking)
1173
+ putBoolean("enableJniReferenceCountTracking", options.enableJniReferenceCountTracking)
1114
1174
  putMap("defaultMemoryChannels", mapOf(
1115
1175
  "managedHeap" to options.defaultMemoryChannels.javaHeap,
1116
1176
  "javaHeap" to options.defaultMemoryChannels.javaHeap,
@@ -1127,10 +1187,11 @@ class AnsightReactNativeModule(
1127
1187
  "quality" to capture.quality,
1128
1188
  "maxWidth" to capture.maxWidth,
1129
1189
  "captureGpuBackedSurfaces" to capture.captureGpuBackedSurfaces,
1130
- "mode" to if (capture.mode == AnsightSessionJpegCaptureMode.ScreenshotAndVisualTree) {
1131
- "screenshotAndVisualTree"
1132
- } else {
1133
- "screenshotOnly"
1190
+ "captureKeyboardPresence" to capture.captureKeyboardPresence,
1191
+ "mode" to when (capture.mode) {
1192
+ AnsightSessionJpegCaptureMode.ScreenshotAndVisualTree -> "screenshotAndVisualTree"
1193
+ AnsightSessionJpegCaptureMode.ScreenshotWithVisualTreeOnTouch -> "screenshotWithVisualTreeOnTouch"
1194
+ else -> "screenshotOnly"
1134
1195
  },
1135
1196
  ).toWritableMap())
1136
1197
  } ?: putNull("sessionJpegCapture")
package/index.d.ts CHANGED
@@ -37,7 +37,8 @@ export interface AnsightSessionJpegCaptureOptions {
37
37
  quality?: number;
38
38
  maxWidth?: number;
39
39
  captureGpuBackedSurfaces?: boolean;
40
- mode?: "screenshotOnly" | "screenshotAndVisualTree";
40
+ captureKeyboardPresence?: boolean;
41
+ mode?: "screenshotOnly" | "screenshotAndVisualTree" | "screenshotWithVisualTreeOnTouch";
41
42
  }
42
43
 
43
44
  export interface AnsightTouchCaptureOptions {
@@ -110,6 +111,8 @@ export interface AnsightOptions {
110
111
  retentionPeriodSeconds?: number;
111
112
  enableFramesPerSecond?: boolean;
112
113
  enableBatteryLevel?: boolean;
114
+ enableOpenFileHandleTracking?: boolean;
115
+ enableJniReferenceCountTracking?: boolean;
113
116
  defaultMemoryChannels?: {
114
117
  managedHeap?: boolean;
115
118
  physicalFootprint?: boolean;
@@ -121,6 +124,7 @@ export interface AnsightOptions {
121
124
  reactNativeMemory?: false | AnsightReactNativeMemoryOptions;
122
125
  sessionJpegCapture?: false | AnsightSessionJpegCaptureOptions;
123
126
  touchCapture?: false | AnsightTouchCaptureOptions;
127
+ crashCapture?: false | AnsightCrashCaptureOptions;
124
128
  lifecycleCapture?: {
125
129
  enabled?: boolean;
126
130
  captureAppLifecycle?: boolean;
@@ -153,6 +157,25 @@ export interface AnsightOptions {
153
157
  lifecycle?: boolean;
154
158
  }
155
159
 
160
+ export interface AnsightCrashCaptureOptions {
161
+ enabled?: boolean;
162
+ studioHandoffEnabled?: boolean;
163
+ offlineCaptureAttachmentEnabled?: boolean;
164
+ maximumPendingReports?: number;
165
+ retentionDays?: number;
166
+ maximumBreadcrumbs?: number;
167
+ maximumTraceBytes?: number;
168
+ }
169
+
170
+ export interface AnsightCrashCandidate {
171
+ runtime?: string;
172
+ kind?: string;
173
+ message?: string;
174
+ stack?: string;
175
+ fatal?: boolean;
176
+ metadata?: string | Record<string, string>;
177
+ }
178
+
156
179
  export interface AnsightOperationResult {
157
180
  success: boolean;
158
181
  message: string;
@@ -251,6 +274,10 @@ export class AnsightOptionsBuilder {
251
274
  withoutFramesPerSecond(): this;
252
275
  withBatteryLevel(): this;
253
276
  withoutBatteryLevel(): this;
277
+ withOpenFileHandleTracking(): this;
278
+ withoutOpenFileHandleTracking(): this;
279
+ withJniReferenceCountTracking(): this;
280
+ withoutJniReferenceCountTracking(): this;
254
281
  withRetentionPeriodSeconds(retentionPeriodSeconds: number): this;
255
282
  withAdditionalChannels(additionalChannels: AnsightChannel[]): this;
256
283
  addAdditionalChannel(additionalChannel: AnsightChannel): this;
@@ -264,11 +291,14 @@ export class AnsightOptionsBuilder {
264
291
  quality?: number,
265
292
  maxWidth?: number | null,
266
293
  captureGpuBackedSurfaces?: boolean,
267
- mode?: "screenshotOnly" | "screenshotAndVisualTree"
294
+ mode?: "screenshotOnly" | "screenshotAndVisualTree" | "screenshotWithVisualTreeOnTouch",
295
+ captureKeyboardPresence?: boolean
268
296
  ): this;
269
297
  withoutSessionJpegCapture(): this;
270
298
  withTouchCapture(touchCapture?: AnsightTouchCaptureOptions): this;
271
299
  withoutTouchCapture(): this;
300
+ withCrashCapture(crashCapture?: AnsightCrashCaptureOptions): this;
301
+ withoutCrashCapture(): this;
272
302
  withLifecycleCapture(lifecycleCapture?: NonNullable<AnsightOptions["lifecycleCapture"]>): this;
273
303
  withToolGuard(toolGuard: NonNullable<AnsightOptions["toolGuard"]>): this;
274
304
  withToolsDisabled(): this;
@@ -512,6 +542,7 @@ export function metric(value: number, channel?: number): Promise<AnsightDebugSna
512
542
  export function recordMetric(value: number, channel?: number): Promise<AnsightDebugSnapshot>;
513
543
  export function event(input: string | { label: string; type?: string; details?: string; channel?: number }): Promise<AnsightDebugSnapshot>;
514
544
  export function recordEvent(input: string | { label: string; type?: string; details?: string; channel?: number }): Promise<AnsightDebugSnapshot>;
545
+ export function recordCrashCandidate(input?: AnsightCrashCandidate): Promise<{ candidateId?: string }>;
515
546
  export function screenViewed(name: string, details?: Record<string, string>): Promise<AnsightDebugSnapshot>;
516
547
  export function trackRoute(name: string, details?: Record<string, string>): Promise<AnsightDebugSnapshot>;
517
548
  export function setAppLifecycleState(state: AnsightLifecycleState): Promise<AnsightDebugSnapshot>;
@@ -585,6 +616,7 @@ declare const Ansight: {
585
616
  recordMetric: typeof recordMetric;
586
617
  event: typeof event;
587
618
  recordEvent: typeof recordEvent;
619
+ recordCrashCandidate: typeof recordCrashCandidate;
588
620
  screenViewed: typeof screenViewed;
589
621
  trackRoute: typeof trackRoute;
590
622
  setAppLifecycleState: typeof setAppLifecycleState;
package/index.js CHANGED
@@ -61,6 +61,9 @@ function cloneOptions(options = {}) {
61
61
  if (options.touchCapture && typeof options.touchCapture === "object") {
62
62
  clone.touchCapture = { ...options.touchCapture };
63
63
  }
64
+ if (options.crashCapture && typeof options.crashCapture === "object") {
65
+ clone.crashCapture = { ...options.crashCapture };
66
+ }
64
67
  if (options.lifecycleCapture) {
65
68
  clone.lifecycleCapture = { ...options.lifecycleCapture };
66
69
  }
@@ -157,6 +160,8 @@ class AnsightOptionsBuilder {
157
160
  retentionPeriodSeconds: 120,
158
161
  enableFramesPerSecond: true,
159
162
  enableBatteryLevel: false,
163
+ enableOpenFileHandleTracking: false,
164
+ enableJniReferenceCountTracking: false,
160
165
  sessionJpegCapture: {
161
166
  intervalMilliseconds: 2000,
162
167
  quality: 60,
@@ -209,6 +214,26 @@ class AnsightOptionsBuilder {
209
214
  return this;
210
215
  }
211
216
 
217
+ withOpenFileHandleTracking() {
218
+ this._options.enableOpenFileHandleTracking = true;
219
+ return this;
220
+ }
221
+
222
+ withoutOpenFileHandleTracking() {
223
+ this._options.enableOpenFileHandleTracking = false;
224
+ return this;
225
+ }
226
+
227
+ withJniReferenceCountTracking() {
228
+ this._options.enableJniReferenceCountTracking = true;
229
+ return this;
230
+ }
231
+
232
+ withoutJniReferenceCountTracking() {
233
+ this._options.enableJniReferenceCountTracking = false;
234
+ return this;
235
+ }
236
+
212
237
  withRetentionPeriodSeconds(retentionPeriodSeconds) {
213
238
  this._options.retentionPeriodSeconds = retentionPeriodSeconds;
214
239
  return this;
@@ -269,6 +294,7 @@ class AnsightOptionsBuilder {
269
294
  maxWidth: arguments.length > 2 ? arguments[2] : 480,
270
295
  captureGpuBackedSurfaces: arguments.length > 3 ? arguments[3] : true,
271
296
  mode: arguments.length > 4 ? arguments[4] : "screenshotOnly",
297
+ captureKeyboardPresence: arguments.length > 5 ? arguments[5] : false,
272
298
  };
273
299
  return this;
274
300
  }
@@ -276,6 +302,7 @@ class AnsightOptionsBuilder {
276
302
  intervalMilliseconds: 2000,
277
303
  quality: 60,
278
304
  maxWidth: 480,
305
+ captureKeyboardPresence: false,
279
306
  mode: "screenshotOnly",
280
307
  ...(optionsOrIntervalMilliseconds || {}),
281
308
  };
@@ -297,6 +324,16 @@ class AnsightOptionsBuilder {
297
324
  return this;
298
325
  }
299
326
 
327
+ withCrashCapture(crashCapture = {}) {
328
+ this._options.crashCapture = { ...crashCapture, enabled: true };
329
+ return this;
330
+ }
331
+
332
+ withoutCrashCapture() {
333
+ this._options.crashCapture = false;
334
+ return this;
335
+ }
336
+
300
337
  withLifecycleCapture(lifecycleCapture = {}) {
301
338
  this._options.lifecycleCapture = { ...lifecycleCapture };
302
339
  return this;
@@ -1391,7 +1428,7 @@ function summarizeProps(props) {
1391
1428
  }
1392
1429
  const keys = Object.keys(props).filter((key) => key !== "children");
1393
1430
  const summary = { keys };
1394
- ["testID", "nativeID", "accessibilityLabel", "role"].forEach((key) => {
1431
+ ["testID", "nativeID", "accessibilityLabel", "accessibilityRole", "role"].forEach((key) => {
1395
1432
  if (props[key] != null && !isSensitiveKey(key)) {
1396
1433
  summary[key] = String(props[key]);
1397
1434
  }
@@ -1403,6 +1440,42 @@ function summarizeProps(props) {
1403
1440
  return summary;
1404
1441
  }
1405
1442
 
1443
+ function reactSemanticRole(type, props, fiberTag) {
1444
+ const declared = props && (props.accessibilityRole || props.role);
1445
+ if (declared) return String(declared).toLowerCase();
1446
+ if (fiberTag === 6 || /text/i.test(type)) return "text";
1447
+ if (/button|pressable|touchable/i.test(type)) return "button";
1448
+ if (/textinput/i.test(type)) return "textbox";
1449
+ if (/switch/i.test(type)) return "switch";
1450
+ if (/scrollview|flatlist|sectionlist/i.test(type)) return "scrollview";
1451
+ return "view";
1452
+ }
1453
+
1454
+ function reactSupportedActions(type, props) {
1455
+ const actions = [];
1456
+ if (props && typeof props.onPress === "function") actions.push("tap");
1457
+ if (props && (typeof props.onChangeText === "function" || /textinput/i.test(type))) {
1458
+ actions.push("typeText", "focus");
1459
+ }
1460
+ if (/scrollview|flatlist|sectionlist/i.test(type) || (props && typeof props.onScroll === "function")) {
1461
+ actions.push("scroll", "swipe");
1462
+ }
1463
+ return actions;
1464
+ }
1465
+
1466
+ function applyReactTargetability(node, fiber, props, type) {
1467
+ const style = StyleSheet && typeof StyleSheet.flatten === "function"
1468
+ ? StyleSheet.flatten(props && props.style)
1469
+ : props && props.style;
1470
+ node.text = node.label || (node.visual && node.visual.text) || null;
1471
+ node.role = reactSemanticRole(type, props, fiber.tag);
1472
+ node.supportedActions = reactSupportedActions(type, props);
1473
+ node.visible = !(style && style.display === "none") && !(props && props.accessibilityElementsHidden);
1474
+ node.enabled = !(props && (props.disabled === true || props.accessibilityState && props.accessibilityState.disabled === true));
1475
+ node.focusable = !!(props && props.focusable) || node.supportedActions.includes("focus");
1476
+ node.interactable = node.visible && node.enabled && node.supportedActions.length > 0;
1477
+ }
1478
+
1406
1479
  function reactColorToArgbHex(value) {
1407
1480
  if (value == null) {
1408
1481
  return undefined;
@@ -1453,6 +1526,28 @@ function createReactVisual(fiber, props, maxStringLength) {
1453
1526
  return visual;
1454
1527
  }
1455
1528
 
1529
+ function createReactZIndex(props) {
1530
+ const style = StyleSheet && typeof StyleSheet.flatten === "function"
1531
+ ? StyleSheet.flatten(props && props.style)
1532
+ : props && props.style;
1533
+ const zIndex = style && style.zIndex != null ? Number(style.zIndex) : NaN;
1534
+ const elevation = style && style.elevation != null ? Number(style.elevation) : NaN;
1535
+ if (Number.isFinite(zIndex) && zIndex !== 0) return zIndex;
1536
+ if (Number.isFinite(elevation) && elevation !== 0) return elevation;
1537
+ return null;
1538
+ }
1539
+
1540
+ function registerReactType(context, typeName) {
1541
+ const normalizedTypeName = String(typeName || "UnknownComponent").trim() || "UnknownComponent";
1542
+ const existingTypeId = context.typeIdsByName.get(normalizedTypeName);
1543
+ if (existingTypeId != null) return existingTypeId;
1544
+
1545
+ const typeId = context.types.length;
1546
+ context.types.push(normalizedTypeName);
1547
+ context.typeIdsByName.set(normalizedTypeName, typeId);
1548
+ return typeId;
1549
+ }
1550
+
1456
1551
  function nativeTagForFiber(fiber) {
1457
1552
  const stateNode = fiber && fiber.stateNode;
1458
1553
  if (!stateNode) {
@@ -1524,17 +1619,17 @@ function serializeFiber(fiber, context, depth) {
1524
1619
 
1525
1620
  const props = fiber.memoizedProps || fiber.pendingProps || {};
1526
1621
  const type = reactFiberTypeName(fiber);
1527
- const kind = reactFiberKind(fiber.tag);
1528
1622
  const node = {
1529
1623
  id: reactFiberId(fiber),
1530
- type,
1531
- kind,
1624
+ typeId: registerReactType(context, type),
1532
1625
  tag: fiber.tag,
1533
1626
  key: fiber.key == null ? null : String(fiber.key),
1534
1627
  depth,
1535
1628
  visual: createReactVisual(fiber, props, context.maxStringLength),
1536
1629
  children: [],
1537
1630
  };
1631
+ const zIndex = createReactZIndex(props);
1632
+ if (zIndex != null) node.z = zIndex;
1538
1633
 
1539
1634
  if (fiber._debugSource) {
1540
1635
  node.source = {
@@ -1546,7 +1641,7 @@ function serializeFiber(fiber, context, depth) {
1546
1641
 
1547
1642
  const owner = fiber._debugOwner && reactFiberTypeName(fiber._debugOwner);
1548
1643
  if (owner) {
1549
- node.owner = owner;
1644
+ node.ownerTypeId = registerReactType(context, owner);
1550
1645
  }
1551
1646
 
1552
1647
  const nativeTag = nativeTagForFiber(fiber);
@@ -1577,6 +1672,8 @@ function serializeFiber(fiber, context, depth) {
1577
1672
  node.state = sanitizeValue(fiber.memoizedState, context);
1578
1673
  }
1579
1674
 
1675
+ applyReactTargetability(node, fiber, props, type);
1676
+
1580
1677
  if (depth < context.maxDepth) {
1581
1678
  let child = fiber.child;
1582
1679
  while (child) {
@@ -1600,16 +1697,18 @@ function isShadowTreeFiber(fiber) {
1600
1697
 
1601
1698
  function createShadowTreeNode(fiber, context, depth) {
1602
1699
  const props = fiber.memoizedProps || fiber.pendingProps || {};
1700
+ const type = reactFiberTypeName(fiber);
1603
1701
  const node = {
1604
1702
  id: reactFiberId(fiber),
1605
- type: reactFiberTypeName(fiber),
1606
- kind: fiber.tag === 3 ? "root" : fiber.tag === 6 ? "text" : "host",
1703
+ typeId: registerReactType(context, type),
1607
1704
  tag: fiber.tag,
1608
1705
  key: fiber.key == null ? null : String(fiber.key),
1609
1706
  depth,
1610
1707
  visual: createReactVisual(fiber, props, context.maxStringLength),
1611
1708
  children: [],
1612
1709
  };
1710
+ const zIndex = createReactZIndex(props);
1711
+ if (zIndex != null) node.z = zIndex;
1613
1712
 
1614
1713
  const nativeTag = nativeTagForFiber(fiber);
1615
1714
  if (nativeTag != null) {
@@ -1635,6 +1734,8 @@ function createShadowTreeNode(fiber, context, depth) {
1635
1734
  }
1636
1735
  }
1637
1736
 
1737
+ applyReactTargetability(node, fiber, props, type);
1738
+
1638
1739
  return node;
1639
1740
  }
1640
1741
 
@@ -1691,19 +1792,21 @@ async function captureReactVisualTree(rawOptions = {}) {
1691
1792
  maxStringLength: rawOptions.maxStringLength || 180,
1692
1793
  maxValueDepth: rawOptions.maxValueDepth || 2,
1693
1794
  nodesWithNativeTags: [],
1795
+ typeIdsByName: new Map(),
1796
+ types: [],
1694
1797
  count: 0,
1695
1798
  truncated: false,
1696
1799
  visited: new Set(),
1697
1800
  };
1698
1801
  const roots = getReactRoots();
1699
- const rootNodes = roots.roots.map((root, index) => {
1802
+ const rootNodes = roots.roots.map((root) => {
1700
1803
  const node = serializeFiber(root.fiber, context, 0);
1701
1804
  if (node) {
1702
1805
  node.rendererId = root.rendererId;
1703
- node.rootIndex = index;
1704
1806
  }
1705
1807
  return node;
1706
1808
  }).filter(Boolean);
1809
+ const rootTypeId = registerReactType(context, "ReactRoots");
1707
1810
 
1708
1811
  if (includeBounds && context.nodesWithNativeTags.length > 0) {
1709
1812
  await Promise.all(context.nodesWithNativeTags.slice(0, 300).map(async (node) => {
@@ -1715,6 +1818,7 @@ async function captureReactVisualTree(rawOptions = {}) {
1715
1818
  }
1716
1819
 
1717
1820
  return {
1821
+ format: "ansight.react.visual-tree.compact.v2",
1718
1822
  platform: Platform.OS,
1719
1823
  source: "react",
1720
1824
  adapter: "react.fiber",
@@ -1724,11 +1828,11 @@ async function captureReactVisualTree(rawOptions = {}) {
1724
1828
  renderers: roots.renderers,
1725
1829
  root: {
1726
1830
  id: "react:roots",
1727
- type: "ReactRoots",
1728
- kind: "container",
1831
+ typeId: rootTypeId,
1832
+ childCount: rootNodes.length,
1729
1833
  children: rootNodes,
1730
1834
  },
1731
- roots: rootNodes,
1835
+ types: context.types,
1732
1836
  nodeCount: context.count,
1733
1837
  truncated: context.truncated,
1734
1838
  unavailableReason: roots.hookAvailable ? undefined : "React DevTools global hook is not available in this runtime.",
@@ -1748,19 +1852,21 @@ async function captureReactShadowTree(rawOptions = {}) {
1748
1852
  maxStringLength: rawOptions.maxStringLength || 180,
1749
1853
  maxValueDepth: rawOptions.maxValueDepth || 2,
1750
1854
  nodesWithNativeTags: [],
1855
+ typeIdsByName: new Map(),
1856
+ types: [],
1751
1857
  count: 0,
1752
1858
  truncated: false,
1753
1859
  visited: new Set(),
1754
1860
  };
1755
1861
  const roots = getReactRoots();
1756
- const rootNodes = roots.roots.flatMap((root, index) => {
1862
+ const rootNodes = roots.roots.flatMap((root) => {
1757
1863
  const nodes = serializeShadowFiber(root.fiber, context, 0);
1758
1864
  nodes.forEach((node) => {
1759
1865
  node.rendererId = root.rendererId;
1760
- node.rootIndex = index;
1761
1866
  });
1762
1867
  return nodes;
1763
1868
  });
1869
+ const rootTypeId = registerReactType(context, "ReactNativeShadowRoots");
1764
1870
 
1765
1871
  if (includeBounds && context.nodesWithNativeTags.length > 0) {
1766
1872
  await Promise.all(context.nodesWithNativeTags.slice(0, 300).map(async (node) => {
@@ -1772,6 +1878,7 @@ async function captureReactShadowTree(rawOptions = {}) {
1772
1878
  }
1773
1879
 
1774
1880
  return {
1881
+ format: "ansight.react.visual-tree.compact.v2",
1775
1882
  platform: Platform.OS,
1776
1883
  source: "react-native",
1777
1884
  adapter: "react-native.host-fiber",
@@ -1781,11 +1888,11 @@ async function captureReactShadowTree(rawOptions = {}) {
1781
1888
  renderers: roots.renderers,
1782
1889
  root: {
1783
1890
  id: "react:shadow-roots",
1784
- type: "ReactNativeShadowRoots",
1785
- kind: "container",
1891
+ typeId: rootTypeId,
1892
+ childCount: rootNodes.length,
1786
1893
  children: rootNodes,
1787
1894
  },
1788
- roots: rootNodes,
1895
+ types: context.types,
1789
1896
  nodeCount: context.count,
1790
1897
  truncated: context.truncated,
1791
1898
  unavailableReason: roots.hookAvailable ? undefined : "React DevTools global hook is not available in this runtime.",
@@ -1801,13 +1908,18 @@ function flattenReactTree(node, output = []) {
1801
1908
  return output;
1802
1909
  }
1803
1910
 
1804
- function nodeSearchText(node) {
1911
+ function createReactNodeSnapshot(node) {
1912
+ const snapshot = { ...node };
1913
+ delete snapshot.children;
1914
+ return snapshot;
1915
+ }
1916
+
1917
+ function nodeSearchText(node, types) {
1805
1918
  return [
1806
1919
  node.id,
1807
- node.type,
1808
- node.kind,
1920
+ types[node.typeId],
1809
1921
  node.label,
1810
- node.owner,
1922
+ types[node.ownerTypeId],
1811
1923
  node.propsSummary && node.propsSummary.testID,
1812
1924
  node.propsSummary && node.propsSummary.nativeID,
1813
1925
  node.propsSummary && node.propsSummary.accessibilityLabel,
@@ -1815,14 +1927,14 @@ function nodeSearchText(node) {
1815
1927
  ].filter(Boolean).join(" ").toLowerCase();
1816
1928
  }
1817
1929
 
1818
- function matchesReactNode(node, args) {
1930
+ function matchesReactNode(node, args, types) {
1819
1931
  const query = args.query ? String(args.query).toLowerCase() : null;
1820
1932
  const type = args.type ? String(args.type).toLowerCase() : null;
1821
1933
  const testID = args.testID ? String(args.testID).toLowerCase() : null;
1822
1934
  const text = args.text ? String(args.text).toLowerCase() : null;
1823
- const searchText = nodeSearchText(node);
1935
+ const searchText = nodeSearchText(node, types);
1824
1936
  return (!query || searchText.includes(query)) &&
1825
- (!type || String(node.type || "").toLowerCase().includes(type)) &&
1937
+ (!type || String(types[node.typeId] || "").toLowerCase().includes(type)) &&
1826
1938
  (!testID || String((node.propsSummary && node.propsSummary.testID) || "").toLowerCase() === testID) &&
1827
1939
  (!text || searchText.includes(text));
1828
1940
  }
@@ -2043,12 +2155,14 @@ function installReactTools(options = {}) {
2043
2155
  });
2044
2156
  const maxResults = parseInteger(args.maxResults, 50, 1, 500);
2045
2157
  const matches = flattenReactTree(tree.root)
2046
- .filter((node) => node.id !== "react:roots" && matchesReactNode(node, args))
2047
- .slice(0, maxResults);
2158
+ .filter((node) => node.id !== "react:roots" && matchesReactNode(node, args, tree.types))
2159
+ .slice(0, maxResults)
2160
+ .map(createReactNodeSnapshot);
2048
2161
  return {
2049
2162
  success: tree.hookAvailable,
2050
2163
  message: `Found ${matches.length} React component(s).`,
2051
2164
  result: {
2165
+ types: tree.types,
2052
2166
  matches,
2053
2167
  count: matches.length,
2054
2168
  truncated: matches.length === maxResults,
@@ -2060,7 +2174,11 @@ function installReactTools(options = {}) {
2060
2174
  const tree = await captureReactVisualTree(reactToolOptions(options, args));
2061
2175
  const node = flattenReactTree(tree.root).find((candidate) => candidate.id === args.nodeId);
2062
2176
  return node
2063
- ? { success: true, message: "React component captured.", result: node }
2177
+ ? {
2178
+ success: true,
2179
+ message: "React component captured.",
2180
+ result: { types: tree.types, node: createReactNodeSnapshot(node) },
2181
+ }
2064
2182
  : { success: false, message: `React component '${args.nodeId}' was not found.`, errorCode: "react_component_not_found" };
2065
2183
  }
2066
2184
 
@@ -2101,7 +2219,7 @@ function installReactTools(options = {}) {
2101
2219
  result: {
2102
2220
  nodeId: args.nodeId,
2103
2221
  prop,
2104
- type: node.type,
2222
+ type: tree.types[node.typeId],
2105
2223
  },
2106
2224
  };
2107
2225
  }
@@ -2137,6 +2255,14 @@ function installErrorHandlers(options = {}) {
2137
2255
 
2138
2256
  if (global.ErrorUtils && global.ErrorUtils.setGlobalHandler) {
2139
2257
  global.ErrorUtils.setGlobalHandler((error, isFatal) => {
2258
+ nativeModule.recordCrashCandidate({
2259
+ runtime: "react-native-javascript",
2260
+ kind: "unhandled_javascript_error",
2261
+ message: error && error.message,
2262
+ stack: error && error.stack,
2263
+ fatal: !!isFatal,
2264
+ metadata: JSON.stringify({ name: error && error.name }),
2265
+ }).catch(() => {});
2140
2266
  nativeModule.recordEvent({
2141
2267
  label: error && error.message ? error.message : "Unhandled JavaScript error",
2142
2268
  type: "Exception",
@@ -2159,6 +2285,13 @@ function installErrorHandlers(options = {}) {
2159
2285
  global.__ansightUnhandledRejectionTrackingInstalled = true;
2160
2286
  global.addEventListener("unhandledrejection", (event) => {
2161
2287
  const reason = event && event.reason;
2288
+ nativeModule.recordCrashCandidate({
2289
+ runtime: "react-native-javascript",
2290
+ kind: "unhandled_promise_rejection",
2291
+ message: reason && reason.message ? reason.message : String(reason),
2292
+ stack: reason && reason.stack,
2293
+ fatal: false,
2294
+ }).catch(() => {});
2162
2295
  nativeModule.recordEvent({
2163
2296
  label: reason && reason.message ? reason.message : "Unhandled JavaScript promise rejection",
2164
2297
  type: "Exception",
@@ -2253,6 +2386,16 @@ function recordEvent(input) {
2253
2386
  return nativeModule.recordEvent(input || {});
2254
2387
  }
2255
2388
 
2389
+ function recordCrashCandidate(input = {}) {
2390
+ const metadata = input.metadata && typeof input.metadata === "object"
2391
+ ? JSON.stringify(input.metadata)
2392
+ : input.metadata;
2393
+ return nativeModule.recordCrashCandidate({
2394
+ ...input,
2395
+ metadata,
2396
+ });
2397
+ }
2398
+
2256
2399
  function screenViewed(name, details) {
2257
2400
  return nativeModule.screenViewed(name, details || {});
2258
2401
  }
@@ -2295,6 +2438,7 @@ const Ansight = {
2295
2438
  recordMetric: (value, channel = 255) => nativeModule.recordMetric(value, channel),
2296
2439
  event: recordEvent,
2297
2440
  recordEvent,
2441
+ recordCrashCandidate,
2298
2442
  screenViewed,
2299
2443
  trackRoute,
2300
2444
  setAppLifecycleState: (state) => nativeModule.setAppLifecycleState(state),
@@ -292,6 +292,31 @@ final class AnsightReactNative: RCTEventEmitter {
292
292
  }
293
293
  }
294
294
 
295
+ @objc(recordCrashCandidate:resolver:rejecter:)
296
+ func recordCrashCandidate(
297
+ _ input: NSDictionary,
298
+ resolver resolve: RCTPromiseResolveBlock,
299
+ rejecter reject: RCTPromiseRejectBlock
300
+ ) {
301
+ let metadata: [String: String]
302
+ if let metadataJson = stringValue(input, "metadata"),
303
+ let data = metadataJson.data(using: .utf8),
304
+ let decoded = try? JSONDecoder().decode([String: String].self, from: data) {
305
+ metadata = decoded
306
+ } else {
307
+ metadata = [:]
308
+ }
309
+ let candidateId = AnsightRuntime.shared.recordCrashCandidate(
310
+ runtime: stringValue(input, "runtime") ?? "react-native-javascript",
311
+ kind: stringValue(input, "kind") ?? "unhandled_javascript_error",
312
+ message: stringValue(input, "message"),
313
+ stack: stringValue(input, "stack"),
314
+ fatal: boolValue(input, "fatal", defaultValue: false),
315
+ metadata: metadata
316
+ )
317
+ resolve(candidateId.map { ["candidateId": $0] } ?? [:])
318
+ }
319
+
295
320
  @objc(screenViewed:details:resolver:rejecter:)
296
321
  func screenViewed(
297
322
  _ name: NSString,
@@ -895,6 +920,13 @@ final class AnsightReactNative: RCTEventEmitter {
895
920
  if hasBool(dictionary, "enableBatteryLevel") {
896
921
  options.enableBatteryLevel = boolValue(dictionary, "enableBatteryLevel", defaultValue: options.enableBatteryLevel)
897
922
  }
923
+ if hasBool(dictionary, "enableOpenFileHandleTracking") {
924
+ options.enableOpenFileHandleTracking = boolValue(
925
+ dictionary,
926
+ "enableOpenFileHandleTracking",
927
+ defaultValue: options.enableOpenFileHandleTracking
928
+ )
929
+ }
898
930
  if let memory = dictionary?["defaultMemoryChannels"] as? NSDictionary {
899
931
  var channels: DefaultMemoryChannels = []
900
932
  if boolValue(memory, "managedHeap", defaultValue: boolValue(memory, "javaHeap", defaultValue: false)) {
@@ -942,9 +974,14 @@ final class AnsightReactNative: RCTEventEmitter {
942
974
  "captureGpuBackedSurfaces",
943
975
  defaultValue: AnsightSessionJpegCaptureOptions.defaultCaptureGpuBackedSurfaces
944
976
  ),
945
- mode: stringValue(jpeg, "mode") == "screenshotAndVisualTree"
946
- ? .screenshotAndVisualTree
947
- : .screenshotOnly
977
+ mode: AnsightSessionJpegCaptureMode(
978
+ rawValue: stringValue(jpeg, "mode") ?? ""
979
+ ) ?? .screenshotOnly,
980
+ captureKeyboardPresence: boolValue(
981
+ jpeg,
982
+ "captureKeyboardPresence",
983
+ defaultValue: AnsightSessionJpegCaptureOptions.defaultCaptureKeyboardPresence
984
+ )
948
985
  )
949
986
  }
950
987
  }
@@ -960,6 +997,21 @@ final class AnsightReactNative: RCTEventEmitter {
960
997
  )
961
998
  }
962
999
  }
1000
+ if let raw = dictionary?["crashCapture"] {
1001
+ if (raw as? Bool) == false {
1002
+ options.crashCapture.enabled = false
1003
+ } else if let crash = raw as? NSDictionary {
1004
+ options.crashCapture = AnsightCrashCaptureOptions(
1005
+ enabled: boolValue(crash, "enabled", defaultValue: true),
1006
+ studioHandoffEnabled: boolValue(crash, "studioHandoffEnabled", defaultValue: true),
1007
+ offlineCaptureAttachmentEnabled: boolValue(crash, "offlineCaptureAttachmentEnabled", defaultValue: true),
1008
+ maximumPendingReports: intValue(crash, "maximumPendingReports", defaultValue: 8),
1009
+ retentionDays: intValue(crash, "retentionDays", defaultValue: 7),
1010
+ maximumBreadcrumbs: intValue(crash, "maximumBreadcrumbs", defaultValue: 64),
1011
+ maximumTraceBytes: intValue(crash, "maximumTraceBytes", defaultValue: 1_048_576)
1012
+ )
1013
+ }
1014
+ }
963
1015
  if let lifecycle = dictionary?["lifecycleCapture"] as? NSDictionary {
964
1016
  options.lifecycleCapture = AnsightLifecycleCaptureOptions(
965
1017
  enabled: boolValue(lifecycle, "enabled", defaultValue: options.lifecycleCapture.enabled),
@@ -1271,9 +1323,14 @@ final class AnsightReactNative: RCTEventEmitter {
1271
1323
  "captureGpuBackedSurfaces",
1272
1324
  defaultValue: AnsightSessionJpegCaptureOptions.defaultCaptureGpuBackedSurfaces
1273
1325
  ),
1274
- mode: stringValue(dictionary, "mode") == "screenshotAndVisualTree"
1275
- ? .screenshotAndVisualTree
1276
- : .screenshotOnly
1326
+ mode: AnsightSessionJpegCaptureMode(
1327
+ rawValue: stringValue(dictionary, "mode") ?? ""
1328
+ ) ?? .screenshotOnly,
1329
+ captureKeyboardPresence: boolValue(
1330
+ dictionary,
1331
+ "captureKeyboardPresence",
1332
+ defaultValue: AnsightSessionJpegCaptureOptions.defaultCaptureKeyboardPresence
1333
+ )
1277
1334
  )
1278
1335
  }
1279
1336
 
@@ -1283,6 +1340,8 @@ final class AnsightReactNative: RCTEventEmitter {
1283
1340
  "retentionPeriodSeconds": options.retentionPeriodSeconds,
1284
1341
  "enableFramesPerSecond": options.enableFramesPerSecond,
1285
1342
  "enableBatteryLevel": options.enableBatteryLevel,
1343
+ "enableOpenFileHandleTracking": options.enableOpenFileHandleTracking,
1344
+ "enableJniReferenceCountTracking": false,
1286
1345
  "defaultMemoryChannels": [
1287
1346
  "managedHeap": options.defaultMemoryChannels.contains(.managedHeap),
1288
1347
  "javaHeap": options.defaultMemoryChannels.contains(.managedHeap),
@@ -1322,6 +1381,7 @@ final class AnsightReactNative: RCTEventEmitter {
1322
1381
  "quality": capture.quality,
1323
1382
  "maxWidth": capture.maxWidth as Any,
1324
1383
  "captureGpuBackedSurfaces": capture.captureGpuBackedSurfaces,
1384
+ "captureKeyboardPresence": capture.captureKeyboardPresence,
1325
1385
  "mode": capture.mode.rawValue,
1326
1386
  ]
1327
1387
  } else {
@@ -33,6 +33,10 @@ RCT_EXTERN_METHOD(recordEvent:(NSDictionary *)input
33
33
  resolver:(RCTPromiseResolveBlock)resolve
34
34
  rejecter:(RCTPromiseRejectBlock)reject)
35
35
 
36
+ RCT_EXTERN_METHOD(recordCrashCandidate:(NSDictionary *)input
37
+ resolver:(RCTPromiseResolveBlock)resolve
38
+ rejecter:(RCTPromiseRejectBlock)reject)
39
+
36
40
  RCT_EXTERN_METHOD(screenViewed:(NSString *)name
37
41
  details:(NSDictionary *)details
38
42
  resolver:(RCTPromiseResolveBlock)resolve
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ansight/react-native",
3
- "version": "1.3.0-preview.1",
3
+ "version": "1.3.0-preview.10",
4
4
  "description": "React Native bridge for the Ansight mobile SDK.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",