@ansight/react-native 1.2.0-preview.2 → 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.2.0-preview.2`
81
- - Maven: `ai.ansight:ansight-android:1.2.0-preview.2`
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.2.0-preview.2"
71
- implementation("ai.ansight:ansight-android:1.2.0-preview.2")
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
@@ -16,6 +17,7 @@ import ai.ansight.runtime.AnsightOptionsBuilder
16
17
  import ai.ansight.runtime.AnsightRuntime
17
18
  import ai.ansight.runtime.AnsightSecureStorageOptions
18
19
  import ai.ansight.runtime.AnsightSessionJpegCaptureOptions
20
+ import ai.ansight.runtime.AnsightSessionJpegCaptureMode
19
21
  import ai.ansight.runtime.AnsightToolGuard
20
22
  import ai.ansight.runtime.AnsightTouchCaptureOptions
21
23
  import ai.ansight.runtime.AppLifecycleState
@@ -224,6 +226,21 @@ class AnsightReactNativeModule(
224
226
  }.resolve(promise)
225
227
  }
226
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
+
227
244
  @ReactMethod
228
245
  fun screenViewed(name: String, details: ReadableMap?, promise: Promise) {
229
246
  runCatching {
@@ -692,6 +709,22 @@ class AnsightReactNativeModule(
692
709
  if (map.hasBoolean("enableBatteryLevel")) {
693
710
  options = options.copy(enableBatteryLevel = map.booleanValue("enableBatteryLevel", options.enableBatteryLevel))
694
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
+ }
695
728
  if (map.hasMap("defaultMemoryChannels")) {
696
729
  val memory = map.getMapOrNull("defaultMemoryChannels")
697
730
  val managedHeap = if (memory.hasBoolean("managedHeap")) {
@@ -749,6 +782,11 @@ class AnsightReactNativeModule(
749
782
  "captureGpuBackedSurfaces",
750
783
  AnsightSessionJpegCaptureOptions.DefaultCaptureGpuBackedSurfaces,
751
784
  ),
785
+ mode = sessionJpegCaptureMode(jpeg.stringValue("mode")),
786
+ captureKeyboardPresence = jpeg.booleanValue(
787
+ "captureKeyboardPresence",
788
+ AnsightSessionJpegCaptureOptions.DefaultCaptureKeyboardPresence,
789
+ ),
752
790
  )
753
791
  },
754
792
  )
@@ -766,6 +804,24 @@ class AnsightReactNativeModule(
766
804
  },
767
805
  )
768
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
+ }
769
825
  if (map.hasString("toolGuard")) {
770
826
  options = options.copy(toolGuard = toolGuard(map.stringValue("toolGuard")))
771
827
  }
@@ -928,9 +984,21 @@ class AnsightReactNativeModule(
928
984
  "captureGpuBackedSurfaces",
929
985
  AnsightSessionJpegCaptureOptions.DefaultCaptureGpuBackedSurfaces,
930
986
  ),
987
+ mode = sessionJpegCaptureMode(map.stringValue("mode")),
988
+ captureKeyboardPresence = map.booleanValue(
989
+ "captureKeyboardPresence",
990
+ AnsightSessionJpegCaptureOptions.DefaultCaptureKeyboardPresence,
991
+ ),
931
992
  )
932
993
  }
933
994
 
995
+ private fun sessionJpegCaptureMode(value: String?): AnsightSessionJpegCaptureMode =
996
+ when (value) {
997
+ "screenshotAndVisualTree" -> AnsightSessionJpegCaptureMode.ScreenshotAndVisualTree
998
+ "screenshotWithVisualTreeOnTouch" -> AnsightSessionJpegCaptureMode.ScreenshotWithVisualTreeOnTouch
999
+ else -> AnsightSessionJpegCaptureMode.ScreenshotOnly
1000
+ }
1001
+
934
1002
  private fun toolDefinition(map: ReadableMap): ToolDefinition =
935
1003
  ToolDefinition(
936
1004
  id = map.stringValue("id") ?: "",
@@ -1101,6 +1169,8 @@ class AnsightReactNativeModule(
1101
1169
  putInt("retentionPeriodSeconds", options.retentionPeriodSeconds)
1102
1170
  putBoolean("enableFramesPerSecond", options.enableFramesPerSecond)
1103
1171
  putBoolean("enableBatteryLevel", options.enableBatteryLevel)
1172
+ putBoolean("enableOpenFileHandleTracking", options.enableOpenFileHandleTracking)
1173
+ putBoolean("enableJniReferenceCountTracking", options.enableJniReferenceCountTracking)
1104
1174
  putMap("defaultMemoryChannels", mapOf(
1105
1175
  "managedHeap" to options.defaultMemoryChannels.javaHeap,
1106
1176
  "javaHeap" to options.defaultMemoryChannels.javaHeap,
@@ -1117,6 +1187,12 @@ class AnsightReactNativeModule(
1117
1187
  "quality" to capture.quality,
1118
1188
  "maxWidth" to capture.maxWidth,
1119
1189
  "captureGpuBackedSurfaces" to capture.captureGpuBackedSurfaces,
1190
+ "captureKeyboardPresence" to capture.captureKeyboardPresence,
1191
+ "mode" to when (capture.mode) {
1192
+ AnsightSessionJpegCaptureMode.ScreenshotAndVisualTree -> "screenshotAndVisualTree"
1193
+ AnsightSessionJpegCaptureMode.ScreenshotWithVisualTreeOnTouch -> "screenshotWithVisualTreeOnTouch"
1194
+ else -> "screenshotOnly"
1195
+ },
1120
1196
  ).toWritableMap())
1121
1197
  } ?: putNull("sessionJpegCapture")
1122
1198
  options.touchCapture?.let { touch ->
package/index.d.ts CHANGED
@@ -37,6 +37,8 @@ export interface AnsightSessionJpegCaptureOptions {
37
37
  quality?: number;
38
38
  maxWidth?: number;
39
39
  captureGpuBackedSurfaces?: boolean;
40
+ captureKeyboardPresence?: boolean;
41
+ mode?: "screenshotOnly" | "screenshotAndVisualTree" | "screenshotWithVisualTreeOnTouch";
40
42
  }
41
43
 
42
44
  export interface AnsightTouchCaptureOptions {
@@ -109,6 +111,8 @@ export interface AnsightOptions {
109
111
  retentionPeriodSeconds?: number;
110
112
  enableFramesPerSecond?: boolean;
111
113
  enableBatteryLevel?: boolean;
114
+ enableOpenFileHandleTracking?: boolean;
115
+ enableJniReferenceCountTracking?: boolean;
112
116
  defaultMemoryChannels?: {
113
117
  managedHeap?: boolean;
114
118
  physicalFootprint?: boolean;
@@ -120,6 +124,7 @@ export interface AnsightOptions {
120
124
  reactNativeMemory?: false | AnsightReactNativeMemoryOptions;
121
125
  sessionJpegCapture?: false | AnsightSessionJpegCaptureOptions;
122
126
  touchCapture?: false | AnsightTouchCaptureOptions;
127
+ crashCapture?: false | AnsightCrashCaptureOptions;
123
128
  lifecycleCapture?: {
124
129
  enabled?: boolean;
125
130
  captureAppLifecycle?: boolean;
@@ -152,6 +157,25 @@ export interface AnsightOptions {
152
157
  lifecycle?: boolean;
153
158
  }
154
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
+
155
179
  export interface AnsightOperationResult {
156
180
  success: boolean;
157
181
  message: string;
@@ -250,6 +274,10 @@ export class AnsightOptionsBuilder {
250
274
  withoutFramesPerSecond(): this;
251
275
  withBatteryLevel(): this;
252
276
  withoutBatteryLevel(): this;
277
+ withOpenFileHandleTracking(): this;
278
+ withoutOpenFileHandleTracking(): this;
279
+ withJniReferenceCountTracking(): this;
280
+ withoutJniReferenceCountTracking(): this;
253
281
  withRetentionPeriodSeconds(retentionPeriodSeconds: number): this;
254
282
  withAdditionalChannels(additionalChannels: AnsightChannel[]): this;
255
283
  addAdditionalChannel(additionalChannel: AnsightChannel): this;
@@ -262,11 +290,15 @@ export class AnsightOptionsBuilder {
262
290
  intervalMilliseconds: number,
263
291
  quality?: number,
264
292
  maxWidth?: number | null,
265
- captureGpuBackedSurfaces?: boolean
293
+ captureGpuBackedSurfaces?: boolean,
294
+ mode?: "screenshotOnly" | "screenshotAndVisualTree" | "screenshotWithVisualTreeOnTouch",
295
+ captureKeyboardPresence?: boolean
266
296
  ): this;
267
297
  withoutSessionJpegCapture(): this;
268
298
  withTouchCapture(touchCapture?: AnsightTouchCaptureOptions): this;
269
299
  withoutTouchCapture(): this;
300
+ withCrashCapture(crashCapture?: AnsightCrashCaptureOptions): this;
301
+ withoutCrashCapture(): this;
270
302
  withLifecycleCapture(lifecycleCapture?: NonNullable<AnsightOptions["lifecycleCapture"]>): this;
271
303
  withToolGuard(toolGuard: NonNullable<AnsightOptions["toolGuard"]>): this;
272
304
  withToolsDisabled(): this;
@@ -510,6 +542,7 @@ export function metric(value: number, channel?: number): Promise<AnsightDebugSna
510
542
  export function recordMetric(value: number, channel?: number): Promise<AnsightDebugSnapshot>;
511
543
  export function event(input: string | { label: string; type?: string; details?: string; channel?: number }): Promise<AnsightDebugSnapshot>;
512
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 }>;
513
546
  export function screenViewed(name: string, details?: Record<string, string>): Promise<AnsightDebugSnapshot>;
514
547
  export function trackRoute(name: string, details?: Record<string, string>): Promise<AnsightDebugSnapshot>;
515
548
  export function setAppLifecycleState(state: AnsightLifecycleState): Promise<AnsightDebugSnapshot>;
@@ -583,6 +616,7 @@ declare const Ansight: {
583
616
  recordMetric: typeof recordMetric;
584
617
  event: typeof event;
585
618
  recordEvent: typeof recordEvent;
619
+ recordCrashCandidate: typeof recordCrashCandidate;
586
620
  screenViewed: typeof screenViewed;
587
621
  trackRoute: typeof trackRoute;
588
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;
@@ -268,6 +293,8 @@ class AnsightOptionsBuilder {
268
293
  quality: arguments.length > 1 ? arguments[1] : 60,
269
294
  maxWidth: arguments.length > 2 ? arguments[2] : 480,
270
295
  captureGpuBackedSurfaces: arguments.length > 3 ? arguments[3] : true,
296
+ mode: arguments.length > 4 ? arguments[4] : "screenshotOnly",
297
+ captureKeyboardPresence: arguments.length > 5 ? arguments[5] : false,
271
298
  };
272
299
  return this;
273
300
  }
@@ -275,6 +302,8 @@ class AnsightOptionsBuilder {
275
302
  intervalMilliseconds: 2000,
276
303
  quality: 60,
277
304
  maxWidth: 480,
305
+ captureKeyboardPresence: false,
306
+ mode: "screenshotOnly",
278
307
  ...(optionsOrIntervalMilliseconds || {}),
279
308
  };
280
309
  return this;
@@ -295,6 +324,16 @@ class AnsightOptionsBuilder {
295
324
  return this;
296
325
  }
297
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
+
298
337
  withLifecycleCapture(lifecycleCapture = {}) {
299
338
  this._options.lifecycleCapture = { ...lifecycleCapture };
300
339
  return this;
@@ -1389,7 +1428,7 @@ function summarizeProps(props) {
1389
1428
  }
1390
1429
  const keys = Object.keys(props).filter((key) => key !== "children");
1391
1430
  const summary = { keys };
1392
- ["testID", "nativeID", "accessibilityLabel", "role"].forEach((key) => {
1431
+ ["testID", "nativeID", "accessibilityLabel", "accessibilityRole", "role"].forEach((key) => {
1393
1432
  if (props[key] != null && !isSensitiveKey(key)) {
1394
1433
  summary[key] = String(props[key]);
1395
1434
  }
@@ -1401,6 +1440,42 @@ function summarizeProps(props) {
1401
1440
  return summary;
1402
1441
  }
1403
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
+
1404
1479
  function reactColorToArgbHex(value) {
1405
1480
  if (value == null) {
1406
1481
  return undefined;
@@ -1451,6 +1526,28 @@ function createReactVisual(fiber, props, maxStringLength) {
1451
1526
  return visual;
1452
1527
  }
1453
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
+
1454
1551
  function nativeTagForFiber(fiber) {
1455
1552
  const stateNode = fiber && fiber.stateNode;
1456
1553
  if (!stateNode) {
@@ -1522,17 +1619,17 @@ function serializeFiber(fiber, context, depth) {
1522
1619
 
1523
1620
  const props = fiber.memoizedProps || fiber.pendingProps || {};
1524
1621
  const type = reactFiberTypeName(fiber);
1525
- const kind = reactFiberKind(fiber.tag);
1526
1622
  const node = {
1527
1623
  id: reactFiberId(fiber),
1528
- type,
1529
- kind,
1624
+ typeId: registerReactType(context, type),
1530
1625
  tag: fiber.tag,
1531
1626
  key: fiber.key == null ? null : String(fiber.key),
1532
1627
  depth,
1533
1628
  visual: createReactVisual(fiber, props, context.maxStringLength),
1534
1629
  children: [],
1535
1630
  };
1631
+ const zIndex = createReactZIndex(props);
1632
+ if (zIndex != null) node.z = zIndex;
1536
1633
 
1537
1634
  if (fiber._debugSource) {
1538
1635
  node.source = {
@@ -1544,7 +1641,7 @@ function serializeFiber(fiber, context, depth) {
1544
1641
 
1545
1642
  const owner = fiber._debugOwner && reactFiberTypeName(fiber._debugOwner);
1546
1643
  if (owner) {
1547
- node.owner = owner;
1644
+ node.ownerTypeId = registerReactType(context, owner);
1548
1645
  }
1549
1646
 
1550
1647
  const nativeTag = nativeTagForFiber(fiber);
@@ -1575,6 +1672,8 @@ function serializeFiber(fiber, context, depth) {
1575
1672
  node.state = sanitizeValue(fiber.memoizedState, context);
1576
1673
  }
1577
1674
 
1675
+ applyReactTargetability(node, fiber, props, type);
1676
+
1578
1677
  if (depth < context.maxDepth) {
1579
1678
  let child = fiber.child;
1580
1679
  while (child) {
@@ -1598,16 +1697,18 @@ function isShadowTreeFiber(fiber) {
1598
1697
 
1599
1698
  function createShadowTreeNode(fiber, context, depth) {
1600
1699
  const props = fiber.memoizedProps || fiber.pendingProps || {};
1700
+ const type = reactFiberTypeName(fiber);
1601
1701
  const node = {
1602
1702
  id: reactFiberId(fiber),
1603
- type: reactFiberTypeName(fiber),
1604
- kind: fiber.tag === 3 ? "root" : fiber.tag === 6 ? "text" : "host",
1703
+ typeId: registerReactType(context, type),
1605
1704
  tag: fiber.tag,
1606
1705
  key: fiber.key == null ? null : String(fiber.key),
1607
1706
  depth,
1608
1707
  visual: createReactVisual(fiber, props, context.maxStringLength),
1609
1708
  children: [],
1610
1709
  };
1710
+ const zIndex = createReactZIndex(props);
1711
+ if (zIndex != null) node.z = zIndex;
1611
1712
 
1612
1713
  const nativeTag = nativeTagForFiber(fiber);
1613
1714
  if (nativeTag != null) {
@@ -1633,6 +1734,8 @@ function createShadowTreeNode(fiber, context, depth) {
1633
1734
  }
1634
1735
  }
1635
1736
 
1737
+ applyReactTargetability(node, fiber, props, type);
1738
+
1636
1739
  return node;
1637
1740
  }
1638
1741
 
@@ -1689,19 +1792,21 @@ async function captureReactVisualTree(rawOptions = {}) {
1689
1792
  maxStringLength: rawOptions.maxStringLength || 180,
1690
1793
  maxValueDepth: rawOptions.maxValueDepth || 2,
1691
1794
  nodesWithNativeTags: [],
1795
+ typeIdsByName: new Map(),
1796
+ types: [],
1692
1797
  count: 0,
1693
1798
  truncated: false,
1694
1799
  visited: new Set(),
1695
1800
  };
1696
1801
  const roots = getReactRoots();
1697
- const rootNodes = roots.roots.map((root, index) => {
1802
+ const rootNodes = roots.roots.map((root) => {
1698
1803
  const node = serializeFiber(root.fiber, context, 0);
1699
1804
  if (node) {
1700
1805
  node.rendererId = root.rendererId;
1701
- node.rootIndex = index;
1702
1806
  }
1703
1807
  return node;
1704
1808
  }).filter(Boolean);
1809
+ const rootTypeId = registerReactType(context, "ReactRoots");
1705
1810
 
1706
1811
  if (includeBounds && context.nodesWithNativeTags.length > 0) {
1707
1812
  await Promise.all(context.nodesWithNativeTags.slice(0, 300).map(async (node) => {
@@ -1713,6 +1818,7 @@ async function captureReactVisualTree(rawOptions = {}) {
1713
1818
  }
1714
1819
 
1715
1820
  return {
1821
+ format: "ansight.react.visual-tree.compact.v2",
1716
1822
  platform: Platform.OS,
1717
1823
  source: "react",
1718
1824
  adapter: "react.fiber",
@@ -1722,11 +1828,11 @@ async function captureReactVisualTree(rawOptions = {}) {
1722
1828
  renderers: roots.renderers,
1723
1829
  root: {
1724
1830
  id: "react:roots",
1725
- type: "ReactRoots",
1726
- kind: "container",
1831
+ typeId: rootTypeId,
1832
+ childCount: rootNodes.length,
1727
1833
  children: rootNodes,
1728
1834
  },
1729
- roots: rootNodes,
1835
+ types: context.types,
1730
1836
  nodeCount: context.count,
1731
1837
  truncated: context.truncated,
1732
1838
  unavailableReason: roots.hookAvailable ? undefined : "React DevTools global hook is not available in this runtime.",
@@ -1746,19 +1852,21 @@ async function captureReactShadowTree(rawOptions = {}) {
1746
1852
  maxStringLength: rawOptions.maxStringLength || 180,
1747
1853
  maxValueDepth: rawOptions.maxValueDepth || 2,
1748
1854
  nodesWithNativeTags: [],
1855
+ typeIdsByName: new Map(),
1856
+ types: [],
1749
1857
  count: 0,
1750
1858
  truncated: false,
1751
1859
  visited: new Set(),
1752
1860
  };
1753
1861
  const roots = getReactRoots();
1754
- const rootNodes = roots.roots.flatMap((root, index) => {
1862
+ const rootNodes = roots.roots.flatMap((root) => {
1755
1863
  const nodes = serializeShadowFiber(root.fiber, context, 0);
1756
1864
  nodes.forEach((node) => {
1757
1865
  node.rendererId = root.rendererId;
1758
- node.rootIndex = index;
1759
1866
  });
1760
1867
  return nodes;
1761
1868
  });
1869
+ const rootTypeId = registerReactType(context, "ReactNativeShadowRoots");
1762
1870
 
1763
1871
  if (includeBounds && context.nodesWithNativeTags.length > 0) {
1764
1872
  await Promise.all(context.nodesWithNativeTags.slice(0, 300).map(async (node) => {
@@ -1770,6 +1878,7 @@ async function captureReactShadowTree(rawOptions = {}) {
1770
1878
  }
1771
1879
 
1772
1880
  return {
1881
+ format: "ansight.react.visual-tree.compact.v2",
1773
1882
  platform: Platform.OS,
1774
1883
  source: "react-native",
1775
1884
  adapter: "react-native.host-fiber",
@@ -1779,11 +1888,11 @@ async function captureReactShadowTree(rawOptions = {}) {
1779
1888
  renderers: roots.renderers,
1780
1889
  root: {
1781
1890
  id: "react:shadow-roots",
1782
- type: "ReactNativeShadowRoots",
1783
- kind: "container",
1891
+ typeId: rootTypeId,
1892
+ childCount: rootNodes.length,
1784
1893
  children: rootNodes,
1785
1894
  },
1786
- roots: rootNodes,
1895
+ types: context.types,
1787
1896
  nodeCount: context.count,
1788
1897
  truncated: context.truncated,
1789
1898
  unavailableReason: roots.hookAvailable ? undefined : "React DevTools global hook is not available in this runtime.",
@@ -1799,13 +1908,18 @@ function flattenReactTree(node, output = []) {
1799
1908
  return output;
1800
1909
  }
1801
1910
 
1802
- 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) {
1803
1918
  return [
1804
1919
  node.id,
1805
- node.type,
1806
- node.kind,
1920
+ types[node.typeId],
1807
1921
  node.label,
1808
- node.owner,
1922
+ types[node.ownerTypeId],
1809
1923
  node.propsSummary && node.propsSummary.testID,
1810
1924
  node.propsSummary && node.propsSummary.nativeID,
1811
1925
  node.propsSummary && node.propsSummary.accessibilityLabel,
@@ -1813,14 +1927,14 @@ function nodeSearchText(node) {
1813
1927
  ].filter(Boolean).join(" ").toLowerCase();
1814
1928
  }
1815
1929
 
1816
- function matchesReactNode(node, args) {
1930
+ function matchesReactNode(node, args, types) {
1817
1931
  const query = args.query ? String(args.query).toLowerCase() : null;
1818
1932
  const type = args.type ? String(args.type).toLowerCase() : null;
1819
1933
  const testID = args.testID ? String(args.testID).toLowerCase() : null;
1820
1934
  const text = args.text ? String(args.text).toLowerCase() : null;
1821
- const searchText = nodeSearchText(node);
1935
+ const searchText = nodeSearchText(node, types);
1822
1936
  return (!query || searchText.includes(query)) &&
1823
- (!type || String(node.type || "").toLowerCase().includes(type)) &&
1937
+ (!type || String(types[node.typeId] || "").toLowerCase().includes(type)) &&
1824
1938
  (!testID || String((node.propsSummary && node.propsSummary.testID) || "").toLowerCase() === testID) &&
1825
1939
  (!text || searchText.includes(text));
1826
1940
  }
@@ -2041,12 +2155,14 @@ function installReactTools(options = {}) {
2041
2155
  });
2042
2156
  const maxResults = parseInteger(args.maxResults, 50, 1, 500);
2043
2157
  const matches = flattenReactTree(tree.root)
2044
- .filter((node) => node.id !== "react:roots" && matchesReactNode(node, args))
2045
- .slice(0, maxResults);
2158
+ .filter((node) => node.id !== "react:roots" && matchesReactNode(node, args, tree.types))
2159
+ .slice(0, maxResults)
2160
+ .map(createReactNodeSnapshot);
2046
2161
  return {
2047
2162
  success: tree.hookAvailable,
2048
2163
  message: `Found ${matches.length} React component(s).`,
2049
2164
  result: {
2165
+ types: tree.types,
2050
2166
  matches,
2051
2167
  count: matches.length,
2052
2168
  truncated: matches.length === maxResults,
@@ -2058,7 +2174,11 @@ function installReactTools(options = {}) {
2058
2174
  const tree = await captureReactVisualTree(reactToolOptions(options, args));
2059
2175
  const node = flattenReactTree(tree.root).find((candidate) => candidate.id === args.nodeId);
2060
2176
  return node
2061
- ? { 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
+ }
2062
2182
  : { success: false, message: `React component '${args.nodeId}' was not found.`, errorCode: "react_component_not_found" };
2063
2183
  }
2064
2184
 
@@ -2099,7 +2219,7 @@ function installReactTools(options = {}) {
2099
2219
  result: {
2100
2220
  nodeId: args.nodeId,
2101
2221
  prop,
2102
- type: node.type,
2222
+ type: tree.types[node.typeId],
2103
2223
  },
2104
2224
  };
2105
2225
  }
@@ -2135,6 +2255,14 @@ function installErrorHandlers(options = {}) {
2135
2255
 
2136
2256
  if (global.ErrorUtils && global.ErrorUtils.setGlobalHandler) {
2137
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(() => {});
2138
2266
  nativeModule.recordEvent({
2139
2267
  label: error && error.message ? error.message : "Unhandled JavaScript error",
2140
2268
  type: "Exception",
@@ -2157,6 +2285,13 @@ function installErrorHandlers(options = {}) {
2157
2285
  global.__ansightUnhandledRejectionTrackingInstalled = true;
2158
2286
  global.addEventListener("unhandledrejection", (event) => {
2159
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(() => {});
2160
2295
  nativeModule.recordEvent({
2161
2296
  label: reason && reason.message ? reason.message : "Unhandled JavaScript promise rejection",
2162
2297
  type: "Exception",
@@ -2251,6 +2386,16 @@ function recordEvent(input) {
2251
2386
  return nativeModule.recordEvent(input || {});
2252
2387
  }
2253
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
+
2254
2399
  function screenViewed(name, details) {
2255
2400
  return nativeModule.screenViewed(name, details || {});
2256
2401
  }
@@ -2293,6 +2438,7 @@ const Ansight = {
2293
2438
  recordMetric: (value, channel = 255) => nativeModule.recordMetric(value, channel),
2294
2439
  event: recordEvent,
2295
2440
  recordEvent,
2441
+ recordCrashCandidate,
2296
2442
  screenViewed,
2297
2443
  trackRoute,
2298
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)) {
@@ -941,6 +973,14 @@ final class AnsightReactNative: RCTEventEmitter {
941
973
  jpeg,
942
974
  "captureGpuBackedSurfaces",
943
975
  defaultValue: AnsightSessionJpegCaptureOptions.defaultCaptureGpuBackedSurfaces
976
+ ),
977
+ mode: AnsightSessionJpegCaptureMode(
978
+ rawValue: stringValue(jpeg, "mode") ?? ""
979
+ ) ?? .screenshotOnly,
980
+ captureKeyboardPresence: boolValue(
981
+ jpeg,
982
+ "captureKeyboardPresence",
983
+ defaultValue: AnsightSessionJpegCaptureOptions.defaultCaptureKeyboardPresence
944
984
  )
945
985
  )
946
986
  }
@@ -957,6 +997,21 @@ final class AnsightReactNative: RCTEventEmitter {
957
997
  )
958
998
  }
959
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
+ }
960
1015
  if let lifecycle = dictionary?["lifecycleCapture"] as? NSDictionary {
961
1016
  options.lifecycleCapture = AnsightLifecycleCaptureOptions(
962
1017
  enabled: boolValue(lifecycle, "enabled", defaultValue: options.lifecycleCapture.enabled),
@@ -1267,6 +1322,14 @@ final class AnsightReactNative: RCTEventEmitter {
1267
1322
  dictionary,
1268
1323
  "captureGpuBackedSurfaces",
1269
1324
  defaultValue: AnsightSessionJpegCaptureOptions.defaultCaptureGpuBackedSurfaces
1325
+ ),
1326
+ mode: AnsightSessionJpegCaptureMode(
1327
+ rawValue: stringValue(dictionary, "mode") ?? ""
1328
+ ) ?? .screenshotOnly,
1329
+ captureKeyboardPresence: boolValue(
1330
+ dictionary,
1331
+ "captureKeyboardPresence",
1332
+ defaultValue: AnsightSessionJpegCaptureOptions.defaultCaptureKeyboardPresence
1270
1333
  )
1271
1334
  )
1272
1335
  }
@@ -1277,6 +1340,8 @@ final class AnsightReactNative: RCTEventEmitter {
1277
1340
  "retentionPeriodSeconds": options.retentionPeriodSeconds,
1278
1341
  "enableFramesPerSecond": options.enableFramesPerSecond,
1279
1342
  "enableBatteryLevel": options.enableBatteryLevel,
1343
+ "enableOpenFileHandleTracking": options.enableOpenFileHandleTracking,
1344
+ "enableJniReferenceCountTracking": false,
1280
1345
  "defaultMemoryChannels": [
1281
1346
  "managedHeap": options.defaultMemoryChannels.contains(.managedHeap),
1282
1347
  "javaHeap": options.defaultMemoryChannels.contains(.managedHeap),
@@ -1316,6 +1381,8 @@ final class AnsightReactNative: RCTEventEmitter {
1316
1381
  "quality": capture.quality,
1317
1382
  "maxWidth": capture.maxWidth as Any,
1318
1383
  "captureGpuBackedSurfaces": capture.captureGpuBackedSurfaces,
1384
+ "captureKeyboardPresence": capture.captureKeyboardPresence,
1385
+ "mode": capture.mode.rawValue,
1319
1386
  ]
1320
1387
  } else {
1321
1388
  dictionary["sessionJpegCapture"] = NSNull()
@@ -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.2.0-preview.2",
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",