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

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.10`
81
- - Maven: `ai.ansight:ansight-android:1.3.0-preview.10`
80
+ - CocoaPods: `Ansight`, `AnsightObjC` version `1.3.0-preview.11`
81
+ - Maven: `ai.ansight:ansight-android:1.3.0-preview.11`
82
82
 
83
83
  ## Quickstart
84
84
 
@@ -151,6 +151,7 @@ The TypeScript `AnsightOptions` surface mirrors Android `AnsightOptions`, iOS
151
151
  | `secureStorage` | Compatibility alias for native secure-storage allow-list settings. |
152
152
  | `remoteTools` | Native visual tree, file, database, preferences, reflection, and secure-storage tool options. |
153
153
  | `lifecycle` | JS AppState tracking toggle. Defaults to true. |
154
+ | `networkCapture` | Opt-in `fetch` and `XMLHttpRequest` metadata capture. Accepts `true` or sanitization/capture options. |
154
155
 
155
156
  Use `withOpenFileHandleTracking()` and `withJniReferenceCountTracking()` to
156
157
  opt in through the builder. Matching `without...` methods disable the channels
@@ -201,13 +202,49 @@ await Ansight.initializeAndActivate({
201
202
  ```
202
203
 
203
204
  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
+ Move and cancel events do not trigger capture. Rapid boundaries are coalesced
206
+ and rate-limited to protect screenshot cadence. Native touch capture and
205
207
  visual-tree tools/providers must remain enabled.
206
208
 
207
209
  On iOS, `captureGpuBackedSurfaces` defaults to `true` so Metal, SceneKit, and
208
210
  similar GPU-backed views are included. Set it to `false` to use a lower-overhead
209
211
  capture path when those surfaces are not needed.
210
212
 
213
+ ## Network capture
214
+
215
+ Network capture is explicitly enabled by the React Native layer, which
216
+ instruments `fetch` and `XMLHttpRequest`, then sends a typed metadata record
217
+ through the native bridge. It is opt-in:
218
+
219
+ ```ts
220
+ await Ansight.initializeAndActivate(
221
+ Ansight.createOptionsBuilder()
222
+ .withAnsightDefaults()
223
+ .withNetworkCapture({
224
+ maximumBodyBytes: 64 * 1024,
225
+ additionalSensitiveHeaderNames: ["x-tenant-secret"],
226
+ additionalSensitiveQueryParameterNames: ["session"],
227
+ requestSanitizer: request =>
228
+ request.url.includes("/health") ? null : request,
229
+ })
230
+ .withoutNetworkRequestBodies() // optional, independent opt-out
231
+ .build(),
232
+ );
233
+ ```
234
+
235
+ Text request and response bodies are included by default after network capture
236
+ is explicitly enabled, with a 64 KiB default per-body limit. Set a larger
237
+ `maximumBodyBytes` when needed; use the request/response body builder methods to
238
+ opt either side out or back in, and `captureBinaryBodies` to explicitly allow
239
+ Base64 binary content. Standard credentials, cloud signed-URL fields, cookies,
240
+ URL user information, and sensitive text-body assignments are redacted before
241
+ the bridge. Capture hooks detach whenever the native host is disconnected.
242
+
243
+ Use `installNetworkCapture(...)` and `uninstallNetworkCapture()` when capture
244
+ must be controlled independently of initialization. `recordNetworkRequest(...)`
245
+ supports custom HTTP stacks, and `sanitizeNetworkRequest(...)` exposes the same
246
+ app-side policy for inspection or testing.
247
+
211
248
  ## Native Tool Options
212
249
 
213
250
  `remoteTools` configures the native tool suites registered by the bridge. `useNativeAllInOneDefaults: true` enables visual tree tools by default so Studio can pair `ui.get_visual_tree` data with `ui.get_screenshot` frames. Apps that do not use all-in-one defaults can opt in explicitly:
@@ -389,6 +426,19 @@ await Ansight.clearSessionProperties();
389
426
  When connected, property mutations are sent immediately. When disconnected, the
390
427
  latest values are included in the next `session.open`.
391
428
 
