@npaw/forge-react-native 9.6.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/LICENSE +10 -0
- package/NPAWForgeReactNative.podspec +44 -0
- package/README.md +51 -0
- package/android/build.gradle +77 -0
- package/android/src/main/AndroidManifest.xml +14 -0
- package/android/src/main/java/com/npaw/forge/reactnative/ForgeDtoMapper.java +295 -0
- package/android/src/main/java/com/npaw/forge/reactnative/ForgeInstanceRegistry.java +61 -0
- package/android/src/main/java/com/npaw/forge/reactnative/NPAWForgeModule.java +735 -0
- package/android/src/main/java/com/npaw/forge/reactnative/NPAWForgePackage.java +53 -0
- package/android/src/test/java/com/npaw/forge/reactnative/ForgeDtoMapperTest.java +133 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/Info.plist +82 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/ForgeKernelFFI.h +17 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/forge_kernelFFI.h +873 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/module.modulemap +7 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/ForgeKernelFFI.h +17 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/forge_kernelFFI.h +873 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/module.modulemap +7 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/ForgeKernelFFI.h +17 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/forge_kernelFFI.h +873 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/module.modulemap +7 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/ForgeKernelFFI.h +17 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/forge_kernelFFI.h +873 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/module.modulemap +7 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernel.swift +3931 -0
- package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernelFFI.h +873 -0
- package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernelFFI.modulemap +7 -0
- package/ios/ForgeApple/Sources/NPAWForgeAVPlayerAdapter/AVPlayerAccessLogFacts.swift +152 -0
- package/ios/ForgeApple/Sources/NPAWForgeAVPlayerAdapter/AVPlayerAdapter.swift +877 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/AttributionCapture.swift +158 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/CapturePlanGate.swift +189 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/CrashCapture.swift +120 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/ForgeAutomaticCapture.swift +639 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsCapture.swift +1176 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsEngine.swift +674 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsMarkers.swift +105 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsSwiftUI.swift +284 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/SwiftUICapture.swift +44 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/AdAdapter.swift +199 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/AdSemantics.swift +136 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeApple.swift +596 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeCapturePlan.swift +297 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeClock.swift +29 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeLogging.swift +109 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeOutcome.swift +89 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeRuntime.swift +291 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeStorage.swift +70 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ManualAdReporter.swift +180 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/NPAWForge.swift +453 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/NativeUniFFIRuntime.swift +594 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/NetworkRequestRecord.swift +191 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ObjectiveCWrappers.swift +191 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/PlayerAdapter.swift +299 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/PlayerSemantics.swift +266 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/PrivacySanitizer.swift +59 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/VideoAdapter.swift +478 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/VideoOptions.swift +431 -0
- package/ios/ForgeApple/Sources/NPAWForgeNetworkCapture/ForgeNetworkCapture.swift +116 -0
- package/ios/ForgeApple/Sources/NPAWForgeNetworkCaptureObjC/NPAWForgeNetworkSwizzler.m +293 -0
- package/ios/ForgeApple/Sources/NPAWForgeNetworkCaptureObjC/include/NPAWForgeNetworkSwizzler.h +33 -0
- package/ios/ForgeModuleImpl.swift +1131 -0
- package/ios/NPAWForgeTurboModule.mm +399 -0
- package/package.json +33 -0
- package/src/ForgeAdReporter.ts +101 -0
- package/src/ForgeVideo.ts +169 -0
- package/src/NPAWForge.ts +177 -0
- package/src/capture.ts +192 -0
- package/src/events.ts +40 -0
- package/src/index.ts +37 -0
- package/src/specs/NativeNPAWForge.ts +168 -0
- package/src/types.ts +114 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) NPAW - All Rights Reserved
|
|
3
|
+
*
|
|
4
|
+
* This source code is protected under international copyright law. All rights
|
|
5
|
+
* reserved and protected by the copyright holders.
|
|
6
|
+
* This file is confidential and only available to authorized individuals with the
|
|
7
|
+
* permission of the copyright holders. If you encounter this file and do not have
|
|
8
|
+
* permission, please contact the copyright holders and delete this file.
|
|
9
|
+
*
|
|
10
|
+
*
|
|
11
|
+
* @author: Victor Soto <victor.soto@nicepeopleatwork.com>
|
|
12
|
+
* @description: Apple network request observation model forwarded to the Rust runtime.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import Foundation
|
|
16
|
+
|
|
17
|
+
public enum NetworkRequestOutcome: String {
|
|
18
|
+
case success
|
|
19
|
+
case httpError = "http_error"
|
|
20
|
+
case networkError = "network_error"
|
|
21
|
+
case cancelled
|
|
22
|
+
case timeout
|
|
23
|
+
case blocked
|
|
24
|
+
case unknown
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public enum NetworkRequestSource: String {
|
|
28
|
+
case automaticURLSession = "automatic_urlsession"
|
|
29
|
+
case automaticNSURLConnection = "automatic_nsurlconnection"
|
|
30
|
+
case manual
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public struct NetworkRequestRecord: Equatable {
|
|
34
|
+
public var eventId: String
|
|
35
|
+
public var source: NetworkRequestSource
|
|
36
|
+
public var targetUrl: String
|
|
37
|
+
public var method: String
|
|
38
|
+
public var timestampMs: Int64
|
|
39
|
+
public var durationMs: Int64
|
|
40
|
+
public var responseStatusCode: Int
|
|
41
|
+
public var outcome: NetworkRequestOutcome
|
|
42
|
+
public var responseStatusText: String?
|
|
43
|
+
public var errorMessage: String?
|
|
44
|
+
public var traceparent: String?
|
|
45
|
+
public var dnsDurationMs: Int64?
|
|
46
|
+
public var connectDurationMs: Int64?
|
|
47
|
+
public var sslDurationMs: Int64?
|
|
48
|
+
public var requestDurationMs: Int64?
|
|
49
|
+
public var timeToFirstByteMs: Int64?
|
|
50
|
+
public var responseDownloadDurationMs: Int64?
|
|
51
|
+
public var redirectDurationMs: Int64?
|
|
52
|
+
public var redirectCount: Int64?
|
|
53
|
+
public var networkProtocol: String?
|
|
54
|
+
/// Request body size in bytes (measured uploaded byte count, request
|
|
55
|
+
/// `Content-Length` fallback). Body content is never captured; only this
|
|
56
|
+
/// size is reported.
|
|
57
|
+
public var requestBytes: Int64?
|
|
58
|
+
/// Response body size in bytes (measured downloaded byte count, response
|
|
59
|
+
/// `Content-Length` fallback). Body content is never captured; only this
|
|
60
|
+
/// size is reported.
|
|
61
|
+
public var responseBytes: Int64?
|
|
62
|
+
public var requestQuery: [String: String]
|
|
63
|
+
public var requestHeaders: [String: String]
|
|
64
|
+
public var responseHeaders: [String: String]
|
|
65
|
+
|
|
66
|
+
public init(
|
|
67
|
+
targetUrl: String,
|
|
68
|
+
method: String,
|
|
69
|
+
responseStatusCode: Int,
|
|
70
|
+
durationMs: Int64,
|
|
71
|
+
outcome: NetworkRequestOutcome,
|
|
72
|
+
eventId: String = UUID().uuidString,
|
|
73
|
+
source: NetworkRequestSource = .manual,
|
|
74
|
+
timestampMs: Int64 = 0
|
|
75
|
+
) {
|
|
76
|
+
self.eventId = eventId
|
|
77
|
+
self.source = source
|
|
78
|
+
self.targetUrl = targetUrl
|
|
79
|
+
self.method = method
|
|
80
|
+
self.timestampMs = timestampMs
|
|
81
|
+
self.durationMs = max(0, durationMs)
|
|
82
|
+
self.responseStatusCode = max(0, responseStatusCode)
|
|
83
|
+
self.outcome = outcome
|
|
84
|
+
self.requestQuery = [:]
|
|
85
|
+
self.requestHeaders = [:]
|
|
86
|
+
self.responseHeaders = [:]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public func sanitized(configuration: NetworkPrivacyConfiguration) -> NetworkRequestRecord {
|
|
90
|
+
var copy = self
|
|
91
|
+
copy.targetUrl = configuration.sanitizeUrl(targetUrl)
|
|
92
|
+
copy.requestQuery = configuration.filterMap(requestQuery, allowlist: configuration.queryAllowlist)
|
|
93
|
+
copy.requestHeaders = configuration.filterMap(requestHeaders, allowlist: configuration.requestHeaderAllowlist)
|
|
94
|
+
copy.responseHeaders = configuration.filterMap(responseHeaders, allowlist: configuration.responseHeaderAllowlist)
|
|
95
|
+
copy.errorMessage = errorMessage.map(configuration.sanitizeText)
|
|
96
|
+
return copy
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public struct NetworkPrivacyConfiguration: Equatable {
|
|
101
|
+
public var queryAllowlist: Set<String>
|
|
102
|
+
public var requestHeaderAllowlist: Set<String>
|
|
103
|
+
public var responseHeaderAllowlist: Set<String>
|
|
104
|
+
public var redactionKeys: Set<String>
|
|
105
|
+
public var maxStringLength: Int
|
|
106
|
+
|
|
107
|
+
public init(
|
|
108
|
+
queryAllowlist: Set<String> = [],
|
|
109
|
+
requestHeaderAllowlist: Set<String> = [],
|
|
110
|
+
responseHeaderAllowlist: Set<String> = [],
|
|
111
|
+
redactionKeys: Set<String> = Self.defaultRedactionKeys,
|
|
112
|
+
maxStringLength: Int = 512
|
|
113
|
+
) {
|
|
114
|
+
self.queryAllowlist = queryAllowlist.map(Self.normalizeKey).asSet
|
|
115
|
+
self.requestHeaderAllowlist = requestHeaderAllowlist.map(Self.normalizeKey).asSet
|
|
116
|
+
self.responseHeaderAllowlist = responseHeaderAllowlist.map(Self.normalizeKey).asSet
|
|
117
|
+
self.redactionKeys = redactionKeys.map(Self.normalizeKey).asSet
|
|
118
|
+
self.maxStringLength = max(1, maxStringLength)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public static let defaultRedactionKeys: Set<String> = [
|
|
122
|
+
"authorization",
|
|
123
|
+
"proxy-authorization",
|
|
124
|
+
"cookie",
|
|
125
|
+
"set-cookie",
|
|
126
|
+
"api-key",
|
|
127
|
+
"x-api-key",
|
|
128
|
+
"token",
|
|
129
|
+
"access-token",
|
|
130
|
+
"refresh-token",
|
|
131
|
+
"password",
|
|
132
|
+
"passwd",
|
|
133
|
+
"credential",
|
|
134
|
+
"secret",
|
|
135
|
+
"email",
|
|
136
|
+
"phone",
|
|
137
|
+
"payment",
|
|
138
|
+
"card",
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
public func sanitizeUrl(_ value: String) -> String {
|
|
142
|
+
guard var components = URLComponents(string: value) else {
|
|
143
|
+
return String(value.split(separator: "?").first.map(String.init) ?? value).prefixString(maxStringLength)
|
|
144
|
+
}
|
|
145
|
+
components.user = nil
|
|
146
|
+
components.password = nil
|
|
147
|
+
components.query = nil
|
|
148
|
+
components.fragment = nil
|
|
149
|
+
return (components.string ?? value).prefixString(maxStringLength)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public func filterMap(_ value: [String: String], allowlist: Set<String>) -> [String: String] {
|
|
153
|
+
guard !allowlist.isEmpty else { return [:] }
|
|
154
|
+
var output: [String: String] = [:]
|
|
155
|
+
for (key, rawValue) in value {
|
|
156
|
+
guard output.count < 32 else { break }
|
|
157
|
+
let normalized = Self.normalizeKey(key)
|
|
158
|
+
guard allowlist.contains(normalized), !key.isEmpty, !rawValue.isEmpty else { continue }
|
|
159
|
+
output[key] = isSensitive(normalized) ? "[REDACTED]" : rawValue.prefixString(maxStringLength)
|
|
160
|
+
}
|
|
161
|
+
return output
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public func sanitizeText(_ value: String) -> String {
|
|
165
|
+
let normalized = Self.normalizeKey(value)
|
|
166
|
+
if redactionKeys.contains(where: { normalized.contains($0) }) {
|
|
167
|
+
return "[REDACTED]"
|
|
168
|
+
}
|
|
169
|
+
return value.prefixString(maxStringLength)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private func isSensitive(_ key: String) -> Bool {
|
|
173
|
+
redactionKeys.contains(where: { key.contains($0) })
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private static func normalizeKey(_ value: String) -> String {
|
|
177
|
+
value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
178
|
+
.lowercased()
|
|
179
|
+
.replacingOccurrences(of: "_", with: "-")
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private extension Array where Element == String {
|
|
184
|
+
var asSet: Set<String> { Set(self) }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private extension String {
|
|
188
|
+
func prefixString(_ length: Int) -> String {
|
|
189
|
+
String(prefix(length))
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) NPAW - All Rights Reserved
|
|
3
|
+
*
|
|
4
|
+
* This source code is protected under international copyright law. All rights
|
|
5
|
+
* reserved and protected by the copyright holders.
|
|
6
|
+
* This file is confidential and only available to authorized individuals with the
|
|
7
|
+
* permission of the copyright holders. If you encounter this file and do not have
|
|
8
|
+
* permission, please contact the copyright holders and delete this file.
|
|
9
|
+
*
|
|
10
|
+
*
|
|
11
|
+
* @author: Victor Soto <victor.soto@nicepeopleatwork.com>
|
|
12
|
+
* @description: Objective-C compatible wrappers for core Apple binding use cases.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import Foundation
|
|
16
|
+
|
|
17
|
+
@objc(NPAWForgeObjCBuilder)
|
|
18
|
+
public final class NPAWForgeObjCBuilder: NSObject {
|
|
19
|
+
private let builder: NPAWForge.Builder
|
|
20
|
+
|
|
21
|
+
@objc public init(accountCode: String) {
|
|
22
|
+
self.builder = NPAWForge.Builder(accountCode: accountCode)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@discardableResult
|
|
26
|
+
@objc(setAppName:)
|
|
27
|
+
public func setAppName(_ value: String?) -> NPAWForgeObjCBuilder {
|
|
28
|
+
builder.setAppName(value)
|
|
29
|
+
return self
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@discardableResult
|
|
33
|
+
@objc(setAppReleaseVersion:)
|
|
34
|
+
public func setAppReleaseVersion(_ value: String?) -> NPAWForgeObjCBuilder {
|
|
35
|
+
builder.setAppReleaseVersion(value)
|
|
36
|
+
return self
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@discardableResult
|
|
40
|
+
@objc(setLmaEndpoint:)
|
|
41
|
+
public func setLmaEndpoint(_ value: String?) -> NPAWForgeObjCBuilder {
|
|
42
|
+
builder.setLmaEndpoint(value)
|
|
43
|
+
return self
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@discardableResult
|
|
47
|
+
@objc(setLogLevelName:)
|
|
48
|
+
public func setLogLevelName(_ value: String?) -> NPAWForgeObjCBuilder {
|
|
49
|
+
builder.setLogLevel(value)
|
|
50
|
+
return self
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@discardableResult
|
|
54
|
+
@objc(setStartSessionOnInitialize:)
|
|
55
|
+
public func setStartSessionOnInitialize(_ enabled: Bool) -> NPAWForgeObjCBuilder {
|
|
56
|
+
builder.setStartSessionOnInitialize(enabled)
|
|
57
|
+
return self
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@objc public func build() -> NPAWForgeObjC {
|
|
61
|
+
NPAWForgeObjC(swiftForge: builder.build())
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@objc(NPAWForgeObjC)
|
|
66
|
+
public final class NPAWForgeObjC: NSObject {
|
|
67
|
+
public let swiftForge: NPAWForge
|
|
68
|
+
|
|
69
|
+
public init(swiftForge: NPAWForge) {
|
|
70
|
+
self.swiftForge = swiftForge
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@objc public func startSession() -> ForgeOutcome {
|
|
74
|
+
swiftForge.startSession()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@objc(startSessionWithMetadata:)
|
|
78
|
+
public func startSession(metadata: NSDictionary?) -> ForgeOutcome {
|
|
79
|
+
swiftForge.startSession(metadata.toStringAnyMap())
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@objc public func endSession() -> ForgeOutcome {
|
|
83
|
+
swiftForge.endSession()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@objc(endSessionWithMetadata:)
|
|
87
|
+
public func endSession(metadata: NSDictionary?) -> ForgeOutcome {
|
|
88
|
+
swiftForge.endSession(metadata.toStringAnyMap())
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@objc(trackSessionEventWithName:metadata:)
|
|
92
|
+
public func trackSessionEvent(name: String, metadata: NSDictionary?) -> ForgeOutcome {
|
|
93
|
+
swiftForge.trackSessionEvent(name: name, metadata: metadata.toStringAnyMap())
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@objc(trackSessionErrorWithCode:message:metadata:)
|
|
97
|
+
public func trackSessionError(code: String, message: String, metadata: NSDictionary?) -> ForgeOutcome {
|
|
98
|
+
swiftForge.trackSessionError(errorCode: code, errorMessage: message, metadata: metadata.toStringAnyMap())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@objc(trackRawEventWithName:params:)
|
|
102
|
+
public func trackRawEvent(name: String, params: NSDictionary?) -> ForgeOutcome {
|
|
103
|
+
swiftForge.trackRawEvent(name: name, params: params.toStringAnyMap())
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@objc(trackSearchWithQuery:resultCount:)
|
|
107
|
+
public func trackSearch(query: String, resultCount: NSNumber?) -> ForgeOutcome {
|
|
108
|
+
swiftForge.trackSearch(query: query, resultCount: resultCount?.intValue)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@objc(trackSearchResultClickWithQuery:contentId:position:)
|
|
112
|
+
public func trackSearchResultClick(query: String, contentId: String?, position: NSNumber?) -> ForgeOutcome {
|
|
113
|
+
swiftForge.trackSearchResultClick(
|
|
114
|
+
query: query,
|
|
115
|
+
options: ForgeSearchResultClickOptions(contentId: contentId, position: position?.intValue)
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@objc(trackBusinessEventWithName:attributes:)
|
|
120
|
+
public func trackBusinessEvent(name: String, attributes: NSDictionary?) -> ForgeOutcome {
|
|
121
|
+
swiftForge.trackBusinessEvent(name: name, attributes: attributes.toStringAnyMap())
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@objc(trackAttributionWithOptions:)
|
|
125
|
+
public func trackAttribution(options: NSDictionary?) -> ForgeOutcome {
|
|
126
|
+
let values = options.toStringStringMap()
|
|
127
|
+
return swiftForge.trackAttribution(options: ForgeAttributionOptions(
|
|
128
|
+
utmSource: values["utmSource"],
|
|
129
|
+
utmMedium: values["utmMedium"],
|
|
130
|
+
utmCampaign: values["utmCampaign"],
|
|
131
|
+
utmTerm: values["utmTerm"],
|
|
132
|
+
utmContent: values["utmContent"],
|
|
133
|
+
utmUrl: values["utmUrl"]
|
|
134
|
+
))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@objc(loginSuccessWithUserId:options:)
|
|
138
|
+
public func loginSuccess(userId: String, options: NSDictionary?) -> ForgeOutcome {
|
|
139
|
+
swiftForge.loginSuccess(userId: userId, options: options.toStringStringMap())
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@objc(loginFailedWithOptions:)
|
|
143
|
+
public func loginFailed(options: NSDictionary?) -> ForgeOutcome {
|
|
144
|
+
swiftForge.loginFailed(options: options.toStringStringMap())
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@objc(logoutWithOptions:)
|
|
148
|
+
public func logout(options: NSDictionary?) -> ForgeOutcome {
|
|
149
|
+
swiftForge.logout(options: options.toStringStringMap())
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@objc(profileCreatedWithProfileId:options:)
|
|
153
|
+
public func profileCreated(profileId: String, options: NSDictionary?) -> ForgeOutcome {
|
|
154
|
+
swiftForge.profileCreated(profileId: profileId, options: options.toStringStringMap())
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@objc(profileSelectedWithProfileId:options:)
|
|
158
|
+
public func profileSelected(profileId: String, options: NSDictionary?) -> ForgeOutcome {
|
|
159
|
+
swiftForge.profileSelected(profileId: profileId, options: options.toStringStringMap())
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@objc(profileDeletedWithProfileId:)
|
|
163
|
+
public func profileDeleted(profileId: String) -> ForgeOutcome {
|
|
164
|
+
swiftForge.profileDeleted(profileId: profileId)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@objc(setLogLevelName:)
|
|
168
|
+
public func setLogLevelName(_ value: String?) -> ForgeOutcome {
|
|
169
|
+
swiftForge.setLogLevel(value)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@objc public func destroy() -> ForgeOutcome {
|
|
173
|
+
swiftForge.destroy()
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private extension Optional where Wrapped == NSDictionary {
|
|
178
|
+
func toStringAnyMap() -> [String: Any] {
|
|
179
|
+
guard let dictionary = self else { return [:] }
|
|
180
|
+
var output: [String: Any] = [:]
|
|
181
|
+
for (key, value) in dictionary {
|
|
182
|
+
guard let key = key as? String else { continue }
|
|
183
|
+
output[key] = value
|
|
184
|
+
}
|
|
185
|
+
return output
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
func toStringStringMap() -> [String: String] {
|
|
189
|
+
toStringAnyMap().mapValues { String(describing: $0) }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) NPAW - All Rights Reserved
|
|
3
|
+
*
|
|
4
|
+
* This source code is protected under international copyright law. All rights
|
|
5
|
+
* reserved and protected by the copyright holders.
|
|
6
|
+
* This file is confidential and only available to authorized individuals with the
|
|
7
|
+
* permission of the copyright holders. If you encounter this file and do not have
|
|
8
|
+
* permission, please contact the copyright holders and delete this file.
|
|
9
|
+
*
|
|
10
|
+
*
|
|
11
|
+
* @author: Victor Soto <victor.soto@nicepeopleatwork.com>
|
|
12
|
+
* @description: Apple player adapter base class and public manual fire API.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import Foundation
|
|
16
|
+
|
|
17
|
+
open class PlayerAdapter: NSObject {
|
|
18
|
+
public let adapterInstanceId = UUID()
|
|
19
|
+
open var descriptor = AdapterDescriptor(
|
|
20
|
+
adapterId: "generic-player",
|
|
21
|
+
category: .player,
|
|
22
|
+
playerFamily: "Generic Player",
|
|
23
|
+
version: "unknown"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
/// Adapter-level default for the kernel `waitForMetadata` start gate.
|
|
27
|
+
/// Adapters whose players report duration/liveness/rendition only after
|
|
28
|
+
/// the first start signal (for example AVPlayer before the HLS playlist
|
|
29
|
+
/// is parsed) override this to `true` so `/start` waits for those facts.
|
|
30
|
+
/// An explicit `VideoOptions.waitForMetadata` value always wins over this
|
|
31
|
+
/// default, and the kernel default stays `false`.
|
|
32
|
+
open var defaultWaitForMetadata: Bool { false }
|
|
33
|
+
open var playerObject: AnyObject? {
|
|
34
|
+
didSet {
|
|
35
|
+
if oldValue === playerObject { return }
|
|
36
|
+
if isBound { unregisterListeners() }
|
|
37
|
+
if isBound, playerObject != nil { registerListeners() }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
private var isBound = false
|
|
41
|
+
public private(set) weak var videoAdapter: VideoAdapter?
|
|
42
|
+
|
|
43
|
+
/// True between an accepted pause and the accepted resume/stop/error/
|
|
44
|
+
/// destroy that clears it (mirrors the Android base adapter contract).
|
|
45
|
+
public private(set) var isPaused = false
|
|
46
|
+
private var adPlaybackActive = false
|
|
47
|
+
|
|
48
|
+
public init(player: AnyObject? = nil) {
|
|
49
|
+
self.playerObject = player
|
|
50
|
+
super.init()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Marks whether ad playback owns the player right now; called by
|
|
54
|
+
/// companion ad adapters around break start/stop (mirrors the Android
|
|
55
|
+
/// base adapter contract).
|
|
56
|
+
public func setIsPlayingAd(_ playingAd: Bool) {
|
|
57
|
+
adPlaybackActive = playingAd
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public func isPlayingAd() -> Bool { adPlaybackActive }
|
|
61
|
+
|
|
62
|
+
func bindToVideoAdapter(_ videoAdapter: VideoAdapter) {
|
|
63
|
+
self.videoAdapter = videoAdapter
|
|
64
|
+
isBound = true
|
|
65
|
+
if playerObject != nil { registerListeners() }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func unbindFromVideoAdapter() {
|
|
69
|
+
if isBound { unregisterListeners() }
|
|
70
|
+
isBound = false
|
|
71
|
+
videoAdapter = nil
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
open func registerListeners() {}
|
|
75
|
+
open func unregisterListeners() {}
|
|
76
|
+
|
|
77
|
+
/// Remote behavior hints from the latest kernel capture plan (`bh.v`), or
|
|
78
|
+
/// `nil` before the first plan or while unbound. Adapters read this once
|
|
79
|
+
/// at view start (`registerListeners`); hints are applied at the next
|
|
80
|
+
/// view start only, never hot-swapped mid-view.
|
|
81
|
+
public var capturePlanBehaviorHints: ForgeCapturePlan.Behavior? {
|
|
82
|
+
videoAdapter?.capturePlanBehavior()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
open func metadata() -> [String: String] {
|
|
86
|
+
[
|
|
87
|
+
"adapterId": descriptor.adapterId,
|
|
88
|
+
"category": descriptor.category.rawValue,
|
|
89
|
+
"playerFamily": descriptor.playerFamily,
|
|
90
|
+
"version": descriptor.version,
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
open func snapshotMetadata(
|
|
95
|
+
timemarkMs: Int64 = ForgeClock.nowMs(),
|
|
96
|
+
elapsedRealtimeMs: Int64 = ForgeClock.elapsedRealtimeMs()
|
|
97
|
+
) -> PlayerMetadataSnapshot {
|
|
98
|
+
defaultMetadata(nil)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Optional QoS sample (throughput, dropped frames, total bytes, latency,
|
|
102
|
+
/// frame rate). Adapters that can read these from their player override
|
|
103
|
+
/// this; the default returns nil so the runtime omits QoS for adapters
|
|
104
|
+
/// that do not measure it (mirrors the Android base adapter contract).
|
|
105
|
+
open func snapshotQos(
|
|
106
|
+
timemarkMs: Int64 = ForgeClock.nowMs(),
|
|
107
|
+
elapsedRealtimeMs: Int64 = ForgeClock.elapsedRealtimeMs()
|
|
108
|
+
) -> QosSnapshot? {
|
|
109
|
+
nil
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
open func onPlayerEvent(
|
|
113
|
+
_ event: PlayerEvent,
|
|
114
|
+
timestampMs: Int64 = 0,
|
|
115
|
+
metadata: PlayerMetadataSnapshot? = nil
|
|
116
|
+
) -> ForgeOutcome {
|
|
117
|
+
let resolved = metadata ?? PlayerMetadataSnapshot(playerName: descriptor.playerFamily)
|
|
118
|
+
switch event {
|
|
119
|
+
case .initEvent: return fireInit(metadata: resolved, timestampMs: timestampMs)
|
|
120
|
+
case .start: return fireStart(metadata: resolved, timestampMs: timestampMs)
|
|
121
|
+
case .join: return fireJoin(metadata: resolved, timestampMs: timestampMs)
|
|
122
|
+
case .pause: return firePause(metadata: resolved, timestampMs: timestampMs)
|
|
123
|
+
case .resume: return fireResume(metadata: resolved, timestampMs: timestampMs)
|
|
124
|
+
case .seekBegin: return fireSeekBegin(metadata: resolved, timestampMs: timestampMs)
|
|
125
|
+
case .seekEnd: return fireSeekEnd(metadata: resolved, timestampMs: timestampMs)
|
|
126
|
+
case .bufferBegin: return fireBufferBegin(metadata: resolved, timestampMs: timestampMs)
|
|
127
|
+
case .bufferEnd: return fireBufferEnd(metadata: resolved, timestampMs: timestampMs)
|
|
128
|
+
case .stop: return fireStop(metadata: resolved, timestampMs: timestampMs)
|
|
129
|
+
case .error: return fireError(errorCode: resolved.errorCode, errorMessage: resolved.errorMessage, errorMetadata: resolved.errorMetadata, timestampMs: timestampMs)
|
|
130
|
+
case .fatalError: return fireFatalError(errorCode: resolved.errorCode, errorMessage: resolved.errorMessage, errorMetadata: resolved.errorMetadata, timestampMs: timestampMs)
|
|
131
|
+
case .destroy: return fireDestroy(timestampMs: timestampMs)
|
|
132
|
+
case .customVideoEvent: return fireEvent(timestampMs: timestampMs)
|
|
133
|
+
case .customVideoEventEnd: return fireEventEnd(timestampMs: timestampMs)
|
|
134
|
+
case .metadataUpdate: return fireMetadataUpdate(metadata: resolved, timestampMs: timestampMs)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
open func fireInit(params: [String: String]? = [:], metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
|
|
139
|
+
record(.initEvent, metadata ?? defaultMetadata(params), timestampMs)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
open func fireStart(params: [String: String]? = [:], forceInit: Bool? = false, metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
|
|
143
|
+
var snapshot = metadata ?? defaultMetadata(params)
|
|
144
|
+
snapshot.forceInit = forceInit ?? false
|
|
145
|
+
return record(.start, snapshot, timestampMs)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
open func fireJoin(params: [String: String]? = [:], metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
|
|
149
|
+
record(.join, metadata ?? defaultMetadata(params), timestampMs)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
open func firePause(params: [String: String]? = [:], metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
|
|
153
|
+
record(.pause, metadata ?? defaultMetadata(params), timestampMs)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
open func fireResume(params: [String: String]? = [:], metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
|
|
157
|
+
record(.resume, metadata ?? defaultMetadata(params), timestampMs)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
open func fireSeekBegin(convertFromBuffer: Bool? = true, params: [String: String]? = [:], metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
|
|
161
|
+
var snapshot = metadata ?? defaultMetadata(params)
|
|
162
|
+
snapshot.convertFromBuffer = convertFromBuffer ?? true
|
|
163
|
+
return record(.seekBegin, snapshot, timestampMs)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
open func fireSeekEnd(params: [String: String]? = [:], metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
|
|
167
|
+
record(.seekEnd, metadata ?? defaultMetadata(params), timestampMs)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
open func fireBufferBegin(convertFromSeek: Bool? = false, params: [String: String]? = [:], metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
|
|
171
|
+
var snapshot = metadata ?? defaultMetadata(params)
|
|
172
|
+
snapshot.convertFromSeek = convertFromSeek ?? false
|
|
173
|
+
return record(.bufferBegin, snapshot, timestampMs)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
open func fireBufferEnd(params: [String: String]? = [:], metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
|
|
177
|
+
record(.bufferEnd, metadata ?? defaultMetadata(params), timestampMs)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
open func fireStop(params: [String: String]? = [:], metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
|
|
181
|
+
record(.stop, metadata ?? defaultMetadata(params), timestampMs)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
open func fireError(errorCode: String? = nil, errorMessage: String? = nil, errorMetadata: String? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
|
|
185
|
+
var metadata = defaultMetadata(nil)
|
|
186
|
+
metadata.errorCode = errorCode
|
|
187
|
+
metadata.errorMessage = errorMessage
|
|
188
|
+
metadata.errorMetadata = errorMetadata
|
|
189
|
+
return record(.error, metadata, timestampMs)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
open func fireFatalError(errorCode: String? = nil, errorMessage: String? = nil, errorMetadata: String? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
|
|
193
|
+
var metadata = defaultMetadata(nil)
|
|
194
|
+
metadata.errorCode = errorCode
|
|
195
|
+
metadata.errorMessage = errorMessage
|
|
196
|
+
metadata.errorMetadata = errorMetadata
|
|
197
|
+
metadata.errorLevel = "fatal"
|
|
198
|
+
return record(.fatalError, metadata, timestampMs)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
open func fireDestroy(timestampMs: Int64 = 0) -> ForgeOutcome {
|
|
202
|
+
record(.destroy, defaultMetadata(nil), timestampMs)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
open func fireEvent(
|
|
206
|
+
eventName: String = "",
|
|
207
|
+
dimensions: [String: String] = [:],
|
|
208
|
+
values: [String: Double] = [:],
|
|
209
|
+
topLevelDimensions: [String: String] = [:],
|
|
210
|
+
hasEndDatetime: Bool = false,
|
|
211
|
+
timestampMs: Int64 = 0
|
|
212
|
+
) -> ForgeOutcome {
|
|
213
|
+
var metadata = defaultMetadata(nil)
|
|
214
|
+
metadata.customEvent = CustomVideoEventPayload(
|
|
215
|
+
name: eventName,
|
|
216
|
+
dimensions: dimensions,
|
|
217
|
+
values: values,
|
|
218
|
+
topLevel: topLevelDimensions,
|
|
219
|
+
hasEndDatetime: hasEndDatetime
|
|
220
|
+
)
|
|
221
|
+
return record(.customVideoEvent, metadata, timestampMs)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
open func fireEventEnd(
|
|
225
|
+
eventName: String = "",
|
|
226
|
+
dimensions: [String: String] = [:],
|
|
227
|
+
values: [String: Double] = [:],
|
|
228
|
+
topLevelDimensions: [String: String] = [:],
|
|
229
|
+
timestampMs: Int64 = 0
|
|
230
|
+
) -> ForgeOutcome {
|
|
231
|
+
var metadata = defaultMetadata(nil)
|
|
232
|
+
metadata.customEvent = CustomVideoEventPayload(
|
|
233
|
+
name: eventName,
|
|
234
|
+
dimensions: dimensions,
|
|
235
|
+
values: values,
|
|
236
|
+
topLevel: topLevelDimensions
|
|
237
|
+
)
|
|
238
|
+
return record(.customVideoEventEnd, metadata, timestampMs)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
open func fireMetadataUpdate(metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
|
|
242
|
+
record(.metadataUpdate, metadata ?? defaultMetadata(nil), timestampMs)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
open func defaultMetadata(_ params: [String: String]?) -> PlayerMetadataSnapshot {
|
|
246
|
+
PlayerMetadataSnapshot(
|
|
247
|
+
playerName: params?["player"] ?? descriptor.playerFamily,
|
|
248
|
+
playerVersion: params?["playerVersion"] ?? descriptor.version,
|
|
249
|
+
adapterVersion: descriptor.version,
|
|
250
|
+
resourceUrl: params?["resource"],
|
|
251
|
+
parsedResource: params?["parsedResource"] ?? params?["urlToParse"],
|
|
252
|
+
title: params?["title"],
|
|
253
|
+
contentType: (params?["contentType"] ?? params?["content_type"]).flatMap(ContentType.from) ?? .unknown,
|
|
254
|
+
playheadMs: params?["playhead"].flatMap(Self.secondsToMillis),
|
|
255
|
+
durationMs: params?["mediaDuration"].flatMap(Self.secondsToMillis),
|
|
256
|
+
bitrate: params?["bitrate"].flatMap(Int64.init),
|
|
257
|
+
rendition: params?["rendition"],
|
|
258
|
+
videoCodec: params?["videoCodec"],
|
|
259
|
+
audioCodec: params?["audioCodec"],
|
|
260
|
+
codecProfile: params?["codecProfile"],
|
|
261
|
+
containerFormat: params?["containerFormat"],
|
|
262
|
+
errorMetadata: params?["errorMetadata"],
|
|
263
|
+
errorLevel: params?["errorLevel"],
|
|
264
|
+
program: params?["program"]
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
func record(_ event: PlayerEvent, _ metadata: PlayerMetadataSnapshot, _ timestampMs: Int64) -> ForgeOutcome {
|
|
269
|
+
let outcome = videoAdapter?.recordPlayerEvent(event: event, timestampMs: timestampMs, metadata: metadata) ?? .ignoredInvalidState
|
|
270
|
+
if outcome.isAccepted { updatePlaybackState(event) }
|
|
271
|
+
return outcome
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private func updatePlaybackState(_ event: PlayerEvent) {
|
|
275
|
+
switch event {
|
|
276
|
+
case .pause:
|
|
277
|
+
isPaused = true
|
|
278
|
+
case .resume, .stop, .error, .fatalError, .destroy:
|
|
279
|
+
isPaused = false
|
|
280
|
+
default:
|
|
281
|
+
break
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private static func secondsToMillis(_ value: String) -> Int64? {
|
|
286
|
+
Double(value).map { Int64($0 * 1000.0) }
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private extension ContentType {
|
|
291
|
+
static func from(_ value: String) -> ContentType {
|
|
292
|
+
switch value.lowercased() {
|
|
293
|
+
case "live": return .live
|
|
294
|
+
case "vod": return .vod
|
|
295
|
+
case "audio": return .audio
|
|
296
|
+
default: return .unknown
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|