@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.
- package/AnsightReactNative.podspec +20 -0
- package/LICENSE +200 -0
- package/README.md +436 -0
- package/android/build.gradle +70 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/CMakeLists.txt +16 -0
- package/android/src/main/cpp/ansight_react_native_memory.cpp +57 -0
- package/android/src/main/kotlin/ai/ansight/reactnative/AnsightReactNativeModule.kt +1353 -0
- package/android/src/main/kotlin/ai/ansight/reactnative/AnsightReactNativePackage.kt +14 -0
- package/android/src/main/kotlin/ai/ansight/reactnative/ReactNativeMemoryProfiler.kt +167 -0
- package/index.d.ts +633 -0
- package/index.js +2303 -0
- package/ios/AnsightReactNative.swift +1635 -0
- package/ios/AnsightReactNativeMemorySampler.mm +130 -0
- package/ios/AnsightReactNativeModule.m +167 -0
- package/package.json +50 -0
- package/react-native.config.js +12 -0
- package/tsconfig.json +12 -0
|
@@ -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
|
+
}
|