@bigcrunch/react-native-ads 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/android/bigcrunch-ads/com/bigcrunch/ads/BigCrunchAds.kt +434 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/BigCrunchBannerView.kt +484 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/BigCrunchInterstitial.kt +403 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/BigCrunchRewarded.kt +409 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/adapters/GoogleAdsAdapter.kt +592 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/AdOrchestrator.kt +623 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/AnalyticsClient.kt +719 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/BidRequestClient.kt +364 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/ConfigManager.kt +301 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/DeviceContext.kt +385 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/RewardedCallback.kt +42 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/SessionManager.kt +330 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/internal/DeviceHelper.kt +60 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/internal/HttpClient.kt +114 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/internal/Logger.kt +71 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/internal/PrivacyStore.kt +125 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/internal/Storage.kt +88 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/listeners/BannerAdListener.kt +55 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/listeners/InterstitialAdListener.kt +55 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/listeners/RewardedAdListener.kt +58 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/models/AdEvent.kt +880 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/models/AppConfig.kt +90 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/models/DeviceData.kt +18 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/models/PlacementConfig.kt +70 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/models/SessionInfo.kt +21 -0
- package/android/build.gradle +22 -10
- package/android/settings.gradle +2 -6
- package/ios/BigCrunchAds/Sources/Adapters/GoogleAdsAdapter.swift +512 -0
- package/ios/BigCrunchAds/Sources/BigCrunchAds.swift +387 -0
- package/ios/BigCrunchAds/Sources/BigCrunchBannerView.swift +448 -0
- package/ios/BigCrunchAds/Sources/BigCrunchInterstitial.swift +412 -0
- package/ios/BigCrunchAds/Sources/BigCrunchRewarded.swift +523 -0
- package/ios/BigCrunchAds/Sources/Core/AdOrchestrator.swift +514 -0
- package/ios/BigCrunchAds/Sources/Core/AnalyticsClient.swift +874 -0
- package/ios/BigCrunchAds/Sources/Core/BidRequestClient.swift +344 -0
- package/ios/BigCrunchAds/Sources/Core/ConfigManager.swift +306 -0
- package/ios/BigCrunchAds/Sources/Core/DeviceContext.swift +284 -0
- package/ios/BigCrunchAds/Sources/Core/SessionManager.swift +392 -0
- package/ios/BigCrunchAds/Sources/Internal/HTTPClient.swift +146 -0
- package/ios/BigCrunchAds/Sources/Internal/Logger.swift +62 -0
- package/ios/BigCrunchAds/Sources/Internal/PrivacyStore.swift +129 -0
- package/ios/BigCrunchAds/Sources/Internal/Storage.swift +73 -0
- package/ios/BigCrunchAds/Sources/Models/AdEvent.swift +784 -0
- package/ios/BigCrunchAds/Sources/Models/AppConfig.swift +100 -0
- package/ios/BigCrunchAds/Sources/Models/DeviceData.swift +68 -0
- package/ios/BigCrunchAds/Sources/Models/PlacementConfig.swift +137 -0
- package/ios/BigCrunchAds/Sources/Models/SessionInfo.swift +48 -0
- package/ios/BigCrunchAdsModule.swift +0 -1
- package/ios/BigCrunchBannerViewManager.swift +0 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -2
- package/package.json +8 -2
- package/react-native-bigcrunch-ads.podspec +0 -1
- package/scripts/inject-version.js +55 -0
- package/src/index.ts +3 -2
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Analytics client for tracking ad events
|
|
6
|
+
*
|
|
7
|
+
* AnalyticsClient sends all ad-related events to the BigCrunch pipeline:
|
|
8
|
+
* - Page/screen views → `/pageviews`
|
|
9
|
+
* - Ad impressions (includes request + auction + revenue) → `/impressions`
|
|
10
|
+
* - Ad clicks → `/clicks`
|
|
11
|
+
* - Ad viewability → `/viewability`
|
|
12
|
+
* - User engagement → `/engagement`
|
|
13
|
+
*
|
|
14
|
+
* All events are fire-and-forget (non-blocking) and sent asynchronously.
|
|
15
|
+
* Failures are logged but do not propagate to the caller.
|
|
16
|
+
*/
|
|
17
|
+
internal class AnalyticsClient {
|
|
18
|
+
|
|
19
|
+
private let httpClient: HTTPClient
|
|
20
|
+
private let baseURL: String
|
|
21
|
+
|
|
22
|
+
/// Nil UUID for fields that require UUID format but have no value
|
|
23
|
+
private static let nilUUID = "00000000-0000-0000-0000-000000000000"
|
|
24
|
+
|
|
25
|
+
/// Track impression contexts for correlating events
|
|
26
|
+
private var impressionContexts: [String: ImpressionContext] = [:]
|
|
27
|
+
private let contextLock = NSLock()
|
|
28
|
+
|
|
29
|
+
/// Previous screen name for referrer tracking
|
|
30
|
+
private var previousScreenName: String?
|
|
31
|
+
|
|
32
|
+
/// Batching for impressions (250ms delay)
|
|
33
|
+
private var impressionBatch: [ImpressionBatchEvent] = []
|
|
34
|
+
private var impressionBatchTimer: Timer?
|
|
35
|
+
private let impressionBatchLock = NSLock()
|
|
36
|
+
|
|
37
|
+
/// Batching for viewability (250ms delay)
|
|
38
|
+
private var viewabilityBatch: [ViewabilityEvent] = []
|
|
39
|
+
private var viewabilityBatchTimer: Timer?
|
|
40
|
+
private let viewabilityBatchLock = NSLock()
|
|
41
|
+
|
|
42
|
+
init(httpClient: HTTPClient, baseURL: String) {
|
|
43
|
+
self.httpClient = httpClient
|
|
44
|
+
self.baseURL = baseURL
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// MARK: - Session Context
|
|
48
|
+
|
|
49
|
+
private var sessionManager: SessionManager {
|
|
50
|
+
return SessionManager.shared
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private var propertyId: String {
|
|
54
|
+
return BigCrunchAds.propertyId
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// MARK: - Event Sending
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Send an event to a specific endpoint (fire-and-forget)
|
|
61
|
+
* Wraps single event in an array as required by server
|
|
62
|
+
*/
|
|
63
|
+
private func sendEvent<T: Encodable>(_ event: T, endpoint: String) {
|
|
64
|
+
Task {
|
|
65
|
+
do {
|
|
66
|
+
let encoder = JSONEncoder()
|
|
67
|
+
// Wrap single event in array as server expects arrays
|
|
68
|
+
let eventArray = [event]
|
|
69
|
+
let data = try encoder.encode(eventArray)
|
|
70
|
+
let json = String(data: data, encoding: .utf8)!
|
|
71
|
+
let url = "\(baseURL)/\(endpoint)"
|
|
72
|
+
|
|
73
|
+
BCLogger.debug("Sending event to: \(endpoint)")
|
|
74
|
+
|
|
75
|
+
let result = await httpClient.post(url: url, body: json)
|
|
76
|
+
|
|
77
|
+
switch result {
|
|
78
|
+
case .success:
|
|
79
|
+
BCLogger.verbose("Event sent successfully: \(endpoint)")
|
|
80
|
+
case .failure(let error):
|
|
81
|
+
BCLogger.warning("Failed to send event: \(endpoint) - \(error)")
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
BCLogger.error("Error sending event: \(error)")
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// MARK: - Common Event Fields
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get common web schema fields (device context, attribution, etc.)
|
|
93
|
+
* Returns a tuple with all the common fields needed for events
|
|
94
|
+
*/
|
|
95
|
+
private func getCommonEventFields() -> (
|
|
96
|
+
browser: String,
|
|
97
|
+
device: String,
|
|
98
|
+
os: String,
|
|
99
|
+
country: String,
|
|
100
|
+
region: String,
|
|
101
|
+
sessionSource: String,
|
|
102
|
+
sessionMedium: String,
|
|
103
|
+
utmSource: String?,
|
|
104
|
+
utmMedium: String?,
|
|
105
|
+
utmCampaign: String?,
|
|
106
|
+
utmTerm: String?,
|
|
107
|
+
utmContent: String?
|
|
108
|
+
) {
|
|
109
|
+
let deviceContext = DeviceContext.shared
|
|
110
|
+
let webFields = deviceContext.getWebSchemaFields()
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
browser: webFields["browser"] as! String,
|
|
114
|
+
device: webFields["device"] as! String,
|
|
115
|
+
os: webFields["os"] as! String,
|
|
116
|
+
country: webFields["country"] as! String,
|
|
117
|
+
region: webFields["region"] as! String,
|
|
118
|
+
sessionSource: sessionManager.sessionSource,
|
|
119
|
+
sessionMedium: sessionManager.sessionMedium,
|
|
120
|
+
utmSource: sessionManager.utmSource,
|
|
121
|
+
utmMedium: sessionManager.utmMedium,
|
|
122
|
+
utmCampaign: sessionManager.utmCampaign,
|
|
123
|
+
utmTerm: sessionManager.utmTerm,
|
|
124
|
+
utmContent: sessionManager.utmContent
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// MARK: - Page View Tracking
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Track a screen view
|
|
132
|
+
*
|
|
133
|
+
* - Parameter screenName: Name of the screen being viewed
|
|
134
|
+
*/
|
|
135
|
+
func trackScreenView(_ screenName: String) {
|
|
136
|
+
// Start new page view and get IDs
|
|
137
|
+
let pageId = sessionManager.startPageView()
|
|
138
|
+
|
|
139
|
+
// Get common event fields
|
|
140
|
+
let common = getCommonEventFields()
|
|
141
|
+
|
|
142
|
+
// Get GAM network code from config if available
|
|
143
|
+
let gamNetworkCode = BigCrunchAds.getConfigManager().getGamNetworkCode() ?? ""
|
|
144
|
+
|
|
145
|
+
// Region must be 2 chars or empty per backend validation
|
|
146
|
+
let regionCode = common.region.count == 2 ? common.region : ""
|
|
147
|
+
|
|
148
|
+
let event = PageViewEvent(
|
|
149
|
+
payloadVersion: "1.0.0", // Must be semver format
|
|
150
|
+
configVersion: 1,
|
|
151
|
+
browserTimestamp: sessionManager.getCurrentTimestamp(),
|
|
152
|
+
sessionId: sessionManager.sessionId,
|
|
153
|
+
userId: sessionManager.userId,
|
|
154
|
+
propertyId: propertyId,
|
|
155
|
+
newUser: sessionManager.isNewUser,
|
|
156
|
+
pageId: pageId,
|
|
157
|
+
sessionDepth: sessionManager.sessionDepth,
|
|
158
|
+
pageUrl: "", // page_url must be valid URL or empty - mobile doesn't have URLs
|
|
159
|
+
pageSearch: "",
|
|
160
|
+
pageReferrer: "", // page_referrer must be valid URL or empty
|
|
161
|
+
browser: common.browser,
|
|
162
|
+
device: common.device,
|
|
163
|
+
os: common.os,
|
|
164
|
+
country: common.country,
|
|
165
|
+
region: regionCode,
|
|
166
|
+
sessionSource: common.sessionSource,
|
|
167
|
+
sessionMedium: common.sessionMedium,
|
|
168
|
+
utmSource: common.utmSource ?? "",
|
|
169
|
+
utmMedium: common.utmMedium ?? "",
|
|
170
|
+
utmCampaign: common.utmCampaign ?? "",
|
|
171
|
+
utmTerm: common.utmTerm ?? "",
|
|
172
|
+
utmContent: common.utmContent ?? "",
|
|
173
|
+
gclid: "",
|
|
174
|
+
fbclid: "",
|
|
175
|
+
acctType: "anonymous",
|
|
176
|
+
diiSource: "",
|
|
177
|
+
gamNetworkCode: gamNetworkCode,
|
|
178
|
+
amznPubId: AnalyticsClient.nilUUID, // amzn_pub_id must be valid UUID
|
|
179
|
+
customDimensions: [:]
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
sendEvent(event, endpoint: "pageviews")
|
|
183
|
+
|
|
184
|
+
// Update previous screen for next referrer
|
|
185
|
+
previousScreenName = screenName
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Track an ad request
|
|
190
|
+
*
|
|
191
|
+
* - Parameters:
|
|
192
|
+
* - placementId: Placement identifier
|
|
193
|
+
* - format: Ad format (banner, interstitial, rewarded)
|
|
194
|
+
*/
|
|
195
|
+
func trackAdRequest(placementId: String, format: String) {
|
|
196
|
+
// For now, this is a placeholder as ad requests are tracked via createImpressionContext
|
|
197
|
+
// We may expand this in the future to track requests that don't result in impressions
|
|
198
|
+
print("[BigCrunchAds] Ad request tracked: \(placementId) format: \(format)")
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// MARK: - Impression Context Management
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Create and store an impression context for tracking
|
|
205
|
+
*
|
|
206
|
+
* Call this when starting to load an ad to generate an impression ID.
|
|
207
|
+
*/
|
|
208
|
+
func createImpressionContext(
|
|
209
|
+
placementId: String,
|
|
210
|
+
gamAdUnit: String,
|
|
211
|
+
format: String,
|
|
212
|
+
width: Int? = nil,
|
|
213
|
+
height: Int? = nil
|
|
214
|
+
) -> ImpressionContext {
|
|
215
|
+
let context = ImpressionContext(
|
|
216
|
+
placementId: placementId,
|
|
217
|
+
gamAdUnit: gamAdUnit,
|
|
218
|
+
format: format,
|
|
219
|
+
width: width,
|
|
220
|
+
height: height
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
contextLock.lock()
|
|
224
|
+
impressionContexts[context.impressionId] = context
|
|
225
|
+
contextLock.unlock()
|
|
226
|
+
|
|
227
|
+
BCLogger.debug("Created impression context: \(context.impressionId) for \(placementId)")
|
|
228
|
+
|
|
229
|
+
return context
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Update auction data for an impression context
|
|
234
|
+
*/
|
|
235
|
+
func updateAuctionData(impressionId: String, auctionData: AuctionData) {
|
|
236
|
+
contextLock.lock()
|
|
237
|
+
impressionContexts[impressionId]?.auctionData = auctionData
|
|
238
|
+
contextLock.unlock()
|
|
239
|
+
|
|
240
|
+
BCLogger.debug("Updated auction data for impression: \(impressionId)")
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get impression context by ID
|
|
245
|
+
*/
|
|
246
|
+
func getImpressionContext(_ impressionId: String) -> ImpressionContext? {
|
|
247
|
+
contextLock.lock()
|
|
248
|
+
defer { contextLock.unlock() }
|
|
249
|
+
return impressionContexts[impressionId]
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// MARK: - Impression Tracking
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Track an ad impression with auction data
|
|
256
|
+
*
|
|
257
|
+
* - Parameter context: The impression context with all tracking data
|
|
258
|
+
*/
|
|
259
|
+
func trackAdImpression(context: ImpressionContext) {
|
|
260
|
+
let adSize: String
|
|
261
|
+
if let width = context.width, let height = context.height {
|
|
262
|
+
adSize = "\(width)x\(height)"
|
|
263
|
+
} else {
|
|
264
|
+
adSize = ""
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Get common event fields
|
|
268
|
+
let common = getCommonEventFields()
|
|
269
|
+
|
|
270
|
+
// Get GAM network code from config if available
|
|
271
|
+
let gamNetworkCode = BigCrunchAds.getConfigManager().getGamNetworkCode() ?? ""
|
|
272
|
+
|
|
273
|
+
// Region must be 2 chars or empty per backend validation
|
|
274
|
+
let regionCode = common.region.count == 2 ? common.region : ""
|
|
275
|
+
|
|
276
|
+
// auction_id is required and must be a valid UUID - generate one if not provided
|
|
277
|
+
let auctionId = context.auctionData.auctionId?.isEmpty == false
|
|
278
|
+
? context.auctionData.auctionId!
|
|
279
|
+
: UUID().uuidString
|
|
280
|
+
|
|
281
|
+
// Create the impression record
|
|
282
|
+
let impressionRecord = ImpressionRecord(
|
|
283
|
+
slotId: context.placementId,
|
|
284
|
+
gamUnit: context.gamAdUnit,
|
|
285
|
+
gamPriceBucket: "",
|
|
286
|
+
impressionId: context.impressionId,
|
|
287
|
+
auctionId: auctionId, // Must be valid UUID
|
|
288
|
+
refreshCount: 0,
|
|
289
|
+
adBidder: context.auctionData.bidder ?? "",
|
|
290
|
+
adSize: adSize,
|
|
291
|
+
adPrice: context.auctionData.bidPriceCpm ?? 0.0,
|
|
292
|
+
adFloorPrice: 0.0,
|
|
293
|
+
minBidToWin: 0.0,
|
|
294
|
+
advertiserId: "",
|
|
295
|
+
campaignId: "",
|
|
296
|
+
lineItemId: "",
|
|
297
|
+
creativeId: context.auctionData.creativeId ?? "",
|
|
298
|
+
adAmznbid: "",
|
|
299
|
+
adAmznp: "",
|
|
300
|
+
adDemandType: "",
|
|
301
|
+
demandChannel: "",
|
|
302
|
+
customDimensions: [:]
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
// Wrap in batch event with session context
|
|
306
|
+
let batchEvent = ImpressionBatchEvent(
|
|
307
|
+
payloadVersion: "1.0.0", // Must be semver format
|
|
308
|
+
configVersion: 1,
|
|
309
|
+
browserTimestamp: sessionManager.getCurrentTimestamp(),
|
|
310
|
+
sessionId: sessionManager.sessionId,
|
|
311
|
+
userId: sessionManager.userId,
|
|
312
|
+
propertyId: propertyId,
|
|
313
|
+
newUser: sessionManager.isNewUser,
|
|
314
|
+
pageId: sessionManager.getOrCreatePageId(),
|
|
315
|
+
sessionDepth: sessionManager.sessionDepth,
|
|
316
|
+
pageUrl: "", // page_url must be valid URL or empty
|
|
317
|
+
pageSearch: "",
|
|
318
|
+
pageReferrer: "",
|
|
319
|
+
browser: common.browser,
|
|
320
|
+
device: common.device,
|
|
321
|
+
os: common.os,
|
|
322
|
+
country: common.country,
|
|
323
|
+
region: regionCode,
|
|
324
|
+
sessionSource: common.sessionSource,
|
|
325
|
+
sessionMedium: common.sessionMedium,
|
|
326
|
+
utmSource: common.utmSource ?? "",
|
|
327
|
+
utmMedium: common.utmMedium ?? "",
|
|
328
|
+
utmCampaign: common.utmCampaign ?? "",
|
|
329
|
+
utmTerm: common.utmTerm ?? "",
|
|
330
|
+
utmContent: common.utmContent ?? "",
|
|
331
|
+
gclid: "",
|
|
332
|
+
fbclid: "",
|
|
333
|
+
acctType: "anonymous",
|
|
334
|
+
diiSource: "",
|
|
335
|
+
gamNetworkCode: gamNetworkCode,
|
|
336
|
+
amznPubId: AnalyticsClient.nilUUID, // amzn_pub_id must be valid UUID
|
|
337
|
+
customDimensions: [:],
|
|
338
|
+
impressions: [impressionRecord]
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
batchImpressionEvent(batchEvent)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Track an ad impression (simple version without auction data)
|
|
346
|
+
*/
|
|
347
|
+
func trackAdImpression(
|
|
348
|
+
placementId: String,
|
|
349
|
+
format: String,
|
|
350
|
+
refreshCount: Int = 0,
|
|
351
|
+
advertiserId: String? = nil,
|
|
352
|
+
campaignId: String? = nil,
|
|
353
|
+
lineItemId: String? = nil,
|
|
354
|
+
creativeId: String? = nil
|
|
355
|
+
) {
|
|
356
|
+
// Get common event fields
|
|
357
|
+
let common = getCommonEventFields()
|
|
358
|
+
|
|
359
|
+
// Get GAM network code from config if available
|
|
360
|
+
let gamNetworkCode = BigCrunchAds.getConfigManager().getGamNetworkCode() ?? ""
|
|
361
|
+
|
|
362
|
+
// Region must be 2 chars or empty per backend validation
|
|
363
|
+
let regionCode = common.region.count == 2 ? common.region : ""
|
|
364
|
+
|
|
365
|
+
// Create the impression record with generated auction_id (required)
|
|
366
|
+
let impressionRecord = ImpressionRecord(
|
|
367
|
+
slotId: placementId,
|
|
368
|
+
gamUnit: "",
|
|
369
|
+
gamPriceBucket: "",
|
|
370
|
+
impressionId: UUID().uuidString,
|
|
371
|
+
auctionId: UUID().uuidString, // auction_id is required and must be valid UUID
|
|
372
|
+
refreshCount: refreshCount,
|
|
373
|
+
adBidder: "",
|
|
374
|
+
adSize: "",
|
|
375
|
+
adPrice: 0.0,
|
|
376
|
+
adFloorPrice: 0.0,
|
|
377
|
+
minBidToWin: 0.0,
|
|
378
|
+
advertiserId: advertiserId ?? "",
|
|
379
|
+
campaignId: campaignId ?? "",
|
|
380
|
+
lineItemId: lineItemId ?? "",
|
|
381
|
+
creativeId: creativeId ?? "",
|
|
382
|
+
adAmznbid: "",
|
|
383
|
+
adAmznp: "",
|
|
384
|
+
adDemandType: "",
|
|
385
|
+
demandChannel: "",
|
|
386
|
+
customDimensions: [:]
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
// Wrap in batch event with session context
|
|
390
|
+
let batchEvent = ImpressionBatchEvent(
|
|
391
|
+
payloadVersion: "1.0.0", // Must be semver format
|
|
392
|
+
configVersion: 1,
|
|
393
|
+
browserTimestamp: sessionManager.getCurrentTimestamp(),
|
|
394
|
+
sessionId: sessionManager.sessionId,
|
|
395
|
+
userId: sessionManager.userId,
|
|
396
|
+
propertyId: propertyId,
|
|
397
|
+
newUser: sessionManager.isNewUser,
|
|
398
|
+
pageId: sessionManager.getOrCreatePageId(),
|
|
399
|
+
sessionDepth: sessionManager.sessionDepth,
|
|
400
|
+
pageUrl: "", // page_url must be valid URL or empty
|
|
401
|
+
pageSearch: "",
|
|
402
|
+
pageReferrer: "",
|
|
403
|
+
browser: common.browser,
|
|
404
|
+
device: common.device,
|
|
405
|
+
os: common.os,
|
|
406
|
+
country: common.country,
|
|
407
|
+
region: regionCode,
|
|
408
|
+
sessionSource: common.sessionSource,
|
|
409
|
+
sessionMedium: common.sessionMedium,
|
|
410
|
+
utmSource: common.utmSource ?? "",
|
|
411
|
+
utmMedium: common.utmMedium ?? "",
|
|
412
|
+
utmCampaign: common.utmCampaign ?? "",
|
|
413
|
+
utmTerm: common.utmTerm ?? "",
|
|
414
|
+
utmContent: common.utmContent ?? "",
|
|
415
|
+
gclid: "",
|
|
416
|
+
fbclid: "",
|
|
417
|
+
acctType: "anonymous",
|
|
418
|
+
diiSource: "",
|
|
419
|
+
gamNetworkCode: gamNetworkCode,
|
|
420
|
+
amznPubId: AnalyticsClient.nilUUID, // amzn_pub_id must be valid UUID
|
|
421
|
+
customDimensions: [:],
|
|
422
|
+
impressions: [impressionRecord]
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
batchImpressionEvent(batchEvent)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// MARK: - Click Tracking
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Track an ad click with impression context
|
|
432
|
+
*
|
|
433
|
+
* - Parameters:
|
|
434
|
+
* - impressionId: The impression that was clicked
|
|
435
|
+
* - placementId: Placement ID
|
|
436
|
+
* - format: Ad format
|
|
437
|
+
*/
|
|
438
|
+
func trackAdClick(impressionId: String, placementId: String, format: String) {
|
|
439
|
+
// Get common event fields
|
|
440
|
+
let common = getCommonEventFields()
|
|
441
|
+
|
|
442
|
+
// Get GAM network code from config if available
|
|
443
|
+
let gamNetworkCode = BigCrunchAds.getConfigManager().getGamNetworkCode() ?? ""
|
|
444
|
+
|
|
445
|
+
// Region must be 2 chars or empty per backend validation
|
|
446
|
+
let regionCode = common.region.count == 2 ? common.region : ""
|
|
447
|
+
|
|
448
|
+
// Create nested click data per backend schema
|
|
449
|
+
// Note: customDimensions values must be string arrays per backend validation
|
|
450
|
+
let clickData = ClickData(
|
|
451
|
+
clickId: UUID().uuidString,
|
|
452
|
+
slotId: placementId,
|
|
453
|
+
impressionId: impressionId.isEmpty ? UUID().uuidString : impressionId, // Must be valid UUID
|
|
454
|
+
refreshCount: 0,
|
|
455
|
+
adAmznp: "",
|
|
456
|
+
adBidder: "",
|
|
457
|
+
adSize: "",
|
|
458
|
+
advertiserId: "",
|
|
459
|
+
campaignId: "",
|
|
460
|
+
lineItemId: "",
|
|
461
|
+
creativeId: "",
|
|
462
|
+
adDemandType: "",
|
|
463
|
+
demandChannel: "",
|
|
464
|
+
customDimensions: [:] // Empty dict - values would be [String] arrays
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
let event = ClickEvent(
|
|
468
|
+
payloadVersion: "1.0.0", // Must be semver format
|
|
469
|
+
configVersion: 1,
|
|
470
|
+
browserTimestamp: sessionManager.getCurrentTimestamp(),
|
|
471
|
+
sessionId: sessionManager.sessionId,
|
|
472
|
+
userId: sessionManager.userId,
|
|
473
|
+
propertyId: propertyId,
|
|
474
|
+
newUser: sessionManager.isNewUser,
|
|
475
|
+
pageId: sessionManager.getOrCreatePageId(),
|
|
476
|
+
sessionDepth: sessionManager.sessionDepth,
|
|
477
|
+
pageUrl: "",
|
|
478
|
+
pageSearch: "",
|
|
479
|
+
pageReferrer: "",
|
|
480
|
+
browser: common.browser,
|
|
481
|
+
device: common.device,
|
|
482
|
+
os: common.os,
|
|
483
|
+
country: common.country,
|
|
484
|
+
region: regionCode,
|
|
485
|
+
sessionSource: common.sessionSource,
|
|
486
|
+
sessionMedium: common.sessionMedium,
|
|
487
|
+
utmSource: common.utmSource ?? "",
|
|
488
|
+
utmMedium: common.utmMedium ?? "",
|
|
489
|
+
utmCampaign: common.utmCampaign ?? "",
|
|
490
|
+
utmTerm: common.utmTerm ?? "",
|
|
491
|
+
utmContent: common.utmContent ?? "",
|
|
492
|
+
gclid: "",
|
|
493
|
+
fbclid: "",
|
|
494
|
+
acctType: "anonymous",
|
|
495
|
+
diiSource: "",
|
|
496
|
+
gamNetworkCode: gamNetworkCode,
|
|
497
|
+
amznPubId: AnalyticsClient.nilUUID,
|
|
498
|
+
customDimensions: [:],
|
|
499
|
+
click: clickData
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
sendEvent(event, endpoint: "clicks")
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Track an ad click (simple version without impression ID)
|
|
507
|
+
*/
|
|
508
|
+
func trackAdClick(placementId: String, format: String) {
|
|
509
|
+
trackAdClick(impressionId: "", placementId: placementId, format: format)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// MARK: - Viewability Tracking
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Track ad viewability with impression context
|
|
516
|
+
*
|
|
517
|
+
* - Parameters:
|
|
518
|
+
* - impressionId: The impression that became viewable (must be valid UUID)
|
|
519
|
+
* - placementId: Placement ID (used as slot_id)
|
|
520
|
+
* - format: Ad format (not sent directly, used for logging)
|
|
521
|
+
* - viewableTimeMs: Time the ad was viewable in milliseconds (not in current schema)
|
|
522
|
+
* - percentVisible: Percentage of ad that was visible (not in current schema)
|
|
523
|
+
*/
|
|
524
|
+
func trackAdViewable(
|
|
525
|
+
impressionId: String,
|
|
526
|
+
placementId: String,
|
|
527
|
+
format: String,
|
|
528
|
+
viewableTimeMs: Int64,
|
|
529
|
+
percentVisible: Int
|
|
530
|
+
) {
|
|
531
|
+
// Get common event fields
|
|
532
|
+
let common = getCommonEventFields()
|
|
533
|
+
|
|
534
|
+
// Get GAM network code from config if available
|
|
535
|
+
let gamNetworkCode = BigCrunchAds.getConfigManager().getGamNetworkCode() ?? ""
|
|
536
|
+
|
|
537
|
+
// Region must be 2 chars or empty per backend validation
|
|
538
|
+
let regionCode = common.region.count == 2 ? common.region : ""
|
|
539
|
+
|
|
540
|
+
// Create viewability data with nested structure per backend schema
|
|
541
|
+
let viewabilityData = ViewabilityData(
|
|
542
|
+
slotId: placementId,
|
|
543
|
+
impressionId: impressionId.isEmpty ? UUID().uuidString : impressionId, // Must be valid UUID
|
|
544
|
+
refreshCount: 0,
|
|
545
|
+
adAmznp: "",
|
|
546
|
+
adBidder: "",
|
|
547
|
+
adSize: "",
|
|
548
|
+
advertiserId: "",
|
|
549
|
+
campaignId: "",
|
|
550
|
+
lineItemId: "",
|
|
551
|
+
creativeId: "",
|
|
552
|
+
adDemandType: "",
|
|
553
|
+
demandChannel: "",
|
|
554
|
+
customDimensions: [:]
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
let event = ViewabilityEvent(
|
|
558
|
+
payloadVersion: "1.0.0", // Must be semver format
|
|
559
|
+
configVersion: 1,
|
|
560
|
+
browserTimestamp: sessionManager.getCurrentTimestamp(),
|
|
561
|
+
sessionId: sessionManager.sessionId,
|
|
562
|
+
userId: sessionManager.userId,
|
|
563
|
+
propertyId: propertyId,
|
|
564
|
+
newUser: sessionManager.isNewUser,
|
|
565
|
+
pageId: sessionManager.getOrCreatePageId(),
|
|
566
|
+
sessionDepth: sessionManager.sessionDepth,
|
|
567
|
+
pageUrl: "",
|
|
568
|
+
pageSearch: "",
|
|
569
|
+
pageReferrer: "",
|
|
570
|
+
browser: common.browser,
|
|
571
|
+
device: common.device,
|
|
572
|
+
os: common.os,
|
|
573
|
+
country: common.country,
|
|
574
|
+
region: regionCode,
|
|
575
|
+
sessionSource: common.sessionSource,
|
|
576
|
+
sessionMedium: common.sessionMedium,
|
|
577
|
+
utmSource: common.utmSource ?? "",
|
|
578
|
+
utmMedium: common.utmMedium ?? "",
|
|
579
|
+
utmCampaign: common.utmCampaign ?? "",
|
|
580
|
+
utmTerm: common.utmTerm ?? "",
|
|
581
|
+
utmContent: common.utmContent ?? "",
|
|
582
|
+
gclid: "",
|
|
583
|
+
fbclid: "",
|
|
584
|
+
acctType: "anonymous",
|
|
585
|
+
diiSource: "",
|
|
586
|
+
gamNetworkCode: gamNetworkCode,
|
|
587
|
+
amznPubId: AnalyticsClient.nilUUID,
|
|
588
|
+
viewability: [viewabilityData]
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
batchViewabilityEvent(event)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Track ad viewability (simple version without metrics)
|
|
596
|
+
*/
|
|
597
|
+
func trackAdViewable(placementId: String, format: String) {
|
|
598
|
+
// Get common event fields
|
|
599
|
+
let common = getCommonEventFields()
|
|
600
|
+
|
|
601
|
+
// Get GAM network code from config if available
|
|
602
|
+
let gamNetworkCode = BigCrunchAds.getConfigManager().getGamNetworkCode() ?? ""
|
|
603
|
+
|
|
604
|
+
// Region must be 2 chars or empty per backend validation
|
|
605
|
+
let regionCode = common.region.count == 2 ? common.region : ""
|
|
606
|
+
|
|
607
|
+
// Create viewability data with nested structure per backend schema
|
|
608
|
+
let viewabilityData = ViewabilityData(
|
|
609
|
+
slotId: placementId,
|
|
610
|
+
impressionId: UUID().uuidString, // Generate UUID since none provided
|
|
611
|
+
refreshCount: 0,
|
|
612
|
+
adAmznp: "",
|
|
613
|
+
adBidder: "",
|
|
614
|
+
adSize: "",
|
|
615
|
+
advertiserId: "",
|
|
616
|
+
campaignId: "",
|
|
617
|
+
lineItemId: "",
|
|
618
|
+
creativeId: "",
|
|
619
|
+
adDemandType: "",
|
|
620
|
+
demandChannel: "",
|
|
621
|
+
customDimensions: [:]
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
let event = ViewabilityEvent(
|
|
625
|
+
payloadVersion: "1.0.0", // Must be semver format
|
|
626
|
+
configVersion: 1,
|
|
627
|
+
browserTimestamp: sessionManager.getCurrentTimestamp(),
|
|
628
|
+
sessionId: sessionManager.sessionId,
|
|
629
|
+
userId: sessionManager.userId,
|
|
630
|
+
propertyId: propertyId,
|
|
631
|
+
newUser: sessionManager.isNewUser,
|
|
632
|
+
pageId: sessionManager.getOrCreatePageId(),
|
|
633
|
+
sessionDepth: sessionManager.sessionDepth,
|
|
634
|
+
pageUrl: "",
|
|
635
|
+
pageSearch: "",
|
|
636
|
+
pageReferrer: "",
|
|
637
|
+
browser: common.browser,
|
|
638
|
+
device: common.device,
|
|
639
|
+
os: common.os,
|
|
640
|
+
country: common.country,
|
|
641
|
+
region: regionCode,
|
|
642
|
+
sessionSource: common.sessionSource,
|
|
643
|
+
sessionMedium: common.sessionMedium,
|
|
644
|
+
utmSource: common.utmSource ?? "",
|
|
645
|
+
utmMedium: common.utmMedium ?? "",
|
|
646
|
+
utmCampaign: common.utmCampaign ?? "",
|
|
647
|
+
utmTerm: common.utmTerm ?? "",
|
|
648
|
+
utmContent: common.utmContent ?? "",
|
|
649
|
+
gclid: "",
|
|
650
|
+
fbclid: "",
|
|
651
|
+
acctType: "anonymous",
|
|
652
|
+
diiSource: "",
|
|
653
|
+
gamNetworkCode: gamNetworkCode,
|
|
654
|
+
amznPubId: AnalyticsClient.nilUUID,
|
|
655
|
+
viewability: [viewabilityData]
|
|
656
|
+
)
|
|
657
|
+
batchViewabilityEvent(event)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// MARK: - Engagement Tracking
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Track user engagement with content
|
|
664
|
+
*
|
|
665
|
+
* - Parameters:
|
|
666
|
+
* - engagedTime: Time actively engaged in seconds
|
|
667
|
+
* - timeOnPage: Total time on page/screen in seconds
|
|
668
|
+
* - scrollDepth: Maximum scroll depth percentage (0-100)
|
|
669
|
+
*/
|
|
670
|
+
func trackEngagement(engagedTime: Int, timeOnPage: Int, scrollDepth: Int = 0) {
|
|
671
|
+
// Get common event fields
|
|
672
|
+
let common = getCommonEventFields()
|
|
673
|
+
|
|
674
|
+
// Get GAM network code from config if available
|
|
675
|
+
let gamNetworkCode = BigCrunchAds.getConfigManager().getGamNetworkCode() ?? ""
|
|
676
|
+
|
|
677
|
+
// Region must be 2 chars or empty per backend validation
|
|
678
|
+
let regionCode = common.region.count == 2 ? common.region : ""
|
|
679
|
+
|
|
680
|
+
let event = EngagementEvent(
|
|
681
|
+
payloadVersion: "1.0.0", // Must be semver format
|
|
682
|
+
configVersion: 1,
|
|
683
|
+
browserTimestamp: sessionManager.getCurrentTimestamp(),
|
|
684
|
+
sessionId: sessionManager.sessionId,
|
|
685
|
+
userId: sessionManager.userId,
|
|
686
|
+
propertyId: propertyId,
|
|
687
|
+
newUser: sessionManager.isNewUser,
|
|
688
|
+
pageId: sessionManager.getOrCreatePageId(),
|
|
689
|
+
sessionDepth: sessionManager.sessionDepth,
|
|
690
|
+
pageUrl: "",
|
|
691
|
+
pageSearch: "",
|
|
692
|
+
pageReferrer: "",
|
|
693
|
+
browser: common.browser,
|
|
694
|
+
device: common.device,
|
|
695
|
+
os: common.os,
|
|
696
|
+
country: common.country,
|
|
697
|
+
region: regionCode,
|
|
698
|
+
sessionSource: common.sessionSource,
|
|
699
|
+
sessionMedium: common.sessionMedium,
|
|
700
|
+
utmSource: common.utmSource ?? "",
|
|
701
|
+
utmMedium: common.utmMedium ?? "",
|
|
702
|
+
utmCampaign: common.utmCampaign ?? "",
|
|
703
|
+
utmTerm: common.utmTerm ?? "",
|
|
704
|
+
utmContent: common.utmContent ?? "",
|
|
705
|
+
gclid: "",
|
|
706
|
+
fbclid: "",
|
|
707
|
+
acctType: "anonymous",
|
|
708
|
+
diiSource: "",
|
|
709
|
+
gamNetworkCode: gamNetworkCode,
|
|
710
|
+
amznPubId: AnalyticsClient.nilUUID,
|
|
711
|
+
customDimensions: [:], // Values would be [String] arrays per backend schema
|
|
712
|
+
engagedTime: engagedTime,
|
|
713
|
+
timeOnPage: timeOnPage,
|
|
714
|
+
scrollDepth: scrollDepth
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
sendEvent(event, endpoint: "engagement")
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// MARK: - Revenue Tracking
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Track ad revenue from ILRD (Impression-Level Revenue Data)
|
|
724
|
+
*
|
|
725
|
+
* Note: Revenue is now primarily tracked via the impression event's bidPriceCpm field.
|
|
726
|
+
* This method logs revenue data for debugging but does not send a separate event.
|
|
727
|
+
*
|
|
728
|
+
* - Parameters:
|
|
729
|
+
* - placementId: The placement ID
|
|
730
|
+
* - format: Ad format (banner, interstitial, rewarded)
|
|
731
|
+
* - valueMicros: Revenue value in micros (1 micro = 0.000001 of currency)
|
|
732
|
+
* - currency: ISO 4217 currency code (e.g., "USD")
|
|
733
|
+
*/
|
|
734
|
+
func trackAdRevenue(placementId: String, format: String, valueMicros: Int64, currency: String) {
|
|
735
|
+
// Revenue is now tracked via ImpressionEvent.bidPriceCpm
|
|
736
|
+
// This method is kept for backwards compatibility and logging
|
|
737
|
+
let valueInCurrency = Double(valueMicros) / 1_000_000.0
|
|
738
|
+
BCLogger.debug("Ad revenue: \(placementId) (\(format)) - \(valueInCurrency) \(currency)")
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// MARK: - Batching Helpers
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Add impression event to batch queue
|
|
745
|
+
*
|
|
746
|
+
* Batches impressions for 250ms before sending to reduce network requests.
|
|
747
|
+
*/
|
|
748
|
+
private func batchImpressionEvent(_ event: ImpressionBatchEvent) {
|
|
749
|
+
impressionBatchLock.lock()
|
|
750
|
+
impressionBatch.append(event)
|
|
751
|
+
|
|
752
|
+
// Start timer if not already running
|
|
753
|
+
if impressionBatchTimer == nil {
|
|
754
|
+
impressionBatchTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false) { [weak self] _ in
|
|
755
|
+
self?.flushImpressionBatch()
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
impressionBatchLock.unlock()
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Flush all batched impression events
|
|
764
|
+
*/
|
|
765
|
+
private func flushImpressionBatch() {
|
|
766
|
+
impressionBatchLock.lock()
|
|
767
|
+
let events = impressionBatch
|
|
768
|
+
impressionBatch.removeAll()
|
|
769
|
+
impressionBatchTimer?.invalidate()
|
|
770
|
+
impressionBatchTimer = nil
|
|
771
|
+
impressionBatchLock.unlock()
|
|
772
|
+
|
|
773
|
+
// Send batched events as array
|
|
774
|
+
if !events.isEmpty {
|
|
775
|
+
Task {
|
|
776
|
+
do {
|
|
777
|
+
let encoder = JSONEncoder()
|
|
778
|
+
let data = try encoder.encode(events)
|
|
779
|
+
let json = String(data: data, encoding: .utf8)!
|
|
780
|
+
let url = "\(baseURL)/impressions"
|
|
781
|
+
|
|
782
|
+
BCLogger.debug("Sending \(events.count) batched impressions")
|
|
783
|
+
|
|
784
|
+
let result = await httpClient.post(url: url, body: json)
|
|
785
|
+
|
|
786
|
+
switch result {
|
|
787
|
+
case .success:
|
|
788
|
+
BCLogger.verbose("Impression batch sent successfully")
|
|
789
|
+
case .failure(let error):
|
|
790
|
+
BCLogger.warning("Failed to send impression batch: \(error)")
|
|
791
|
+
}
|
|
792
|
+
} catch {
|
|
793
|
+
BCLogger.error("Error sending impression batch: \(error)")
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Add viewability event to batch queue
|
|
801
|
+
*
|
|
802
|
+
* Batches viewability events for 250ms before sending to reduce network requests.
|
|
803
|
+
*/
|
|
804
|
+
private func batchViewabilityEvent(_ event: ViewabilityEvent) {
|
|
805
|
+
viewabilityBatchLock.lock()
|
|
806
|
+
viewabilityBatch.append(event)
|
|
807
|
+
|
|
808
|
+
// Start timer if not already running
|
|
809
|
+
if viewabilityBatchTimer == nil {
|
|
810
|
+
viewabilityBatchTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false) { [weak self] _ in
|
|
811
|
+
self?.flushViewabilityBatch()
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
viewabilityBatchLock.unlock()
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Flush all batched viewability events
|
|
820
|
+
*/
|
|
821
|
+
private func flushViewabilityBatch() {
|
|
822
|
+
viewabilityBatchLock.lock()
|
|
823
|
+
let events = viewabilityBatch
|
|
824
|
+
viewabilityBatch.removeAll()
|
|
825
|
+
viewabilityBatchTimer?.invalidate()
|
|
826
|
+
viewabilityBatchTimer = nil
|
|
827
|
+
viewabilityBatchLock.unlock()
|
|
828
|
+
|
|
829
|
+
// Send batched events as array
|
|
830
|
+
if !events.isEmpty {
|
|
831
|
+
Task {
|
|
832
|
+
do {
|
|
833
|
+
let encoder = JSONEncoder()
|
|
834
|
+
let data = try encoder.encode(events)
|
|
835
|
+
let json = String(data: data, encoding: .utf8)!
|
|
836
|
+
let url = "\(baseURL)/viewability"
|
|
837
|
+
|
|
838
|
+
BCLogger.debug("Sending \(events.count) batched viewability events")
|
|
839
|
+
|
|
840
|
+
let result = await httpClient.post(url: url, body: json)
|
|
841
|
+
|
|
842
|
+
switch result {
|
|
843
|
+
case .success:
|
|
844
|
+
BCLogger.verbose("Viewability batch sent successfully")
|
|
845
|
+
case .failure(let error):
|
|
846
|
+
BCLogger.warning("Failed to send viewability batch: \(error)")
|
|
847
|
+
}
|
|
848
|
+
} catch {
|
|
849
|
+
BCLogger.error("Error sending viewability batch: \(error)")
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// MARK: - Cleanup
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Remove an impression context (call when ad is destroyed)
|
|
859
|
+
*/
|
|
860
|
+
func removeImpressionContext(_ impressionId: String) {
|
|
861
|
+
contextLock.lock()
|
|
862
|
+
impressionContexts.removeValue(forKey: impressionId)
|
|
863
|
+
contextLock.unlock()
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Clear all impression contexts
|
|
868
|
+
*/
|
|
869
|
+
func clearImpressionContexts() {
|
|
870
|
+
contextLock.lock()
|
|
871
|
+
impressionContexts.removeAll()
|
|
872
|
+
contextLock.unlock()
|
|
873
|
+
}
|
|
874
|
+
}
|