429
+ The bridge automatically adds these property groups:
430
+
431
+ | Group | Properties |
432
+ | --- | --- |
433
+ | `reactNative` | Ansight SDK, React Native and React versions; platform and runtime language; JavaScript engine and available Hermes version/bytecode details; legacy/new architecture; bridgeless state; development mode. |
434
+ | `localization` | Canonical locale, language, optional region, IANA time zone when exposed by `Intl`, and UTC offset in minutes. |
435
+
436
+ App-provided values override an automatic value with the same group and key.
437
+ Clearing all properties, or removing one automatic property, restores the
438
+ current bridge-owned value. `localization` reflects the JavaScript runtime
439
+ locale; if the app selects a different language through an i18n library, set
440
+ `localization.locale`, `language`, and `region` explicitly.
441
+
392
442
  ## Tool Guards
393
443
 
394
444
  | Value | Allowed scopes |
@@ -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.10"
71
- implementation("ai.ansight:ansight-android:1.3.0-preview.10")
70
+ findProperty("ansightAndroidVersion") ?: "1.3.0-preview.11"
71
+ implementation("ai.ansight:ansight-android:1.3.0-preview.11")
72
72
  }
@@ -12,6 +12,7 @@ import ai.ansight.runtime.AnsightHostConnectionOptions
12
12
  import ai.ansight.runtime.AnsightLogCallback
13
13
  import ai.ansight.runtime.AnsightLogLevel
14
14
  import ai.ansight.runtime.AnsightLogger
15
+ import ai.ansight.runtime.AnsightNetworkRequest
15
16
  import ai.ansight.runtime.AnsightOptions
16
17
  import ai.ansight.runtime.AnsightOptionsBuilder
17
18
  import ai.ansight.runtime.AnsightRuntime
@@ -28,6 +29,7 @@ import ai.ansight.runtime.HostConnectionRequestKind
28
29
  import ai.ansight.runtime.HostConnectionCapabilities
29
30
  import ai.ansight.runtime.HostConnectionResult
30
31
  import ai.ansight.runtime.HostConnectionStatus
32
+ import ai.ansight.runtime.HostConnectionStatusSubscription
31
33
  import ai.ansight.runtime.OperationResult
32
34
  import ai.ansight.runtime.OpenSessionResult
33
35
  import ai.ansight.runtime.PairingOpenOptions
@@ -104,16 +106,21 @@ class AnsightReactNativeModule(
104
106
  private val logCallback = AnsightLogCallback { level, message, throwable ->
105
107
  emitLogEvent(level, message, throwable)
106
108
  }
109
+ private val hostConnectionStatusSubscription: HostConnectionStatusSubscription
107
110
 
108
111
  init {
109
112
  reactContext.addLifecycleEventListener(this)
110
113
  AnsightLogger.registerCallback(logCallback)
114
+ hostConnectionStatusSubscription = AnsightRuntime.addHostConnectionStatusListener(
115
+ listener = { status, _ -> emitHostConnectionStatusEvent(status) },
116
+ )
111
117
  }
112
118
 
113
119
  override fun getName(): String = "AnsightReactNative"
114
120
 
115
121
  override fun invalidate() {
116
122
  AnsightLogger.removeCallback(logCallback)
123
+ hostConnectionStatusSubscription.remove()
117
124
  reactContext.removeLifecycleEventListener(this)
118
125
  super.invalidate()
119
126
  }
@@ -226,6 +233,17 @@ class AnsightReactNativeModule(
226
233
  }.resolve(promise)
227
234
  }
228
235
 
236
+ @ReactMethod
237
+ fun recordNetworkRequest(input: ReadableMap, promise: Promise) {
238
+ runCatching {
239
+ val request = AnsightNetworkRequest.fromJson(JSONObject(input.toHashMap()))
240
+ ?: return@runCatching operationResultMap(
241
+ OperationResult.failure("Network request must use the ansight.network-request.v1 schema."),
242
+ )
243
+ operationResultMap(AnsightRuntime.recordNetworkRequest(request))
244
+ }.resolve(promise)
245
+ }
246
+
229
247
  @ReactMethod
