@bigcrunch/react-native-ads 0.11.0 → 0.13.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 +7 -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 +199 -40
- 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 +44 -2
- package/ios/BigCrunchAds/Sources/Adapters/GoogleAdsAdapter.swift +7 -0
- package/ios/BigCrunchAds/Sources/BigCrunchAds.swift +10 -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 +179 -77
- 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 +1 -0
- package/ios/BigCrunchAdsModule.swift +29 -2
- package/ios/BigCrunchBannerViewManager.swift +1 -0
- package/lib/BigCrunchAds.d.ts +19 -2
- package/lib/BigCrunchAds.d.ts.map +1 -1
- package/lib/BigCrunchAds.js +19 -2
- package/lib/NativeBigCrunchAds.d.ts +2 -2
- package/lib/NativeBigCrunchAds.d.ts.map +1 -1
- package/lib/types/index.d.ts +36 -0
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/react-native-bigcrunch-ads.podspec +1 -1
- package/src/BigCrunchAds.ts +23 -2
- package/src/NativeBigCrunchAds.ts +2 -2
- package/src/types/index.ts +38 -0
|
@@ -12,6 +12,7 @@ import com.bigcrunch.ads.internal.PrivacyStore
|
|
|
12
12
|
import com.bigcrunch.ads.internal.SharedPreferencesStore
|
|
13
13
|
import com.bigcrunch.ads.models.AppConfig
|
|
14
14
|
import com.bigcrunch.ads.models.DeviceData
|
|
15
|
+
import com.bigcrunch.ads.models.ScreenViewOptions
|
|
15
16
|
import com.bigcrunch.ads.models.SessionInfo
|
|
16
17
|
import com.google.android.gms.ads.MobileAds
|
|
17
18
|
import com.google.android.gms.ads.RequestConfiguration
|
|
@@ -231,13 +232,14 @@ object BigCrunchAds {
|
|
|
231
232
|
* Track screen view for analytics
|
|
232
233
|
*
|
|
233
234
|
* @param screenName Name of the screen being viewed
|
|
235
|
+
* @param options Optional overrides for page URL, content metadata, and custom dimensions
|
|
234
236
|
*/
|
|
235
|
-
fun trackScreen(screenName: String) = runSafely {
|
|
237
|
+
fun trackScreen(screenName: String, options: ScreenViewOptions? = null) = runSafely {
|
|
236
238
|
if (!initialized) {
|
|
237
239
|
BCLogger.w(TAG, "trackScreen called before initialization, ignoring")
|
|
238
240
|
return@runSafely
|
|
239
241
|
}
|
|
240
|
-
analyticsClient.trackScreenView(screenName)
|
|
242
|
+
analyticsClient.trackScreenView(screenName, options)
|
|
241
243
|
SessionManager.incrementScreenViewCount()
|
|
242
244
|
}
|
|
243
245
|
|
|
@@ -245,9 +247,10 @@ object BigCrunchAds {
|
|
|
245
247
|
* Track screen view for analytics (alias for React Native compatibility)
|
|
246
248
|
*
|
|
247
249
|
* @param screenName Name of the screen being viewed
|
|
250
|
+
* @param options Optional overrides for page URL, content metadata, and custom dimensions
|
|
248
251
|
*/
|
|
249
|
-
fun trackScreenView(screenName: String) = runSafely {
|
|
250
|
-
trackScreen(screenName)
|
|
252
|
+
fun trackScreenView(screenName: String, options: ScreenViewOptions? = null) = runSafely {
|
|
253
|
+
trackScreen(screenName, options)
|
|
251
254
|
}
|
|
252
255
|
|
|
253
256
|
/**
|
|
@@ -230,12 +230,17 @@ internal class GoogleAdsAdapter(
|
|
|
230
230
|
|
|
231
231
|
// Extract GAM metadata from ResponseInfo
|
|
232
232
|
val responseInfo = adView.responseInfo
|
|
233
|
-
val advertiserId = responseInfo?.loadedAdapterResponseInfo?.adSourceId
|
|
234
233
|
val adMetadata = extractAdMetadata(responseInfo)
|
|
235
234
|
|
|
235
|
+
// Extract ad size from the loaded banner
|
|
236
|
+
val loadedAdSize = adView.adSize
|
|
237
|
+
val adSizeString = if (loadedAdSize != null) "${loadedAdSize.width}x${loadedAdSize.height}" else ""
|
|
238
|
+
|
|
236
239
|
analyticsClient.trackAdImpression(
|
|
237
240
|
placementId = placementConfig.placementId,
|
|
238
241
|
format = placementConfig.format,
|
|
242
|
+
gamAdUnit = placementConfig.gamAdUnit,
|
|
243
|
+
adSize = adSizeString,
|
|
239
244
|
advertiserId = adMetadata["advertiser_id"] as? String,
|
|
240
245
|
campaignId = adMetadata["campaign_id"] as? String,
|
|
241
246
|
lineItemId = adMetadata["line_item_id"] as? String,
|
|
@@ -394,6 +399,7 @@ internal class GoogleAdsAdapter(
|
|
|
394
399
|
analyticsClient.trackAdImpression(
|
|
395
400
|
placementId = placementConfig.placementId,
|
|
396
401
|
format = placementConfig.format,
|
|
402
|
+
gamAdUnit = placementConfig.gamAdUnit,
|
|
397
403
|
advertiserId = adMetadata["advertiser_id"] as? String,
|
|
398
404
|
campaignId = adMetadata["campaign_id"] as? String,
|
|
399
405
|
lineItemId = adMetadata["line_item_id"] as? String,
|
|
@@ -501,6 +507,7 @@ internal class GoogleAdsAdapter(
|
|
|
501
507
|
analyticsClient.trackAdImpression(
|
|
502
508
|
placementId = placementConfig.placementId,
|
|
503
509
|
format = placementConfig.format,
|
|
510
|
+
gamAdUnit = placementConfig.gamAdUnit,
|
|
504
511
|
advertiserId = adMetadata["advertiser_id"] as? String,
|
|
505
512
|
campaignId = adMetadata["campaign_id"] as? String,
|
|
506
513
|
lineItemId = adMetadata["line_item_id"] as? String,
|
|
@@ -8,6 +8,7 @@ import com.bigcrunch.ads.adapters.GoogleAdsAdapter
|
|
|
8
8
|
import com.bigcrunch.ads.adapters.InterstitialAdCallback
|
|
9
9
|
import com.bigcrunch.ads.adapters.RewardedAdCallback
|
|
10
10
|
import com.bigcrunch.ads.internal.BCLogger
|
|
11
|
+
import com.bigcrunch.ads.models.AuctionData
|
|
11
12
|
import com.bigcrunch.ads.models.PlacementConfig
|
|
12
13
|
import com.google.android.gms.ads.admanager.AdManagerAdRequest
|
|
13
14
|
import com.google.android.gms.ads.admanager.AdManagerAdView
|
|
@@ -164,16 +165,24 @@ internal class AdOrchestrator(
|
|
|
164
165
|
val adRequestBuilder = AdManagerAdRequest.Builder()
|
|
165
166
|
|
|
166
167
|
// 3. Fetch S2S demand (even if it fails, continue with Google)
|
|
167
|
-
val
|
|
168
|
-
if (
|
|
169
|
-
for ((key, value) in targeting) {
|
|
168
|
+
val bidResponse = bidRequestClient.fetchDemand(placement)
|
|
169
|
+
if (bidResponse != null && bidResponse.targeting.isNotEmpty()) {
|
|
170
|
+
for ((key, value) in bidResponse.targeting) {
|
|
170
171
|
adRequestBuilder.addCustomTargeting(key, value)
|
|
171
172
|
}
|
|
172
|
-
BCLogger.d(TAG, "S2S demand fetched for: ${placement.placementId} (${targeting.size} keys)")
|
|
173
|
+
BCLogger.d(TAG, "S2S demand fetched for: ${placement.placementId} (${bidResponse.targeting.size} keys)")
|
|
173
174
|
} else {
|
|
174
175
|
BCLogger.w(TAG, "S2S demand fetch returned no targeting, continuing with Google only")
|
|
175
176
|
}
|
|
176
177
|
|
|
178
|
+
// Store auction data for analytics (or default to GAM if no S2S)
|
|
179
|
+
val auctionData = bidResponse?.auctionData ?: AuctionData(
|
|
180
|
+
bidder = "google_ad_exchange",
|
|
181
|
+
floorPrice = placement.floorPrice,
|
|
182
|
+
demandChannel = "Google Ad Exchange"
|
|
183
|
+
)
|
|
184
|
+
analyticsClient.setAuctionData(placement.placementId, auctionData)
|
|
185
|
+
|
|
177
186
|
// 4. Load Google ad with the (possibly enriched) ad request
|
|
178
187
|
val adRequest = adRequestBuilder.build()
|
|
179
188
|
val adapterCallback = object : BannerAdCallback {
|
|
@@ -292,16 +301,24 @@ internal class AdOrchestrator(
|
|
|
292
301
|
val adRequestBuilder = AdManagerAdRequest.Builder()
|
|
293
302
|
|
|
294
303
|
// 3. Fetch S2S demand
|
|
295
|
-
val
|
|
296
|
-
if (
|
|
297
|
-
for ((key, value) in targeting) {
|
|
304
|
+
val bidResponse = bidRequestClient.fetchDemand(placement)
|
|
305
|
+
if (bidResponse != null && bidResponse.targeting.isNotEmpty()) {
|
|
306
|
+
for ((key, value) in bidResponse.targeting) {
|
|
298
307
|
adRequestBuilder.addCustomTargeting(key, value)
|
|
299
308
|
}
|
|
300
|
-
BCLogger.d(TAG, "S2S demand fetched for: ${placement.placementId} (${targeting.size} keys)")
|
|
309
|
+
BCLogger.d(TAG, "S2S demand fetched for: ${placement.placementId} (${bidResponse.targeting.size} keys)")
|
|
301
310
|
} else {
|
|
302
311
|
BCLogger.w(TAG, "S2S demand fetch returned no targeting, continuing with Google only")
|
|
303
312
|
}
|
|
304
313
|
|
|
314
|
+
// Store auction data for analytics (or default to GAM if no S2S)
|
|
315
|
+
val auctionData = bidResponse?.auctionData ?: AuctionData(
|
|
316
|
+
bidder = "google_ad_exchange",
|
|
317
|
+
floorPrice = placement.floorPrice,
|
|
318
|
+
demandChannel = "Google Ad Exchange"
|
|
319
|
+
)
|
|
320
|
+
analyticsClient.setAuctionData(placement.placementId, auctionData)
|
|
321
|
+
|
|
305
322
|
// Get test ads flag from config
|
|
306
323
|
val useTestAds = configManager.shouldUseTestAds()
|
|
307
324
|
|
|
@@ -466,16 +483,24 @@ internal class AdOrchestrator(
|
|
|
466
483
|
val adRequestBuilder = AdManagerAdRequest.Builder()
|
|
467
484
|
|
|
468
485
|
// 3. Fetch S2S demand
|
|
469
|
-
val
|
|
470
|
-
if (
|
|
471
|
-
for ((key, value) in targeting) {
|
|
486
|
+
val bidResponse = bidRequestClient.fetchDemand(placement)
|
|
487
|
+
if (bidResponse != null && bidResponse.targeting.isNotEmpty()) {
|
|
488
|
+
for ((key, value) in bidResponse.targeting) {
|
|
472
489
|
adRequestBuilder.addCustomTargeting(key, value)
|
|
473
490
|
}
|
|
474
|
-
BCLogger.d(TAG, "S2S demand fetched for: ${placement.placementId} (${targeting.size} keys)")
|
|
491
|
+
BCLogger.d(TAG, "S2S demand fetched for: ${placement.placementId} (${bidResponse.targeting.size} keys)")
|
|
475
492
|
} else {
|
|
476
493
|
BCLogger.w(TAG, "S2S demand fetch returned no targeting, continuing with Google only")
|
|
477
494
|
}
|
|
478
495
|
|
|
496
|
+
// Store auction data for analytics (or default to GAM if no S2S)
|
|
497
|
+
val auctionData = bidResponse?.auctionData ?: AuctionData(
|
|
498
|
+
bidder = "google_ad_exchange",
|
|
499
|
+
floorPrice = placement.floorPrice,
|
|
500
|
+
demandChannel = "Google Ad Exchange"
|
|
501
|
+
)
|
|
502
|
+
analyticsClient.setAuctionData(placement.placementId, auctionData)
|
|
503
|
+
|
|
479
504
|
// Get test ads flag from config
|
|
480
505
|
val useTestAds = configManager.shouldUseTestAds()
|
|
481
506
|
|
|
@@ -50,16 +50,33 @@ internal class AnalyticsClient(
|
|
|
50
50
|
/** Track impression contexts for correlating events */
|
|
51
51
|
private val impressionContexts = ConcurrentHashMap<String, ImpressionContext>()
|
|
52
52
|
|
|
53
|
+
/** Per-placement auction data from S2S responses (set by AdOrchestrator, consumed by trackAdImpression) */
|
|
54
|
+
private val placementAuctionData = ConcurrentHashMap<String, AuctionData>()
|
|
55
|
+
|
|
56
|
+
/** Batching for impression events */
|
|
57
|
+
private val impressionBatch = mutableListOf<ImpressionBatchEvent>()
|
|
58
|
+
private var impressionBatchHandler: android.os.Handler? = null
|
|
59
|
+
private val impressionBatchLock = Any()
|
|
60
|
+
|
|
53
61
|
/** Batching for viewability events */
|
|
54
62
|
private val viewabilityBatch = mutableListOf<ViewabilityEvent>()
|
|
55
63
|
private var viewabilityBatchHandler: android.os.Handler? = null
|
|
56
|
-
private val
|
|
64
|
+
private val viewabilityBatchLock = Any()
|
|
65
|
+
|
|
57
66
|
private val BATCH_DELAY_MS = 250L
|
|
58
67
|
|
|
59
68
|
/** Previous screen name for referrer tracking */
|
|
60
69
|
@Volatile
|
|
61
70
|
private var previousScreenName: String? = null
|
|
62
71
|
|
|
72
|
+
/** Current page URL (set by trackScreenView, used by all events) */
|
|
73
|
+
@Volatile
|
|
74
|
+
private var currentPageUrl: String = ""
|
|
75
|
+
|
|
76
|
+
/** Current custom dimensions (set by trackScreenView, used by all events) */
|
|
77
|
+
@Volatile
|
|
78
|
+
private var currentCustomDimensions: Map<String, String> = emptyMap()
|
|
79
|
+
|
|
63
80
|
// MARK: - Session Context
|
|
64
81
|
|
|
65
82
|
private val sessionManager: SessionManager
|
|
@@ -177,15 +194,37 @@ internal class AnalyticsClient(
|
|
|
177
194
|
|
|
178
195
|
// MARK: - Page View Tracking
|
|
179
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Generate the default page URL from screen name
|
|
199
|
+
*/
|
|
200
|
+
private fun generatePageUrl(screenName: String): String {
|
|
201
|
+
val appName = try {
|
|
202
|
+
BigCrunchAds.getAppConfig()?.appName ?: "app"
|
|
203
|
+
} catch (e: Exception) {
|
|
204
|
+
"app"
|
|
205
|
+
}
|
|
206
|
+
val sanitizedAppName = appName.lowercase()
|
|
207
|
+
.replace(" ", "-")
|
|
208
|
+
.let { java.net.URLEncoder.encode(it, "UTF-8") }
|
|
209
|
+
val encodedScreen = java.net.URLEncoder.encode(screenName, "UTF-8")
|
|
210
|
+
return "https://$sanitizedAppName.mobile.app/$encodedScreen"
|
|
211
|
+
}
|
|
212
|
+
|
|
180
213
|
/**
|
|
181
214
|
* Track a screen view
|
|
182
215
|
*
|
|
183
216
|
* @param screenName Name of the screen being viewed
|
|
217
|
+
* @param options Optional overrides for page URL, metadata, and custom dimensions
|
|
184
218
|
*/
|
|
185
|
-
fun trackScreenView(screenName: String) {
|
|
219
|
+
fun trackScreenView(screenName: String, options: ScreenViewOptions? = null) {
|
|
186
220
|
// Start new page view and get IDs (pass screen name for impression tracking)
|
|
187
221
|
val pageId = sessionManager.startPageView(screenName)
|
|
188
222
|
|
|
223
|
+
// Resolve page URL: explicit override > auto-generated
|
|
224
|
+
val pageUrl = options?.pageUrl ?: generatePageUrl(screenName)
|
|
225
|
+
currentPageUrl = pageUrl
|
|
226
|
+
currentCustomDimensions = options?.customDimensions ?: emptyMap()
|
|
227
|
+
|
|
189
228
|
// Get common event fields
|
|
190
229
|
val common = getCommonEventFields()
|
|
191
230
|
|
|
@@ -201,7 +240,7 @@ internal class AnalyticsClient(
|
|
|
201
240
|
|
|
202
241
|
val event = PageViewEvent(
|
|
203
242
|
payloadVersion = "1.0.0",
|
|
204
|
-
configVersion = 1,
|
|
243
|
+
configVersion = 1,
|
|
205
244
|
browserTimestamp = sessionManager.getCurrentTimestamp(),
|
|
206
245
|
sessionId = sessionManager.sessionId,
|
|
207
246
|
userId = sessionManager.userId,
|
|
@@ -209,9 +248,9 @@ internal class AnalyticsClient(
|
|
|
209
248
|
newUser = sessionManager.isNewUser,
|
|
210
249
|
pageId = pageId,
|
|
211
250
|
sessionDepth = sessionManager.sessionDepth,
|
|
212
|
-
pageUrl =
|
|
213
|
-
pageSearch = "",
|
|
214
|
-
pageReferrer = "",
|
|
251
|
+
pageUrl = pageUrl,
|
|
252
|
+
pageSearch = "",
|
|
253
|
+
pageReferrer = "",
|
|
215
254
|
browser = common.browser,
|
|
216
255
|
device = common.device,
|
|
217
256
|
os = common.os,
|
|
@@ -229,8 +268,9 @@ internal class AnalyticsClient(
|
|
|
229
268
|
acctType = "anonymous",
|
|
230
269
|
diiSource = "",
|
|
231
270
|
gamNetworkCode = gamNetworkCode,
|
|
232
|
-
amznPubId = NIL_UUID,
|
|
233
|
-
customDimensions =
|
|
271
|
+
amznPubId = NIL_UUID,
|
|
272
|
+
customDimensions = currentCustomDimensions,
|
|
273
|
+
pageMetaData = options?.pageMeta
|
|
234
274
|
)
|
|
235
275
|
|
|
236
276
|
sendEvent(event, pageViewAdapter, "pageviews")
|
|
@@ -286,6 +326,32 @@ internal class AnalyticsClient(
|
|
|
286
326
|
return impressionContexts[impressionId]
|
|
287
327
|
}
|
|
288
328
|
|
|
329
|
+
// MARK: - Auction Data Storage
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Store auction data for a placement (called by AdOrchestrator after S2S demand fetch)
|
|
333
|
+
*/
|
|
334
|
+
fun setAuctionData(placementId: String, auctionData: AuctionData) {
|
|
335
|
+
placementAuctionData[placementId] = auctionData
|
|
336
|
+
BCLogger.d(TAG, "Stored auction data for placement: $placementId (channel: ${auctionData.demandChannel ?: "unknown"})")
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Consume auction data for a placement (returns and removes stored data)
|
|
341
|
+
*/
|
|
342
|
+
private fun consumeAuctionData(placementId: String): AuctionData? {
|
|
343
|
+
return placementAuctionData.remove(placementId)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Compute min_bid_to_win: $0.01 above the higher of second-highest bid or floor price
|
|
348
|
+
*/
|
|
349
|
+
private fun computeMinBidToWin(auctionData: AuctionData?): Double {
|
|
350
|
+
if (auctionData == null) return 0.0
|
|
351
|
+
val baseline = maxOf(auctionData.secondHighestBid ?: 0.0, auctionData.floorPrice ?: 0.0)
|
|
352
|
+
return if (baseline > 0) baseline + 0.01 else 0.0
|
|
353
|
+
}
|
|
354
|
+
|
|
289
355
|
// MARK: - Enhanced Impression Tracking
|
|
290
356
|
|
|
291
357
|
/**
|
|
@@ -316,16 +382,25 @@ internal class AnalyticsClient(
|
|
|
316
382
|
val auctionId = context.auctionData.auctionId?.takeIf { it.isNotEmpty() }
|
|
317
383
|
?: UUID.randomUUID().toString()
|
|
318
384
|
|
|
319
|
-
//
|
|
385
|
+
// Look up stored auction data (may have been set by AdOrchestrator)
|
|
386
|
+
val storedAuctionData = consumeAuctionData(context.placementId)
|
|
387
|
+
val effectiveAuctionData = storedAuctionData ?: context.auctionData
|
|
388
|
+
|
|
389
|
+
// Create impression record with auction data fields populated
|
|
320
390
|
val impressionRecord = ImpressionRecord(
|
|
321
391
|
slotId = context.placementId,
|
|
322
392
|
gamUnit = context.gamAdUnit,
|
|
393
|
+
gamPriceBucket = effectiveAuctionData.gamPriceBucket ?: "",
|
|
323
394
|
impressionId = context.impressionId,
|
|
324
395
|
auctionId = auctionId,
|
|
325
|
-
adBidder =
|
|
396
|
+
adBidder = effectiveAuctionData.bidder ?: "",
|
|
326
397
|
adSize = adSize,
|
|
327
|
-
adPrice =
|
|
328
|
-
|
|
398
|
+
adPrice = effectiveAuctionData.bidPriceCpm ?: 0.0,
|
|
399
|
+
adFloorPrice = effectiveAuctionData.floorPrice ?: 0.0,
|
|
400
|
+
minBidToWin = computeMinBidToWin(effectiveAuctionData),
|
|
401
|
+
creativeId = effectiveAuctionData.creativeId ?: "",
|
|
402
|
+
adDemandType = context.format,
|
|
403
|
+
demandChannel = effectiveAuctionData.demandChannel ?: "Google Ad Exchange"
|
|
329
404
|
)
|
|
330
405
|
|
|
331
406
|
// region must be 2 chars or empty per backend validation
|
|
@@ -342,7 +417,7 @@ internal class AnalyticsClient(
|
|
|
342
417
|
newUser = sessionManager.isNewUser,
|
|
343
418
|
pageId = sessionManager.getOrCreatePageId(),
|
|
344
419
|
sessionDepth = sessionManager.sessionDepth,
|
|
345
|
-
pageUrl =
|
|
420
|
+
pageUrl = currentPageUrl,
|
|
346
421
|
pageSearch = "",
|
|
347
422
|
pageReferrer = "",
|
|
348
423
|
browser = common.browser,
|
|
@@ -362,21 +437,22 @@ internal class AnalyticsClient(
|
|
|
362
437
|
acctType = "anonymous",
|
|
363
438
|
diiSource = "",
|
|
364
439
|
gamNetworkCode = getGamNetworkCode(),
|
|
365
|
-
amznPubId = NIL_UUID,
|
|
366
|
-
customDimensions =
|
|
440
|
+
amznPubId = NIL_UUID,
|
|
441
|
+
customDimensions = currentCustomDimensions,
|
|
367
442
|
impressions = listOf(impressionRecord)
|
|
368
443
|
)
|
|
369
444
|
|
|
370
|
-
|
|
445
|
+
batchImpressionEvent(batchEvent)
|
|
371
446
|
}
|
|
372
447
|
|
|
373
448
|
/**
|
|
374
|
-
* Track an ad impression (simple version
|
|
449
|
+
* Track an ad impression (simple version with auction data lookup)
|
|
375
450
|
*/
|
|
376
451
|
fun trackAdImpression(
|
|
377
452
|
placementId: String,
|
|
378
453
|
format: String,
|
|
379
454
|
gamAdUnit: String = "",
|
|
455
|
+
adSize: String = "",
|
|
380
456
|
advertiserId: String? = null,
|
|
381
457
|
campaignId: String? = null,
|
|
382
458
|
lineItemId: String? = null,
|
|
@@ -389,19 +465,31 @@ internal class AnalyticsClient(
|
|
|
389
465
|
// Get common event fields
|
|
390
466
|
val common = getCommonEventFields()
|
|
391
467
|
|
|
392
|
-
//
|
|
393
|
-
val
|
|
468
|
+
// Look up stored auction data from S2S response
|
|
469
|
+
val auctionData = consumeAuctionData(placementId)
|
|
470
|
+
|
|
471
|
+
// auction_id: use S2S auction ID if available, otherwise generate one
|
|
472
|
+
val auctionId = auctionData?.auctionId?.takeIf { it.isNotEmpty() }
|
|
473
|
+
?: UUID.randomUUID().toString()
|
|
394
474
|
|
|
395
|
-
// Create impression record with
|
|
475
|
+
// Create impression record with auction data fields populated
|
|
396
476
|
val impressionRecord = ImpressionRecord(
|
|
397
477
|
slotId = placementId,
|
|
398
478
|
gamUnit = gamAdUnit,
|
|
479
|
+
gamPriceBucket = auctionData?.gamPriceBucket ?: "",
|
|
399
480
|
impressionId = UUID.randomUUID().toString(),
|
|
400
481
|
auctionId = auctionId,
|
|
482
|
+
adBidder = auctionData?.bidder ?: "",
|
|
483
|
+
adSize = adSize,
|
|
484
|
+
adPrice = auctionData?.bidPriceCpm ?: 0.0,
|
|
485
|
+
adFloorPrice = auctionData?.floorPrice ?: 0.0,
|
|
486
|
+
minBidToWin = computeMinBidToWin(auctionData),
|
|
401
487
|
advertiserId = advertiserId ?: "",
|
|
402
488
|
campaignId = campaignId ?: "",
|
|
403
489
|
lineItemId = lineItemId ?: "",
|
|
404
|
-
creativeId = creativeId ?: "",
|
|
490
|
+
creativeId = auctionData?.creativeId ?: creativeId ?: "",
|
|
491
|
+
adDemandType = format,
|
|
492
|
+
demandChannel = auctionData?.demandChannel ?: "Google Ad Exchange",
|
|
405
493
|
refreshCount = refreshCount
|
|
406
494
|
)
|
|
407
495
|
|
|
@@ -419,7 +507,7 @@ internal class AnalyticsClient(
|
|
|
419
507
|
newUser = sessionManager.isNewUser,
|
|
420
508
|
pageId = sessionManager.getOrCreatePageId(),
|
|
421
509
|
sessionDepth = sessionManager.sessionDepth,
|
|
422
|
-
pageUrl =
|
|
510
|
+
pageUrl = currentPageUrl,
|
|
423
511
|
pageSearch = "",
|
|
424
512
|
pageReferrer = "",
|
|
425
513
|
browser = common.browser,
|
|
@@ -439,12 +527,12 @@ internal class AnalyticsClient(
|
|
|
439
527
|
acctType = "anonymous",
|
|
440
528
|
diiSource = "",
|
|
441
529
|
gamNetworkCode = getGamNetworkCode(),
|
|
442
|
-
amznPubId = NIL_UUID,
|
|
443
|
-
customDimensions =
|
|
530
|
+
amznPubId = NIL_UUID,
|
|
531
|
+
customDimensions = currentCustomDimensions,
|
|
444
532
|
impressions = listOf(impressionRecord)
|
|
445
533
|
)
|
|
446
534
|
|
|
447
|
-
|
|
535
|
+
batchImpressionEvent(batchEvent)
|
|
448
536
|
}
|
|
449
537
|
|
|
450
538
|
// MARK: - Click Tracking
|
|
@@ -480,7 +568,7 @@ internal class AnalyticsClient(
|
|
|
480
568
|
newUser = sessionManager.isNewUser,
|
|
481
569
|
pageId = sessionManager.getOrCreatePageId(),
|
|
482
570
|
sessionDepth = sessionManager.sessionDepth,
|
|
483
|
-
pageUrl =
|
|
571
|
+
pageUrl = currentPageUrl,
|
|
484
572
|
pageSearch = "",
|
|
485
573
|
pageReferrer = "",
|
|
486
574
|
browser = common.browser,
|
|
@@ -500,8 +588,8 @@ internal class AnalyticsClient(
|
|
|
500
588
|
acctType = "anonymous",
|
|
501
589
|
diiSource = "",
|
|
502
590
|
gamNetworkCode = getGamNetworkCode(),
|
|
503
|
-
amznPubId = NIL_UUID,
|
|
504
|
-
customDimensions =
|
|
591
|
+
amznPubId = NIL_UUID,
|
|
592
|
+
customDimensions = currentCustomDimensions,
|
|
505
593
|
click = clickData
|
|
506
594
|
)
|
|
507
595
|
|
|
@@ -555,7 +643,7 @@ internal class AnalyticsClient(
|
|
|
555
643
|
newUser = sessionManager.isNewUser,
|
|
556
644
|
pageId = sessionManager.getOrCreatePageId(),
|
|
557
645
|
sessionDepth = sessionManager.sessionDepth,
|
|
558
|
-
pageUrl =
|
|
646
|
+
pageUrl = currentPageUrl,
|
|
559
647
|
pageSearch = "",
|
|
560
648
|
pageReferrer = "",
|
|
561
649
|
browser = common.browser,
|
|
@@ -575,8 +663,8 @@ internal class AnalyticsClient(
|
|
|
575
663
|
acctType = "anonymous",
|
|
576
664
|
diiSource = "",
|
|
577
665
|
gamNetworkCode = getGamNetworkCode(),
|
|
578
|
-
amznPubId = NIL_UUID,
|
|
579
|
-
customDimensions =
|
|
666
|
+
amznPubId = NIL_UUID,
|
|
667
|
+
customDimensions = currentCustomDimensions,
|
|
580
668
|
viewability = listOf(viewabilityData)
|
|
581
669
|
)
|
|
582
670
|
|
|
@@ -616,7 +704,7 @@ internal class AnalyticsClient(
|
|
|
616
704
|
newUser = sessionManager.isNewUser,
|
|
617
705
|
pageId = sessionManager.getOrCreatePageId(),
|
|
618
706
|
sessionDepth = sessionManager.sessionDepth,
|
|
619
|
-
pageUrl =
|
|
707
|
+
pageUrl = currentPageUrl,
|
|
620
708
|
pageSearch = "",
|
|
621
709
|
pageReferrer = "",
|
|
622
710
|
browser = common.browser,
|
|
@@ -636,8 +724,8 @@ internal class AnalyticsClient(
|
|
|
636
724
|
acctType = "anonymous",
|
|
637
725
|
diiSource = "",
|
|
638
726
|
gamNetworkCode = getGamNetworkCode(),
|
|
639
|
-
amznPubId = NIL_UUID,
|
|
640
|
-
customDimensions = emptyMap(),
|
|
727
|
+
amznPubId = NIL_UUID,
|
|
728
|
+
customDimensions = emptyMap(), // Engagement uses array-valued custom dimensions per backend schema
|
|
641
729
|
engagedTime = engagedTime,
|
|
642
730
|
timeOnPage = timeOnPage,
|
|
643
731
|
scrollDepth = scrollDepth
|
|
@@ -678,11 +766,66 @@ internal class AnalyticsClient(
|
|
|
678
766
|
|
|
679
767
|
// MARK: - Batching Helpers
|
|
680
768
|
|
|
769
|
+
/**
|
|
770
|
+
* Batch an impression event (250ms delay)
|
|
771
|
+
*/
|
|
772
|
+
private fun batchImpressionEvent(event: ImpressionBatchEvent) {
|
|
773
|
+
synchronized(impressionBatchLock) {
|
|
774
|
+
impressionBatch.add(event)
|
|
775
|
+
|
|
776
|
+
// Cancel existing timer
|
|
777
|
+
impressionBatchHandler?.removeCallbacksAndMessages(null)
|
|
778
|
+
|
|
779
|
+
// Create new handler if needed
|
|
780
|
+
if (impressionBatchHandler == null) {
|
|
781
|
+
impressionBatchHandler = android.os.Handler(android.os.Looper.getMainLooper())
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Schedule flush after 250ms
|
|
785
|
+
impressionBatchHandler?.postDelayed({
|
|
786
|
+
flushImpressionBatch()
|
|
787
|
+
}, BATCH_DELAY_MS)
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Flush impression batch to backend
|
|
793
|
+
*
|
|
794
|
+
* Merges events with the same pageId into a single object with combined impressions array.
|
|
795
|
+
*/
|
|
796
|
+
private fun flushImpressionBatch() {
|
|
797
|
+
synchronized(impressionBatchLock) {
|
|
798
|
+
if (impressionBatch.isEmpty()) return
|
|
799
|
+
|
|
800
|
+
val events = impressionBatch.toList()
|
|
801
|
+
impressionBatch.clear()
|
|
802
|
+
|
|
803
|
+
// Group by pageId and merge impressions into single objects
|
|
804
|
+
val merged = mutableMapOf<String, ImpressionBatchEvent>()
|
|
805
|
+
for (event in events) {
|
|
806
|
+
val existing = merged[event.pageId]
|
|
807
|
+
if (existing != null) {
|
|
808
|
+
merged[event.pageId] = existing.copy(
|
|
809
|
+
impressions = existing.impressions + event.impressions
|
|
810
|
+
)
|
|
811
|
+
} else {
|
|
812
|
+
merged[event.pageId] = event
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
val mergedEvents = merged.values.toList()
|
|
817
|
+
|
|
818
|
+
BCLogger.d(TAG, "Flushing ${mergedEvents.size} impression groups (${events.size} total impressions)")
|
|
819
|
+
|
|
820
|
+
sendEventBatch(mergedEvents, "impressions")
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
681
824
|
/**
|
|
682
825
|
* Batch a viewability event (250ms delay)
|
|
683
826
|
*/
|
|
684
827
|
private fun batchViewabilityEvent(event: ViewabilityEvent) {
|
|
685
|
-
synchronized(
|
|
828
|
+
synchronized(viewabilityBatchLock) {
|
|
686
829
|
viewabilityBatch.add(event)
|
|
687
830
|
|
|
688
831
|
// Cancel existing timer
|
|
@@ -702,18 +845,34 @@ internal class AnalyticsClient(
|
|
|
702
845
|
|
|
703
846
|
/**
|
|
704
847
|
* Flush viewability batch to backend
|
|
848
|
+
*
|
|
849
|
+
* Merges events with the same pageId into a single object with combined viewability array.
|
|
705
850
|
*/
|
|
706
851
|
private fun flushViewabilityBatch() {
|
|
707
|
-
synchronized(
|
|
852
|
+
synchronized(viewabilityBatchLock) {
|
|
708
853
|
if (viewabilityBatch.isEmpty()) return
|
|
709
854
|
|
|
710
|
-
val
|
|
855
|
+
val events = viewabilityBatch.toList()
|
|
711
856
|
viewabilityBatch.clear()
|
|
712
857
|
|
|
713
|
-
|
|
858
|
+
// Group by pageId and merge viewability into single objects
|
|
859
|
+
val merged = mutableMapOf<String, ViewabilityEvent>()
|
|
860
|
+
for (event in events) {
|
|
861
|
+
val existing = merged[event.pageId]
|
|
862
|
+
if (existing != null) {
|
|
863
|
+
merged[event.pageId] = existing.copy(
|
|
864
|
+
viewability = existing.viewability + event.viewability
|
|
865
|
+
)
|
|
866
|
+
} else {
|
|
867
|
+
merged[event.pageId] = event
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
val mergedEvents = merged.values.toList()
|
|
872
|
+
|
|
873
|
+
BCLogger.d(TAG, "Flushing ${mergedEvents.size} viewability groups (${events.size} total)")
|
|
714
874
|
|
|
715
|
-
|
|
716
|
-
sendEventBatch(eventsToSend, "viewability")
|
|
875
|
+
sendEventBatch(mergedEvents, "viewability")
|
|
717
876
|
}
|
|
718
877
|
}
|
|
719
878
|
}
|