@ansight/react-native 1.3.0-preview.2 → 1.3.0-preview.5

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.2`
81
- - Maven: `ai.ansight:ansight-android:1.3.0-preview.2`
80
+ - CocoaPods: `Ansight`, `AnsightObjC` version `1.3.0-preview.5`
81
+ - Maven: `ai.ansight:ansight-android:1.3.0-preview.5`
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 at gesture start, every 250 ms
204
+ throughout the gesture, and at the final up or cancel. It requires native touch
205
+ capture and visual-tree tools/providers to 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.2"
71
- implementation("ai.ansight:ansight-android:1.3.0-preview.2")
70
+ findProperty("ansightAndroidVersion") ?: "1.3.0-preview.5"
71
+ implementation("ai.ansight:ansight-android:1.3.0-preview.5")
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")) {
@@ -768,6 +800,24 @@ class AnsightReactNativeModule(
768
800
  },
769
801
  )
770
802
  }
803
+ if (map.hasKey("crashCapture")) {
804
+ options = options.copy(
805
+ crashCapture = if (map.isFalse("crashCapture")) {
806
+ options.crashCapture.copy(enabled = false)
807
+ } else {
808
+ val crash = map.getMapOrNull("crashCapture")
809
+ AnsightCrashCaptureOptions(
810
+ enabled = crash.booleanValue("enabled", true),
811
+ studioHandoffEnabled = crash.booleanValue("studioHandoffEnabled", true),
812
+ offlineCaptureAttachmentEnabled = crash.booleanValue("offlineCaptureAttachmentEnabled", true),
813
+ maximumPendingReports = crash.intValue("maximumPendingReports", 8),
814
+ retentionDays = crash.intValue("retentionDays", 7),
815
+ maximumBreadcrumbs = crash.intValue("maximumBreadcrumbs", 64),
816
+ maximumTraceBytes = crash.intValue("maximumTraceBytes", 1_048_576),
817
+ )
818
+ },
819
+ )
820
+ }
771
821
  if (map.hasString("toolGuard")) {
772
822
  options = options.copy(toolGuard = toolGuard(map.stringValue("toolGuard")))
773
823
  }
@@ -935,10 +985,10 @@ class AnsightReactNativeModule(
935
985
  }
936
986
 
937
987
  private fun sessionJpegCaptureMode(value: String?): AnsightSessionJpegCaptureMode =
938
- if (value == "screenshotAndVisualTree") {
939
- AnsightSessionJpegCaptureMode.ScreenshotAndVisualTree
940
- } else {
941
- AnsightSessionJpegCaptureMode.ScreenshotOnly
988
+ when (value) {
989
+ "screenshotAndVisualTree" -> AnsightSessionJpegCaptureMode.ScreenshotAndVisualTree
990
+ "screenshotWithVisualTreeOnTouch" -> AnsightSessionJpegCaptureMode.ScreenshotWithVisualTreeOnTouch
991
+ else -> AnsightSessionJpegCaptureMode.ScreenshotOnly
942
992
  }
943
993
 
944
994
  private fun toolDefinition(map: ReadableMap): ToolDefinition =
@@ -1111,6 +1161,8 @@ class AnsightReactNativeModule(
1111
1161
  putInt("retentionPeriodSeconds", options.retentionPeriodSeconds)
1112
1162
  putBoolean("enableFramesPerSecond", options.enableFramesPerSecond)
1113
1163
  putBoolean("enableBatteryLevel", options.enableBatteryLevel)
1164
+ putBoolean("enableOpenFileHandleTracking", options.enableOpenFileHandleTracking)
1165
+ putBoolean("enableJniReferenceCountTracking", options.enableJniReferenceCountTracking)
1114
1166
  putMap("defaultMemoryChannels", mapOf(
1115
1167
  "managedHeap" to options.defaultMemoryChannels.javaHeap,
1116
1168
  "javaHeap" to options.defaultMemoryChannels.javaHeap,
@@ -1127,10 +1179,10 @@ class AnsightReactNativeModule(
1127
1179
  "quality" to capture.quality,
1128
1180
  "maxWidth" to capture.maxWidth,
1129
1181
  "captureGpuBackedSurfaces" to capture.captureGpuBackedSurfaces,
1130
- "mode" to if (capture.mode == AnsightSessionJpegCaptureMode.ScreenshotAndVisualTree) {
1131
- "screenshotAndVisualTree"
1132
- } else {
1133
- "screenshotOnly"
1182
+ "mode" to when (capture.mode) {
1183
+ AnsightSessionJpegCaptureMode.ScreenshotAndVisualTree -> "screenshotAndVisualTree"
1184
+ AnsightSessionJpegCaptureMode.ScreenshotWithVisualTreeOnTouch -> "screenshotWithVisualTreeOnTouch"
1185
+ else -> "screenshotOnly"
1134
1186
  },
1135
1187
  ).toWritableMap())
1136
1188
  } ?: putNull("sessionJpegCapture")
package/index.d.ts CHANGED
@@ -37,7 +37,7 @@ export interface AnsightSessionJpegCaptureOptions {
37
37
  quality?: number;
38
38
  maxWidth?: number;
39
39
  captureGpuBackedSurfaces?: boolean;
40
- mode?: "screenshotOnly" | "screenshotAndVisualTree";
40
+ mode?: "screenshotOnly" | "screenshotAndVisualTree" | "screenshotWithVisualTreeOnTouch";
41
41
  }
42
42
 
43
43
  export interface AnsightTouchCaptureOptions {
@@ -110,6 +110,8 @@ export interface AnsightOptions {
110
110
  retentionPeriodSeconds?: number;
111
111
  enableFramesPerSecond?: boolean;
112
112
  enableBatteryLevel?: boolean;
113
+ enableOpenFileHandleTracking?: boolean;
114
+ enableJniReferenceCountTracking?: boolean;
113
115
  defaultMemoryChannels?: {
114
116
  managedHeap?: boolean;
115
117
  physicalFootprint?: boolean;
@@ -121,6 +123,7 @@ export interface AnsightOptions {
121
123
  reactNativeMemory?: false | AnsightReactNativeMemoryOptions;
122
124
  sessionJpegCapture?: false | AnsightSessionJpegCaptureOptions;
123
125
  touchCapture?: false | AnsightTouchCaptureOptions;
126
+ crashCapture?: false | AnsightCrashCaptureOptions;
124
127
  lifecycleCapture?: {
125
128
  enabled?: boolean;
126
129
  captureAppLifecycle?: boolean;
@@ -153,6 +156,25 @@ export interface AnsightOptions {
153
156
  lifecycle?: boolean;
154
157
  }
155
158
 
159
+ export interface AnsightCrashCaptureOptions {
160
+ enabled?: boolean;
161
+ studioHandoffEnabled?: boolean;
162
+ offlineCaptureAttachmentEnabled?: boolean;
163
+ maximumPendingReports?: number;
164
+ retentionDays?: number;
165
+ maximumBreadcrumbs?: number;
166
+ maximumTraceBytes?: number;
167
+ }
168
+
169
+ export interface AnsightCrashCandidate {
170
+ runtime?: string;
171
+ kind?: string;
172
+ message?: string;
173
+ stack?: string;
174
+ fatal?: boolean;
175
+ metadata?: string | Record<string, string>;
176
+ }
177
+
156
178
  export interface AnsightOperationResult {
157
179
  success: boolean;
158
180
  message: string;
@@ -251,6 +273,10 @@ export class AnsightOptionsBuilder {
251
273
  withoutFramesPerSecond(): this;
252
274
  withBatteryLevel(): this;
253
275
  withoutBatteryLevel(): this;
276
+ withOpenFileHandleTracking(): this;
277
+ withoutOpenFileHandleTracking(): this;
278
+ withJniReferenceCountTracking(): this;
279
+ withoutJniReferenceCountTracking(): this;
254
280
  withRetentionPeriodSeconds(retentionPeriodSeconds: number): this;
255
281
  withAdditionalChannels(additionalChannels: AnsightChannel[]): this;
256
282
  addAdditionalChannel(additionalChannel: AnsightChannel): this;
@@ -264,11 +290,13 @@ export class AnsightOptionsBuilder {
264
290
  quality?: number,
265
291
  maxWidth?: number | null,
266
292
  captureGpuBackedSurfaces?: boolean,
267
- mode?: "screenshotOnly" | "screenshotAndVisualTree"
293
+ mode?: "screenshotOnly" | "screenshotAndVisualTree" | "screenshotWithVisualTreeOnTouch"
268
294
  ): this;
269
295
  withoutSessionJpegCapture(): this;
270
296
  withTouchCapture(touchCapture?: AnsightTouchCaptureOptions): this;
271
297
  withoutTouchCapture(): this;
298
+ withCrashCapture(crashCapture?: AnsightCrashCaptureOptions): this;
299
+ withoutCrashCapture(): this;
272
300
  withLifecycleCapture(lifecycleCapture?: NonNullable<AnsightOptions["lifecycleCapture"]>): this;
273
301
  withToolGuard(toolGuard: NonNullable<AnsightOptions["toolGuard"]>): this;
274
302
  withToolsDisabled(): this;
@@ -512,6 +540,7 @@ export function metric(value: number, channel?: number): Promise<AnsightDebugSna
512
540
  export function recordMetric(value: number, channel?: number): Promise<AnsightDebugSnapshot>;
513
541
  export function event(input: string | { label: string; type?: string; details?: string; channel?: number }): Promise<AnsightDebugSnapshot>;
514
542
  export function recordEvent(input: string | { label: string; type?: string; details?: string; channel?: number }): Promise<AnsightDebugSnapshot>;
543
+ export function recordCrashCandidate(input?: AnsightCrashCandidate): Promise<{ candidateId?: string }>;
515
544
  export function screenViewed(name: string, details?: Record<string, string>): Promise<AnsightDebugSnapshot>;
516
545
  export function trackRoute(name: string, details?: Record<string, string>): Promise<AnsightDebugSnapshot>;
517
546
  export function setAppLifecycleState(state: AnsightLifecycleState): Promise<AnsightDebugSnapshot>;
@@ -585,6 +614,7 @@ declare const Ansight: {
585
614
  recordMetric: typeof recordMetric;
586
615
  event: typeof event;
587
616
  recordEvent: typeof recordEvent;
617
+ recordCrashCandidate: typeof recordCrashCandidate;
588
618
  screenViewed: typeof screenViewed;
589
619
  trackRoute: typeof trackRoute;
590
620
  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;
@@ -297,6 +322,16 @@ class AnsightOptionsBuilder {
297
322
  return this;
298
323
  }
299
324
 
325
+ withCrashCapture(crashCapture = {}) {
326
+ this._options.crashCapture = { ...crashCapture, enabled: true };
327
+ return this;
328
+ }
329
+
330
+ withoutCrashCapture() {
331
+ this._options.crashCapture = false;
332
+ return this;
333
+ }
334
+
300
335
  withLifecycleCapture(lifecycleCapture = {}) {
301
336
  this._options.lifecycleCapture = { ...lifecycleCapture };
302
337
  return this;
@@ -1489,6 +1524,28 @@ function createReactVisual(fiber, props, maxStringLength) {
1489
1524
  return visual;
1490
1525
  }
1491
1526
 
1527
+ function createReactZIndex(props) {
1528
+ const style = StyleSheet && typeof StyleSheet.flatten === "function"
1529
+ ? StyleSheet.flatten(props && props.style)
1530
+ : props && props.style;
1531
+ const zIndex = style && style.zIndex != null ? Number(style.zIndex) : NaN;
1532
+ const elevation = style && style.elevation != null ? Number(style.elevation) : NaN;
1533
+ if (Number.isFinite(zIndex) && zIndex !== 0) return zIndex;
1534
+ if (Number.isFinite(elevation) && elevation !== 0) return elevation;
1535
+ return null;
1536
+ }
1537
+
1538
+ function registerReactType(context, typeName) {
1539
+ const normalizedTypeName = String(typeName || "UnknownComponent").trim() || "UnknownComponent";
1540
+ const existingTypeId = context.typeIdsByName.get(normalizedTypeName);
1541
+ if (existingTypeId != null) return existingTypeId;
1542
+
1543
+ const typeId = context.types.length;
1544
+ context.types.push(normalizedTypeName);
1545
+ context.typeIdsByName.set(normalizedTypeName, typeId);
1546
+ return typeId;
1547
+ }
1548
+
1492
1549
  function nativeTagForFiber(fiber) {
1493
1550
  const stateNode = fiber && fiber.stateNode;
1494
1551
  if (!stateNode) {
@@ -1560,17 +1617,17 @@ function serializeFiber(fiber, context, depth) {
1560
1617
 
1561
1618
  const props = fiber.memoizedProps || fiber.pendingProps || {};
1562
1619
  const type = reactFiberTypeName(fiber);
1563
- const kind = reactFiberKind(fiber.tag);
1564
1620
  const node = {
1565
1621
  id: reactFiberId(fiber),
1566
- type,
1567
- kind,
1622
+ typeId: registerReactType(context, type),
1568
1623
  tag: fiber.tag,
1569
1624
  key: fiber.key == null ? null : String(fiber.key),
1570
1625
  depth,
1571
1626
  visual: createReactVisual(fiber, props, context.maxStringLength),
1572
1627
  children: [],
1573
1628
  };
1629
+ const zIndex = createReactZIndex(props);
1630
+ if (zIndex != null) node.z = zIndex;
1574
1631
 
1575
1632
  if (fiber._debugSource) {
1576
1633
  node.source = {
@@ -1582,7 +1639,7 @@ function serializeFiber(fiber, context, depth) {
1582
1639
 
1583
1640
  const owner = fiber._debugOwner && reactFiberTypeName(fiber._debugOwner);
1584
1641
  if (owner) {
1585
- node.owner = owner;
1642
+ node.ownerTypeId = registerReactType(context, owner);
1586
1643
  }
1587
1644
 
1588
1645
  const nativeTag = nativeTagForFiber(fiber);
@@ -1641,14 +1698,15 @@ function createShadowTreeNode(fiber, context, depth) {
1641
1698
  const type = reactFiberTypeName(fiber);
1642
1699
  const node = {
1643
1700
  id: reactFiberId(fiber),
1644
- type,
1645
- kind: fiber.tag === 3 ? "root" : fiber.tag === 6 ? "text" : "host",
1701
+ typeId: registerReactType(context, type),
1646
1702
  tag: fiber.tag,
1647
1703
  key: fiber.key == null ? null : String(fiber.key),
1648
1704
  depth,
1649
1705
  visual: createReactVisual(fiber, props, context.maxStringLength),
1650
1706
  children: [],
1651
1707
  };
1708
+ const zIndex = createReactZIndex(props);
1709
+ if (zIndex != null) node.z = zIndex;
1652
1710
 
1653
1711
  const nativeTag = nativeTagForFiber(fiber);
1654
1712
  if (nativeTag != null) {
@@ -1732,19 +1790,21 @@ async function captureReactVisualTree(rawOptions = {}) {
1732
1790
  maxStringLength: rawOptions.maxStringLength || 180,
1733
1791
  maxValueDepth: rawOptions.maxValueDepth || 2,
1734
1792
  nodesWithNativeTags: [],
1793
+ typeIdsByName: new Map(),
1794
+ types: [],
1735
1795
  count: 0,
1736
1796
  truncated: false,
1737
1797
  visited: new Set(),
1738
1798
  };
1739
1799
  const roots = getReactRoots();
1740
- const rootNodes = roots.roots.map((root, index) => {
1800
+ const rootNodes = roots.roots.map((root) => {
1741
1801
  const node = serializeFiber(root.fiber, context, 0);
1742
1802
  if (node) {
1743
1803
  node.rendererId = root.rendererId;
1744
- node.rootIndex = index;
1745
1804
  }
1746
1805
  return node;
1747
1806
  }).filter(Boolean);
1807
+ const rootTypeId = registerReactType(context, "ReactRoots");
1748
1808
 
1749
1809
  if (includeBounds && context.nodesWithNativeTags.length > 0) {
1750
1810
  await Promise.all(context.nodesWithNativeTags.slice(0, 300).map(async (node) => {
@@ -1756,6 +1816,7 @@ async function captureReactVisualTree(rawOptions = {}) {
1756
1816
  }
1757
1817
 
1758
1818
  return {
1819
+ format: "ansight.react.visual-tree.compact.v2",
1759
1820
  platform: Platform.OS,
1760
1821
  source: "react",
1761
1822
  adapter: "react.fiber",
@@ -1765,11 +1826,11 @@ async function captureReactVisualTree(rawOptions = {}) {
1765
1826
  renderers: roots.renderers,
1766
1827
  root: {
1767
1828
  id: "react:roots",
1768
- type: "ReactRoots",
1769
- kind: "container",
1829
+ typeId: rootTypeId,
1830
+ childCount: rootNodes.length,
1770
1831
  children: rootNodes,
1771
1832
  },
1772
- roots: rootNodes,
1833
+ types: context.types,
1773
1834
  nodeCount: context.count,
1774
1835
  truncated: context.truncated,
1775
1836
  unavailableReason: roots.hookAvailable ? undefined : "React DevTools global hook is not available in this runtime.",
@@ -1789,19 +1850,21 @@ async function captureReactShadowTree(rawOptions = {}) {
1789
1850
  maxStringLength: rawOptions.maxStringLength || 180,
1790
1851
  maxValueDepth: rawOptions.maxValueDepth || 2,
1791
1852
  nodesWithNativeTags: [],
1853
+ typeIdsByName: new Map(),
1854
+ types: [],
1792
1855
  count: 0,
1793
1856
  truncated: false,
1794
1857
  visited: new Set(),
1795
1858
  };
1796
1859
  const roots = getReactRoots();
1797
- const rootNodes = roots.roots.flatMap((root, index) => {
1860
+ const rootNodes = roots.roots.flatMap((root) => {
1798
1861
  const nodes = serializeShadowFiber(root.fiber, context, 0);
1799
1862
  nodes.forEach((node) => {
1800
1863
  node.rendererId = root.rendererId;
1801
- node.rootIndex = index;
1802
1864
  });
1803
1865
  return nodes;
1804
1866
  });
1867
+ const rootTypeId = registerReactType(context, "ReactNativeShadowRoots");
1805
1868
 
1806
1869
  if (includeBounds && context.nodesWithNativeTags.length > 0) {
1807
1870
  await Promise.all(context.nodesWithNativeTags.slice(0, 300).map(async (node) => {
@@ -1813,6 +1876,7 @@ async function captureReactShadowTree(rawOptions = {}) {
1813
1876
  }
1814
1877
 
1815
1878
  return {
1879
+ format: "ansight.react.visual-tree.compact.v2",
1816
1880
  platform: Platform.OS,
1817
1881
  source: "react-native",
1818
1882
  adapter: "react-native.host-fiber",
@@ -1822,11 +1886,11 @@ async function captureReactShadowTree(rawOptions = {}) {
1822
1886
  renderers: roots.renderers,
1823
1887
  root: {
1824
1888
  id: "react:shadow-roots",
1825
- type: "ReactNativeShadowRoots",
1826
- kind: "container",
1889
+ typeId: rootTypeId,
1890
+ childCount: rootNodes.length,
1827
1891
  children: rootNodes,
1828
1892
  },
1829
- roots: rootNodes,
1893
+ types: context.types,
1830
1894
  nodeCount: context.count,
1831
1895
  truncated: context.truncated,
1832
1896
  unavailableReason: roots.hookAvailable ? undefined : "React DevTools global hook is not available in this runtime.",
@@ -1842,13 +1906,18 @@ function flattenReactTree(node, output = []) {
1842
1906
  return output;
1843
1907
  }
1844
1908
 
1845
- function nodeSearchText(node) {
1909
+ function createReactNodeSnapshot(node) {
1910
+ const snapshot = { ...node };
1911
+ delete snapshot.children;
1912
+ return snapshot;
1913
+ }
1914
+
1915
+ function nodeSearchText(node, types) {
1846
1916
  return [
1847
1917
  node.id,
1848
- node.type,
1849
- node.kind,
1918
+ types[node.typeId],
1850
1919
  node.label,
1851
- node.owner,
1920
+ types[node.ownerTypeId],
1852
1921
  node.propsSummary && node.propsSummary.testID,
1853
1922
  node.propsSummary && node.propsSummary.nativeID,
1854
1923
  node.propsSummary && node.propsSummary.accessibilityLabel,
@@ -1856,14 +1925,14 @@ function nodeSearchText(node) {
1856
1925
  ].filter(Boolean).join(" ").toLowerCase();
1857
1926
  }
1858
1927
 
1859
- function matchesReactNode(node, args) {
1928
+ function matchesReactNode(node, args, types) {
1860
1929
  const query = args.query ? String(args.query).toLowerCase() : null;
1861
1930
  const type = args.type ? String(args.type).toLowerCase() : null;
1862
1931
  const testID = args.testID ? String(args.testID).toLowerCase() : null;
1863
1932
  const text = args.text ? String(args.text).toLowerCase() : null;
1864
- const searchText = nodeSearchText(node);
1933
+ const searchText = nodeSearchText(node, types);
1865
1934
  return (!query || searchText.includes(query)) &&
1866
- (!type || String(node.type || "").toLowerCase().includes(type)) &&
1935
+ (!type || String(types[node.typeId] || "").toLowerCase().includes(type)) &&
1867
1936
  (!testID || String((node.propsSummary && node.propsSummary.testID) || "").toLowerCase() === testID) &&
1868
1937
  (!text || searchText.includes(text));
1869
1938
  }
@@ -2084,12 +2153,14 @@ function installReactTools(options = {}) {
2084
2153
  });
2085
2154
  const maxResults = parseInteger(args.maxResults, 50, 1, 500);
2086
2155
  const matches = flattenReactTree(tree.root)
2087
- .filter((node) => node.id !== "react:roots" && matchesReactNode(node, args))
2088
- .slice(0, maxResults);
2156
+ .filter((node) => node.id !== "react:roots" && matchesReactNode(node, args, tree.types))
2157
+ .slice(0, maxResults)
2158
+ .map(createReactNodeSnapshot);
2089
2159
  return {
2090
2160
  success: tree.hookAvailable,
2091
2161
  message: `Found ${matches.length} React component(s).`,
2092
2162
  result: {
2163
+ types: tree.types,
2093
2164
  matches,
2094
2165
  count: matches.length,
2095
2166
  truncated: matches.length === maxResults,
@@ -2101,7 +2172,11 @@ function installReactTools(options = {}) {
2101
2172
  const tree = await captureReactVisualTree(reactToolOptions(options, args));
2102
2173
  const node = flattenReactTree(tree.root).find((candidate) => candidate.id === args.nodeId);
2103
2174
  return node
2104
- ? { success: true, message: "React component captured.", result: node }
2175
+ ? {
2176
+ success: true,
2177
+ message: "React component captured.",
2178
+ result: { types: tree.types, node: createReactNodeSnapshot(node) },
2179
+ }
2105
2180
  : { success: false, message: `React component '${args.nodeId}' was not found.`, errorCode: "react_component_not_found" };
2106
2181
  }
2107
2182
 
@@ -2142,7 +2217,7 @@ function installReactTools(options = {}) {
2142
2217
  result: {
2143
2218
  nodeId: args.nodeId,
2144
2219
  prop,
2145
- type: node.type,
2220
+ type: tree.types[node.typeId],
2146
2221
  },
2147
2222
  };
2148
2223
  }
@@ -2178,6 +2253,14 @@ function installErrorHandlers(options = {}) {
2178
2253
 
2179
2254
  if (global.ErrorUtils && global.ErrorUtils.setGlobalHandler) {
2180
2255
  global.ErrorUtils.setGlobalHandler((error, isFatal) => {
2256
+ nativeModule.recordCrashCandidate({
2257
+ runtime: "react-native-javascript",
2258
+ kind: "unhandled_javascript_error",
2259
+ message: error && error.message,
2260
+ stack: error && error.stack,
2261
+ fatal: !!isFatal,
2262
+ metadata: JSON.stringify({ name: error && error.name }),
2263
+ }).catch(() => {});
2181
2264
  nativeModule.recordEvent({
2182
2265
  label: error && error.message ? error.message : "Unhandled JavaScript error",
2183
2266
  type: "Exception",
@@ -2200,6 +2283,13 @@ function installErrorHandlers(options = {}) {
2200
2283
  global.__ansightUnhandledRejectionTrackingInstalled = true;
2201
2284
  global.addEventListener("unhandledrejection", (event) => {
2202
2285
  const reason = event && event.reason;
2286
+ nativeModule.recordCrashCandidate({
2287
+ runtime: "react-native-javascript",
2288
+ kind: "unhandled_promise_rejection",
2289
+ message: reason && reason.message ? reason.message : String(reason),
2290
+ stack: reason && reason.stack,
2291
+ fatal: false,
2292
+ }).catch(() => {});
2203
2293
  nativeModule.recordEvent({
2204
2294
  label: reason && reason.message ? reason.message : "Unhandled JavaScript promise rejection",
2205
2295
  type: "Exception",
@@ -2294,6 +2384,16 @@ function recordEvent(input) {
2294
2384
  return nativeModule.recordEvent(input || {});
2295
2385
  }
2296
2386
 
2387
+ function recordCrashCandidate(input = {}) {
2388
+ const metadata = input.metadata && typeof input.metadata === "object"
2389
+ ? JSON.stringify(input.metadata)
2390
+ : input.metadata;
2391
+ return nativeModule.recordCrashCandidate({
2392
+ ...input,
2393
+ metadata,
2394
+ });
2395
+ }
2396
+
2297
2397
  function screenViewed(name, details) {
2298
2398
  return nativeModule.screenViewed(name, details || {});
2299
2399
  }
@@ -2336,6 +2436,7 @@ const Ansight = {
2336
2436
  recordMetric: (value, channel = 255) => nativeModule.recordMetric(value, channel),
2337
2437
  event: recordEvent,
2338
2438
  recordEvent,
2439
+ recordCrashCandidate,
2339
2440
  screenViewed,
2340
2441
  trackRoute,
2341
2442
  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,9 @@ 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
948
980
  )
949
981
  }
950
982
  }
@@ -960,6 +992,21 @@ final class AnsightReactNative: RCTEventEmitter {
960
992
  )
961
993
  }
962
994
  }
995
+ if let raw = dictionary?["crashCapture"] {
996
+ if (raw as? Bool) == false {
997
+ options.crashCapture.enabled = false
998
+ } else if let crash = raw as? NSDictionary {
999
+ options.crashCapture = AnsightCrashCaptureOptions(
1000
+ enabled: boolValue(crash, "enabled", defaultValue: true),
1001
+ studioHandoffEnabled: boolValue(crash, "studioHandoffEnabled", defaultValue: true),
1002
+ offlineCaptureAttachmentEnabled: boolValue(crash, "offlineCaptureAttachmentEnabled", defaultValue: true),
1003
+ maximumPendingReports: intValue(crash, "maximumPendingReports", defaultValue: 8),
1004
+ retentionDays: intValue(crash, "retentionDays", defaultValue: 7),
1005
+ maximumBreadcrumbs: intValue(crash, "maximumBreadcrumbs", defaultValue: 64),
1006
+ maximumTraceBytes: intValue(crash, "maximumTraceBytes", defaultValue: 1_048_576)
1007
+ )
1008
+ }
1009
+ }
963
1010
  if let lifecycle = dictionary?["lifecycleCapture"] as? NSDictionary {
964
1011
  options.lifecycleCapture = AnsightLifecycleCaptureOptions(
965
1012
  enabled: boolValue(lifecycle, "enabled", defaultValue: options.lifecycleCapture.enabled),
@@ -1271,9 +1318,9 @@ final class AnsightReactNative: RCTEventEmitter {
1271
1318
  "captureGpuBackedSurfaces",
1272
1319
  defaultValue: AnsightSessionJpegCaptureOptions.defaultCaptureGpuBackedSurfaces
1273
1320
  ),
1274
- mode: stringValue(dictionary, "mode") == "screenshotAndVisualTree"
1275
- ? .screenshotAndVisualTree
1276
- : .screenshotOnly
1321
+ mode: AnsightSessionJpegCaptureMode(
1322
+ rawValue: stringValue(dictionary, "mode") ?? ""
1323
+ ) ?? .screenshotOnly
1277
1324
  )
1278
1325
  }
1279
1326
 
@@ -1283,6 +1330,8 @@ final class AnsightReactNative: RCTEventEmitter {
1283
1330
  "retentionPeriodSeconds": options.retentionPeriodSeconds,
1284
1331
  "enableFramesPerSecond": options.enableFramesPerSecond,
1285
1332
  "enableBatteryLevel": options.enableBatteryLevel,
1333
+ "enableOpenFileHandleTracking": options.enableOpenFileHandleTracking,
1334
+ "enableJniReferenceCountTracking": false,
1286
1335
  "defaultMemoryChannels": [
1287
1336
  "managedHeap": options.defaultMemoryChannels.contains(.managedHeap),
1288
1337
  "javaHeap": options.defaultMemoryChannels.contains(.managedHeap),
@@ -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.2",
3
+ "version": "1.3.0-preview.5",
4
4
  "description": "React Native bridge for the Ansight mobile SDK.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",