@ansight/react-native 1.0.2-preview.1

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.
@@ -0,0 +1,1635 @@
1
+ import Ansight
2
+ import Foundation
3
+ import React
4
+
5
+ @objc(AnsightReactNative)
6
+ final class AnsightReactNative: RCTEventEmitter {
7
+ private final class PendingToolCall {
8
+ let semaphore = DispatchSemaphore(value: 0)
9
+ var result: AnsightToolExecutionResult?
10
+ }
11
+
12
+ private final class ReactNativeMemorySamplerBox: @unchecked Sendable {
13
+ private let sampler: NSObject
14
+
15
+ init(sampler: NSObject) {
16
+ self.sampler = sampler
17
+ }
18
+
19
+ func attach(to bridge: RCTBridge?) {
20
+ guard let bridge else {
21
+ return
22
+ }
23
+
24
+ let selector = NSSelectorFromString("attachToBridge:")
25
+ guard sampler.responds(to: selector) else {
26
+ return
27
+ }
28
+ _ = sampler.perform(selector, with: bridge)
29
+ }
30
+
31
+ func sample(selectorName: String) -> Int64? {
32
+ let selector = NSSelectorFromString(selectorName)
33
+ guard sampler.responds(to: selector),
34
+ let result = sampler.perform(selector)?.takeUnretainedValue() as? NSNumber else {
35
+ return nil
36
+ }
37
+ return result.int64Value
38
+ }
39
+ }
40
+
41
+ private struct ReactNativeMemoryProfilingOptions {
42
+ var enabled = true
43
+ var jsHeapUsed = true
44
+ var jsHeapTotal = true
45
+
46
+ static let defaults = ReactNativeMemoryProfilingOptions()
47
+ static let disabled = ReactNativeMemoryProfilingOptions(enabled: false, jsHeapUsed: false, jsHeapTotal: false)
48
+
49
+ init(enabled: Bool = true, jsHeapUsed: Bool = true, jsHeapTotal: Bool = true) {
50
+ self.enabled = enabled
51
+ self.jsHeapUsed = jsHeapUsed
52
+ self.jsHeapTotal = jsHeapTotal
53
+ }
54
+
55
+ init(dictionary: NSDictionary?) {
56
+ guard let raw = dictionary?["reactNativeMemory"], !(raw is NSNull) else {
57
+ if dictionary?.object(forKey: "reactNativeMemory") is NSNull {
58
+ self = .disabled
59
+ } else {
60
+ self = .defaults
61
+ }
62
+ return
63
+ }
64
+
65
+ if let enabled = raw as? NSNumber {
66
+ self = enabled.boolValue ? .defaults : .disabled
67
+ return
68
+ }
69
+
70
+ guard let options = raw as? NSDictionary else {
71
+ self = .defaults
72
+ return
73
+ }
74
+
75
+ let enabled = boolValue(options, "enabled", defaultValue: true)
76
+ let jsHeap = boolValue(options, "jsHeap", defaultValue: true)
77
+ self.init(
78
+ enabled: enabled,
79
+ jsHeapUsed: boolValue(options, "jsHeapUsed", defaultValue: jsHeap),
80
+ jsHeapTotal: boolValue(options, "jsHeapTotal", defaultValue: jsHeap)
81
+ )
82
+ }
83
+
84
+ var dictionary: [String: Any] {
85
+ [
86
+ "enabled": enabled,
87
+ "jsHeapUsed": jsHeapUsed,
88
+ "jsHeapTotal": jsHeapTotal,
89
+ ]
90
+ }
91
+ }
92
+
93
+ private enum ReactNativeMemoryChannels {
94
+ static let jsHeapUsed = AnsightChannel(
95
+ id: 32,
96
+ name: "React Native JS heap used",
97
+ colorHex: "#61DAFB",
98
+ unit: "bytes",
99
+ type: "memory",
100
+ source: "reactNative",
101
+ group: "React Native",
102
+ kind: "react_native_js_heap_used"
103
+ )
104
+
105
+ static let jsHeapTotal = AnsightChannel(
106
+ id: 33,
107
+ name: "React Native JS heap total",
108
+ colorHex: "#0A84FF",
109
+ unit: "bytes",
110
+ type: "memory",
111
+ source: "reactNative",
112
+ group: "React Native",
113
+ kind: "react_native_js_heap_total"
114
+ )
115
+ }
116
+
117
+ private final class ReactNativeTool: AnsightTool, @unchecked Sendable {
118
+ let descriptor: AnsightToolDescriptor
119
+ private weak var module: AnsightReactNative?
120
+ private let timeoutMilliseconds: Int
121
+
122
+ init(descriptor: AnsightToolDescriptor, module: AnsightReactNative, timeoutMilliseconds: Int) {
123
+ self.descriptor = descriptor
124
+ self.module = module
125
+ self.timeoutMilliseconds = timeoutMilliseconds
126
+ }
127
+
128
+ func execute(arguments: [String: String]) throws -> AnsightToolExecutionResult {
129
+ guard let module else {
130
+ return .failure("React Native bridge is no longer available.", errorCode: "javascript_bridge_unavailable")
131
+ }
132
+ return module.executeJavaScriptTool(
133
+ toolId: descriptor.id,
134
+ arguments: arguments,
135
+ timeoutMilliseconds: timeoutMilliseconds
136
+ )
137
+ }
138
+ }
139
+
140
+ private let lock = NSLock()
141
+ private var hasListeners = false
142
+ private var activeCustomToolIds: Set<String> = []
143
+ private var pendingToolCalls: [String: PendingToolCall] = [:]
144
+ private var reactNativeMemorySampler: ReactNativeMemorySamplerBox?
145
+ private var currentReactNativeMemoryOptions = ReactNativeMemoryProfilingOptions.defaults
146
+ private lazy var logCallback = AnsightClosureLogCallback { [weak self] level, message, error in
147
+ self?.emitLogEvent(level: level, message: message, error: error)
148
+ }
149
+
150
+ override init() {
151
+ super.init()
152
+ AnsightLogger.registerCallback(logCallback)
153
+ }
154
+
155
+ deinit {
156
+ AnsightLogger.removeCallback(logCallback)
157
+ }
158
+
159
+ override static func requiresMainQueueSetup() -> Bool {
160
+ false
161
+ }
162
+
163
+ override func supportedEvents() -> [String]! {
164
+ ["AnsightToolCall", "AnsightLog"]
165
+ }
166
+
167
+ override func startObserving() {
168
+ _ = lock.withLock {
169
+ hasListeners = true
170
+ }
171
+ }
172
+
173
+ override func stopObserving() {
174
+ _ = lock.withLock {
175
+ hasListeners = false
176
+ }
177
+ }
178
+
179
+ @objc(initialize:resolver:rejecter:)
180
+ func initialize(
181
+ _ options: NSDictionary?,
182
+ resolver resolve: RCTPromiseResolveBlock,
183
+ rejecter reject: RCTPromiseRejectBlock
184
+ ) {
185
+ do {
186
+ let toolOptions = remoteToolOptions(options)
187
+ try AnsightRuntime.shared.initialize(options: buildOptions(options))
188
+ try configureReactNativeMemoryProfiling(options)
189
+ try AnsightRuntime.shared.registerAnsightRemoteTools(options: toolOptions)
190
+ resolve(snapshotDictionary())
191
+ } catch {
192
+ reject("ansight_error", error.localizedDescription, error)
193
+ }
194
+ }
195
+
196
+ @objc(initializeAndActivate:resolver:rejecter:)
197
+ func initializeAndActivate(
198
+ _ options: NSDictionary?,
199
+ resolver resolve: RCTPromiseResolveBlock,
200
+ rejecter reject: RCTPromiseRejectBlock
201
+ ) {
202
+ do {
203
+ try AnsightRuntime.shared.initializeAndActivateAnsightSdk(
204
+ options: buildOptions(options),
205
+ remoteToolOptions: remoteToolOptions(options)
206
+ )
207
+ try configureReactNativeMemoryProfiling(options)
208
+ resolve(snapshotDictionary())
209
+ } catch {
210
+ reject("ansight_error", error.localizedDescription, error)
211
+ }
212
+ }
213
+
214
+ @objc(activate:rejecter:)
215
+ func activate(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
216
+ do {
217
+ try AnsightRuntime.shared.activate()
218
+ resolve(snapshotDictionary())
219
+ } catch {
220
+ reject("ansight_error", error.localizedDescription, error)
221
+ }
222
+ }
223
+
224
+ @objc(deactivate:rejecter:)
225
+ func deactivate(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
226
+ AnsightRuntime.shared.deactivate()
227
+ resolve(snapshotDictionary())
228
+ }
229
+
230
+ @objc(clear:rejecter:)
231
+ func clear(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
232
+ AnsightRuntime.shared.clear()
233
+ resolve(snapshotDictionary())
234
+ }
235
+
236
+ @objc(registerMetricChannel:resolver:rejecter:)
237
+ func registerMetricChannel(
238
+ _ channel: NSDictionary,
239
+ resolver resolve: RCTPromiseResolveBlock,
240
+ rejecter reject: RCTPromiseRejectBlock
241
+ ) {
242
+ do {
243
+ try AnsightRuntime.shared.registerMetricChannel(
244
+ AnsightChannel(
245
+ id: intValue(channel, "id", defaultValue: -1),
246
+ name: stringValue(channel, "name") ?? "",
247
+ colorHex: stringValue(channel, "colorHex"),
248
+ unit: stringValue(channel, "unit"),
249
+ type: stringValue(channel, "type") ?? "custom",
250
+ source: stringValue(channel, "source"),
251
+ group: stringValue(channel, "group"),
252
+ kind: stringValue(channel, "kind")
253
+ )
254
+ )
255
+ resolve(snapshotDictionary())
256
+ } catch {
257
+ reject("ansight_error", error.localizedDescription, error)
258
+ }
259
+ }
260
+
261
+ @objc(recordMetric:channel:resolver:rejecter:)
262
+ func recordMetric(
263
+ _ value: Double,
264
+ channel: Double,
265
+ resolver resolve: RCTPromiseResolveBlock,
266
+ rejecter reject: RCTPromiseRejectBlock
267
+ ) {
268
+ do {
269
+ try AnsightRuntime.shared.metric(Int64(value), channel: Int(channel))
270
+ resolve(snapshotDictionary())
271
+ } catch {
272
+ reject("ansight_error", error.localizedDescription, error)
273
+ }
274
+ }
275
+
276
+ @objc(recordEvent:resolver:rejecter:)
277
+ func recordEvent(
278
+ _ input: NSDictionary,
279
+ resolver resolve: RCTPromiseResolveBlock,
280
+ rejecter reject: RCTPromiseRejectBlock
281
+ ) {
282
+ do {
283
+ try AnsightRuntime.shared.event(
284
+ stringValue(input, "label") ?? "",
285
+ type: eventType(stringValue(input, "type")),
286
+ details: stringValue(input, "details"),
287
+ channel: intValue(input, "channel", defaultValue: AnsightChannels.unspecified)
288
+ )
289
+ resolve(snapshotDictionary())
290
+ } catch {
291
+ reject("ansight_error", error.localizedDescription, error)
292
+ }
293
+ }
294
+
295
+ @objc(screenViewed:details:resolver:rejecter:)
296
+ func screenViewed(
297
+ _ name: NSString,
298
+ details: NSDictionary?,
299
+ resolver resolve: RCTPromiseResolveBlock,
300
+ rejecter reject: RCTPromiseRejectBlock
301
+ ) {
302
+ do {
303
+ try AnsightRuntime.shared.screenViewed(name as String, details: stringDictionary(details))
304
+ resolve(snapshotDictionary())
305
+ } catch {
306
+ reject("ansight_error", error.localizedDescription, error)
307
+ }
308
+ }
309
+
310
+ @objc(setAppLifecycleState:resolver:rejecter:)
311
+ func setAppLifecycleState(
312
+ _ state: NSString,
313
+ resolver resolve: RCTPromiseResolveBlock,
314
+ rejecter reject: RCTPromiseRejectBlock
315
+ ) {
316
+ AnsightRuntime.shared.setAppLifecycleState(lifecycleState(state as String))
317
+ resolve(snapshotDictionary())
318
+ }
319
+
320
+ @objc(connect:options:resolver:rejecter:)
321
+ func connect(
322
+ _ pairingPayload: NSString?,
323
+ options: NSDictionary?,
324
+ resolver resolve: @escaping RCTPromiseResolveBlock,
325
+ rejecter reject: @escaping RCTPromiseRejectBlock
326
+ ) {
327
+ Task {
328
+ let clientName = stringValue(options, "clientName")
329
+ let expectedAppId = stringValue(options, "expectedAppId")
330
+ let hostAddressOverride = stringValue(options, "hostAddressOverride")
331
+ let request: HostConnectionRequest
332
+ if let payload = pairingPayload as String?, !payload.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
333
+ request = .payloadText(
334
+ payload,
335
+ clientName: clientName,
336
+ expectedAppId: expectedAppId,
337
+ hostAddressOverride: hostAddressOverride,
338
+ sourceDescription: "React Native"
339
+ )
340
+ } else {
341
+ request = .auto(
342
+ clientName: clientName,
343
+ expectedAppId: expectedAppId,
344
+ hostAddressOverride: hostAddressOverride,
345
+ sourceDescription: "React Native"
346
+ )
347
+ }
348
+ let result = await AnsightRuntime.shared.connect(request)
349
+ resolve(hostConnectionResultDictionary(result))
350
+ }
351
+ }
352
+
353
+ @objc(openSession:options:resolver:rejecter:)
354
+ func openSession(
355
+ _ pairingPayload: NSString?,
356
+ options: NSDictionary?,
357
+ resolver resolve: @escaping RCTPromiseResolveBlock,
358
+ rejecter reject: @escaping RCTPromiseRejectBlock
359
+ ) {
360
+ Task {
361
+ do {
362
+ let result = try await AnsightRuntime.shared.openLiveSession(
363
+ pairingJson: pairingPayload as String? ?? "",
364
+ options: pairingOpenOptions(options)
365
+ )
366
+ resolve(openSessionResultDictionary(result))
367
+ } catch {
368
+ reject("ansight_error", error.localizedDescription, error)
369
+ }
370
+ }
371
+ }
372
+
373
+ @objc(disconnect:rejecter:)
374
+ func disconnect(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
375
+ Task {
376
+ let result = await AnsightRuntime.shared.disconnect()
377
+ resolve(hostConnectionResultDictionary(result))
378
+ }
379
+ }
380
+
381
+ @objc(completeSession:rejecter:)
382
+ func completeSession(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
383
+ Task {
384
+ let result = await AnsightRuntime.shared.completeLiveSession()
385
+ resolve(operationResultDictionary(result))
386
+ }
387
+ }
388
+
389
+ @objc(closeSession:rejecter:)
390
+ func closeSession(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
391
+ AnsightRuntime.shared.closeSession()
392
+ resolve(operationResultDictionary(.success("Session closed.")))
393
+ }
394
+
395
+ @objc(savePairingConfig:options:resolver:rejecter:)
396
+ func savePairingConfig(
397
+ _ pairingPayload: NSString?,
398
+ options: NSDictionary?,
399
+ resolver resolve: RCTPromiseResolveBlock,
400
+ rejecter reject: RCTPromiseRejectBlock
401
+ ) {
402
+ let result = AnsightRuntime.shared.savePairingConfig(
403
+ pairingPayload as String? ?? "",
404
+ expectedAppId: stringValue(options, "expectedAppId")
405
+ )
406
+ resolve(hostConnectionResultDictionary(result))
407
+ }
408
+
409
+ @objc(clearSavedPairing:rejecter:)
410
+ func clearSavedPairing(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
411
+ AnsightRuntime.shared.clearSavedPairing()
412
+ resolve(hostConnectionResultDictionary(HostConnectionResult(
413
+ success: true,
414
+ message: "Saved pairing config cleared.",
415
+ kind: .savedConfig,
416
+ source: .savedConfig
417
+ )))
418
+ }
419
+
420
+ @objc(clearCachedSession:rejecter:)
421
+ func clearCachedSession(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
422
+ AnsightRuntime.shared.clearCachedSession()
423
+ resolve(operationResultDictionary(.success("Cached live session cleared.")))
424
+ }
425
+
426
+ @objc(notifyHostConnectionConfigChanged:rejecter:)
427
+ func notifyHostConnectionConfigChanged(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
428
+ resolve(hostConnectionResultDictionary(AnsightRuntime.shared.notifyHostConnectionConfigChanged()))
429
+ }
430
+
431
+ @objc(status:rejecter:)
432
+ func status(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
433
+ resolve(snapshotDictionary())
434
+ }
435
+
436
+ @objc(snapshot:rejecter:)
437
+ func snapshot(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
438
+ resolve(snapshotDictionary())
439
+ }
440
+
441
+ @objc(hostConnectionStatus:rejecter:)
442
+ func hostConnectionStatus(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
443
+ resolve(hostConnectionStatusDictionary(AnsightRuntime.shared.hostConnectionStatus()))
444
+ }
445
+
446
+ @objc(hostConnectionCapabilities:rejecter:)
447
+ func hostConnectionCapabilities(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
448
+ resolve(hostConnectionCapabilitiesDictionary(AnsightRuntime.shared.hostConnectionCapabilities()))
449
+ }
450
+
451
+ @objc(currentOptions:rejecter:)
452
+ func currentOptions(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
453
+ resolve(optionsDictionary(AnsightRuntime.shared.currentOptions()))
454
+ }
455
+
456
+ @objc(recordedMetrics:resolver:rejecter:)
457
+ func recordedMetrics(
458
+ _ limit: NSNumber,
459
+ resolver resolve: RCTPromiseResolveBlock,
460
+ rejecter reject: RCTPromiseRejectBlock
461
+ ) {
462
+ let metrics = AnsightRuntime.shared.recordedMetrics()
463
+ let count = max(0, limit.intValue)
464
+ resolve((count > 0 ? Array(metrics.suffix(count)) : metrics).map(metricDictionary))
465
+ }
466
+
467
+ @objc(recordedEvents:resolver:rejecter:)
468
+ func recordedEvents(
469
+ _ limit: NSNumber,
470
+ resolver resolve: RCTPromiseResolveBlock,
471
+ rejecter reject: RCTPromiseRejectBlock
472
+ ) {
473
+ let events = AnsightRuntime.shared.recordedEvents()
474
+ let count = max(0, limit.intValue)
475
+ resolve((count > 0 ? Array(events.suffix(count)) : events).map(eventDictionary))
476
+ }
477
+
478
+ @objc(sendClientLog:resolver:rejecter:)
479
+ func sendClientLog(
480
+ _ line: NSString,
481
+ resolver resolve: @escaping RCTPromiseResolveBlock,
482
+ rejecter reject: @escaping RCTPromiseRejectBlock
483
+ ) {
484
+ Task {
485
+ let result = await AnsightRuntime.shared.sendClientLog(line as String)
486
+ resolve(operationResultDictionary(result))
487
+ }
488
+ }
489
+
490
+ @objc(captureBuiltInTelemetrySample:rejecter:)
491
+ func captureBuiltInTelemetrySample(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
492
+ AnsightRuntime.shared.captureBuiltInTelemetrySample()
493
+ resolve(snapshotDictionary())
494
+ }
495
+
496
+ @objc(isFramesPerSecondEnabled:rejecter:)
497
+ func isFramesPerSecondEnabled(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
498
+ resolve(AnsightRuntime.shared.isFramesPerSecondEnabled)
499
+ }
500
+
501
+ @objc(enableFramesPerSecond:rejecter:)
502
+ func enableFramesPerSecond(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
503
+ AnsightRuntime.shared.enableFramesPerSecond()
504
+ resolve(snapshotDictionary())
505
+ }
506
+
507
+ @objc(disableFramesPerSecond:rejecter:)
508
+ func disableFramesPerSecond(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
509
+ AnsightRuntime.shared.disableFramesPerSecond()
510
+ resolve(snapshotDictionary())
511
+ }
512
+
513
+ @objc(captureScreenFrame:resolver:rejecter:)
514
+ func captureScreenFrame(
515
+ _ options: NSDictionary?,
516
+ resolver resolve: @escaping RCTPromiseResolveBlock,
517
+ rejecter reject: @escaping RCTPromiseRejectBlock
518
+ ) {
519
+ Task {
520
+ let result = await AnsightRuntime.shared.captureScreenFrame(options: screenCaptureOptions(options))
521
+ resolve(operationResultDictionary(result))
522
+ }
523
+ }
524
+
525
+ @objc(enableTouchCapture:rejecter:)
526
+ func enableTouchCapture(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
527
+ AnsightRuntime.shared.enableTouchCapture()
528
+ resolve(snapshotDictionary())
529
+ }
530
+
531
+ @objc(disableTouchCapture:rejecter:)
532
+ func disableTouchCapture(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
533
+ AnsightRuntime.shared.disableTouchCapture()
534
+ resolve(snapshotDictionary())
535
+ }
536
+
537
+ @objc(updateSessionProperties:resolver:rejecter:)
538
+ func updateSessionProperties(
539
+ _ properties: NSDictionary?,
540
+ resolver resolve: @escaping RCTPromiseResolveBlock,
541
+ rejecter reject: @escaping RCTPromiseRejectBlock
542
+ ) {
543
+ Task {
544
+ let result = await AnsightRuntime.shared.updateSessionProperties(groupedStringDictionary(properties))
545
+ resolve(operationResultDictionary(result))
546
+ }
547
+ }
548
+
549
+ @objc(clearSessionProperties:rejecter:)
550
+ func clearSessionProperties(
551
+ _ resolve: @escaping RCTPromiseResolveBlock,
552
+ rejecter reject: @escaping RCTPromiseRejectBlock
553
+ ) {
554
+ Task {
555
+ let result = await AnsightRuntime.shared.clearSessionProperties()
556
+ resolve(operationResultDictionary(result))
557
+ }
558
+ }
559
+
560
+ @objc(registerCustomProperty:key:value:resolver:rejecter:)
561
+ func registerCustomProperty(
562
+ _ group: NSString,
563
+ key: NSString,
564
+ value: NSString,
565
+ resolver resolve: @escaping RCTPromiseResolveBlock,
566
+ rejecter reject: @escaping RCTPromiseRejectBlock
567
+ ) {
568
+ Task {
569
+ let normalizedGroup = (group as String).trimmingCharacters(in: .whitespacesAndNewlines)
570
+ let normalizedKey = (key as String).trimmingCharacters(in: .whitespacesAndNewlines)
571
+ guard !normalizedGroup.isEmpty else {
572
+ resolve(operationResultDictionary(.failure("Custom property group must not be blank.")))
573
+ return
574
+ }
575
+ guard !normalizedKey.isEmpty else {
576
+ resolve(operationResultDictionary(.failure("Custom property key must not be blank.")))
577
+ return
578
+ }
579
+ var properties = AnsightRuntime.shared.currentOptions().customProperties
580
+ var groupProperties = properties[normalizedGroup] ?? [:]
581
+ groupProperties[normalizedKey] = (value as String).trimmingCharacters(in: .whitespacesAndNewlines)
582
+ properties[normalizedGroup] = groupProperties
583
+ let result = await AnsightRuntime.shared.updateSessionProperties(properties)
584
+ resolve(operationResultDictionary(result))
585
+ }
586
+ }
587
+
588
+ @objc(removeCustomProperty:key:resolver:rejecter:)
589
+ func removeCustomProperty(
590
+ _ group: NSString,
591
+ key: NSString,
592
+ resolver resolve: @escaping RCTPromiseResolveBlock,
593
+ rejecter reject: @escaping RCTPromiseRejectBlock
594
+ ) {
595
+ Task {
596
+ let normalizedGroup = (group as String).trimmingCharacters(in: .whitespacesAndNewlines)
597
+ let normalizedKey = (key as String).trimmingCharacters(in: .whitespacesAndNewlines)
598
+ guard !normalizedGroup.isEmpty else {
599
+ resolve(operationResultDictionary(.failure("Custom property group must not be blank.")))
600
+ return
601
+ }
602
+ guard !normalizedKey.isEmpty else {
603
+ resolve(operationResultDictionary(.failure("Custom property key must not be blank.")))
604
+ return
605
+ }
606
+ var properties = AnsightRuntime.shared.currentOptions().customProperties
607
+ if var groupProperties = properties[normalizedGroup] {
608
+ groupProperties.removeValue(forKey: normalizedKey)
609
+ if groupProperties.isEmpty {
610
+ properties.removeValue(forKey: normalizedGroup)
611
+ } else {
612
+ properties[normalizedGroup] = groupProperties
613
+ }
614
+ }
615
+ let result = await AnsightRuntime.shared.updateSessionProperties(properties)
616
+ resolve(operationResultDictionary(result))
617
+ }
618
+ }
619
+
620
+ @objc(registerCustomTool:resolver:rejecter:)
621
+ func registerCustomTool(
622
+ _ definition: NSDictionary,
623
+ resolver resolve: RCTPromiseResolveBlock,
624
+ rejecter reject: RCTPromiseRejectBlock
625
+ ) {
626
+ do {
627
+ let descriptor = try toolDescriptor(definition)
628
+ let timeout = max(250, intValue(definition, "timeoutMilliseconds", defaultValue: 30_000))
629
+ _ = lock.withLock {
630
+ activeCustomToolIds.insert(descriptor.id)
631
+ }
632
+ try AnsightRuntime.shared.registerTool(
633
+ ReactNativeTool(descriptor: descriptor, module: self, timeoutMilliseconds: timeout),
634
+ replaceExisting: true
635
+ )
636
+ resolve(["id": descriptor.id, "registered": true])
637
+ } catch {
638
+ reject("ansight_error", error.localizedDescription, error)
639
+ }
640
+ }
641
+
642
+ @objc(unregisterCustomTool:resolver:rejecter:)
643
+ func unregisterCustomTool(
644
+ _ toolId: NSString,
645
+ resolver resolve: RCTPromiseResolveBlock,
646
+ rejecter reject: RCTPromiseRejectBlock
647
+ ) {
648
+ let id = (toolId as String).trimmingCharacters(in: .whitespacesAndNewlines)
649
+ _ = lock.withLock {
650
+ activeCustomToolIds.remove(id)
651
+ }
652
+ resolve(["id": id, "registered": false])
653
+ }
654
+
655
+ @objc(clearRegisteredCustomTools:rejecter:)
656
+ func clearRegisteredCustomTools(
657
+ _ resolve: RCTPromiseResolveBlock,
658
+ rejecter reject: RCTPromiseRejectBlock
659
+ ) {
660
+ _ = lock.withLock {
661
+ activeCustomToolIds.removeAll()
662
+ }
663
+ resolve(["cleared": true])
664
+ }
665
+
666
+ @objc(resolveToolCall:result:resolver:rejecter:)
667
+ func resolveToolCall(
668
+ _ requestId: NSString,
669
+ result: NSDictionary,
670
+ resolver resolve: RCTPromiseResolveBlock,
671
+ rejecter reject: RCTPromiseRejectBlock
672
+ ) {
673
+ let id = requestId as String
674
+ let pending = lock.withLock { pendingToolCalls[id] }
675
+ guard let pending else {
676
+ resolve(["requestId": id, "accepted": false])
677
+ return
678
+ }
679
+
680
+ let success = boolValue(result, "success", defaultValue: true)
681
+ let message = stringValue(result, "message")
682
+ let errorCode = stringValue(result, "errorCode")
683
+ let payload = jsonValue(result["result"])
684
+ pending.result = success
685
+ ? .success(payload, message: message)
686
+ : .failure(message ?? "JavaScript tool failed.", errorCode: errorCode, result: payload)
687
+ pending.semaphore.signal()
688
+ resolve(["requestId": id, "accepted": true])
689
+ }
690
+
691
+ @objc(queueBinaryTransfer:base64Data:chunkBytes:resolver:rejecter:)
692
+ func queueBinaryTransfer(
693
+ _ requestId: NSString,
694
+ base64Data: NSString,
695
+ chunkBytes: NSNumber,
696
+ resolver resolve: RCTPromiseResolveBlock,
697
+ rejecter reject: RCTPromiseRejectBlock
698
+ ) {
699
+ let normalizedRequestId = (requestId as String).trimmingCharacters(in: .whitespacesAndNewlines)
700
+ guard !normalizedRequestId.isEmpty else {
701
+ resolve([
702
+ "success": false,
703
+ "message": "Binary transfer requires a live tool request id.",
704
+ "errorCode": "artifact_request_unavailable",
705
+ ])
706
+ return
707
+ }
708
+
709
+ guard let data = Data(base64Encoded: base64Data as String) else {
710
+ resolve([
711
+ "success": false,
712
+ "message": "Binary transfer payload must be base64 encoded.",
713
+ "errorCode": "artifact_payload_invalid",
714
+ ])
715
+ return
716
+ }
717
+
718
+ let transferId = UUID()
719
+ let normalizedChunkBytes = min(max(chunkBytes.intValue, 1_024), 512 * 1_024)
720
+ let result = AnsightRuntime.shared.queueBinaryTransfer(
721
+ requestId: normalizedRequestId,
722
+ transferId: transferId,
723
+ data: data,
724
+ chunkBytes: normalizedChunkBytes,
725
+ description: "react-native-artifact:\(transferId.uuidString.replacingOccurrences(of: "-", with: "").lowercased())"
726
+ )
727
+ var payload: [String: Any] = [
728
+ "success": result.success,
729
+ "message": result.message,
730
+ ]
731
+ payload["transferId"] = transferId.uuidString.replacingOccurrences(of: "-", with: "").lowercased()
732
+ payload["deliveryMode"] = "websocket_binary"
733
+ payload["wireProtocol"] = PairingFileTransferWireProtocol.protocolName
734
+ payload["status"] = result.success ? "queued" : "failed"
735
+ payload["chunkBytes"] = normalizedChunkBytes
736
+ payload["sizeBytes"] = data.count
737
+ if !result.success {
738
+ payload["errorCode"] = "artifact_transfer_unavailable"
739
+ }
740
+ resolve(payload)
741
+ }
742
+
743
+ fileprivate func executeJavaScriptTool(
744
+ toolId: String,
745
+ arguments: [String: String],
746
+ timeoutMilliseconds: Int
747
+ ) -> AnsightToolExecutionResult {
748
+ let requestId = "ios.\(UUID().uuidString.replacingOccurrences(of: "-", with: ""))"
749
+ let pending = PendingToolCall()
750
+ let canCallJavaScript = lock.withLock { () -> Bool in
751
+ guard activeCustomToolIds.contains(toolId) else {
752
+ return false
753
+ }
754
+ pendingToolCalls[requestId] = pending
755
+ return hasListeners
756
+ }
757
+
758
+ guard canCallJavaScript else {
759
+ _ = lock.withLock {
760
+ pendingToolCalls.removeValue(forKey: requestId)
761
+ }
762
+ return .failure("React Native JavaScript bridge is not listening for Ansight tool calls.", errorCode: "javascript_bridge_unavailable")
763
+ }
764
+
765
+ DispatchQueue.main.async {
766
+ var body: [String: Any] = [
767
+ "requestId": requestId,
768
+ "toolId": toolId,
769
+ "platform": "ios",
770
+ "arguments": arguments,
771
+ ]
772
+ if let nativeRequestId = arguments[AnsightToolExecutionArgumentNames.requestId] {
773
+ body["nativeRequestId"] = nativeRequestId
774
+ }
775
+ if let sessionId = arguments[AnsightToolExecutionArgumentNames.sessionId] {
776
+ body["sessionId"] = sessionId
777
+ }
778
+
779
+ self.sendEvent(
780
+ withName: "AnsightToolCall",
781
+ body: body
782
+ )
783
+ }
784
+
785
+ let deadline = DispatchTime.now() + .milliseconds(timeoutMilliseconds)
786
+ guard pending.semaphore.wait(timeout: deadline) == .success else {
787
+ _ = lock.withLock {
788
+ pendingToolCalls.removeValue(forKey: requestId)
789
+ }
790
+ return .failure("JavaScript handler for tool '\(toolId)' timed out.", errorCode: "javascript_tool_timeout")
791
+ }
792
+
793
+ _ = lock.withLock {
794
+ pendingToolCalls.removeValue(forKey: requestId)
795
+ }
796
+ return pending.result ?? .failure("JavaScript handler for tool '\(toolId)' returned no result.", errorCode: "javascript_tool_empty_result")
797
+ }
798
+
799
+ private func emitLogEvent(level: AnsightLogLevel, message: String, error: Error?) {
800
+ let shouldEmit = lock.withLock { hasListeners }
801
+ guard shouldEmit else {
802
+ return
803
+ }
804
+
805
+ var body: [String: Any] = [
806
+ "level": level.rawValue,
807
+ "message": message,
808
+ "platform": "ios",
809
+ ]
810
+ if let error {
811
+ body["error"] = error.localizedDescription
812
+ }
813
+
814
+ DispatchQueue.main.async {
815
+ self.sendEvent(withName: "AnsightLog", body: body)
816
+ }
817
+ }
818
+
819
+ private func configureReactNativeMemoryProfiling(_ dictionary: NSDictionary?) throws {
820
+ let options = ReactNativeMemoryProfilingOptions(dictionary: dictionary)
821
+ currentReactNativeMemoryOptions = options
822
+
823
+ guard options.enabled, let sampler = reactNativeMemorySamplerBox() else {
824
+ return
825
+ }
826
+
827
+ sampler.attach(to: bridge)
828
+ if options.jsHeapUsed {
829
+ try AnsightRuntime.shared.registerMetricStream(
830
+ AnsightMetricStream(channel: ReactNativeMemoryChannels.jsHeapUsed) {
831
+ sampler.sample(selectorName: "jsHeapUsedBytes")
832
+ }
833
+ )
834
+ }
835
+ if options.jsHeapTotal {
836
+ try AnsightRuntime.shared.registerMetricStream(
837
+ AnsightMetricStream(channel: ReactNativeMemoryChannels.jsHeapTotal) {
838
+ sampler.sample(selectorName: "jsHeapTotalBytes")
839
+ }
840
+ )
841
+ }
842
+ }
843
+
844
+ private func reactNativeMemorySamplerBox() -> ReactNativeMemorySamplerBox? {
845
+ if let reactNativeMemorySampler {
846
+ return reactNativeMemorySampler
847
+ }
848
+
849
+ guard let samplerType = NSClassFromString("AnsightReactNativeMemorySampler") as? NSObject.Type else {
850
+ return nil
851
+ }
852
+
853
+ let sampler = ReactNativeMemorySamplerBox(sampler: samplerType.init())
854
+ reactNativeMemorySampler = sampler
855
+ return sampler
856
+ }
857
+
858
+ private func buildOptions(_ dictionary: NSDictionary?) throws -> AnsightOptions {
859
+ let useNativeAllInOneDefaults = boolValue(dictionary, "useNativeAllInOneDefaults", defaultValue: false)
860
+ var options = useNativeAllInOneDefaults ? AnsightOptions.ansightDeveloperDefaults : AnsightOptions()
861
+
862
+ if let value = stringValue(dictionary, "pairingConfigJson") {
863
+ if useNativeAllInOneDefaults {
864
+ options.hostConnection.bundledDeveloperConfigJson = value
865
+ } else {
866
+ options.hostConnection.bundledConfigJson = value
867
+ }
868
+ }
869
+ if useNativeAllInOneDefaults && stringValue(dictionary, "toolGuard") == nil {
870
+ options.toolGuard = .readOnly
871
+ }
872
+ if let value = stringValue(dictionary, "clientName") {
873
+ options.hostAutoProbe.clientName = value
874
+ }
875
+ if hasNumber(dictionary, "sampleFrequencyMilliseconds") {
876
+ options.sampleFrequencyMilliseconds = intValue(dictionary, "sampleFrequencyMilliseconds", defaultValue: options.sampleFrequencyMilliseconds)
877
+ }
878
+ if hasNumber(dictionary, "retentionPeriodSeconds") {
879
+ options.retentionPeriodSeconds = intValue(dictionary, "retentionPeriodSeconds", defaultValue: options.retentionPeriodSeconds)
880
+ }
881
+ if hasBool(dictionary, "enableFramesPerSecond") {
882
+ options.enableFramesPerSecond = boolValue(dictionary, "enableFramesPerSecond", defaultValue: options.enableFramesPerSecond)
883
+ }
884
+ if hasBool(dictionary, "enableBatteryLevel") {
885
+ options.enableBatteryLevel = boolValue(dictionary, "enableBatteryLevel", defaultValue: options.enableBatteryLevel)
886
+ }
887
+ if let memory = dictionary?["defaultMemoryChannels"] as? NSDictionary {
888
+ var channels: DefaultMemoryChannels = []
889
+ if boolValue(memory, "managedHeap", defaultValue: boolValue(memory, "javaHeap", defaultValue: false)) {
890
+ channels.insert(.managedHeap)
891
+ }
892
+ if boolValue(memory, "nativeHeap", defaultValue: false) {
893
+ channels.insert(.nativeHeap)
894
+ }
895
+ if boolValue(memory, "residentSetSize", defaultValue: boolValue(memory, "rss", defaultValue: false)) {
896
+ channels.insert(.residentSetSize)
897
+ }
898
+ if boolValue(memory, "physicalFootprint", defaultValue: boolValue(memory, "rss", defaultValue: false)) {
899
+ channels.insert(.physicalFootprint)
900
+ }
901
+ options.defaultMemoryChannels = channels
902
+ }
903
+ if let channels = dictionary?["additionalChannels"] as? [NSDictionary] {
904
+ options.additionalChannels = channels.map {
905
+ AnsightChannel(
906
+ id: intValue($0, "id", defaultValue: -1),
907
+ name: stringValue($0, "name") ?? "",
908
+ colorHex: stringValue($0, "colorHex"),
909
+ unit: stringValue($0, "unit"),
910
+ type: stringValue($0, "type") ?? "custom",
911
+ source: stringValue($0, "source"),
912
+ group: stringValue($0, "group"),
913
+ kind: stringValue($0, "kind")
914
+ )
915
+ }
916
+ }
917
+ if let raw = dictionary?["sessionJpegCapture"] {
918
+ if let enabled = raw as? Bool, enabled == false {
919
+ options.sessionJpegCapture = nil
920
+ } else if let jpeg = raw as? NSDictionary {
921
+ options.sessionJpegCapture = AnsightSessionJpegCaptureOptions(
922
+ intervalMilliseconds: intValue(
923
+ jpeg,
924
+ "intervalMilliseconds",
925
+ defaultValue: AnsightSessionJpegCaptureOptions.defaultIntervalMilliseconds
926
+ ),
927
+ quality: intValue(jpeg, "quality", defaultValue: AnsightSessionJpegCaptureOptions.defaultQuality),
928
+ maxWidth: optionalInt(jpeg, "maxWidth") ?? AnsightSessionJpegCaptureOptions.defaultMaxWidth
929
+ )
930
+ }
931
+ }
932
+ if let raw = dictionary?["touchCapture"] {
933
+ if let enabled = raw as? Bool, enabled == false {
934
+ options.touchCapture = nil
935
+ } else if let touch = raw as? NSDictionary {
936
+ options.touchCapture = AnsightTouchCaptureOptions(
937
+ captureMoveEvents: boolValue(touch, "captureMoveEvents", defaultValue: true),
938
+ captureCancelEvents: boolValue(touch, "captureCancelEvents", defaultValue: true),
939
+ moveCaptureDistanceThreshold: doubleValue(touch, "moveCaptureDistanceThreshold", defaultValue: AnsightTouchCaptureOptions.defaultMoveCaptureDistanceThreshold),
940
+ moveCaptureFramesPerSecond: intValue(touch, "moveCaptureFramesPerSecond", defaultValue: AnsightTouchCaptureOptions.defaultMoveCaptureFramesPerSecond)
941
+ )
942
+ }
943
+ }
944
+ if let lifecycle = dictionary?["lifecycleCapture"] as? NSDictionary {
945
+ options.lifecycleCapture = AnsightLifecycleCaptureOptions(
946
+ enabled: boolValue(lifecycle, "enabled", defaultValue: options.lifecycleCapture.enabled),
947
+ captureAppLifecycle: boolValue(lifecycle, "captureAppLifecycle", defaultValue: options.lifecycleCapture.captureAppLifecycle),
948
+ captureScreenViews: boolValue(lifecycle, "captureScreenViews", defaultValue: options.lifecycleCapture.captureScreenViews),
949
+ minimumScreenViewIntervalMilliseconds: intValue(
950
+ lifecycle,
951
+ "minimumScreenViewIntervalMilliseconds",
952
+ defaultValue: options.lifecycleCapture.minimumScreenViewIntervalMilliseconds
953
+ )
954
+ )
955
+ }
956
+ if let guardName = stringValue(dictionary, "toolGuard") {
957
+ options.toolGuard = toolGuard(guardName)
958
+ }
959
+ if let properties = dictionary?["customProperties"] as? NSDictionary {
960
+ options.customProperties = groupedStringDictionary(properties)
961
+ }
962
+ if let autoProbe = dictionary?["hostAutoProbe"] as? NSDictionary {
963
+ options.hostAutoProbe = AnsightHostAutoProbeOptions(
964
+ enabled: boolValue(autoProbe, "enabled", defaultValue: options.hostAutoProbe.enabled),
965
+ initialDelayMilliseconds: intValue(autoProbe, "initialDelayMilliseconds", defaultValue: options.hostAutoProbe.initialDelayMilliseconds),
966
+ probeIntervalMilliseconds: intValue(autoProbe, "probeIntervalMilliseconds", defaultValue: options.hostAutoProbe.probeIntervalMilliseconds),
967
+ reconnectDelayMilliseconds: intValue(autoProbe, "reconnectDelayMilliseconds", defaultValue: options.hostAutoProbe.reconnectDelayMilliseconds),
968
+ clientName: stringValue(autoProbe, "clientName") ?? options.hostAutoProbe.clientName
969
+ )
970
+ }
971
+ if let host = dictionary?["hostConnection"] as? NSDictionary {
972
+ options.hostConnection = AnsightHostConnectionOptions(
973
+ savedConfigKey: stringValue(host, "savedConfigKey") ?? options.hostConnection.savedConfigKey,
974
+ connectionProfileRetentionSeconds: intValue(host, "connectionProfileRetentionSeconds", defaultValue: options.hostConnection.connectionProfileRetentionSeconds),
975
+ discoveryPort: optionalInt(host, "discoveryPort") ?? options.hostConnection.discoveryPort,
976
+ bundledDeveloperConfigJson: stringValue(host, "bundledDeveloperConfigJson") ?? options.hostConnection.bundledDeveloperConfigJson,
977
+ bundledConfigJson: stringValue(host, "bundledConfigJson") ?? options.hostConnection.bundledConfigJson
978
+ )
979
+ }
980
+ return try options.validated()
981
+ }
982
+
983
+ private func remoteToolOptions(_ dictionary: NSDictionary?) -> AnsightRemoteToolOptions {
984
+ let remoteTools = dictionary?["remoteTools"] as? NSDictionary
985
+ return AnsightRemoteToolOptions(
986
+ visualTree: toolSuiteEnabled(remoteTools?["visualTree"]),
987
+ database: databaseToolsOptions(remoteTools?["database"] as? NSDictionary),
988
+ fileSystem: fileSystemToolsOptions(remoteTools?["fileSystem"] as? NSDictionary),
989
+ preferences: preferencesToolsOptions(remoteTools?["preferences"] as? NSDictionary),
990
+ reflection: reflectionToolsOptions(remoteTools?["reflection"] as? NSDictionary),
991
+ secureStorage: secureStorageToolsOptions(
992
+ remoteTools?["secureStorage"] as? NSDictionary ?? dictionary?["secureStorage"] as? NSDictionary
993
+ )
994
+ )
995
+ }
996
+
997
+ private func toolSuiteEnabled(_ value: Any?) -> Bool {
998
+ if let enabled = value as? Bool {
999
+ return enabled
1000
+ }
1001
+ if let dictionary = value as? NSDictionary {
1002
+ return boolValue(dictionary, "enabled", defaultValue: true)
1003
+ }
1004
+ return false
1005
+ }
1006
+
1007
+ private func fileSystemToolsOptions(_ dictionary: NSDictionary?) -> AnsightFileSystemToolsOptions {
1008
+ AnsightFileSystemToolsOptions(
1009
+ additionalRoots: rootDictionaries(dictionary?["additionalRoots"]).map {
1010
+ AnsightFileSystemRoot(
1011
+ alias: stringValue($0, "alias") ?? "",
1012
+ path: stringValue($0, "path") ?? ""
1013
+ )
1014
+ }
1015
+ )
1016
+ }
1017
+
1018
+ private func databaseToolsOptions(_ dictionary: NSDictionary?) -> AnsightDatabaseToolsOptions {
1019
+ AnsightDatabaseToolsOptions(
1020
+ additionalRoots: rootDictionaries(dictionary?["additionalRoots"]).map {
1021
+ AnsightDatabaseRoot(
1022
+ alias: stringValue($0, "alias") ?? "",
1023
+ path: stringValue($0, "path") ?? ""
1024
+ )
1025
+ },
1026
+ includePlatformRoots: boolValue(dictionary, "includePlatformRoots", defaultValue: true)
1027
+ )
1028
+ }
1029
+
1030
+ private func preferencesToolsOptions(_ dictionary: NSDictionary?) -> AnsightPreferencesToolOptions {
1031
+ AnsightPreferencesToolOptions(
1032
+ defaultStore: stringValue(dictionary, "defaultStore"),
1033
+ allowedStores: stringArray(dictionary, "allowedStores"),
1034
+ allowedKeys: stringArray(dictionary, "allowedKeys"),
1035
+ allowedKeyPrefixes: stringArray(dictionary, "allowedKeyPrefixes")
1036
+ )
1037
+ }
1038
+
1039
+ private func reflectionToolsOptions(_ dictionary: NSDictionary?) -> AnsightReflectionToolsOptions {
1040
+ AnsightReflectionToolsOptions(
1041
+ includeBuiltInRoots: boolValue(dictionary, "includeBuiltInRoots", defaultValue: true),
1042
+ allowedRootIds: stringArray(dictionary, "allowedRootIds"),
1043
+ allowedTypePrefixes: stringArray(dictionary, "allowedTypePrefixes")
1044
+ )
1045
+ }
1046
+
1047
+ private func secureStorageToolsOptions(_ dictionary: NSDictionary?) -> AnsightSecureStorageToolsOptions {
1048
+ AnsightSecureStorageToolsOptions(
1049
+ appleService: stringValue(dictionary, "appleService"),
1050
+ allowedKeys: stringArray(dictionary, "allowedKeys"),
1051
+ allowedKeyPrefixes: stringArray(dictionary, "allowedKeyPrefixes") + stringArray(dictionary, "allowedPrefixes")
1052
+ )
1053
+ }
1054
+
1055
+ private func toolDescriptor(_ dictionary: NSDictionary) throws -> AnsightToolDescriptor {
1056
+ AnsightToolDescriptor(
1057
+ id: stringValue(dictionary, "id") ?? "",
1058
+ name: stringValue(dictionary, "name") ?? stringValue(dictionary, "id") ?? "",
1059
+ description: stringValue(dictionary, "description") ?? "",
1060
+ category: stringValue(dictionary, "category") ?? "custom",
1061
+ scope: toolScope(stringValue(dictionary, "scope")).rawValue,
1062
+ keywords: keywords(dictionary["keywords"]),
1063
+ security: toolSecurity(dictionary["security"] as? NSDictionary),
1064
+ argumentsSchema: AnsightToolSchema(json: jsonValue(dictionary["argumentsSchema"]) ?? .object([:])),
1065
+ resultSchema: AnsightToolSchema(json: jsonValue(dictionary["resultSchema"]) ?? .object([:]))
1066
+ )
1067
+ }
1068
+
1069
+ private func snapshotDictionary() -> NSDictionary {
1070
+ let snapshot = AnsightRuntime.shared.snapshot()
1071
+ var result: [String: Any] = [
1072
+ "initialized": snapshot.initialized,
1073
+ "active": snapshot.active,
1074
+ "sessionOpen": snapshot.sessionOpen,
1075
+ "lifecycleState": snapshot.lifecycleState.rawValue,
1076
+ "metricsRecorded": snapshot.metricsRecorded,
1077
+ "eventsRecorded": snapshot.eventsRecorded,
1078
+ "executableTools": snapshot.executableTools,
1079
+ "toolDiscoveryEnabled": snapshot.toolDiscoveryEnabled,
1080
+ "toolExecutionEnabled": snapshot.toolExecutionEnabled,
1081
+ "embeddedDeveloperPairingAvailable": snapshot.embeddedDeveloperPairingAvailable,
1082
+ "detectedBundledTools": snapshot.detectedBundledTools,
1083
+ "touchesRecorded": snapshot.touchesCaptured,
1084
+ "touchesCaptured": snapshot.touchesCaptured,
1085
+ "touchesSent": snapshot.touchesSent,
1086
+ "touchCaptureEnabled": snapshot.touchCaptureEnabled,
1087
+ "touchCaptureActive": snapshot.touchCaptureActive,
1088
+ "touchCaptureStreamingActive": snapshot.touchCaptureStreamingActive,
1089
+ "screenCaptureActive": snapshot.screenCaptureActive,
1090
+ "screenFramesCaptured": snapshot.screenFramesCaptured,
1091
+ "screenFramesSent": snapshot.screenFramesSent,
1092
+ "frameRateCaptureActive": snapshot.frameRateCaptureActive,
1093
+ "registeredTools": snapshot.registeredTools,
1094
+ "connectionStatus": hostConnectionStatusDictionary(snapshot.hostConnectionStatus),
1095
+ "channels": snapshot.channels.map(channelDictionary),
1096
+ ]
1097
+ if let metric = snapshot.lastMetric {
1098
+ result["lastMetric"] = metricDictionary(metric)
1099
+ }
1100
+ if let event = snapshot.lastEvent {
1101
+ result["lastEvent"] = eventDictionary(event)
1102
+ }
1103
+ if let message = snapshot.sessionMessage {
1104
+ result["sessionMessage"] = message
1105
+ }
1106
+ if let pairingConfigId = snapshot.lastPairingConfigId {
1107
+ result["lastPairingConfigId"] = pairingConfigId
1108
+ }
1109
+ if let hostAddress = snapshot.resolvedHostAddress {
1110
+ result["resolvedHostAddress"] = hostAddress
1111
+ }
1112
+ if let message = snapshot.lastScreenCaptureMessage {
1113
+ result["lastScreenCaptureMessage"] = message
1114
+ }
1115
+ if let frameRate = snapshot.lastFrameRate {
1116
+ result["lastFrameRate"] = frameRate
1117
+ }
1118
+ if let message = snapshot.lastTouchCaptureMessage {
1119
+ result["lastTouchCaptureMessage"] = message
1120
+ }
1121
+ if let screen = snapshot.currentScreen {
1122
+ result["currentScreen"] = [
1123
+ "name": screen.name,
1124
+ "capturedAtUtc": screen.capturedAtUtc,
1125
+ "details": screen.details,
1126
+ ]
1127
+ }
1128
+ return result as NSDictionary
1129
+ }
1130
+
1131
+ private func hostConnectionStatusDictionary(_ status: HostConnectionStatus) -> NSDictionary {
1132
+ [
1133
+ "isRuntimeActive": status.isRuntimeActive,
1134
+ "isConnected": status.isConnected,
1135
+ "connectionState": status.connectionState.rawValue,
1136
+ "hasCachedSession": status.hasCachedSession,
1137
+ "hasSavedConfig": status.hasSavedConfig,
1138
+ "hasBundledConfig": status.hasBundledConfig,
1139
+ "summaryKind": status.summaryKind.rawValue,
1140
+ "summaryMessage": status.summaryMessage,
1141
+ ] as NSDictionary
1142
+ }
1143
+
1144
+ private func hostConnectionCapabilitiesDictionary(_ capabilities: HostConnectionCapabilities) -> NSDictionary {
1145
+ [
1146
+ "canConnectUsingSavedConfig": capabilities.canConnectUsingSavedConfig,
1147
+ "canConnectUsingBundledConfig": capabilities.canConnectUsingBundledConfig,
1148
+ "canChooseConfigFile": capabilities.canChooseConfigFile,
1149
+ "canScanConfigQrCode": capabilities.canScanConfigQrCode,
1150
+ "canClearSavedConfigs": capabilities.canClearSavedConfigs,
1151
+ ] as NSDictionary
1152
+ }
1153
+
1154
+ private func hostConnectionResultDictionary(_ result: HostConnectionResult) -> NSDictionary {
1155
+ var dictionary: [String: Any] = [
1156
+ "success": result.success,
1157
+ "message": result.message,
1158
+ "kind": result.kind.rawValue,
1159
+ "source": result.source.rawValue,
1160
+ ]
1161
+ if let reasonCode = result.reasonCode ?? result.openSession?.reasonCode {
1162
+ dictionary["reasonCode"] = reasonCode
1163
+ }
1164
+ if let session = result.openSession {
1165
+ dictionary["accepted"] = session.accepted
1166
+ dictionary["usedEmbeddedDeveloperPairing"] = session.usedEmbeddedDeveloperPairing
1167
+ if let value = session.sessionId {
1168
+ dictionary["sessionId"] = value
1169
+ }
1170
+ if let value = session.configId {
1171
+ dictionary["configId"] = value
1172
+ }
1173
+ if let value = session.appId {
1174
+ dictionary["appId"] = value
1175
+ }
1176
+ if let value = session.resolvedHostAddress {
1177
+ dictionary["resolvedHostAddress"] = value
1178
+ }
1179
+ if let value = session.discoverySource {
1180
+ dictionary["discoverySource"] = value
1181
+ }
1182
+ if let value = session.hostId {
1183
+ dictionary["hostId"] = value
1184
+ }
1185
+ if let value = session.hostName {
1186
+ dictionary["hostName"] = value
1187
+ }
1188
+ }
1189
+ return dictionary as NSDictionary
1190
+ }
1191
+
1192
+ private func openSessionResultDictionary(_ result: OpenSessionResult) -> NSDictionary {
1193
+ var dictionary: [String: Any] = [
1194
+ "success": result.success,
1195
+ "message": result.message,
1196
+ "accepted": result.accepted,
1197
+ "usedEmbeddedDeveloperPairing": result.usedEmbeddedDeveloperPairing,
1198
+ ]
1199
+ if let value = result.sessionId {
1200
+ dictionary["sessionId"] = value
1201
+ }
1202
+ if let value = result.configId {
1203
+ dictionary["configId"] = value
1204
+ }
1205
+ if let value = result.appId {
1206
+ dictionary["appId"] = value
1207
+ }
1208
+ if let value = result.resolvedHostAddress {
1209
+ dictionary["resolvedHostAddress"] = value
1210
+ }
1211
+ if let value = result.discoverySource {
1212
+ dictionary["discoverySource"] = value
1213
+ }
1214
+ if let value = result.reasonCode {
1215
+ dictionary["reasonCode"] = value
1216
+ }
1217
+ if let value = result.hostId {
1218
+ dictionary["hostId"] = value
1219
+ }
1220
+ if let value = result.hostName {
1221
+ dictionary["hostName"] = value
1222
+ }
1223
+ return dictionary as NSDictionary
1224
+ }
1225
+
1226
+ private func operationResultDictionary(_ result: OperationResult) -> NSDictionary {
1227
+ [
1228
+ "success": result.success,
1229
+ "message": result.message,
1230
+ ] as NSDictionary
1231
+ }
1232
+
1233
+ private func screenCaptureOptions(_ dictionary: NSDictionary?) -> AnsightSessionJpegCaptureOptions? {
1234
+ guard let dictionary else {
1235
+ return nil
1236
+ }
1237
+ return AnsightSessionJpegCaptureOptions(
1238
+ intervalMilliseconds: intValue(
1239
+ dictionary,
1240
+ "intervalMilliseconds",
1241
+ defaultValue: AnsightSessionJpegCaptureOptions.defaultIntervalMilliseconds
1242
+ ),
1243
+ quality: intValue(dictionary, "quality", defaultValue: AnsightSessionJpegCaptureOptions.defaultQuality),
1244
+ maxWidth: optionalInt(dictionary, "maxWidth") ?? AnsightSessionJpegCaptureOptions.defaultMaxWidth
1245
+ )
1246
+ }
1247
+
1248
+ private func optionsDictionary(_ options: AnsightOptions) -> NSDictionary {
1249
+ var dictionary: [String: Any] = [
1250
+ "sampleFrequencyMilliseconds": options.sampleFrequencyMilliseconds,
1251
+ "retentionPeriodSeconds": options.retentionPeriodSeconds,
1252
+ "enableFramesPerSecond": options.enableFramesPerSecond,
1253
+ "enableBatteryLevel": options.enableBatteryLevel,
1254
+ "defaultMemoryChannels": [
1255
+ "managedHeap": options.defaultMemoryChannels.contains(.managedHeap),
1256
+ "javaHeap": options.defaultMemoryChannels.contains(.managedHeap),
1257
+ "nativeHeap": options.defaultMemoryChannels.contains(.nativeHeap),
1258
+ "residentSetSize": options.defaultMemoryChannels.contains(.residentSetSize),
1259
+ "rss": options.defaultMemoryChannels.contains(.residentSetSize),
1260
+ "physicalFootprint": options.defaultMemoryChannels.contains(.physicalFootprint),
1261
+ ],
1262
+ "reactNativeMemory": currentReactNativeMemoryOptions.dictionary,
1263
+ "additionalChannels": options.additionalChannels.map(channelDictionary),
1264
+ "toolGuard": toolGuardName(options.toolGuard),
1265
+ "customProperties": options.customProperties,
1266
+ "lifecycleCapture": [
1267
+ "enabled": options.lifecycleCapture.enabled,
1268
+ "captureAppLifecycle": options.lifecycleCapture.captureAppLifecycle,
1269
+ "captureScreenViews": options.lifecycleCapture.captureScreenViews,
1270
+ "minimumScreenViewIntervalMilliseconds": options.lifecycleCapture.minimumScreenViewIntervalMilliseconds,
1271
+ ],
1272
+ "hostAutoProbe": [
1273
+ "enabled": options.hostAutoProbe.enabled,
1274
+ "initialDelayMilliseconds": options.hostAutoProbe.initialDelayMilliseconds,
1275
+ "probeIntervalMilliseconds": options.hostAutoProbe.probeIntervalMilliseconds,
1276
+ "reconnectDelayMilliseconds": options.hostAutoProbe.reconnectDelayMilliseconds,
1277
+ "clientName": options.hostAutoProbe.clientName as Any,
1278
+ ],
1279
+ "hostConnection": [
1280
+ "savedConfigKey": options.hostConnection.savedConfigKey,
1281
+ "connectionProfileRetentionSeconds": options.hostConnection.connectionProfileRetentionSeconds,
1282
+ "discoveryPort": options.hostConnection.discoveryPort as Any,
1283
+ "hasBundledDeveloperConfigJson": options.hostConnection.bundledDeveloperConfigJson != nil,
1284
+ "hasBundledConfigJson": options.hostConnection.bundledConfigJson != nil,
1285
+ ],
1286
+ ]
1287
+ if let capture = options.sessionJpegCapture {
1288
+ dictionary["sessionJpegCapture"] = [
1289
+ "intervalMilliseconds": capture.intervalMilliseconds,
1290
+ "quality": capture.quality,
1291
+ "maxWidth": capture.maxWidth as Any,
1292
+ ]
1293
+ } else {
1294
+ dictionary["sessionJpegCapture"] = NSNull()
1295
+ }
1296
+ if let touch = options.touchCapture {
1297
+ dictionary["touchCapture"] = [
1298
+ "captureMoveEvents": touch.captureMoveEvents,
1299
+ "captureCancelEvents": touch.captureCancelEvents,
1300
+ "moveCaptureDistanceThreshold": touch.moveCaptureDistanceThreshold,
1301
+ "moveCaptureFramesPerSecond": touch.moveCaptureFramesPerSecond,
1302
+ ]
1303
+ } else {
1304
+ dictionary["touchCapture"] = NSNull()
1305
+ }
1306
+ return dictionary as NSDictionary
1307
+ }
1308
+
1309
+ private func pairingOpenOptions(_ dictionary: NSDictionary?) -> PairingOpenOptions {
1310
+ PairingOpenOptions(
1311
+ clientName: stringValue(dictionary, "clientName") ?? "React Native",
1312
+ expectedAppId: stringValue(dictionary, "expectedAppId"),
1313
+ hostAddressOverride: stringValue(dictionary, "hostAddressOverride"),
1314
+ discoveryPort: optionalInt(dictionary, "discoveryPort")
1315
+ )
1316
+ }
1317
+
1318
+ private func channelDictionary(_ channel: AnsightChannel) -> [String: Any] {
1319
+ var dictionary: [String: Any] = [
1320
+ "id": channel.id,
1321
+ "name": channel.name,
1322
+ "type": channel.type,
1323
+ ]
1324
+ if let unit = channel.unit {
1325
+ dictionary["unit"] = unit
1326
+ }
1327
+ if let color = channel.colorHex {
1328
+ dictionary["colorHex"] = color
1329
+ }
1330
+ if let source = channel.source {
1331
+ dictionary["source"] = source
1332
+ }
1333
+ if let group = channel.group {
1334
+ dictionary["group"] = group
1335
+ }
1336
+ if let kind = channel.kind {
1337
+ dictionary["kind"] = kind
1338
+ }
1339
+ return dictionary
1340
+ }
1341
+
1342
+ private func metricDictionary(_ metric: RecordedMetric) -> [String: Any] {
1343
+ [
1344
+ "value": metric.value,
1345
+ "capturedAtUtc": metric.capturedAtUtc,
1346
+ "capturedAtEpochMs": metric.capturedAtEpochMs,
1347
+ "channel": metric.channel,
1348
+ "sequence": metric.sequence,
1349
+ ]
1350
+ }
1351
+
1352
+ private func eventDictionary(_ event: RecordedEvent) -> [String: Any] {
1353
+ var dictionary: [String: Any] = [
1354
+ "id": event.id,
1355
+ "label": event.label,
1356
+ "type": eventTypeName(event.type),
1357
+ "capturedAtUtc": event.capturedAtUtc,
1358
+ "capturedAtEpochMs": event.capturedAtEpochMs,
1359
+ "channel": event.channel,
1360
+ "sequence": event.sequence,
1361
+ ]
1362
+ if let details = event.details {
1363
+ dictionary["details"] = details
1364
+ }
1365
+ if let externalId = event.externalId {
1366
+ dictionary["externalId"] = externalId
1367
+ }
1368
+ return dictionary
1369
+ }
1370
+
1371
+ private func eventTypeName(_ type: AnsightEventType) -> String {
1372
+ switch type {
1373
+ case .event:
1374
+ return "Event"
1375
+ case .debug:
1376
+ return "Debug"
1377
+ case .info:
1378
+ return "Info"
1379
+ case .warning:
1380
+ return "Warning"
1381
+ case .error:
1382
+ return "Error"
1383
+ case .exception:
1384
+ return "Exception"
1385
+ case .gc:
1386
+ return "Gc"
1387
+ case .navigation:
1388
+ return "Navigation"
1389
+ case .screenViewed:
1390
+ return "ScreenViewed"
1391
+ case .lifecycle:
1392
+ return "Lifecycle"
1393
+ }
1394
+ }
1395
+
1396
+ private func toolGuardName(_ guardPolicy: AnsightToolGuard) -> String {
1397
+ if guardPolicy == .disabled {
1398
+ return "disabled"
1399
+ }
1400
+ if guardPolicy == .readOnly {
1401
+ return "readOnly"
1402
+ }
1403
+ if guardPolicy == .readWrite {
1404
+ return "readWrite"
1405
+ }
1406
+ if guardPolicy == .fullAccess {
1407
+ return "fullAccess"
1408
+ }
1409
+ return "custom"
1410
+ }
1411
+ }
1412
+
1413
+ private extension NSLock {
1414
+ func withLock<T>(_ body: () -> T) -> T {
1415
+ lock()
1416
+ defer { unlock() }
1417
+ return body()
1418
+ }
1419
+ }
1420
+
1421
+ private func stringValue(_ dictionary: NSDictionary?, _ key: String) -> String? {
1422
+ guard let value = dictionary?[key], !(value is NSNull) else {
1423
+ return nil
1424
+ }
1425
+ if let string = value as? String {
1426
+ let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines)
1427
+ return trimmed.isEmpty ? nil : trimmed
1428
+ }
1429
+ return "\(value)"
1430
+ }
1431
+
1432
+ private func hasNumber(_ dictionary: NSDictionary?, _ key: String) -> Bool {
1433
+ dictionary?[key] is NSNumber
1434
+ }
1435
+
1436
+ private func hasBool(_ dictionary: NSDictionary?, _ key: String) -> Bool {
1437
+ guard let number = dictionary?[key] as? NSNumber else {
1438
+ return false
1439
+ }
1440
+ return CFGetTypeID(number) == CFBooleanGetTypeID()
1441
+ }
1442
+
1443
+ private func boolValue(_ dictionary: NSDictionary?, _ key: String, defaultValue: Bool) -> Bool {
1444
+ dictionary?[key] as? Bool ?? defaultValue
1445
+ }
1446
+
1447
+ private func intValue(_ dictionary: NSDictionary?, _ key: String, defaultValue: Int) -> Int {
1448
+ (dictionary?[key] as? NSNumber)?.intValue ?? defaultValue
1449
+ }
1450
+
1451
+ private func optionalInt(_ dictionary: NSDictionary?, _ key: String) -> Int? {
1452
+ (dictionary?[key] as? NSNumber)?.intValue
1453
+ }
1454
+
1455
+ private func doubleValue(_ dictionary: NSDictionary?, _ key: String, defaultValue: Double) -> Double {
1456
+ (dictionary?[key] as? NSNumber)?.doubleValue ?? defaultValue
1457
+ }
1458
+
1459
+ private func stringDictionary(_ dictionary: NSDictionary?) -> [String: String] {
1460
+ guard let dictionary else {
1461
+ return [:]
1462
+ }
1463
+ var result: [String: String] = [:]
1464
+ for (key, value) in dictionary {
1465
+ guard let key = key as? String, !(value is NSNull) else {
1466
+ continue
1467
+ }
1468
+ result[key] = value as? String ?? "\(value)"
1469
+ }
1470
+ return result
1471
+ }
1472
+
1473
+ private func stringArray(_ dictionary: NSDictionary?, _ key: String) -> [String] {
1474
+ guard let array = dictionary?[key] as? [Any] else {
1475
+ return []
1476
+ }
1477
+ return array.compactMap { value in
1478
+ if value is NSNull {
1479
+ return nil
1480
+ }
1481
+ let normalized = (value as? String ?? "\(value)").trimmingCharacters(in: .whitespacesAndNewlines)
1482
+ return normalized.isEmpty ? nil : normalized
1483
+ }
1484
+ }
1485
+
1486
+ private func rootDictionaries(_ value: Any?) -> [NSDictionary] {
1487
+ guard let array = value as? [Any] else {
1488
+ return []
1489
+ }
1490
+ return array.compactMap { $0 as? NSDictionary }
1491
+ }
1492
+
1493
+ private func groupedStringDictionary(_ dictionary: NSDictionary?) -> [String: [String: String]] {
1494
+ guard let dictionary else {
1495
+ return [:]
1496
+ }
1497
+ var result: [String: [String: String]] = [:]
1498
+ for (key, value) in dictionary {
1499
+ guard let key = key as? String, let group = value as? NSDictionary else {
1500
+ continue
1501
+ }
1502
+ result[key] = stringDictionary(group)
1503
+ }
1504
+ return result
1505
+ }
1506
+
1507
+ private func eventType(_ rawValue: String?) -> AnsightEventType {
1508
+ switch rawValue?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
1509
+ case "event":
1510
+ return .event
1511
+ case "debug":
1512
+ return .debug
1513
+ case "warning", "warn":
1514
+ return .warning
1515
+ case "error":
1516
+ return .error
1517
+ case "exception":
1518
+ return .exception
1519
+ case "gc":
1520
+ return .gc
1521
+ case "navigation":
1522
+ return .navigation
1523
+ case "screenviewed", "screen_viewed":
1524
+ return .screenViewed
1525
+ case "lifecycle":
1526
+ return .lifecycle
1527
+ default:
1528
+ return .info
1529
+ }
1530
+ }
1531
+
1532
+ private func lifecycleState(_ rawValue: String) -> AppLifecycleState {
1533
+ switch rawValue.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
1534
+ case "foreground", "active":
1535
+ return .foreground
1536
+ case "background", "inactive":
1537
+ return .background
1538
+ default:
1539
+ return .unknown
1540
+ }
1541
+ }
1542
+
1543
+ private func toolGuard(_ rawValue: String) -> AnsightToolGuard {
1544
+ switch rawValue.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
1545
+ case "readonly", "read_only", "read":
1546
+ return .readOnly
1547
+ case "readwrite", "read_write", "write":
1548
+ return .readWrite
1549
+ case "full", "fullaccess", "full_access":
1550
+ return .fullAccess
1551
+ default:
1552
+ return .disabled
1553
+ }
1554
+ }
1555
+
1556
+ private func toolScope(_ rawValue: String?) -> AnsightToolScope {
1557
+ switch rawValue?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
1558
+ case "write":
1559
+ return .write
1560
+ case "delete":
1561
+ return .delete
1562
+ default:
1563
+ return .read
1564
+ }
1565
+ }
1566
+
1567
+ private func toolSecurity(_ dictionary: NSDictionary?) -> AnsightToolSecurity {
1568
+ guard let dictionary else {
1569
+ return .unspecified
1570
+ }
1571
+ let level: AnsightToolSecurityLevel
1572
+ switch stringValue(dictionary, "level")?.lowercased() {
1573
+ case "low":
1574
+ level = .low
1575
+ case "medium", "moderate":
1576
+ level = .moderate
1577
+ case "high":
1578
+ level = .high
1579
+ case "critical":
1580
+ level = .critical
1581
+ default:
1582
+ level = .unspecified
1583
+ }
1584
+ return AnsightToolSecurity(
1585
+ level: level,
1586
+ summary: stringValue(dictionary, "summary") ?? "",
1587
+ implications: (dictionary["implications"] as? [Any])?.compactMap { $0 as? String } ?? []
1588
+ )
1589
+ }
1590
+
1591
+ private func keywords(_ value: Any?) -> String {
1592
+ if let string = value as? String {
1593
+ return string
1594
+ }
1595
+ if let array = value as? [Any] {
1596
+ return array.compactMap { $0 as? String }.joined(separator: " ")
1597
+ }
1598
+ return "react native custom tool"
1599
+ }
1600
+
1601
+ private func jsonValue(_ value: Any?) -> JSONValue? {
1602
+ guard let value, !(value is NSNull) else {
1603
+ return nil
1604
+ }
1605
+ if let dictionary = value as? NSDictionary {
1606
+ var object: [String: JSONValue] = [:]
1607
+ for (key, value) in dictionary {
1608
+ guard let key = key as? String, let converted = jsonValue(value) else {
1609
+ continue
1610
+ }
1611
+ object[key] = converted
1612
+ }
1613
+ return .object(object)
1614
+ }
1615
+ if let array = value as? [Any] {
1616
+ return .array(array.map { jsonValue($0) ?? .null })
1617
+ }
1618
+ if let string = value as? String {
1619
+ return .string(string)
1620
+ }
1621
+ if let number = value as? NSNumber {
1622
+ if CFGetTypeID(number) == CFBooleanGetTypeID() {
1623
+ return .bool(number.boolValue)
1624
+ }
1625
+ let double = number.doubleValue
1626
+ if double.rounded() == double {
1627
+ return .integer(number.int64Value)
1628
+ }
1629
+ return .number(double)
1630
+ }
1631
+ if let bool = value as? Bool {
1632
+ return .bool(bool)
1633
+ }
1634
+ return .string("\(value)")
1635
+ }