@npaw/forge-react-native 9.7.2 → 9.7.4
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/android/build.gradle +1 -1
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/Info.plist +7 -7
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/forge_kernelFFI.h +60 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/forge_kernelFFI.h +60 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/forge_kernelFFI.h +60 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/forge_kernelFFI.h +60 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernel.swift +415 -1
- package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernelFFI.h +60 -0
- package/ios/ForgeApple/Sources/NPAWForgeAVPlayerAdapter/AVPlayerAdapter.swift +22 -1
- package/ios/ForgeApple/Sources/NPAWForgeCore/DeviceInsightsCollector.swift +494 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeApple.swift +51 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeCapturePlan.swift +14 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeRuntime.swift +48 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/NPAWForge.swift +49 -1
- package/ios/ForgeApple/Sources/NPAWForgeCore/NativeUniFFIRuntime.swift +77 -0
- package/package.json +1 -1
|
@@ -71,6 +71,18 @@ public protocol ForgeRuntime: AnyObject {
|
|
|
71
71
|
/// Forwards a location fix. Fixes only - the kernel owns every threshold,
|
|
72
72
|
/// so the three platforms cannot drift into different definitions of moving.
|
|
73
73
|
func recordLocationFix(_ fix: ForgeLocationFix) -> ForgeOutcome
|
|
74
|
+
/// Forwards a device runtime health sample. Every field is optional; the
|
|
75
|
+
/// kernel validates field-wise, derives `dev_cpu` between samples and
|
|
76
|
+
/// attaches the set to the next session beat and video ping. Refused
|
|
77
|
+
/// (`ignoredInvalidState`) while device insights are off remotely.
|
|
78
|
+
func recordDeviceSample(_ sample: ForgeDeviceSample) -> ForgeOutcome
|
|
79
|
+
/// Registers (or clears with `nil`) the receiver of the kernel's device
|
|
80
|
+
/// sample requests. The kernel owns the cadence: it asks for one sample per
|
|
81
|
+
/// tick with the operator's family allowlist (`cpu`, `mem`, `bat`, `thm`,
|
|
82
|
+
/// `disk`), and the host answers through ``recordDeviceSample(_:)``. A
|
|
83
|
+
/// request is not replayed to a late listener - the kernel asks again on
|
|
84
|
+
/// the next tick.
|
|
85
|
+
func setDeviceSampleListener(_ listener: (([String]) -> Void)?) -> ForgeOutcome
|
|
74
86
|
func createVideoView(timestampMs: Int64, options: VideoOptions) -> RuntimeVideoView
|
|
75
87
|
func recordEvent(_ event: ForgeRecordedEvent) -> ForgeOutcome
|
|
76
88
|
/// Records a typed identity transition (`loginSuccess`, `loginFailed`,
|
|
@@ -131,6 +143,12 @@ public extension ForgeRuntime {
|
|
|
131
143
|
func setAutoConnectionType(_ connectionType: String?) -> ForgeOutcome { .accepted }
|
|
132
144
|
/// Additive default: no kernel behind it means nothing to derive motion with.
|
|
133
145
|
func recordLocationFix(_ fix: ForgeLocationFix) -> ForgeOutcome { .accepted }
|
|
146
|
+
/// Additive default: a runtime with no kernel behind it has no sampler to
|
|
147
|
+
/// feed, so it accepts and forgets.
|
|
148
|
+
func recordDeviceSample(_ sample: ForgeDeviceSample) -> ForgeOutcome { .accepted }
|
|
149
|
+
/// Additive default: no kernel means no cadence, so registration succeeds
|
|
150
|
+
/// and simply never delivers a request.
|
|
151
|
+
func setDeviceSampleListener(_ listener: (([String]) -> Void)?) -> ForgeOutcome { .accepted }
|
|
134
152
|
}
|
|
135
153
|
|
|
136
154
|
public protocol ForgeRuntimeFactory: AnyObject {
|
|
@@ -148,6 +166,8 @@ public final class UnavailableForgeRuntime: ForgeRuntime {
|
|
|
148
166
|
public func setLogLevel(_ level: ForgeLogLevel) -> ForgeOutcome { .accepted }
|
|
149
167
|
public func setAutoConnectionType(_ connectionType: String?) -> ForgeOutcome { .accepted }
|
|
150
168
|
public func recordLocationFix(_ fix: ForgeLocationFix) -> ForgeOutcome { .accepted }
|
|
169
|
+
public func recordDeviceSample(_ sample: ForgeDeviceSample) -> ForgeOutcome { .accepted }
|
|
170
|
+
public func setDeviceSampleListener(_ listener: (([String]) -> Void)?) -> ForgeOutcome { .accepted }
|
|
151
171
|
public func createVideoView(timestampMs: Int64, options: VideoOptions) -> RuntimeVideoView { RuntimeVideoView(id: -1) }
|
|
152
172
|
public func recordEvent(_ event: ForgeRecordedEvent) -> ForgeOutcome { failed() }
|
|
153
173
|
public func recordIdentityEvent(kind: String, timemarkMs: Int64, fields: [String: String]) -> ForgeOutcome { failed() }
|
|
@@ -215,6 +235,14 @@ public final class RecordingForgeRuntime: ForgeRuntime {
|
|
|
215
235
|
public private(set) var publishedConnectionTypes: [String?] = []
|
|
216
236
|
/// Every location fix forwarded, in order.
|
|
217
237
|
public private(set) var recordedLocationFixes: [ForgeLocationFix] = []
|
|
238
|
+
/// Every device sample forwarded, in order.
|
|
239
|
+
public private(set) var recordedDeviceSamples: [ForgeDeviceSample] = []
|
|
240
|
+
/// The device sample request listener currently registered, `nil` once
|
|
241
|
+
/// cleared.
|
|
242
|
+
public private(set) var deviceSampleListener: (([String]) -> Void)?
|
|
243
|
+
/// Device samples arrive from the collector's utility queue while a test
|
|
244
|
+
/// reads them from the main thread; only those two members are guarded.
|
|
245
|
+
private let recordingLock = NSLock()
|
|
218
246
|
private var nextViewId: Int64 = 1
|
|
219
247
|
|
|
220
248
|
public init() {}
|
|
@@ -229,6 +257,26 @@ public final class RecordingForgeRuntime: ForgeRuntime {
|
|
|
229
257
|
recordedLocationFixes.append(fix)
|
|
230
258
|
return .accepted
|
|
231
259
|
}
|
|
260
|
+
public func recordDeviceSample(_ sample: ForgeDeviceSample) -> ForgeOutcome {
|
|
261
|
+
recordingLock.lock()
|
|
262
|
+
defer { recordingLock.unlock() }
|
|
263
|
+
recordedDeviceSamples.append(sample)
|
|
264
|
+
return .accepted
|
|
265
|
+
}
|
|
266
|
+
public func setDeviceSampleListener(_ listener: (([String]) -> Void)?) -> ForgeOutcome {
|
|
267
|
+
recordingLock.lock()
|
|
268
|
+
defer { recordingLock.unlock() }
|
|
269
|
+
deviceSampleListener = listener
|
|
270
|
+
return .accepted
|
|
271
|
+
}
|
|
272
|
+
/// Test hook mirroring the kernel's request push path: hands the family
|
|
273
|
+
/// allowlist to the registered listener, if any. Nothing is replayed.
|
|
274
|
+
public func emitDeviceSampleRequest(_ families: [String]) {
|
|
275
|
+
recordingLock.lock()
|
|
276
|
+
let listener = deviceSampleListener
|
|
277
|
+
recordingLock.unlock()
|
|
278
|
+
listener?(families)
|
|
279
|
+
}
|
|
232
280
|
public func createVideoView(timestampMs: Int64, options: VideoOptions) -> RuntimeVideoView {
|
|
233
281
|
createdVideoTimestamps.append(timestampMs)
|
|
234
282
|
defer { nextViewId += 1 }
|
|
@@ -40,7 +40,7 @@ public final class NPAWForge: NSObject {
|
|
|
40
40
|
/// `scripts/apple/publish-local-shipped.sh` rewrites this literal to the
|
|
41
41
|
/// release version when staging the shipped package, and fails loudly if
|
|
42
42
|
/// it cannot find it. Keep it a plain stored literal for that reason.
|
|
43
|
-
public static let defaultPluginVersion = "9.7.
|
|
43
|
+
public static let defaultPluginVersion = "9.7.4"
|
|
44
44
|
public static let defaultAppName = "apple"
|
|
45
45
|
public static let defaultBuildVariant = "release"
|
|
46
46
|
public static let defaultLmaEndpoint = "https://lma.npaw.com"
|
|
@@ -95,6 +95,11 @@ public final class NPAWForge: NSObject {
|
|
|
95
95
|
/// Set when the integrator allowed location observation. It reads nothing
|
|
96
96
|
/// until a capture plan carries the location bit.
|
|
97
97
|
private var locationObserver: ForgeLocationObserver?
|
|
98
|
+
/// Answers the kernel's device sample requests. Inert until an operator
|
|
99
|
+
/// enables device insights remotely; held so `destroy()` can stop it and
|
|
100
|
+
/// return battery monitoring to what it was.
|
|
101
|
+
private var deviceInsightsCollector: ForgeDeviceInsightsCollector?
|
|
102
|
+
private var deviceInsightsSubscriptions: [ForgeEventSubscription] = []
|
|
98
103
|
|
|
99
104
|
public init(kernel: ForgeApple) {
|
|
100
105
|
self.kernel = kernel
|
|
@@ -250,6 +255,10 @@ public final class NPAWForge: NSObject {
|
|
|
250
255
|
connectionTypeObserver = nil
|
|
251
256
|
locationObserver?.stop()
|
|
252
257
|
locationObserver = nil
|
|
258
|
+
deviceInsightsSubscriptions.forEach { $0.cancel() }
|
|
259
|
+
deviceInsightsSubscriptions.removeAll()
|
|
260
|
+
deviceInsightsCollector?.stop()
|
|
261
|
+
deviceInsightsCollector = nil
|
|
253
262
|
let adapters = videoAdapters
|
|
254
263
|
videoAdapters.removeAll()
|
|
255
264
|
adapters.forEach { _ = $0.destroy() }
|
|
@@ -270,6 +279,17 @@ public final class NPAWForge: NSObject {
|
|
|
270
279
|
kernel.recordLocationFix(fix)
|
|
271
280
|
}
|
|
272
281
|
|
|
282
|
+
/// Forwards a device runtime health sample to the kernel. Forge answers the
|
|
283
|
+
/// kernel's requests itself (process CPU time, memory, battery on iOS,
|
|
284
|
+
/// thermal state); use this only to supply a value Forge does not read,
|
|
285
|
+
/// such as `diskFreeBytes` from an app that declares the required-reason
|
|
286
|
+
/// API in its own privacy manifest. Every field is optional. Ignored while
|
|
287
|
+
/// device insights are off remotely.
|
|
288
|
+
@discardableResult
|
|
289
|
+
public func recordDeviceSample(_ sample: ForgeDeviceSample) -> ForgeOutcome {
|
|
290
|
+
kernel.recordDeviceSample(sample)
|
|
291
|
+
}
|
|
292
|
+
|
|
273
293
|
/// Starts or stops location observation to match the capture plan. Nothing
|
|
274
294
|
/// reads location - not even the authorization status - until an operator
|
|
275
295
|
/// enables one of the two motion switches.
|
|
@@ -295,6 +315,24 @@ public final class NPAWForge: NSObject {
|
|
|
295
315
|
observer.start()
|
|
296
316
|
}
|
|
297
317
|
|
|
318
|
+
/// Wires the collector to the kernel: capture plans pre-arm the observers a
|
|
319
|
+
/// family needs (battery monitoring for `bat`), sample requests trigger the
|
|
320
|
+
/// reads. Both subscriptions are cancelled in `destroy()`.
|
|
321
|
+
func installDeviceInsightsCollector(_ collector: ForgeDeviceInsightsCollector) {
|
|
322
|
+
lock.lock()
|
|
323
|
+
deviceInsightsCollector = collector
|
|
324
|
+
lock.unlock()
|
|
325
|
+
let planSubscription = kernel.addCapturePlanListener { [weak collector] plan in
|
|
326
|
+
collector?.applyCapturePlan(plan)
|
|
327
|
+
}
|
|
328
|
+
let requestSubscription = kernel.addDeviceSampleRequestListener { [weak collector] families in
|
|
329
|
+
collector?.handleSampleRequest(families)
|
|
330
|
+
}
|
|
331
|
+
lock.lock()
|
|
332
|
+
deviceInsightsSubscriptions.append(contentsOf: [planSubscription, requestSubscription])
|
|
333
|
+
lock.unlock()
|
|
334
|
+
}
|
|
335
|
+
|
|
298
336
|
func buildVideoAdapter(
|
|
299
337
|
identifier: String,
|
|
300
338
|
options: VideoOptions,
|
|
@@ -554,6 +592,16 @@ public final class NPAWForge: NSObject {
|
|
|
554
592
|
}
|
|
555
593
|
)
|
|
556
594
|
}
|
|
595
|
+
// Always installed and inert by construction: it reads nothing
|
|
596
|
+
// until the kernel asks, and the kernel asks only once an operator
|
|
597
|
+
// enabled device insights remotely (that enablement is the whole
|
|
598
|
+
// authorisation - device health is non-personal and needs no OS
|
|
599
|
+
// permission on any platform).
|
|
600
|
+
forge.installDeviceInsightsCollector(
|
|
601
|
+
ForgeDeviceInsightsCollector { [weak forge] sample in
|
|
602
|
+
forge?.recordDeviceSample(sample)
|
|
603
|
+
}
|
|
604
|
+
)
|
|
557
605
|
ForgeAutomaticCaptureRegistry.installIfAvailable(forge)
|
|
558
606
|
return forge
|
|
559
607
|
}
|
|
@@ -27,6 +27,7 @@ final class UniFFIForgeRuntime: ForgeRuntime {
|
|
|
27
27
|
private var sessionPreSendHook: UniFFISessionPreSendHook?
|
|
28
28
|
private var capturePlanListener: UniFFICapturePlanListener?
|
|
29
29
|
private var logListener: UniFFILogListener?
|
|
30
|
+
private var deviceSampleListener: UniFFIDeviceSampleListener?
|
|
30
31
|
|
|
31
32
|
init(config: ForgeAppleConfig) {
|
|
32
33
|
bridge = ForgeRuntimeBridge(config: AppleRuntimeConfig(
|
|
@@ -71,6 +72,37 @@ final class UniFFIForgeRuntime: ForgeRuntime {
|
|
|
71
72
|
))
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
func recordDeviceSample(_ sample: ForgeDeviceSample) -> ForgeOutcome {
|
|
76
|
+
outcome(bridge.recordDeviceSample(sample: appleDeviceSample(sample)))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func setDeviceSampleListener(_ listener: (([String]) -> Void)?) -> ForgeOutcome {
|
|
80
|
+
if let listener {
|
|
81
|
+
let callback = UniFFIDeviceSampleListener(callback: listener)
|
|
82
|
+
// Retained before registering, like the plan and log listeners, so
|
|
83
|
+
// a request that fires as soon as the bridge has the wrapper lands
|
|
84
|
+
// in one the runtime keeps alive.
|
|
85
|
+
callbackLock.lock()
|
|
86
|
+
deviceSampleListener = callback
|
|
87
|
+
callbackLock.unlock()
|
|
88
|
+
let result = outcome(bridge.setDeviceSampleListener(listener: callback))
|
|
89
|
+
if !result.isAccepted {
|
|
90
|
+
callbackLock.lock()
|
|
91
|
+
deviceSampleListener = nil
|
|
92
|
+
callbackLock.unlock()
|
|
93
|
+
}
|
|
94
|
+
return result
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let result = outcome(bridge.clearDeviceSampleListener())
|
|
98
|
+
if result.isAccepted {
|
|
99
|
+
callbackLock.lock()
|
|
100
|
+
deviceSampleListener = nil
|
|
101
|
+
callbackLock.unlock()
|
|
102
|
+
}
|
|
103
|
+
return result
|
|
104
|
+
}
|
|
105
|
+
|
|
74
106
|
func createVideoView(timestampMs: Int64, options: VideoOptions) -> RuntimeVideoView {
|
|
75
107
|
RuntimeVideoView(id: Int64(clamping: bridge.createVideoView(
|
|
76
108
|
nowMs: u64(timestampMs),
|
|
@@ -312,12 +344,14 @@ final class UniFFIForgeRuntime: ForgeRuntime {
|
|
|
312
344
|
let result = outcome(bridge.destroy())
|
|
313
345
|
_ = bridge.clearCapturePlanListener()
|
|
314
346
|
_ = bridge.clearLogListener()
|
|
347
|
+
_ = bridge.clearDeviceSampleListener()
|
|
315
348
|
callbackLock.lock()
|
|
316
349
|
contentMetadataProviders.removeAll()
|
|
317
350
|
videoPreSendHooks.removeAll()
|
|
318
351
|
sessionPreSendHook = nil
|
|
319
352
|
capturePlanListener = nil
|
|
320
353
|
logListener = nil
|
|
354
|
+
deviceSampleListener = nil
|
|
321
355
|
callbackLock.unlock()
|
|
322
356
|
return result
|
|
323
357
|
}
|
|
@@ -508,6 +542,7 @@ private func forgeCapturePlan(_ plan: AppleCapturePlan) -> ForgeCapturePlan {
|
|
|
508
542
|
attributionIncludeLandingUrl: plan.attributionIncludeLandingUrl,
|
|
509
543
|
attributionDeriveFromDeepLinks: plan.attributionDeriveFromDeepLinks,
|
|
510
544
|
attributionInstallReferrerEnabled: plan.attributionInstallReferrerEnabled,
|
|
545
|
+
deviceInsightFamilies: plan.deviceInsightFamilies,
|
|
511
546
|
hash: plan.hash
|
|
512
547
|
)
|
|
513
548
|
}
|
|
@@ -548,6 +583,33 @@ private func appleAdInsertionType(_ insertionType: AdInsertionType) -> AppleAdIn
|
|
|
548
583
|
}
|
|
549
584
|
}
|
|
550
585
|
|
|
586
|
+
/// Maps the typed Swift sample onto the UniFFI dictionary one-to-one. `nil`
|
|
587
|
+
/// stays `nil` on every field: the kernel's "absent" is the UniFFI `None`,
|
|
588
|
+
/// never `0`. A non-positive timestamp becomes `0`, which the bridge stamps.
|
|
589
|
+
private func appleDeviceSample(_ sample: ForgeDeviceSample) -> AppleDeviceSample {
|
|
590
|
+
AppleDeviceSample(
|
|
591
|
+
sampledAtMs: u64(sample.sampledAtMs),
|
|
592
|
+
cpuTimeMs: sample.cpuTimeMs,
|
|
593
|
+
cpuCores: sample.cpuCores,
|
|
594
|
+
memUsedBytes: sample.memUsedBytes,
|
|
595
|
+
memAvailBytes: sample.memAvailBytes,
|
|
596
|
+
batteryLevelPct: sample.batteryLevelPct,
|
|
597
|
+
batteryCharging: sample.batteryCharging,
|
|
598
|
+
thermal: sample.thermal.map(appleThermalState),
|
|
599
|
+
diskFreeBytes: sample.diskFreeBytes
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private func appleThermalState(_ state: ForgeThermalState) -> AppleThermalState {
|
|
604
|
+
switch state {
|
|
605
|
+
case .nominal: return .nominal
|
|
606
|
+
case .fair: return .fair
|
|
607
|
+
case .serious: return .serious
|
|
608
|
+
case .critical: return .critical
|
|
609
|
+
case .emergency: return .emergency
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
551
613
|
private func customEventJson(_ payload: CustomVideoEventPayload) -> String? {
|
|
552
614
|
var object: [String: Any] = [
|
|
553
615
|
"name": payload.name,
|
|
@@ -652,4 +714,19 @@ private final class UniFFILogListener: AppleLogListener, @unchecked Sendable {
|
|
|
652
714
|
callback(ForgeRenderedLog(level: level, tag: log.tag, message: log.message))
|
|
653
715
|
}
|
|
654
716
|
}
|
|
717
|
+
|
|
718
|
+
/// Receives the kernel's device sample requests on the runtime worker thread
|
|
719
|
+
/// and hands the family allowlist to the Swift listener, which must not block
|
|
720
|
+
/// (the collector hops to its own queue).
|
|
721
|
+
private final class UniFFIDeviceSampleListener: AppleDeviceSampleListener, @unchecked Sendable {
|
|
722
|
+
private let callback: ([String]) -> Void
|
|
723
|
+
|
|
724
|
+
init(callback: @escaping ([String]) -> Void) {
|
|
725
|
+
self.callback = callback
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
func onSampleRequest(families: [String]) {
|
|
729
|
+
callback(families)
|
|
730
|
+
}
|
|
731
|
+
}
|
|
655
732
|
#endif
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@npaw/forge-react-native",
|
|
3
|
-
"version": "9.7.
|
|
3
|
+
"version": "9.7.4",
|
|
4
4
|
"description": "NPAW Forge analytics for React Native: New Architecture TurboModule facade over the native Forge Android/Apple SDKs.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Victor Soto <victor.soto@nicepeopleatwork.com>",
|