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

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/index.js CHANGED
@@ -1,7 +1,20 @@
1
1
  "use strict";
2
2
 
3
+ const { ensureReactInspectionHook } = require("./react-inspection-hook");
4
+ const { createReactCoordinateSpace } = require("./react-tree-geometry");
5
+ const { reactSemanticRole, reactSupportedActions } = require("./react-tree-semantics");
6
+
7
+ // Production React Native renderers only look for the DevTools hook once, when
8
+ // the renderer module initializes. Install a minimal root tracker while this
9
+ // package is loading so Fabric and Paper can report their roots before the app's
10
+ // first render. Development runtimes keep their full React DevTools hook.
11
+ if (typeof __DEV__ === "undefined" || !__DEV__) {
12
+ ensureReactInspectionHook(typeof global !== "undefined" ? global : undefined);
13
+ }
14
+
3
15
  const {
4
16
  AppState,
17
+ Dimensions,
5
18
  NativeEventEmitter,
6
19
  NativeModules,
7
20
  Platform,
@@ -10,6 +23,15 @@ const {
10
23
  findNodeHandle,
11
24
  processColor,
12
25
  } = require("react-native");
26
+ const { version: reactVersion } = require("react");
27
+ const {
28
+ createAutomaticSessionProperties,
29
+ mergeSessionProperties,
30
+ } = require("./session-properties");
31
+ const {
32
+ installNetworkCapture: installNetworkCaptureCore,
33
+ sanitizeNetworkRequest,
34
+ } = require("./network");
13
35
 
14
36
  const nativeModule = NativeModules.AnsightReactNative;
15
37
 
@@ -33,6 +55,9 @@ const hostConnectionStatusListeners = new Set();
33
55
  let lastHostConnectionStatusKey = null;
34
56
  const logListeners = new Set();
35
57
  let logEventSubscription = null;
58
+ let networkCaptureSubscription = null;
59
+ let networkCaptureRegistration = null;
60
+ let networkConnectionEventSubscription = null;
36
61
  const REACT_COMPONENT_TREE_TOOL_ID = "react.get_component_tree";
37
62
  const REACT_SHADOW_TREE_TOOL_ID = "react.get_shadow_tree";
38
63
 
@@ -44,7 +69,28 @@ function normalizePairingPayload(payload) {
44
69
  }
45
70
 
46
71
  function normalizeOptions(options = {}) {
47
- return { ...options };
72
+ const normalized = {
73
+ ...options,
74
+ customProperties: mergeSessionProperties(
75
+ automaticSessionProperties(),
76
+ options.customProperties
77
+ ),
78
+ };
79
+ delete normalized.networkCapture;
80
+ return normalized;
81
+ }
82
+
83
+ function automaticSessionProperties() {
84
+ return createAutomaticSessionProperties({
85
+ platform: Platform,
86
+ reactVersion,
87
+ runtimeGlobal: global,
88
+ developmentMode: typeof __DEV__ !== "undefined" && __DEV__,
89
+ });
90
+ }
91
+
92
+ function automaticSessionPropertyValue(group, key) {
93
+ return automaticSessionProperties()[group]?.[key];
48
94
  }
49
95
 
50
96
  function cloneOptions(options = {}) {
@@ -73,6 +119,17 @@ function cloneOptions(options = {}) {
73
119
  if (options.hostConnection) {
74
120
  clone.hostConnection = { ...options.hostConnection };
75
121
  }
122
+ if (options.networkCapture && typeof options.networkCapture === "object") {
123
+ clone.networkCapture = {
124
+ ...options.networkCapture,
125
+ additionalSensitiveHeaderNames: options.networkCapture.additionalSensitiveHeaderNames
126
+ ? [...options.networkCapture.additionalSensitiveHeaderNames]
127
+ : undefined,
128
+ additionalSensitiveQueryParameterNames: options.networkCapture.additionalSensitiveQueryParameterNames
129
+ ? [...options.networkCapture.additionalSensitiveQueryParameterNames]
130
+ : undefined,
131
+ };
132
+ }
76
133
  if (options.secureStorage) {
77
134
  clone.secureStorage = {
78
135
  ...options.secureStorage,
@@ -339,6 +396,52 @@ class AnsightOptionsBuilder {
339
396
  return this;
340
397
  }
341
398
 
399
+ withNetworkCapture(networkCapture = {}) {
400
+ this._options.networkCapture = { ...networkCapture };
401
+ return this;
402
+ }
403
+
404
+ withNetworkRequestBodies(maximumBodyBytes) {
405
+ if (!this._options.networkCapture || typeof this._options.networkCapture !== "object") return this;
406
+ const current = this._options.networkCapture;
407
+ this._options.networkCapture = {
408
+ ...current,
409
+ captureRequestBody: true,
410
+ ...(maximumBodyBytes == null ? {} : { maximumBodyBytes }),
411
+ };
412
+ return this;
413
+ }
414
+
415
+ withoutNetworkRequestBodies() {
416
+ if (!this._options.networkCapture || typeof this._options.networkCapture !== "object") return this;
417
+ const current = this._options.networkCapture;
418
+ this._options.networkCapture = { ...current, captureRequestBody: false };
419
+ return this;
420
+ }
421
+
422
+ withNetworkResponseBodies(maximumBodyBytes) {
423
+ if (!this._options.networkCapture || typeof this._options.networkCapture !== "object") return this;
424
+ const current = this._options.networkCapture;
425
+ this._options.networkCapture = {
426
+ ...current,
427
+ captureResponseBody: true,
428
+ ...(maximumBodyBytes == null ? {} : { maximumBodyBytes }),
429
+ };
430
+ return this;
431
+ }
432
+
433
+ withoutNetworkResponseBodies() {
434
+ if (!this._options.networkCapture || typeof this._options.networkCapture !== "object") return this;
435
+ const current = this._options.networkCapture;
436
+ this._options.networkCapture = { ...current, captureResponseBody: false };
437
+ return this;
438
+ }
439
+
440
+ withoutNetworkCapture() {
441
+ this._options.networkCapture = false;
442
+ return this;
443
+ }
444
+
342
445
  withToolGuard(toolGuard) {
343
446
  this._options.toolGuard = toolGuard;
344
447
  return this;
@@ -581,6 +684,7 @@ function addHostConnectionStatusListener(listener, options = {}) {
581
684
 
582
685
  async function notifyAfterHostConnectionChange(operation) {
583
686
  const result = await operation();
687
+ await refreshNetworkCaptureConnection();
584
688
  await emitHostConnectionStatusChangedIfNeeded();
585
689
  return result;
586
690
  }
@@ -1440,36 +1544,16 @@ function summarizeProps(props) {
1440
1544
  return summary;
1441
1545
  }
1442
1546
 
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
1547
  function applyReactTargetability(node, fiber, props, type) {
1467
1548
  const style = StyleSheet && typeof StyleSheet.flatten === "function"
1468
1549
  ? StyleSheet.flatten(props && props.style)
1469
1550
  : props && props.style;
1470
1551
  node.text = node.label || (node.visual && node.visual.text) || null;
1471
- node.role = reactSemanticRole(type, props, fiber.tag);
1472
1552
  node.supportedActions = reactSupportedActions(type, props);
1553
+ node.role = reactSemanticRole(type, props, fiber.tag);
1554
+ if (node.role === "view" && node.supportedActions.includes("tap")) {
1555
+ node.role = "button";
1556
+ }
1473
1557
  node.visible = !(style && style.display === "none") && !(props && props.accessibilityElementsHidden);
1474
1558
  node.enabled = !(props && (props.disabled === true || props.accessibilityState && props.accessibilityState.disabled === true));
1475
1559
  node.focusable = !!(props && props.focusable) || node.supportedActions.includes("focus");
@@ -1823,6 +1907,7 @@ async function captureReactVisualTree(rawOptions = {}) {
1823
1907
  source: "react",
1824
1908
  adapter: "react.fiber",
1825
1909
  treeKind: "component",
1910
+ coordinateSpace: createReactCoordinateSpace(Dimensions, context.nodesWithNativeTags),
1826
1911
  capturedAtUtc: new Date().toISOString(),
1827
1912
  hookAvailable: roots.hookAvailable,
1828
1913
  renderers: roots.renderers,
@@ -1883,6 +1968,7 @@ async function captureReactShadowTree(rawOptions = {}) {
1883
1968
  source: "react-native",
1884
1969
  adapter: "react-native.host-fiber",
1885
1970
  treeKind: "shadow",
1971
+ coordinateSpace: createReactCoordinateSpace(Dimensions, context.nodesWithNativeTags),
1886
1972
  capturedAtUtc: new Date().toISOString(),
1887
1973
  hookAvailable: roots.hookAvailable,
1888
1974
  renderers: roots.renderers,
@@ -2313,6 +2399,7 @@ function installErrorHandlers(options = {}) {
2313
2399
 
2314
2400
  async function initialize(options = {}) {
2315
2401
  const result = await nativeModule.initialize(normalizeOptions(options));
2402
+ await configureNetworkCapture(options.networkCapture);
2316
2403
  if (options.lifecycle !== false) {
2317
2404
  startAppStateTracking();
2318
2405
  }
@@ -2322,6 +2409,7 @@ async function initialize(options = {}) {
2322
2409
 
2323
2410
  async function initializeAndActivate(options = {}) {
2324
2411
  const result = await nativeModule.initializeAndActivate(normalizeOptions(options));
2412
+ await configureNetworkCapture(options.networkCapture);
2325
2413
  if (options.lifecycle !== false) {
2326
2414
  startAppStateTracking();
2327
2415
  }
@@ -2396,6 +2484,93 @@ function recordCrashCandidate(input = {}) {
2396
2484
  });
2397
2485
  }
2398
2486
 
2487
+ function recordNetworkRequest(input, sanitizationOptions = {}) {
2488
+ const sanitized = sanitizeNetworkRequest(input, sanitizationOptions, global);
2489
+ if (!sanitized) {
2490
+ return Promise.resolve({ success: false, message: "Network request capture was suppressed by the sanitizer." });
2491
+ }
2492
+ return nativeModule.recordNetworkRequest(sanitized);
2493
+ }
2494
+
2495
+ function installNetworkCapture(options = {}) {
2496
+ uninstallNetworkCapture();
2497
+ const registration = { options };
2498
+ networkCaptureRegistration = registration;
2499
+ ensureNetworkConnectionEventSubscription();
2500
+ void refreshNetworkCaptureConnection();
2501
+ return {
2502
+ remove() {
2503
+ if (networkCaptureRegistration === registration) {
2504
+ uninstallNetworkCapture();
2505
+ }
2506
+ },
2507
+ };
2508
+ }
2509
+
2510
+ function uninstallNetworkCapture() {
2511
+ networkCaptureRegistration = null;
2512
+ if (networkConnectionEventSubscription) {
2513
+ networkConnectionEventSubscription.remove();
2514
+ networkConnectionEventSubscription = null;
2515
+ }
2516
+ detachNetworkCapture();
2517
+ }
2518
+
2519
+ function detachNetworkCapture() {
2520
+ if (!networkCaptureSubscription) return;
2521
+ networkCaptureSubscription.remove();
2522
+ networkCaptureSubscription = null;
2523
+ }
2524
+
2525
+ async function refreshNetworkCaptureConnection() {
2526
+ const registration = networkCaptureRegistration;
2527
+ if (!registration) {
2528
+ detachNetworkCapture();
2529
+ return;
2530
+ }
2531
+ try {
2532
+ const status = await nativeModule.hostConnectionStatus();
2533
+ if (networkCaptureRegistration !== registration) return;
2534
+ applyNetworkConnectionStatus(status);
2535
+ } catch {
2536
+ if (networkCaptureRegistration === registration) detachNetworkCapture();
2537
+ }
2538
+ }
2539
+
2540
+ function ensureNetworkConnectionEventSubscription() {
2541
+ if (networkConnectionEventSubscription) return;
2542
+ const emitter = new NativeEventEmitter(nativeModule);
2543
+ networkConnectionEventSubscription = emitter.addListener(
2544
+ "AnsightHostConnectionStatus",
2545
+ applyNetworkConnectionStatus,
2546
+ );
2547
+ }
2548
+
2549
+ function applyNetworkConnectionStatus(status) {
2550
+ const registration = networkCaptureRegistration;
2551
+ if (!registration || !status || status.isConnected !== true) {
2552
+ detachNetworkCapture();
2553
+ return;
2554
+ }
2555
+ if (!networkCaptureSubscription) {
2556
+ networkCaptureSubscription = installNetworkCaptureCore({
2557
+ globalObject: global,
2558
+ options: registration.options,
2559
+ sourcePrefix: "react-native",
2560
+ capture: (request) => nativeModule.recordNetworkRequest(request),
2561
+ });
2562
+ }
2563
+ }
2564
+
2565
+ async function configureNetworkCapture(value) {
2566
+ uninstallNetworkCapture();
2567
+ if (!value) return;
2568
+ const registration = { options: typeof value === "object" ? value : {} };
2569
+ networkCaptureRegistration = registration;
2570
+ ensureNetworkConnectionEventSubscription();
2571
+ await refreshNetworkCaptureConnection();
2572
+ }
2573
+
2399
2574
  function screenViewed(name, details) {
2400
2575
  return nativeModule.screenViewed(name, details || {});
2401
2576
  }
@@ -2439,6 +2614,7 @@ const Ansight = {
2439
2614
  event: recordEvent,
2440
2615
  recordEvent,
2441
2616
  recordCrashCandidate,
2617
+ recordNetworkRequest,
2442
2618
  screenViewed,
2443
2619
  trackRoute,
2444
2620
  setAppLifecycleState: (state) => nativeModule.setAppLifecycleState(state),
@@ -2473,12 +2649,21 @@ const Ansight = {
2473
2649
  captureScreenFrame: (options = {}) => nativeModule.captureScreenFrame(options || {}),
2474
2650
  enableTouchCapture: () => nativeModule.enableTouchCapture(),
2475
2651
  disableTouchCapture: () => nativeModule.disableTouchCapture(),
2476
- updateSessionProperties: (properties) => nativeModule.updateSessionProperties(properties || {}),
2477
- clearSessionProperties: () => nativeModule.clearSessionProperties(),
2478
- updateCustomProperties: (properties) => nativeModule.updateSessionProperties(properties || {}),
2652
+ updateSessionProperties: (properties) => nativeModule.updateSessionProperties(
2653
+ mergeSessionProperties(automaticSessionProperties(), properties || {})
2654
+ ),
2655
+ clearSessionProperties: () => nativeModule.updateSessionProperties(automaticSessionProperties()),
2656
+ updateCustomProperties: (properties) => nativeModule.updateSessionProperties(
2657
+ mergeSessionProperties(automaticSessionProperties(), properties || {})
2658
+ ),
2479
2659
  registerCustomProperty: (group, key, value) => nativeModule.registerCustomProperty(group, key, value),
2480
- removeCustomProperty: (group, key) => nativeModule.removeCustomProperty(group, key),
2481
- clearCustomProperties: () => nativeModule.clearSessionProperties(),
2660
+ removeCustomProperty: (group, key) => {
2661
+ const automaticValue = automaticSessionPropertyValue(group, key);
2662
+ return automaticValue == null
2663
+ ? nativeModule.removeCustomProperty(group, key)
2664
+ : nativeModule.registerCustomProperty(group, key, automaticValue);
2665
+ },
2666
+ clearCustomProperties: () => nativeModule.updateSessionProperties(automaticSessionProperties()),
2482
2667
  registerTool,
2483
2668
  unregisterTool,
2484
2669
  registerArtifactProvider,
@@ -2500,6 +2685,9 @@ const Ansight = {
2500
2685
  installReactTools,
2501
2686
  uninstallReactTools,
2502
2687
  installErrorHandlers,
2688
+ installNetworkCapture,
2689
+ uninstallNetworkCapture,
2690
+ sanitizeNetworkRequest,
2503
2691
  createReactNavigationTracker,
2504
2692
  platform: Platform.OS,
2505
2693
  };
@@ -2518,3 +2706,7 @@ module.exports.registerArtifactProviders = registerArtifactProviders;
2518
2706
  module.exports.unregisterArtifactProvider = unregisterArtifactProvider;
2519
2707
  module.exports.listRegisteredArtifactProviders = listRegisteredArtifactProviders;
2520
2708
  module.exports.clearArtifactProviders = clearArtifactProviders;
2709
+ module.exports.recordNetworkRequest = recordNetworkRequest;
2710
+ module.exports.installNetworkCapture = installNetworkCapture;
2711
+ module.exports.uninstallNetworkCapture = uninstallNetworkCapture;
2712
+ module.exports.sanitizeNetworkRequest = sanitizeNetworkRequest;
@@ -139,6 +139,7 @@ final class AnsightReactNative: RCTEventEmitter {
139
139
 
140
140
  private let lock = NSLock()
141
141
  private var hasListeners = false
142
+ private var hostConnectionStatusSubscription: HostConnectionStatusSubscription?
142
143
  private var activeCustomToolIds: Set<String> = []
143
144
  private var pendingToolCalls: [String: PendingToolCall] = [:]
144
145
  private var reactNativeMemorySampler: ReactNativeMemorySamplerBox?
@@ -150,10 +151,14 @@ final class AnsightReactNative: RCTEventEmitter {
150
151
  override init() {
151
152
  super.init()
152
153
  AnsightLogger.registerCallback(logCallback)
154
+ hostConnectionStatusSubscription = AnsightRuntime.shared.addHostConnectionStatusListener { [weak self] status, _ in
155
+ self?.emitHostConnectionStatusEvent(status)
156
+ }
153
157
  }
154
158
 
155
159
  deinit {
156
160
  AnsightLogger.removeCallback(logCallback)
161
+ hostConnectionStatusSubscription?.remove()
157
162
  }
158
163
 
159
164
  override static func requiresMainQueueSetup() -> Bool {
@@ -161,7 +166,7 @@ final class AnsightReactNative: RCTEventEmitter {
161
166
  }
162
167
 
163
168
  override func supportedEvents() -> [String]! {
164
- ["AnsightToolCall", "AnsightLog"]
169
+ ["AnsightToolCall", "AnsightLog", "AnsightHostConnectionStatus"]
165
170
  }
166
171
 
167
172
  override func startObserving() {
@@ -292,6 +297,27 @@ final class AnsightReactNative: RCTEventEmitter {
292
297
  }
293
298
  }
294
299
 
300
+ @objc(recordNetworkRequest:resolver:rejecter:)
301
+ func recordNetworkRequest(
302
+ _ input: NSDictionary,
303
+ resolver resolve: @escaping RCTPromiseResolveBlock,
304
+ rejecter reject: RCTPromiseRejectBlock
305
+ ) {
306
+ guard JSONSerialization.isValidJSONObject(input),
307
+ let data = try? JSONSerialization.data(withJSONObject: input),
308
+ let request = try? JSONDecoder().decode(AnsightNetworkRequest.self, from: data),
309
+ request.schema == AnsightNetworkRequest.schemaName
310
+ else {
311
+ resolve(operationResultDictionary(
312
+ .failure("Network request must use the ansight.network-request.v1 schema.")
313
+ ))
314
+ return
315
+ }
316
+ Task {
317
+ resolve(operationResultDictionary(await AnsightRuntime.shared.recordNetworkRequest(request)))
318
+ }
319
+ }
320
+
295
321
  @objc(recordCrashCandidate:resolver:rejecter:)
296
322
  func recordCrashCandidate(
297
323
  _ input: NSDictionary,
@@ -859,6 +885,17 @@ final class AnsightReactNative: RCTEventEmitter {
859
885
  }
860
886
  }
861
887
 
888
+ private func emitHostConnectionStatusEvent(_ status: HostConnectionStatus) {
889
+ let shouldEmit = lock.withLock { hasListeners }
890
+ guard shouldEmit else { return }
891
+ DispatchQueue.main.async {
892
+ self.sendEvent(
893
+ withName: "AnsightHostConnectionStatus",
894
+ body: self.hostConnectionStatusDictionary(status)
895
+ )
896
+ }
897
+ }
898
+
862
899
  private func configureReactNativeMemoryProfiling(_ dictionary: NSDictionary?) throws {
863
900
  let options = ReactNativeMemoryProfilingOptions(dictionary: dictionary)
864
901
  currentReactNativeMemoryOptions = options
@@ -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(recordNetworkRequest:(NSDictionary *)input
37
+ resolver:(RCTPromiseResolveBlock)resolve
38
+ rejecter:(RCTPromiseRejectBlock)reject)
39
+
36
40
  RCT_EXTERN_METHOD(recordCrashCandidate:(NSDictionary *)input
37
41
  resolver:(RCTPromiseResolveBlock)resolve
38
42
  rejecter:(RCTPromiseRejectBlock)reject)