230
248
  fun recordCrashCandidate(input: ReadableMap, promise: Promise) {
231
249
  runCatching {
@@ -677,6 +695,16 @@ class AnsightReactNativeModule(
677
695
  }
678
696
  }
679
697
 
698
+ private fun emitHostConnectionStatusEvent(status: HostConnectionStatus) {
699
+ if (listenerCount.get() <= 0) return
700
+ val event = hostConnectionStatusMap(status)
701
+ UiThreadUtil.runOnUiThread {
702
+ reactContext
703
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
704
+ .emit("AnsightHostConnectionStatus", event)
705
+ }
706
+ }
707
+
680
708
  private fun configureReactNativeMemoryProfiling(map: ReadableMap?) {
681
709
  val options = reactNativeMemoryProfilingOptions(map)
682
710
  currentReactNativeMemoryOptions = options
package/index.d.ts CHANGED
@@ -48,6 +48,75 @@ export interface AnsightTouchCaptureOptions {
48
48
  moveCaptureFramesPerSecond?: number;
49
49
  }
50
50
 
51
+ export interface AnsightNetworkHeader {
52
+ name: string;
53
+ value: string;
54
+ }
55
+
56
+ export interface AnsightNetworkBody {
57
+ contentType?: string;
58
+ encoding: "utf8" | "base64";
59
+ data: string;
60
+ capturedBytes: number;
61
+ totalBytes?: number;
62
+ truncated: boolean;
63
+ }
64
+
65
+ /** V1 network metadata with optional, bounded bodies. */
66
+ export interface AnsightNetworkRequest {
67
+ schema: "ansight.network-request.v1";
68
+ id: string;
69
+ source: string;
70
+ startedAtUtc: string;
71
+ completedAtUtc: string;
72
+ durationMilliseconds: number;
73
+ method: string;
74
+ url: string;
75
+ protocol?: string;
76
+ requestHeaders: AnsightNetworkHeader[];
77
+ requestBodySizeBytes?: number;
78
+ requestBody?: AnsightNetworkBody;
79
+ statusCode?: number;
80
+ reasonPhrase?: string;
81
+ responseHeaders: AnsightNetworkHeader[];
82
+ responseBodySizeBytes?: number;
83
+ responseBody?: AnsightNetworkBody;
84
+ errorType?: string;
85
+ errorMessage?: string;
86
+ }
87
+
88
+ export type AnsightNetworkRequestInput = Partial<AnsightNetworkRequest> & {
89
+ method: string;
90
+ url: string;
91
+ };
92
+
93
+ export interface AnsightNetworkSanitizationOptions {
94
+ includeRequestHeaders?: boolean;
95
+ includeResponseHeaders?: boolean;
96
+ includeQueryString?: boolean;
97
+ includeBodySizes?: boolean;
98
+ /** Defaults to true. */
99
+ captureRequestBody?: boolean;
100
+ /** Defaults to true. */
101
+ captureResponseBody?: boolean;
102
+ /** Decoded bytes retained per body. Defaults to 64 KiB; larger explicit limits are honored. */
103
+ maximumBodyBytes?: number;
104
+ captureBinaryBodies?: boolean;
105
+ additionalSensitiveHeaderNames?: string[];
106
+ additionalSensitiveQueryParameterNames?: string[];
107
+ urlSanitizer?: (url: string) => string;
108
+ /** Return a replacement request, or null to suppress capture. */
109
+ requestSanitizer?: (
110
+ request: AnsightNetworkRequest,
111
+ ) => AnsightNetworkRequest | null;
112
+ }
113
+
114
+ export interface AnsightNetworkCaptureOptions
115
+ extends AnsightNetworkSanitizationOptions {
116
+ captureFetch?: boolean;
117
+ captureXmlHttpRequest?: boolean;
118
+ }
119
+
51
120
  export interface AnsightNativeToolRoot {
52
121
  alias: string;
53
122
  path: string;
@@ -155,6 +224,8 @@ export interface AnsightOptions {
155
224
  remoteTools?: AnsightRemoteToolsOptions;
156
225
  additionalChannels?: AnsightChannel[];
157
226
  lifecycle?: boolean;
227
+ /** Explicitly opt in to fetch/XMLHttpRequest instrumentation. */
228
+ networkCapture?: boolean | AnsightNetworkCaptureOptions;
158
229
  }
159
230
 
160
231
  export interface AnsightCrashCaptureOptions {
@@ -300,6 +371,12 @@ export class AnsightOptionsBuilder {
300
371
  withCrashCapture(crashCapture?: AnsightCrashCaptureOptions): this;
301
372
  withoutCrashCapture(): this;
302
373
  withLifecycleCapture(lifecycleCapture?: NonNullable<AnsightOptions["lifecycleCapture"]>): this;
374
+ withNetworkCapture(networkCapture?: AnsightNetworkCaptureOptions): this;
375
+ withNetworkRequestBodies(maximumBodyBytes?: number): this;
376
+ withoutNetworkRequestBodies(): this;
377
+ withNetworkResponseBodies(maximumBodyBytes?: number): this;
378
+ withoutNetworkResponseBodies(): this;
379
+ withoutNetworkCapture(): this;
303
380
  withToolGuard(toolGuard: NonNullable<AnsightOptions["toolGuard"]>): this;
304
381
  withToolsDisabled(): this;
305
382
  withReadOnlyToolAccess(): this;
@@ -543,6 +620,18 @@ export function recordMetric(value: number, channel?: number): Promise<AnsightDe
543
620
  export function event(input: string | { label: string; type?: string; details?: string; channel?: number }): Promise<AnsightDebugSnapshot>;
544
621
  export function recordEvent(input: string | { label: string; type?: string; details?: string; channel?: number }): Promise<AnsightDebugSnapshot>;
545
622
  export function recordCrashCandidate(input?: AnsightCrashCandidate): Promise<{ candidateId?: string }>;
623
+ export function recordNetworkRequest(
624
+ input: AnsightNetworkRequestInput,
625
+ sanitizationOptions?: AnsightNetworkSanitizationOptions,
626
+ ): Promise<AnsightOperationResult>;
627
+ export function sanitizeNetworkRequest(
628
+ input: AnsightNetworkRequestInput,
629
+ sanitizationOptions?: AnsightNetworkSanitizationOptions,
630
+ ): AnsightNetworkRequest | null;
631
+ export function installNetworkCapture(
632
+ options?: AnsightNetworkCaptureOptions,
633
+ ): AnsightSubscription;
634
+ export function uninstallNetworkCapture(): void;
546
635
  export function screenViewed(name: string, details?: Record<string, string>): Promise<AnsightDebugSnapshot>;
547
636
  export function trackRoute(name: string, details?: Record<string, string>): Promise<AnsightDebugSnapshot>;
548
637
  export function setAppLifecycleState(state: AnsightLifecycleState): Promise<AnsightDebugSnapshot>;
@@ -617,6 +706,7 @@ declare const Ansight: {
617
706
  event: typeof event;
618
707
  recordEvent: typeof recordEvent;
619
708
  recordCrashCandidate: typeof recordCrashCandidate;
709
+ recordNetworkRequest: typeof recordNetworkRequest;
620
710
  screenViewed: typeof screenViewed;
621
711
  trackRoute: typeof trackRoute;
622
712
  setAppLifecycleState: typeof setAppLifecycleState;
@@ -671,6 +761,9 @@ declare const Ansight: {
671
761
  installReactTools: typeof installReactTools;
672
762
  uninstallReactTools: typeof uninstallReactTools;
673
763
  installErrorHandlers: typeof installErrorHandlers;
764
+ installNetworkCapture: typeof installNetworkCapture;
765
+ uninstallNetworkCapture: typeof uninstallNetworkCapture;
766
+ sanitizeNetworkRequest: typeof sanitizeNetworkRequest;
674
767
  createReactNavigationTracker: typeof createReactNavigationTracker;
675
768
  platform: "ios" | "android" | string;
676
769
  };
package/index.js CHANGED
@@ -10,6 +10,15 @@ const {
10
10
  findNodeHandle,
11
11
  processColor,
12
12
  } = require("react-native");
13
+ const { version: reactVersion } = require("react");
14
+ const {
15
+ createAutomaticSessionProperties,
16
+ mergeSessionProperties,
17
+ } = require("./session-properties");
18
+ const {
19
+ installNetworkCapture: installNetworkCaptureCore,
20
+ sanitizeNetworkRequest,
21
+ } = require("./network");
13
22
 
14
23
  const nativeModule = NativeModules.AnsightReactNative;
15
24
 
@@ -33,6 +42,9 @@ const hostConnectionStatusListeners = new Set();
33
42
  let lastHostConnectionStatusKey = null;
34
43
  const logListeners = new Set();
35
44
  let logEventSubscription = null;
45
+ let networkCaptureSubscription = null;
46
+ let networkCaptureRegistration = null;
47
+ let networkConnectionEventSubscription = null;
36
48
  const REACT_COMPONENT_TREE_TOOL_ID = "react.get_component_tree";
37
49
  const REACT_SHADOW_TREE_TOOL_ID = "react.get_shadow_tree";
38
50
 
@@ -44,7 +56,28 @@ function normalizePairingPayload(payload) {
44
56
  }
45
57
 
46
58
  function normalizeOptions(options = {}) {
47
- return { ...options };
59
+ const normalized = {
60
+ ...options,
61
+ customProperties: mergeSessionProperties(
62
+ automaticSessionProperties(),
63
+ options.customProperties
64
+ ),
65
+ };
66
+ delete normalized.networkCapture;
67
+ return normalized;
68
+ }
69
+
70
+ function automaticSessionProperties() {
71
+ return createAutomaticSessionProperties({
72
+ platform: Platform,
73
+ reactVersion,
74
+ runtimeGlobal: global,
75
+ developmentMode: typeof __DEV__ !== "undefined" && __DEV__,
76
+ });
77
+ }
78
+
79
+ function automaticSessionPropertyValue(group, key) {
80
+ return automaticSessionProperties()[group]?.[key];
48
81
  }
49
82
 
50
83
  function cloneOptions(options = {}) {
@@ -73,6 +106,17 @@ function cloneOptions(options = {}) {
73
106
  if (options.hostConnection) {
74
107
  clone.hostConnection = { ...options.hostConnection };
75
108
  }
109
+ if (options.networkCapture && typeof options.networkCapture === "object") {
110
+ clone.networkCapture = {
111
+ ...options.networkCapture,
112
+ additionalSensitiveHeaderNames: options.networkCapture.additionalSensitiveHeaderNames
113
+ ? [...options.networkCapture.additionalSensitiveHeaderNames]
114
+ : undefined,
115
+ additionalSensitiveQueryParameterNames: options.networkCapture.additionalSensitiveQueryParameterNames
116
+ ? [...options.networkCapture.additionalSensitiveQueryParameterNames]
117
+ : undefined,
118
+ };
119
+ }
76
120
  if (options.secureStorage) {
77
121
  clone.secureStorage = {
78
122
  ...options.secureStorage,
@@ -339,6 +383,52 @@ class AnsightOptionsBuilder {
339
383
  return this;
340
384
  }
341
385
 
386
+ withNetworkCapture(networkCapture = {}) {
387
+ this._options.networkCapture = { ...networkCapture };
388
+ return this;
389
+ }
390
+
391
+ withNetworkRequestBodies(maximumBodyBytes) {
392
+ if (!this._options.networkCapture || typeof this._options.networkCapture !== "object") return this;
393
+ const current = this._options.networkCapture;
394
+ this._options.networkCapture = {
395
+ ...current,
396
+ captureRequestBody: true,
397
+ ...(maximumBodyBytes == null ? {} : { maximumBodyBytes }),
398
+ };
399
+ return this;
400
+ }
401
+
402
+ withoutNetworkRequestBodies() {
403
+ if (!this._options.networkCapture || typeof this._options.networkCapture !== "object") return this;
404
+ const current = this._options.networkCapture;
405
+ this._options.networkCapture = { ...current, captureRequestBody: false };
406
+ return this;
407
+ }
408
+
409
+ withNetworkResponseBodies(maximumBodyBytes) {
410
+ if (!this._options.networkCapture || typeof this._options.networkCapture !== "object") return this;
411
+ const current = this._options.networkCapture;
412
+ this._options.networkCapture = {
413
+ ...current,
414
+ captureResponseBody: true,
415
+ ...(maximumBodyBytes == null ? {} : { maximumBodyBytes }),
416
+ };
417
+ return this;
418
+ }
419
+
420
+ withoutNetworkResponseBodies() {
421
+ if (!this._options.networkCapture || typeof this._options.networkCapture !== "object") return this;
422
+ const current = this._options.networkCapture;
423
+ this._options.networkCapture = { ...current, captureResponseBody: false };
424
+ return this;
425
+ }
426
+
427
+ withoutNetworkCapture() {
428
+ this._options.networkCapture = false;
429
+ return this;
430
+ }
431
+
342
432
  withToolGuard(toolGuard) {
343
433
  this._options.toolGuard = toolGuard;
344
434
  return this;
@@ -581,6 +671,7 @@ function addHostConnectionStatusListener(listener, options = {}) {
581
671
 
582
672
  async function notifyAfterHostConnectionChange(operation) {
583
673
  const result = await operation();
674
+ await refreshNetworkCaptureConnection();
584
675
  await emitHostConnectionStatusChangedIfNeeded();
585
676
  return result;
586
677
  }
@@ -2313,6 +2404,7 @@ function installErrorHandlers(options = {}) {
2313
2404
 
2314
2405
  async function initialize(options = {}) {
2315
2406
  const result = await nativeModule.initialize(normalizeOptions(options));
2407
+ await configureNetworkCapture(options.networkCapture);
2316
2408
  if (options.lifecycle !== false) {
2317
2409
  startAppStateTracking();
2318
2410
  }
@@ -2322,6 +2414,7 @@ async function initialize(options = {}) {
2322
2414
 
2323
2415
  async function initializeAndActivate(options = {}) {
2324
2416
  const result = await nativeModule.initializeAndActivate(normalizeOptions(options));
2417
+ await configureNetworkCapture(options.networkCapture);
2325
2418
  if (options.lifecycle !== false) {
2326
2419
  startAppStateTracking();
2327
2420
  }
@@ -2396,6 +2489,93 @@ function recordCrashCandidate(input = {}) {
2396
2489
  });
2397
2490
  }
2398
2491
 
2492
+ function recordNetworkRequest(input, sanitizationOptions = {}) {
2493
+ const sanitized = sanitizeNetworkRequest(input, sanitizationOptions, global);
2494
+ if (!sanitized) {
2495
+ return Promise.resolve({ success: false, message: "Network request capture was suppressed by the sanitizer." });
2496
+ }
2497
+ return nativeModule.recordNetworkRequest(sanitized);
2498
+ }
2499
+
2500
+ function installNetworkCapture(options = {}) {
2501
+ uninstallNetworkCapture();
2502
+ const registration = { options };
2503
+ networkCaptureRegistration = registration;
2504
+ ensureNetworkConnectionEventSubscription();
2505
+ void refreshNetworkCaptureConnection();
2506
+ return {
2507
+ remove() {
2508
+ if (networkCaptureRegistration === registration) {
2509
+ uninstallNetworkCapture();
2510
+ }
2511
+ },
2512
+ };
2513
+ }
2514
+
2515
+ function uninstallNetworkCapture() {
2516
+ networkCaptureRegistration = null;
2517
+ if (networkConnectionEventSubscription) {
2518
+ networkConnectionEventSubscription.remove();
2519
+ networkConnectionEventSubscription = null;
2520
+ }
2521
+ detachNetworkCapture();
2522
+ }
2523
+
2524
+ function detachNetworkCapture() {
2525
+ if (!networkCaptureSubscription) return;
2526
+ networkCaptureSubscription.remove();
2527
+ networkCaptureSubscription = null;
2528
+ }
2529
+
2530
+ async function refreshNetworkCaptureConnection() {
2531
+ const registration = networkCaptureRegistration;
2532
+ if (!registration) {
2533
+ detachNetworkCapture();
2534
+ return;
2535
+ }
2536
+ try {
2537
+ const status = await nativeModule.hostConnectionStatus();
2538
+ if (networkCaptureRegistration !== registration) return;
2539
+ applyNetworkConnectionStatus(status);
2540
+ } catch {
2541
+ if (networkCaptureRegistration === registration) detachNetworkCapture();
2542
+ }
2543
+ }
2544
+
2545
+ function ensureNetworkConnectionEventSubscription() {
2546
+ if (networkConnectionEventSubscription) return;
2547
+ const emitter = new NativeEventEmitter(nativeModule);
2548
+ networkConnectionEventSubscription = emitter.addListener(
2549
+ "AnsightHostConnectionStatus",
2550
+ applyNetworkConnectionStatus,
2551
+ );
2552
+ }
2553
+
2554
+ function applyNetworkConnectionStatus(status) {
2555
+ const registration = networkCaptureRegistration;
2556
+ if (!registration || !status || status.isConnected !== true) {
2557
+ detachNetworkCapture();
2558
+ return;
2559
+ }
2560
+ if (!networkCaptureSubscription) {
2561
+ networkCaptureSubscription = installNetworkCaptureCore({
2562
+ globalObject: global,
2563
+ options: registration.options,
2564
+ sourcePrefix: "react-native",
2565
+ capture: (request) => nativeModule.recordNetworkRequest(request),
2566
+ });
2567
+ }
2568
+ }
2569
+
2570
+ async function configureNetworkCapture(value) {
2571
+ uninstallNetworkCapture();
2572
+ if (!value) return;
2573
+ const registration = { options: typeof value === "object" ? value : {} };
2574
+ networkCaptureRegistration = registration;
2575
+ ensureNetworkConnectionEventSubscription();
2576
+ await refreshNetworkCaptureConnection();
2577
+ }
2578
+
2399
2579
  function screenViewed(name, details) {
2400
2580
  return nativeModule.screenViewed(name, details || {});
2401
2581
  }
@@ -2439,6 +2619,7 @@ const Ansight = {
2439
2619
  event: recordEvent,
2440
2620
  recordEvent,
2441
2621
  recordCrashCandidate,
2622
+ recordNetworkRequest,
2442
2623
  screenViewed,
2443
2624
  trackRoute,
2444
2625
  setAppLifecycleState: (state) => nativeModule.setAppLifecycleState(state),
@@ -2473,12 +2654,21 @@ const Ansight = {
2473
2654
  captureScreenFrame: (options = {}) => nativeModule.captureScreenFrame(options || {}),
2474
2655
  enableTouchCapture: () => nativeModule.enableTouchCapture(),
2475
2656
  disableTouchCapture: () => nativeModule.disableTouchCapture(),
2476
- updateSessionProperties: (properties) => nativeModule.updateSessionProperties(properties || {}),
2477
- clearSessionProperties: () => nativeModule.clearSessionProperties(),
2478
- updateCustomProperties: (properties) => nativeModule.updateSessionProperties(properties || {}),
2657
+ updateSessionProperties: (properties) => nativeModule.updateSessionProperties(
2658
+ mergeSessionProperties(automaticSessionProperties(), properties || {})
2659
+ ),
2660
+ clearSessionProperties: () => nativeModule.updateSessionProperties(automaticSessionProperties()),
2661
+ updateCustomProperties: (properties) => nativeModule.updateSessionProperties(
2662
+ mergeSessionProperties(automaticSessionProperties(), properties || {})
2663
+ ),
2479
2664
  registerCustomProperty: (group, key, value) => nativeModule.registerCustomProperty(group, key, value),
2480
- removeCustomProperty: (group, key) => nativeModule.removeCustomProperty(group, key),
2481
- clearCustomProperties: () => nativeModule.clearSessionProperties(),
2665
+ removeCustomProperty: (group, key) => {
2666
+ const automaticValue = automaticSessionPropertyValue(group, key);
2667
+ return automaticValue == null
2668
+ ? nativeModule.removeCustomProperty(group, key)
2669
+ : nativeModule.registerCustomProperty(group, key, automaticValue);
2670
+ },
2671
+ clearCustomProperties: () => nativeModule.updateSessionProperties(automaticSessionProperties()),
2482
2672
  registerTool,
2483
2673
  unregisterTool,
2484
2674
  registerArtifactProvider,
@@ -2500,6 +2690,9 @@ const Ansight = {
2500
2690
  installReactTools,
2501
2691
  uninstallReactTools,
2502
2692
  installErrorHandlers,
2693
+ installNetworkCapture,
2694
+ uninstallNetworkCapture,
2695
+ sanitizeNetworkRequest,
2503
2696
  createReactNavigationTracker,
2504
2697
  platform: Platform.OS,
2505
2698
  };
@@ -2518,3 +2711,7 @@ module.exports.registerArtifactProviders = registerArtifactProviders;
2518
2711
  module.exports.unregisterArtifactProvider = unregisterArtifactProvider;
2519
2712
  module.exports.listRegisteredArtifactProviders = listRegisteredArtifactProviders;
2520
2713
  module.exports.clearArtifactProviders = clearArtifactProviders;
2714
+ module.exports.recordNetworkRequest = recordNetworkRequest;
2715
+ module.exports.installNetworkCapture = installNetworkCapture;
2716
+ module.exports.uninstallNetworkCapture = uninstallNetworkCapture;
2717
+ 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)