@bigcrunch/react-native-ads 0.11.0 → 0.14.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/android/bigcrunch-ads/com/bigcrunch/ads/BigCrunchAds.kt +21 -4
- package/android/bigcrunch-ads/com/bigcrunch/ads/adapters/GoogleAdsAdapter.kt +8 -1
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/AdOrchestrator.kt +37 -12
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/AnalyticsClient.kt +213 -46
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/BidRequestClient.kt +52 -17
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/DeviceContext.kt +1 -1
- package/android/bigcrunch-ads/com/bigcrunch/ads/models/AdEvent.kt +81 -2
- package/android/bigcrunch-ads/com/bigcrunch/ads/models/PlacementConfig.kt +4 -1
- package/android/src/main/java/com/bigcrunch/ads/react/BigCrunchAdsModule.kt +57 -2
- package/ios/BigCrunchAds/Sources/Adapters/GoogleAdsAdapter.swift +7 -0
- package/ios/BigCrunchAds/Sources/BigCrunchAds.swift +29 -6
- package/ios/BigCrunchAds/Sources/BigCrunchRewarded.swift +10 -2
- package/ios/BigCrunchAds/Sources/Core/AdOrchestrator.swift +20 -4
- package/ios/BigCrunchAds/Sources/Core/AnalyticsClient.swift +193 -84
- package/ios/BigCrunchAds/Sources/Core/BidRequestClient.swift +54 -17
- package/ios/BigCrunchAds/Sources/Core/DeviceContext.swift +1 -1
- package/ios/BigCrunchAds/Sources/Models/AdEvent.swift +111 -3
- package/ios/BigCrunchAds/Sources/Models/PlacementConfig.swift +4 -1
- package/ios/BigCrunchAdsModule.m +6 -0
- package/ios/BigCrunchAdsModule.swift +39 -2
- package/ios/BigCrunchBannerViewManager.swift +1 -0
- package/lib/BigCrunchAds.d.ts +33 -2
- package/lib/BigCrunchAds.d.ts.map +1 -1
- package/lib/BigCrunchAds.js +35 -2
- package/lib/NativeBigCrunchAds.d.ts +3 -2
- package/lib/NativeBigCrunchAds.d.ts.map +1 -1
- package/lib/types/index.d.ts +40 -0
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/react-native-bigcrunch-ads.podspec +1 -1
- package/src/BigCrunchAds.ts +41 -2
- package/src/NativeBigCrunchAds.ts +5 -2
- package/src/types/index.ts +43 -0
|
@@ -26,9 +26,21 @@ internal class AnalyticsClient {
|
|
|
26
26
|
private var impressionContexts: [String: ImpressionContext] = [:]
|
|
27
27
|
private let contextLock = NSLock()
|
|
28
28
|
|
|
29
|
+
/// Per-placement auction data from S2S responses (set by AdOrchestrator, consumed by trackAdImpression)
|
|
30
|
+
private var placementAuctionData: [String: AuctionData] = [:]
|
|
31
|
+
|
|
29
32
|
/// Previous screen name for referrer tracking
|
|
30
33
|
private var previousScreenName: String?
|
|
31
34
|
|
|
35
|
+
/// Current page URL (set by trackScreenView, used by all events)
|
|
36
|
+
private var currentPageUrl: String = ""
|
|
37
|
+
|
|
38
|
+
/// Current custom dimensions (set by trackScreenView, used by all events)
|
|
39
|
+
private var currentCustomDimensions: [String: String] = [:]
|
|
40
|
+
|
|
41
|
+
/// Current account type (set by setAccountType, used by all events)
|
|
42
|
+
private var currentAcctType: String = "guest"
|
|
43
|
+
|
|
32
44
|
/// Batching for impressions (250ms delay)
|
|
33
45
|
private var impressionBatch: [ImpressionBatchEvent] = []
|
|
34
46
|
private var impressionBatchTimer: Timer?
|
|
@@ -127,15 +139,34 @@ internal class AnalyticsClient {
|
|
|
127
139
|
|
|
128
140
|
// MARK: - Page View Tracking
|
|
129
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Generate the default page URL from screen name
|
|
144
|
+
*/
|
|
145
|
+
private func generatePageUrl(_ screenName: String) -> String {
|
|
146
|
+
let appName = BigCrunchAds.getAppConfig()?.appName ?? "app"
|
|
147
|
+
let sanitizedAppName = appName.lowercased()
|
|
148
|
+
.replacingOccurrences(of: " ", with: "-")
|
|
149
|
+
.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? appName
|
|
150
|
+
let encodedScreen = screenName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? screenName
|
|
151
|
+
return "https://\(sanitizedAppName).mobile.app/\(encodedScreen)"
|
|
152
|
+
}
|
|
153
|
+
|
|
130
154
|
/**
|
|
131
155
|
* Track a screen view
|
|
132
156
|
*
|
|
133
|
-
* -
|
|
157
|
+
* - Parameters:
|
|
158
|
+
* - screenName: Name of the screen being viewed
|
|
159
|
+
* - options: Optional overrides for page URL, metadata, and custom dimensions
|
|
134
160
|
*/
|
|
135
|
-
func trackScreenView(_ screenName: String) {
|
|
161
|
+
func trackScreenView(_ screenName: String, options: ScreenViewOptions? = nil) {
|
|
136
162
|
// Start new page view and get IDs
|
|
137
163
|
let pageId = sessionManager.startPageView()
|
|
138
164
|
|
|
165
|
+
// Resolve page URL: explicit override > auto-generated
|
|
166
|
+
let pageUrl = options?.pageUrl ?? generatePageUrl(screenName)
|
|
167
|
+
currentPageUrl = pageUrl
|
|
168
|
+
currentCustomDimensions = options?.customDimensions ?? [:]
|
|
169
|
+
|
|
139
170
|
// Get common event fields
|
|
140
171
|
let common = getCommonEventFields()
|
|
141
172
|
|
|
@@ -155,7 +186,7 @@ internal class AnalyticsClient {
|
|
|
155
186
|
newUser: sessionManager.isNewUser,
|
|
156
187
|
pageId: pageId,
|
|
157
188
|
sessionDepth: sessionManager.sessionDepth,
|
|
158
|
-
pageUrl:
|
|
189
|
+
pageUrl: pageUrl,
|
|
159
190
|
pageSearch: "",
|
|
160
191
|
pageReferrer: "", // page_referrer must be valid URL or empty
|
|
161
192
|
browser: common.browser,
|
|
@@ -172,11 +203,12 @@ internal class AnalyticsClient {
|
|
|
172
203
|
utmContent: common.utmContent ?? "",
|
|
173
204
|
gclid: "",
|
|
174
205
|
fbclid: "",
|
|
175
|
-
acctType:
|
|
206
|
+
acctType: currentAcctType,
|
|
176
207
|
diiSource: "",
|
|
177
208
|
gamNetworkCode: gamNetworkCode,
|
|
178
209
|
amznPubId: AnalyticsClient.nilUUID, // amzn_pub_id must be valid UUID
|
|
179
|
-
customDimensions:
|
|
210
|
+
customDimensions: currentCustomDimensions,
|
|
211
|
+
pageMetaData: options?.pageMeta
|
|
180
212
|
)
|
|
181
213
|
|
|
182
214
|
sendEvent(event, endpoint: "pageviews")
|
|
@@ -249,6 +281,41 @@ internal class AnalyticsClient {
|
|
|
249
281
|
return impressionContexts[impressionId]
|
|
250
282
|
}
|
|
251
283
|
|
|
284
|
+
// MARK: - Auction Data Storage
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Store auction data for a placement (called by AdOrchestrator after S2S demand fetch)
|
|
288
|
+
*/
|
|
289
|
+
func setAccountType(_ accountType: String) {
|
|
290
|
+
currentAcctType = accountType
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
func setAuctionData(placementId: String, auctionData: AuctionData) {
|
|
294
|
+
contextLock.lock()
|
|
295
|
+
placementAuctionData[placementId] = auctionData
|
|
296
|
+
contextLock.unlock()
|
|
297
|
+
BCLogger.debug("Stored auction data for placement: \(placementId) (channel: \(auctionData.demandChannel ?? "unknown"))")
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Consume auction data for a placement (returns and removes stored data)
|
|
302
|
+
*/
|
|
303
|
+
private func consumeAuctionData(placementId: String) -> AuctionData? {
|
|
304
|
+
contextLock.lock()
|
|
305
|
+
let data = placementAuctionData.removeValue(forKey: placementId)
|
|
306
|
+
contextLock.unlock()
|
|
307
|
+
return data
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Compute min_bid_to_win: $0.01 above the higher of second-highest bid or floor price
|
|
312
|
+
*/
|
|
313
|
+
private func computeMinBidToWin(_ auctionData: AuctionData?) -> Double {
|
|
314
|
+
guard let data = auctionData else { return 0.0 }
|
|
315
|
+
let baseline = max(data.secondHighestBid ?? 0.0, data.floorPrice ?? 0.0)
|
|
316
|
+
return baseline > 0 ? baseline + 0.01 : 0.0
|
|
317
|
+
}
|
|
318
|
+
|
|
252
319
|
// MARK: - Impression Tracking
|
|
253
320
|
|
|
254
321
|
/**
|
|
@@ -278,27 +345,31 @@ internal class AnalyticsClient {
|
|
|
278
345
|
? context.auctionData.auctionId!
|
|
279
346
|
: UUID().uuidString
|
|
280
347
|
|
|
348
|
+
// Look up stored auction data (may have been set by AdOrchestrator)
|
|
349
|
+
let storedAuctionData = consumeAuctionData(placementId: context.placementId)
|
|
350
|
+
let effectiveAuctionData = storedAuctionData ?? context.auctionData
|
|
351
|
+
|
|
281
352
|
// Create the impression record
|
|
282
353
|
let impressionRecord = ImpressionRecord(
|
|
283
354
|
slotId: context.placementId,
|
|
284
355
|
gamUnit: context.gamAdUnit,
|
|
285
|
-
gamPriceBucket: "",
|
|
356
|
+
gamPriceBucket: effectiveAuctionData.gamPriceBucket ?? "",
|
|
286
357
|
impressionId: context.impressionId,
|
|
287
358
|
auctionId: auctionId, // Must be valid UUID
|
|
288
359
|
refreshCount: 0,
|
|
289
|
-
adBidder:
|
|
360
|
+
adBidder: effectiveAuctionData.bidder ?? "",
|
|
290
361
|
adSize: adSize,
|
|
291
|
-
adPrice:
|
|
292
|
-
adFloorPrice: 0.0,
|
|
293
|
-
minBidToWin:
|
|
362
|
+
adPrice: effectiveAuctionData.bidPriceCpm ?? 0.0,
|
|
363
|
+
adFloorPrice: effectiveAuctionData.floorPrice ?? 0.0,
|
|
364
|
+
minBidToWin: computeMinBidToWin(effectiveAuctionData),
|
|
294
365
|
advertiserId: "",
|
|
295
366
|
campaignId: "",
|
|
296
367
|
lineItemId: "",
|
|
297
|
-
creativeId:
|
|
368
|
+
creativeId: effectiveAuctionData.creativeId ?? "",
|
|
298
369
|
adAmznbid: "",
|
|
299
370
|
adAmznp: "",
|
|
300
|
-
adDemandType:
|
|
301
|
-
demandChannel: "",
|
|
371
|
+
adDemandType: context.format,
|
|
372
|
+
demandChannel: effectiveAuctionData.demandChannel ?? "Google Ad Exchange",
|
|
302
373
|
customDimensions: [:]
|
|
303
374
|
)
|
|
304
375
|
|
|
@@ -313,7 +384,7 @@ internal class AnalyticsClient {
|
|
|
313
384
|
newUser: sessionManager.isNewUser,
|
|
314
385
|
pageId: sessionManager.getOrCreatePageId(),
|
|
315
386
|
sessionDepth: sessionManager.sessionDepth,
|
|
316
|
-
pageUrl:
|
|
387
|
+
pageUrl: currentPageUrl,
|
|
317
388
|
pageSearch: "",
|
|
318
389
|
pageReferrer: "",
|
|
319
390
|
browser: common.browser,
|
|
@@ -330,11 +401,11 @@ internal class AnalyticsClient {
|
|
|
330
401
|
utmContent: common.utmContent ?? "",
|
|
331
402
|
gclid: "",
|
|
332
403
|
fbclid: "",
|
|
333
|
-
acctType:
|
|
404
|
+
acctType: currentAcctType,
|
|
334
405
|
diiSource: "",
|
|
335
406
|
gamNetworkCode: gamNetworkCode,
|
|
336
407
|
amznPubId: AnalyticsClient.nilUUID, // amzn_pub_id must be valid UUID
|
|
337
|
-
customDimensions:
|
|
408
|
+
customDimensions: currentCustomDimensions,
|
|
338
409
|
impressions: [impressionRecord]
|
|
339
410
|
)
|
|
340
411
|
|
|
@@ -342,11 +413,13 @@ internal class AnalyticsClient {
|
|
|
342
413
|
}
|
|
343
414
|
|
|
344
415
|
/**
|
|
345
|
-
* Track an ad impression (simple version
|
|
416
|
+
* Track an ad impression (simple version with auction data lookup)
|
|
346
417
|
*/
|
|
347
418
|
func trackAdImpression(
|
|
348
419
|
placementId: String,
|
|
349
420
|
format: String,
|
|
421
|
+
gamAdUnit: String = "",
|
|
422
|
+
adSize: String = "",
|
|
350
423
|
refreshCount: Int = 0,
|
|
351
424
|
advertiserId: String? = nil,
|
|
352
425
|
campaignId: String? = nil,
|
|
@@ -362,27 +435,35 @@ internal class AnalyticsClient {
|
|
|
362
435
|
// Region must be 2 chars or empty per backend validation
|
|
363
436
|
let regionCode = common.region.count == 2 ? common.region : ""
|
|
364
437
|
|
|
365
|
-
//
|
|
438
|
+
// Look up stored auction data from S2S response
|
|
439
|
+
let auctionData = consumeAuctionData(placementId: placementId)
|
|
440
|
+
|
|
441
|
+
// auction_id: use S2S auction ID if available, otherwise generate one
|
|
442
|
+
let auctionId = auctionData?.auctionId?.isEmpty == false
|
|
443
|
+
? auctionData!.auctionId!
|
|
444
|
+
: UUID().uuidString
|
|
445
|
+
|
|
446
|
+
// Create the impression record with auction data fields populated
|
|
366
447
|
let impressionRecord = ImpressionRecord(
|
|
367
448
|
slotId: placementId,
|
|
368
|
-
gamUnit:
|
|
369
|
-
gamPriceBucket: "",
|
|
449
|
+
gamUnit: gamAdUnit,
|
|
450
|
+
gamPriceBucket: auctionData?.gamPriceBucket ?? "",
|
|
370
451
|
impressionId: UUID().uuidString,
|
|
371
|
-
auctionId:
|
|
452
|
+
auctionId: auctionId,
|
|
372
453
|
refreshCount: refreshCount,
|
|
373
|
-
adBidder: "",
|
|
374
|
-
adSize:
|
|
375
|
-
adPrice: 0.0,
|
|
376
|
-
adFloorPrice: 0.0,
|
|
377
|
-
minBidToWin:
|
|
454
|
+
adBidder: auctionData?.bidder ?? "",
|
|
455
|
+
adSize: adSize,
|
|
456
|
+
adPrice: auctionData?.bidPriceCpm ?? 0.0,
|
|
457
|
+
adFloorPrice: auctionData?.floorPrice ?? 0.0,
|
|
458
|
+
minBidToWin: computeMinBidToWin(auctionData),
|
|
378
459
|
advertiserId: advertiserId ?? "",
|
|
379
460
|
campaignId: campaignId ?? "",
|
|
380
461
|
lineItemId: lineItemId ?? "",
|
|
381
|
-
creativeId: creativeId ?? "",
|
|
462
|
+
creativeId: auctionData?.creativeId ?? creativeId ?? "",
|
|
382
463
|
adAmznbid: "",
|
|
383
464
|
adAmznp: "",
|
|
384
|
-
adDemandType:
|
|
385
|
-
demandChannel: "",
|
|
465
|
+
adDemandType: format,
|
|
466
|
+
demandChannel: auctionData?.demandChannel ?? "Google Ad Exchange",
|
|
386
467
|
customDimensions: [:]
|
|
387
468
|
)
|
|
388
469
|
|
|
@@ -397,7 +478,7 @@ internal class AnalyticsClient {
|
|
|
397
478
|
newUser: sessionManager.isNewUser,
|
|
398
479
|
pageId: sessionManager.getOrCreatePageId(),
|
|
399
480
|
sessionDepth: sessionManager.sessionDepth,
|
|
400
|
-
pageUrl:
|
|
481
|
+
pageUrl: currentPageUrl,
|
|
401
482
|
pageSearch: "",
|
|
402
483
|
pageReferrer: "",
|
|
403
484
|
browser: common.browser,
|
|
@@ -414,11 +495,11 @@ internal class AnalyticsClient {
|
|
|
414
495
|
utmContent: common.utmContent ?? "",
|
|
415
496
|
gclid: "",
|
|
416
497
|
fbclid: "",
|
|
417
|
-
acctType:
|
|
498
|
+
acctType: currentAcctType,
|
|
418
499
|
diiSource: "",
|
|
419
500
|
gamNetworkCode: gamNetworkCode,
|
|
420
501
|
amznPubId: AnalyticsClient.nilUUID, // amzn_pub_id must be valid UUID
|
|
421
|
-
customDimensions:
|
|
502
|
+
customDimensions: currentCustomDimensions,
|
|
422
503
|
impressions: [impressionRecord]
|
|
423
504
|
)
|
|
424
505
|
|
|
@@ -474,7 +555,7 @@ internal class AnalyticsClient {
|
|
|
474
555
|
newUser: sessionManager.isNewUser,
|
|
475
556
|
pageId: sessionManager.getOrCreatePageId(),
|
|
476
557
|
sessionDepth: sessionManager.sessionDepth,
|
|
477
|
-
pageUrl:
|
|
558
|
+
pageUrl: currentPageUrl,
|
|
478
559
|
pageSearch: "",
|
|
479
560
|
pageReferrer: "",
|
|
480
561
|
browser: common.browser,
|
|
@@ -491,11 +572,11 @@ internal class AnalyticsClient {
|
|
|
491
572
|
utmContent: common.utmContent ?? "",
|
|
492
573
|
gclid: "",
|
|
493
574
|
fbclid: "",
|
|
494
|
-
acctType:
|
|
575
|
+
acctType: currentAcctType,
|
|
495
576
|
diiSource: "",
|
|
496
577
|
gamNetworkCode: gamNetworkCode,
|
|
497
578
|
amznPubId: AnalyticsClient.nilUUID,
|
|
498
|
-
customDimensions:
|
|
579
|
+
customDimensions: currentCustomDimensions,
|
|
499
580
|
click: clickData
|
|
500
581
|
)
|
|
501
582
|
|
|
@@ -564,7 +645,7 @@ internal class AnalyticsClient {
|
|
|
564
645
|
newUser: sessionManager.isNewUser,
|
|
565
646
|
pageId: sessionManager.getOrCreatePageId(),
|
|
566
647
|
sessionDepth: sessionManager.sessionDepth,
|
|
567
|
-
pageUrl:
|
|
648
|
+
pageUrl: currentPageUrl,
|
|
568
649
|
pageSearch: "",
|
|
569
650
|
pageReferrer: "",
|
|
570
651
|
browser: common.browser,
|
|
@@ -581,7 +662,7 @@ internal class AnalyticsClient {
|
|
|
581
662
|
utmContent: common.utmContent ?? "",
|
|
582
663
|
gclid: "",
|
|
583
664
|
fbclid: "",
|
|
584
|
-
acctType:
|
|
665
|
+
acctType: currentAcctType,
|
|
585
666
|
diiSource: "",
|
|
586
667
|
gamNetworkCode: gamNetworkCode,
|
|
587
668
|
amznPubId: AnalyticsClient.nilUUID,
|
|
@@ -631,7 +712,7 @@ internal class AnalyticsClient {
|
|
|
631
712
|
newUser: sessionManager.isNewUser,
|
|
632
713
|
pageId: sessionManager.getOrCreatePageId(),
|
|
633
714
|
sessionDepth: sessionManager.sessionDepth,
|
|
634
|
-
pageUrl:
|
|
715
|
+
pageUrl: currentPageUrl,
|
|
635
716
|
pageSearch: "",
|
|
636
717
|
pageReferrer: "",
|
|
637
718
|
browser: common.browser,
|
|
@@ -648,7 +729,7 @@ internal class AnalyticsClient {
|
|
|
648
729
|
utmContent: common.utmContent ?? "",
|
|
649
730
|
gclid: "",
|
|
650
731
|
fbclid: "",
|
|
651
|
-
acctType:
|
|
732
|
+
acctType: currentAcctType,
|
|
652
733
|
diiSource: "",
|
|
653
734
|
gamNetworkCode: gamNetworkCode,
|
|
654
735
|
amznPubId: AnalyticsClient.nilUUID,
|
|
@@ -687,7 +768,7 @@ internal class AnalyticsClient {
|
|
|
687
768
|
newUser: sessionManager.isNewUser,
|
|
688
769
|
pageId: sessionManager.getOrCreatePageId(),
|
|
689
770
|
sessionDepth: sessionManager.sessionDepth,
|
|
690
|
-
pageUrl:
|
|
771
|
+
pageUrl: currentPageUrl,
|
|
691
772
|
pageSearch: "",
|
|
692
773
|
pageReferrer: "",
|
|
693
774
|
browser: common.browser,
|
|
@@ -704,11 +785,11 @@ internal class AnalyticsClient {
|
|
|
704
785
|
utmContent: common.utmContent ?? "",
|
|
705
786
|
gclid: "",
|
|
706
787
|
fbclid: "",
|
|
707
|
-
acctType:
|
|
788
|
+
acctType: currentAcctType,
|
|
708
789
|
diiSource: "",
|
|
709
790
|
gamNetworkCode: gamNetworkCode,
|
|
710
791
|
amznPubId: AnalyticsClient.nilUUID,
|
|
711
|
-
customDimensions: [:], //
|
|
792
|
+
customDimensions: [:], // Engagement uses array-valued custom dimensions per backend schema
|
|
712
793
|
engagedTime: engagedTime,
|
|
713
794
|
timeOnPage: timeOnPage,
|
|
714
795
|
scrollDepth: scrollDepth
|
|
@@ -761,6 +842,8 @@ internal class AnalyticsClient {
|
|
|
761
842
|
|
|
762
843
|
/**
|
|
763
844
|
* Flush all batched impression events
|
|
845
|
+
*
|
|
846
|
+
* Merges events with the same pageId into a single object with combined impressions array.
|
|
764
847
|
*/
|
|
765
848
|
private func flushImpressionBatch() {
|
|
766
849
|
impressionBatchLock.lock()
|
|
@@ -770,28 +853,40 @@ internal class AnalyticsClient {
|
|
|
770
853
|
impressionBatchTimer = nil
|
|
771
854
|
impressionBatchLock.unlock()
|
|
772
855
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
856
|
+
if events.isEmpty { return }
|
|
857
|
+
|
|
858
|
+
// Group by pageId and merge impressions into single objects
|
|
859
|
+
var merged: [String: ImpressionBatchEvent] = [:]
|
|
860
|
+
for event in events {
|
|
861
|
+
if var existing = merged[event.pageId] {
|
|
862
|
+
existing.impressions.append(contentsOf: event.impressions)
|
|
863
|
+
merged[event.pageId] = existing
|
|
864
|
+
} else {
|
|
865
|
+
merged[event.pageId] = event
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
let mergedEvents = Array(merged.values)
|
|
870
|
+
|
|
871
|
+
Task {
|
|
872
|
+
do {
|
|
873
|
+
let encoder = JSONEncoder()
|
|
874
|
+
let data = try encoder.encode(mergedEvents)
|
|
875
|
+
let json = String(data: data, encoding: .utf8)!
|
|
876
|
+
let url = "\(baseURL)/impressions"
|
|
877
|
+
|
|
878
|
+
BCLogger.debug("Sending \(mergedEvents.count) batched impression groups (\(events.count) total impressions)")
|
|
879
|
+
|
|
880
|
+
let result = await httpClient.post(url: url, body: json)
|
|
881
|
+
|
|
882
|
+
switch result {
|
|
883
|
+
case .success:
|
|
884
|
+
BCLogger.verbose("Impression batch sent successfully")
|
|
885
|
+
case .failure(let error):
|
|
886
|
+
BCLogger.warning("Failed to send impression batch: \(error)")
|
|
794
887
|
}
|
|
888
|
+
} catch {
|
|
889
|
+
BCLogger.error("Error sending impression batch: \(error)")
|
|
795
890
|
}
|
|
796
891
|
}
|
|
797
892
|
}
|
|
@@ -817,6 +912,8 @@ internal class AnalyticsClient {
|
|
|
817
912
|
|
|
818
913
|
/**
|
|
819
914
|
* Flush all batched viewability events
|
|
915
|
+
*
|
|
916
|
+
* Merges events with the same pageId into a single object with combined viewability array.
|
|
820
917
|
*/
|
|
821
918
|
private func flushViewabilityBatch() {
|
|
822
919
|
viewabilityBatchLock.lock()
|
|
@@ -826,28 +923,40 @@ internal class AnalyticsClient {
|
|
|
826
923
|
viewabilityBatchTimer = nil
|
|
827
924
|
viewabilityBatchLock.unlock()
|
|
828
925
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
926
|
+
if events.isEmpty { return }
|
|
927
|
+
|
|
928
|
+
// Group by pageId and merge viewability into single objects
|
|
929
|
+
var merged: [String: ViewabilityEvent] = [:]
|
|
930
|
+
for event in events {
|
|
931
|
+
if var existing = merged[event.pageId] {
|
|
932
|
+
existing.viewability.append(contentsOf: event.viewability)
|
|
933
|
+
merged[event.pageId] = existing
|
|
934
|
+
} else {
|
|
935
|
+
merged[event.pageId] = event
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
let mergedEvents = Array(merged.values)
|
|
940
|
+
|
|
941
|
+
Task {
|
|
942
|
+
do {
|
|
943
|
+
let encoder = JSONEncoder()
|
|
944
|
+
let data = try encoder.encode(mergedEvents)
|
|
945
|
+
let json = String(data: data, encoding: .utf8)!
|
|
946
|
+
let url = "\(baseURL)/viewability"
|
|
947
|
+
|
|
948
|
+
BCLogger.debug("Sending \(mergedEvents.count) batched viewability groups (\(events.count) total)")
|
|
949
|
+
|
|
950
|
+
let result = await httpClient.post(url: url, body: json)
|
|
951
|
+
|
|
952
|
+
switch result {
|
|
953
|
+
case .success:
|
|
954
|
+
BCLogger.verbose("Viewability batch sent successfully")
|
|
955
|
+
case .failure(let error):
|
|
956
|
+
BCLogger.warning("Failed to send viewability batch: \(error)")
|
|
850
957
|
}
|
|
958
|
+
} catch {
|
|
959
|
+
BCLogger.error("Error sending viewability batch: \(error)")
|
|
851
960
|
}
|
|
852
961
|
}
|
|
853
962
|
}
|
|
@@ -10,8 +10,8 @@ import Foundation
|
|
|
10
10
|
*
|
|
11
11
|
* Usage:
|
|
12
12
|
* ```swift
|
|
13
|
-
* let
|
|
14
|
-
* //
|
|
13
|
+
* let bidResponse = await bidRequestClient.fetchDemand(placement: placement)
|
|
14
|
+
* // bidResponse contains targeting KVPs and auction metadata
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
17
|
internal class BidRequestClient {
|
|
@@ -29,9 +29,15 @@ internal class BidRequestClient {
|
|
|
29
29
|
|
|
30
30
|
// MARK: - Types
|
|
31
31
|
|
|
32
|
+
/// Bid response containing both targeting KVPs and auction metadata
|
|
33
|
+
internal struct BidResponse {
|
|
34
|
+
let targeting: [String: String]
|
|
35
|
+
let auctionData: AuctionData
|
|
36
|
+
}
|
|
37
|
+
|
|
32
38
|
private struct PendingRequest {
|
|
33
39
|
let placement: PlacementConfig
|
|
34
|
-
let continuation: CheckedContinuation<
|
|
40
|
+
let continuation: CheckedContinuation<BidResponse?, Never>
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
// MARK: - Init
|
|
@@ -59,7 +65,7 @@ internal class BidRequestClient {
|
|
|
59
65
|
* - Parameter placement: The placement to fetch demand for
|
|
60
66
|
* - Returns: Targeting KVPs to apply to GAM request, or nil
|
|
61
67
|
*/
|
|
62
|
-
func fetchDemand(placement: PlacementConfig) async ->
|
|
68
|
+
func fetchDemand(placement: PlacementConfig) async -> BidResponse? {
|
|
63
69
|
guard s2sConfig.enabled else {
|
|
64
70
|
BCLogger.debug("BidRequestClient: S2S is disabled, skipping bid request")
|
|
65
71
|
return nil
|
|
@@ -104,15 +110,15 @@ internal class BidRequestClient {
|
|
|
104
110
|
|
|
105
111
|
// Distribute results to each pending continuation
|
|
106
112
|
for pending in batch {
|
|
107
|
-
let
|
|
108
|
-
pending.continuation.resume(returning:
|
|
113
|
+
let bidResponse = results[pending.placement.placementId]
|
|
114
|
+
pending.continuation.resume(returning: bidResponse)
|
|
109
115
|
}
|
|
110
116
|
}
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
// MARK: - Request Building & Execution
|
|
114
120
|
|
|
115
|
-
private func executeBidRequest(placements: [PlacementConfig]) async -> [String:
|
|
121
|
+
private func executeBidRequest(placements: [PlacementConfig]) async -> [String: BidResponse] {
|
|
116
122
|
let requestBody = buildRequestJSON(placements: placements)
|
|
117
123
|
|
|
118
124
|
guard let jsonData = try? JSONSerialization.data(withJSONObject: requestBody),
|
|
@@ -131,7 +137,7 @@ internal class BidRequestClient {
|
|
|
131
137
|
|
|
132
138
|
switch result {
|
|
133
139
|
case .success(let responseJson):
|
|
134
|
-
return parseResponse(responseJson)
|
|
140
|
+
return parseResponse(responseJson, placements: placements)
|
|
135
141
|
case .failure(let error):
|
|
136
142
|
BCLogger.error("BidRequestClient: Bid request failed: \(error)")
|
|
137
143
|
return [:]
|
|
@@ -310,7 +316,7 @@ internal class BidRequestClient {
|
|
|
310
316
|
*
|
|
311
317
|
* Groups bids by impid, picks winner (highest price), copies ext.targeting.
|
|
312
318
|
*/
|
|
313
|
-
private func parseResponse(_ jsonString: String) -> [String:
|
|
319
|
+
private func parseResponse(_ jsonString: String, placements: [PlacementConfig]) -> [String: BidResponse] {
|
|
314
320
|
guard let data = jsonString.data(using: .utf8),
|
|
315
321
|
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
316
322
|
let seatbids = json["seatbid"] as? [[String: Any]] else {
|
|
@@ -318,10 +324,23 @@ internal class BidRequestClient {
|
|
|
318
324
|
return [:]
|
|
319
325
|
}
|
|
320
326
|
|
|
327
|
+
let auctionId = json["id"] as? String
|
|
328
|
+
|
|
329
|
+
// Build floor price lookup from placement configs
|
|
330
|
+
let floorPrices = Dictionary(uniqueKeysWithValues: placements.map { ($0.placementId, $0.floorPrice) })
|
|
331
|
+
|
|
321
332
|
// Collect all bids grouped by impid
|
|
322
|
-
|
|
333
|
+
struct BidInfo {
|
|
334
|
+
let price: Double
|
|
335
|
+
let targeting: [String: String]
|
|
336
|
+
let seat: String
|
|
337
|
+
let creativeId: String?
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
var bidsByImpId: [String: [BidInfo]] = [:]
|
|
323
341
|
|
|
324
342
|
for seatbid in seatbids {
|
|
343
|
+
let seat = seatbid["seat"] as? String ?? ""
|
|
325
344
|
guard let bids = seatbid["bid"] as? [[String: Any]] else { continue }
|
|
326
345
|
for bid in bids {
|
|
327
346
|
guard let impid = bid["impid"] as? String,
|
|
@@ -330,17 +349,35 @@ internal class BidRequestClient {
|
|
|
330
349
|
let targeting = ext["targeting"] as? [String: String] else {
|
|
331
350
|
continue
|
|
332
351
|
}
|
|
333
|
-
|
|
352
|
+
let crid = bid["crid"] as? String
|
|
353
|
+
bidsByImpId[impid, default: []].append(BidInfo(
|
|
354
|
+
price: price, targeting: targeting, seat: seat, creativeId: crid
|
|
355
|
+
))
|
|
334
356
|
}
|
|
335
357
|
}
|
|
336
358
|
|
|
337
|
-
// Pick winner (highest price) for each impid
|
|
338
|
-
var results: [String:
|
|
359
|
+
// Pick winner (highest price) for each impid and compute auction metadata
|
|
360
|
+
var results: [String: BidResponse] = [:]
|
|
339
361
|
for (impid, bids) in bidsByImpId {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
362
|
+
let sortedBids = bids.sorted { $0.price > $1.price }
|
|
363
|
+
guard let winner = sortedBids.first else { continue }
|
|
364
|
+
|
|
365
|
+
let secondHighestBid = sortedBids.count > 1 ? sortedBids[1].price : nil
|
|
366
|
+
let floorPrice = floorPrices[impid] ?? nil
|
|
367
|
+
|
|
368
|
+
let auctionData = AuctionData(
|
|
369
|
+
auctionId: auctionId,
|
|
370
|
+
bidder: winner.seat,
|
|
371
|
+
bidPriceCpm: winner.price,
|
|
372
|
+
creativeId: winner.creativeId,
|
|
373
|
+
gamPriceBucket: winner.targeting["hb_pb"],
|
|
374
|
+
floorPrice: floorPrice,
|
|
375
|
+
secondHighestBid: secondHighestBid,
|
|
376
|
+
demandChannel: "S2S"
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
results[impid] = BidResponse(targeting: winner.targeting, auctionData: auctionData)
|
|
380
|
+
BCLogger.debug("BidRequestClient: Winner for \(impid): $\(winner.price)")
|
|
344
381
|
}
|
|
345
382
|
|
|
346
383
|
BCLogger.info("BidRequestClient: Parsed \(results.count) winning bids")
|