@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,719 @@
|
|
|
1
|
+
package com.bigcrunch.ads.core
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Build
|
|
5
|
+
import com.bigcrunch.ads.BigCrunchAds
|
|
6
|
+
import com.bigcrunch.ads.internal.BCLogger
|
|
7
|
+
import com.bigcrunch.ads.internal.HttpClient
|
|
8
|
+
import com.bigcrunch.ads.models.*
|
|
9
|
+
import com.squareup.moshi.Moshi
|
|
10
|
+
import kotlinx.coroutines.CoroutineDispatcher
|
|
11
|
+
import kotlinx.coroutines.CoroutineScope
|
|
12
|
+
import kotlinx.coroutines.Dispatchers
|
|
13
|
+
import kotlinx.coroutines.launch
|
|
14
|
+
import java.util.UUID
|
|
15
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Analytics client for tracking ad events
|
|
19
|
+
*
|
|
20
|
+
* AnalyticsClient sends all ad-related events to the BigCrunch pipeline:
|
|
21
|
+
* - Page/screen views → `/pageviews`
|
|
22
|
+
* - Ad impressions (includes request + auction + revenue) → `/impressions`
|
|
23
|
+
* - Ad clicks → `/clicks`
|
|
24
|
+
* - Ad viewability → `/viewability`
|
|
25
|
+
* - User engagement → `/engagement`
|
|
26
|
+
*
|
|
27
|
+
* All events are fire-and-forget (non-blocking) and sent asynchronously.
|
|
28
|
+
* Failures are logged but do not propagate to the caller.
|
|
29
|
+
*/
|
|
30
|
+
internal class AnalyticsClient(
|
|
31
|
+
private val context: Context,
|
|
32
|
+
private val httpClient: HttpClient,
|
|
33
|
+
private val moshi: Moshi,
|
|
34
|
+
private val baseUrl: String,
|
|
35
|
+
dispatcher: CoroutineDispatcher = Dispatchers.IO
|
|
36
|
+
) {
|
|
37
|
+
companion object {
|
|
38
|
+
private const val TAG = "AnalyticsClient"
|
|
39
|
+
// Nil UUID for fields that require UUID format but have no value
|
|
40
|
+
private const val NIL_UUID = "00000000-0000-0000-0000-000000000000"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private val scope = CoroutineScope(dispatcher)
|
|
44
|
+
private val pageViewAdapter = moshi.adapter(PageViewEvent::class.java)
|
|
45
|
+
private val impressionBatchAdapter = moshi.adapter(ImpressionBatchEvent::class.java)
|
|
46
|
+
private val clickAdapter = moshi.adapter(ClickEvent::class.java)
|
|
47
|
+
private val viewabilityAdapter = moshi.adapter(ViewabilityEvent::class.java)
|
|
48
|
+
private val engagementAdapter = moshi.adapter(EngagementEvent::class.java)
|
|
49
|
+
|
|
50
|
+
/** Track impression contexts for correlating events */
|
|
51
|
+
private val impressionContexts = ConcurrentHashMap<String, ImpressionContext>()
|
|
52
|
+
|
|
53
|
+
/** Batching for viewability events */
|
|
54
|
+
private val viewabilityBatch = mutableListOf<ViewabilityEvent>()
|
|
55
|
+
private var viewabilityBatchHandler: android.os.Handler? = null
|
|
56
|
+
private val batchLock = Any()
|
|
57
|
+
private val BATCH_DELAY_MS = 250L
|
|
58
|
+
|
|
59
|
+
/** Previous screen name for referrer tracking */
|
|
60
|
+
@Volatile
|
|
61
|
+
private var previousScreenName: String? = null
|
|
62
|
+
|
|
63
|
+
// MARK: - Session Context
|
|
64
|
+
|
|
65
|
+
private val sessionManager: SessionManager
|
|
66
|
+
get() = SessionManager.getInstance()
|
|
67
|
+
|
|
68
|
+
private val propertyId: String
|
|
69
|
+
get() = BigCrunchAds.propertyId
|
|
70
|
+
|
|
71
|
+
private val configVersion: String?
|
|
72
|
+
get() = null // TODO: Get from ConfigManager when available
|
|
73
|
+
|
|
74
|
+
// MARK: - Event Sending
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Send an event to a specific endpoint (fire-and-forget)
|
|
78
|
+
* Wraps single event in an array as required by server
|
|
79
|
+
*/
|
|
80
|
+
private fun <T> sendEvent(event: T, adapter: com.squareup.moshi.JsonAdapter<T>, endpoint: String) {
|
|
81
|
+
scope.launch {
|
|
82
|
+
try {
|
|
83
|
+
// Wrap single event in array as server expects arrays
|
|
84
|
+
val eventArray = listOf(event)
|
|
85
|
+
val listAdapter = moshi.adapter<List<T>>(
|
|
86
|
+
com.squareup.moshi.Types.newParameterizedType(List::class.java, event!!::class.java)
|
|
87
|
+
)
|
|
88
|
+
val json = listAdapter.toJson(eventArray)
|
|
89
|
+
val url = "$baseUrl/$endpoint"
|
|
90
|
+
|
|
91
|
+
BCLogger.d(TAG, "Sending event to: $endpoint")
|
|
92
|
+
|
|
93
|
+
val result = httpClient.post(url, json)
|
|
94
|
+
|
|
95
|
+
if (result.isSuccess) {
|
|
96
|
+
BCLogger.v(TAG, "Event sent successfully: $endpoint")
|
|
97
|
+
} else {
|
|
98
|
+
BCLogger.w(TAG, "Failed to send event: $endpoint - ${result.exceptionOrNull()?.message}")
|
|
99
|
+
}
|
|
100
|
+
} catch (e: Exception) {
|
|
101
|
+
BCLogger.e(TAG, "Error sending event", e)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Send a batch of events to a specific endpoint (fire-and-forget)
|
|
108
|
+
*/
|
|
109
|
+
private fun <T> sendEventBatch(events: List<T>, endpoint: String) {
|
|
110
|
+
scope.launch {
|
|
111
|
+
try {
|
|
112
|
+
if (events.isEmpty()) return@launch
|
|
113
|
+
|
|
114
|
+
// Get the type from the first event
|
|
115
|
+
val listAdapter = moshi.adapter<List<T>>(
|
|
116
|
+
com.squareup.moshi.Types.newParameterizedType(List::class.java, events.first()!!::class.java)
|
|
117
|
+
)
|
|
118
|
+
val json = listAdapter.toJson(events)
|
|
119
|
+
val url = "$baseUrl/$endpoint"
|
|
120
|
+
|
|
121
|
+
BCLogger.d(TAG, "Sending ${events.size} events to: $endpoint")
|
|
122
|
+
|
|
123
|
+
val result = httpClient.post(url, json)
|
|
124
|
+
|
|
125
|
+
if (result.isSuccess) {
|
|
126
|
+
BCLogger.v(TAG, "Event batch sent successfully: $endpoint")
|
|
127
|
+
} else {
|
|
128
|
+
BCLogger.w(TAG, "Failed to send event batch: $endpoint - ${result.exceptionOrNull()?.message}")
|
|
129
|
+
}
|
|
130
|
+
} catch (e: Exception) {
|
|
131
|
+
BCLogger.e(TAG, "Error sending event batch", e)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// MARK: - Common Event Fields
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get common web schema fields (device context, attribution, etc.)
|
|
140
|
+
*
|
|
141
|
+
* Returns a data class with all the common fields needed for events
|
|
142
|
+
*/
|
|
143
|
+
private data class CommonEventFields(
|
|
144
|
+
val browser: String,
|
|
145
|
+
val device: String,
|
|
146
|
+
val os: String,
|
|
147
|
+
val country: String,
|
|
148
|
+
val region: String,
|
|
149
|
+
val sessionSource: String,
|
|
150
|
+
val sessionMedium: String,
|
|
151
|
+
val utmSource: String?,
|
|
152
|
+
val utmMedium: String?,
|
|
153
|
+
val utmCampaign: String?,
|
|
154
|
+
val utmTerm: String?,
|
|
155
|
+
val utmContent: String?
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
private fun getCommonEventFields(): CommonEventFields {
|
|
159
|
+
val deviceContext = DeviceContext.getInstance()
|
|
160
|
+
val webFields = deviceContext.getWebSchemaFields()
|
|
161
|
+
|
|
162
|
+
return CommonEventFields(
|
|
163
|
+
browser = webFields["browser"] as String,
|
|
164
|
+
device = webFields["device"] as String,
|
|
165
|
+
os = webFields["os"] as String,
|
|
166
|
+
country = webFields["country"] as String,
|
|
167
|
+
region = webFields["region"] as String,
|
|
168
|
+
sessionSource = sessionManager.sessionSource,
|
|
169
|
+
sessionMedium = sessionManager.sessionMedium,
|
|
170
|
+
utmSource = sessionManager.utmSource,
|
|
171
|
+
utmMedium = sessionManager.utmMedium,
|
|
172
|
+
utmCampaign = sessionManager.utmCampaign,
|
|
173
|
+
utmTerm = sessionManager.utmTerm,
|
|
174
|
+
utmContent = sessionManager.utmContent
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// MARK: - Page View Tracking
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Track a screen view
|
|
182
|
+
*
|
|
183
|
+
* @param screenName Name of the screen being viewed
|
|
184
|
+
*/
|
|
185
|
+
fun trackScreenView(screenName: String) {
|
|
186
|
+
// Start new page view and get IDs (pass screen name for impression tracking)
|
|
187
|
+
val pageId = sessionManager.startPageView(screenName)
|
|
188
|
+
|
|
189
|
+
// Get common event fields
|
|
190
|
+
val common = getCommonEventFields()
|
|
191
|
+
|
|
192
|
+
// Get GAM network code from BigCrunchAds if available
|
|
193
|
+
val gamNetworkCode = try {
|
|
194
|
+
BigCrunchAds.getConfigManager().getGamNetworkCode() ?: ""
|
|
195
|
+
} catch (e: Exception) {
|
|
196
|
+
""
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// region must be 2 chars or empty per backend validation
|
|
200
|
+
val regionCode = common.region.takeIf { it.length == 2 } ?: ""
|
|
201
|
+
|
|
202
|
+
val event = PageViewEvent(
|
|
203
|
+
payloadVersion = "1.0.0",
|
|
204
|
+
configVersion = 1, // TODO: Get from ConfigManager when available
|
|
205
|
+
browserTimestamp = sessionManager.getCurrentTimestamp(),
|
|
206
|
+
sessionId = sessionManager.sessionId,
|
|
207
|
+
userId = sessionManager.userId,
|
|
208
|
+
propertyId = propertyId,
|
|
209
|
+
newUser = sessionManager.isNewUser,
|
|
210
|
+
pageId = pageId,
|
|
211
|
+
sessionDepth = sessionManager.sessionDepth,
|
|
212
|
+
pageUrl = "", // page_url must be valid URL or empty - mobile doesn't have URLs
|
|
213
|
+
pageSearch = "", // Not applicable for mobile
|
|
214
|
+
pageReferrer = "", // page_referrer must be valid URL or empty
|
|
215
|
+
browser = common.browser,
|
|
216
|
+
device = common.device,
|
|
217
|
+
os = common.os,
|
|
218
|
+
country = common.country,
|
|
219
|
+
region = regionCode,
|
|
220
|
+
sessionSource = common.sessionSource,
|
|
221
|
+
sessionMedium = common.sessionMedium,
|
|
222
|
+
utmSource = common.utmSource ?: "",
|
|
223
|
+
utmMedium = common.utmMedium ?: "",
|
|
224
|
+
utmCampaign = common.utmCampaign ?: "",
|
|
225
|
+
utmTerm = common.utmTerm ?: "",
|
|
226
|
+
utmContent = common.utmContent ?: "",
|
|
227
|
+
gclid = "",
|
|
228
|
+
fbclid = "",
|
|
229
|
+
acctType = "anonymous",
|
|
230
|
+
diiSource = "",
|
|
231
|
+
gamNetworkCode = gamNetworkCode,
|
|
232
|
+
amznPubId = NIL_UUID, // amzn_pub_id must be valid UUID
|
|
233
|
+
customDimensions = emptyMap()
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
sendEvent(event, pageViewAdapter, "pageviews")
|
|
237
|
+
|
|
238
|
+
// Update previous screen for next referrer
|
|
239
|
+
previousScreenName = screenName
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
// MARK: - Impression Context Management
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Create and store an impression context for tracking
|
|
247
|
+
*
|
|
248
|
+
* Call this when starting to load an ad to generate an impression ID.
|
|
249
|
+
*/
|
|
250
|
+
fun createImpressionContext(
|
|
251
|
+
placementId: String,
|
|
252
|
+
gamAdUnit: String,
|
|
253
|
+
format: String,
|
|
254
|
+
width: Int? = null,
|
|
255
|
+
height: Int? = null
|
|
256
|
+
): ImpressionContext {
|
|
257
|
+
val context = ImpressionContext(
|
|
258
|
+
placementId = placementId,
|
|
259
|
+
gamAdUnit = gamAdUnit,
|
|
260
|
+
format = format,
|
|
261
|
+
width = width,
|
|
262
|
+
height = height
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
impressionContexts[context.impressionId] = context
|
|
266
|
+
|
|
267
|
+
BCLogger.d(TAG, "Created impression context: ${context.impressionId} for $placementId")
|
|
268
|
+
|
|
269
|
+
return context
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Update auction data for an impression context
|
|
274
|
+
*/
|
|
275
|
+
fun updateAuctionData(impressionId: String, auctionData: AuctionData) {
|
|
276
|
+
impressionContexts[impressionId]?.let { context ->
|
|
277
|
+
impressionContexts[impressionId] = context.copy(auctionData = auctionData)
|
|
278
|
+
}
|
|
279
|
+
BCLogger.d(TAG, "Updated auction data for impression: $impressionId")
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get impression context by ID
|
|
284
|
+
*/
|
|
285
|
+
fun getImpressionContext(impressionId: String): ImpressionContext? {
|
|
286
|
+
return impressionContexts[impressionId]
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// MARK: - Enhanced Impression Tracking
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Get GAM network code from config (helper method)
|
|
293
|
+
*/
|
|
294
|
+
private fun getGamNetworkCode(): String {
|
|
295
|
+
return try {
|
|
296
|
+
BigCrunchAds.getConfigManager().getGamNetworkCode() ?: ""
|
|
297
|
+
} catch (e: Exception) {
|
|
298
|
+
""
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Track an ad impression with auction data
|
|
304
|
+
*
|
|
305
|
+
* @param context The impression context with all tracking data
|
|
306
|
+
*/
|
|
307
|
+
fun trackAdImpression(context: ImpressionContext) {
|
|
308
|
+
val adSize = if (context.width != null && context.height != null) {
|
|
309
|
+
"${context.width}x${context.height}"
|
|
310
|
+
} else ""
|
|
311
|
+
|
|
312
|
+
// Get common event fields
|
|
313
|
+
val common = getCommonEventFields()
|
|
314
|
+
|
|
315
|
+
// auction_id is required and must be a valid UUID - generate one if not provided
|
|
316
|
+
val auctionId = context.auctionData.auctionId?.takeIf { it.isNotEmpty() }
|
|
317
|
+
?: UUID.randomUUID().toString()
|
|
318
|
+
|
|
319
|
+
// Create impression record with all fields (defaults to empty string/0.0)
|
|
320
|
+
val impressionRecord = ImpressionRecord(
|
|
321
|
+
slotId = context.placementId,
|
|
322
|
+
gamUnit = context.gamAdUnit,
|
|
323
|
+
impressionId = context.impressionId,
|
|
324
|
+
auctionId = auctionId,
|
|
325
|
+
adBidder = context.auctionData.bidder ?: "",
|
|
326
|
+
adSize = adSize,
|
|
327
|
+
adPrice = context.auctionData.bidPriceCpm ?: 0.0,
|
|
328
|
+
creativeId = context.auctionData.creativeId ?: ""
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
// region must be 2 chars or empty per backend validation
|
|
332
|
+
val regionCode = common.region.takeIf { it.length == 2 } ?: ""
|
|
333
|
+
|
|
334
|
+
// Create batch event with session/page context
|
|
335
|
+
val batchEvent = ImpressionBatchEvent(
|
|
336
|
+
payloadVersion = "1.0.0",
|
|
337
|
+
configVersion = 1,
|
|
338
|
+
browserTimestamp = sessionManager.getCurrentTimestamp(),
|
|
339
|
+
sessionId = sessionManager.sessionId,
|
|
340
|
+
userId = sessionManager.userId,
|
|
341
|
+
propertyId = propertyId,
|
|
342
|
+
newUser = sessionManager.isNewUser,
|
|
343
|
+
pageId = sessionManager.getOrCreatePageId(),
|
|
344
|
+
sessionDepth = sessionManager.sessionDepth,
|
|
345
|
+
pageUrl = "", // page_url must be valid URL or empty - mobile doesn't have URLs
|
|
346
|
+
pageSearch = "",
|
|
347
|
+
pageReferrer = "",
|
|
348
|
+
browser = common.browser,
|
|
349
|
+
device = common.device,
|
|
350
|
+
os = common.os,
|
|
351
|
+
country = common.country,
|
|
352
|
+
region = regionCode,
|
|
353
|
+
sessionSource = common.sessionSource,
|
|
354
|
+
sessionMedium = common.sessionMedium,
|
|
355
|
+
utmSource = common.utmSource ?: "",
|
|
356
|
+
utmMedium = common.utmMedium ?: "",
|
|
357
|
+
utmCampaign = common.utmCampaign ?: "",
|
|
358
|
+
utmTerm = common.utmTerm ?: "",
|
|
359
|
+
utmContent = common.utmContent ?: "",
|
|
360
|
+
gclid = "",
|
|
361
|
+
fbclid = "",
|
|
362
|
+
acctType = "anonymous",
|
|
363
|
+
diiSource = "",
|
|
364
|
+
gamNetworkCode = getGamNetworkCode(),
|
|
365
|
+
amznPubId = NIL_UUID, // amzn_pub_id must be valid UUID
|
|
366
|
+
customDimensions = emptyMap(),
|
|
367
|
+
impressions = listOf(impressionRecord)
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
sendEvent(batchEvent, impressionBatchAdapter, "impressions")
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Track an ad impression (simple version without auction data)
|
|
375
|
+
*/
|
|
376
|
+
fun trackAdImpression(
|
|
377
|
+
placementId: String,
|
|
378
|
+
format: String,
|
|
379
|
+
gamAdUnit: String = "",
|
|
380
|
+
advertiserId: String? = null,
|
|
381
|
+
campaignId: String? = null,
|
|
382
|
+
lineItemId: String? = null,
|
|
383
|
+
creativeId: String? = null,
|
|
384
|
+
refreshCount: Int = 0
|
|
385
|
+
) {
|
|
386
|
+
// Increment session impression counter
|
|
387
|
+
SessionManager.incrementAdImpressionCount()
|
|
388
|
+
|
|
389
|
+
// Get common event fields
|
|
390
|
+
val common = getCommonEventFields()
|
|
391
|
+
|
|
392
|
+
// auction_id is required and must be a valid UUID
|
|
393
|
+
val auctionId = UUID.randomUUID().toString()
|
|
394
|
+
|
|
395
|
+
// Create impression record with all fields (defaults to empty string/0.0)
|
|
396
|
+
val impressionRecord = ImpressionRecord(
|
|
397
|
+
slotId = placementId,
|
|
398
|
+
gamUnit = gamAdUnit,
|
|
399
|
+
impressionId = UUID.randomUUID().toString(),
|
|
400
|
+
auctionId = auctionId,
|
|
401
|
+
advertiserId = advertiserId ?: "",
|
|
402
|
+
campaignId = campaignId ?: "",
|
|
403
|
+
lineItemId = lineItemId ?: "",
|
|
404
|
+
creativeId = creativeId ?: "",
|
|
405
|
+
refreshCount = refreshCount
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
// region must be 2 chars or empty per backend validation
|
|
409
|
+
val regionCode = common.region.takeIf { it.length == 2 } ?: ""
|
|
410
|
+
|
|
411
|
+
// Create batch event with session/page context
|
|
412
|
+
val batchEvent = ImpressionBatchEvent(
|
|
413
|
+
payloadVersion = "1.0.0",
|
|
414
|
+
configVersion = 1,
|
|
415
|
+
browserTimestamp = sessionManager.getCurrentTimestamp(),
|
|
416
|
+
sessionId = sessionManager.sessionId,
|
|
417
|
+
userId = sessionManager.userId,
|
|
418
|
+
propertyId = propertyId,
|
|
419
|
+
newUser = sessionManager.isNewUser,
|
|
420
|
+
pageId = sessionManager.getOrCreatePageId(),
|
|
421
|
+
sessionDepth = sessionManager.sessionDepth,
|
|
422
|
+
pageUrl = "", // page_url must be valid URL or empty - mobile doesn't have URLs
|
|
423
|
+
pageSearch = "",
|
|
424
|
+
pageReferrer = "",
|
|
425
|
+
browser = common.browser,
|
|
426
|
+
device = common.device,
|
|
427
|
+
os = common.os,
|
|
428
|
+
country = common.country,
|
|
429
|
+
region = regionCode,
|
|
430
|
+
sessionSource = common.sessionSource,
|
|
431
|
+
sessionMedium = common.sessionMedium,
|
|
432
|
+
utmSource = common.utmSource ?: "",
|
|
433
|
+
utmMedium = common.utmMedium ?: "",
|
|
434
|
+
utmCampaign = common.utmCampaign ?: "",
|
|
435
|
+
utmTerm = common.utmTerm ?: "",
|
|
436
|
+
utmContent = common.utmContent ?: "",
|
|
437
|
+
gclid = "",
|
|
438
|
+
fbclid = "",
|
|
439
|
+
acctType = "anonymous",
|
|
440
|
+
diiSource = "",
|
|
441
|
+
gamNetworkCode = getGamNetworkCode(),
|
|
442
|
+
amznPubId = NIL_UUID, // amzn_pub_id must be valid UUID
|
|
443
|
+
customDimensions = emptyMap(),
|
|
444
|
+
impressions = listOf(impressionRecord)
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
sendEvent(batchEvent, impressionBatchAdapter, "impressions")
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// MARK: - Click Tracking
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Track an ad click with impression context
|
|
454
|
+
*
|
|
455
|
+
* @param impressionId The impression that was clicked
|
|
456
|
+
* @param placementId Placement ID
|
|
457
|
+
* @param format Ad format
|
|
458
|
+
*/
|
|
459
|
+
fun trackAdClick(impressionId: String, placementId: String, format: String) {
|
|
460
|
+
// Get common event fields
|
|
461
|
+
val common = getCommonEventFields()
|
|
462
|
+
|
|
463
|
+
// region must be 2 chars or empty per backend validation
|
|
464
|
+
val regionCode = common.region.takeIf { it.length == 2 } ?: ""
|
|
465
|
+
|
|
466
|
+
// Create nested click data - impressionId must be valid UUID
|
|
467
|
+
val clickData = ClickData(
|
|
468
|
+
clickId = UUID.randomUUID().toString(),
|
|
469
|
+
slotId = placementId,
|
|
470
|
+
impressionId = impressionId.takeIf { it.isNotEmpty() } ?: UUID.randomUUID().toString()
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
val event = ClickEvent(
|
|
474
|
+
payloadVersion = "1.0.0", // Must be semver format
|
|
475
|
+
configVersion = 1,
|
|
476
|
+
browserTimestamp = sessionManager.getCurrentTimestamp(),
|
|
477
|
+
sessionId = sessionManager.sessionId,
|
|
478
|
+
userId = sessionManager.userId,
|
|
479
|
+
propertyId = propertyId,
|
|
480
|
+
newUser = sessionManager.isNewUser,
|
|
481
|
+
pageId = sessionManager.getOrCreatePageId(),
|
|
482
|
+
sessionDepth = sessionManager.sessionDepth,
|
|
483
|
+
pageUrl = "", // page_url must be valid URL or empty - mobile doesn't have URLs
|
|
484
|
+
pageSearch = "",
|
|
485
|
+
pageReferrer = "",
|
|
486
|
+
browser = common.browser,
|
|
487
|
+
device = common.device,
|
|
488
|
+
os = common.os,
|
|
489
|
+
country = common.country,
|
|
490
|
+
region = regionCode,
|
|
491
|
+
sessionSource = common.sessionSource,
|
|
492
|
+
sessionMedium = common.sessionMedium,
|
|
493
|
+
utmSource = common.utmSource ?: "",
|
|
494
|
+
utmMedium = common.utmMedium ?: "",
|
|
495
|
+
utmCampaign = common.utmCampaign ?: "",
|
|
496
|
+
utmTerm = common.utmTerm ?: "",
|
|
497
|
+
utmContent = common.utmContent ?: "",
|
|
498
|
+
gclid = "",
|
|
499
|
+
fbclid = "",
|
|
500
|
+
acctType = "anonymous",
|
|
501
|
+
diiSource = "",
|
|
502
|
+
gamNetworkCode = getGamNetworkCode(),
|
|
503
|
+
amznPubId = NIL_UUID, // amzn_pub_id must be valid UUID
|
|
504
|
+
customDimensions = emptyMap(),
|
|
505
|
+
click = clickData
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
sendEvent(event, clickAdapter, "clicks")
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Track an ad click (simple version without impression ID)
|
|
513
|
+
*/
|
|
514
|
+
fun trackAdClick(placementId: String, format: String) {
|
|
515
|
+
trackAdClick("", placementId, format)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// MARK: - Viewability Tracking
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Track ad viewability with impression context
|
|
522
|
+
*
|
|
523
|
+
* @param impressionId The impression that became viewable
|
|
524
|
+
* @param placementId Placement ID
|
|
525
|
+
* @param format Ad format
|
|
526
|
+
* @param viewableTimeMs Time the ad was viewable in milliseconds (unused, kept for API compatibility)
|
|
527
|
+
* @param percentVisible Percentage of ad that was visible (unused, kept for API compatibility)
|
|
528
|
+
*/
|
|
529
|
+
fun trackAdViewable(
|
|
530
|
+
impressionId: String,
|
|
531
|
+
placementId: String,
|
|
532
|
+
format: String,
|
|
533
|
+
viewableTimeMs: Long,
|
|
534
|
+
percentVisible: Int
|
|
535
|
+
) {
|
|
536
|
+
// Get common event fields
|
|
537
|
+
val common = getCommonEventFields()
|
|
538
|
+
|
|
539
|
+
// region must be 2 chars or empty per backend validation
|
|
540
|
+
val regionCode = common.region.takeIf { it.length == 2 } ?: ""
|
|
541
|
+
|
|
542
|
+
// Create nested viewability data - impressionId must be valid UUID
|
|
543
|
+
val viewabilityData = ViewabilityData(
|
|
544
|
+
slotId = placementId,
|
|
545
|
+
impressionId = impressionId.takeIf { it.isNotEmpty() } ?: UUID.randomUUID().toString()
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
val event = ViewabilityEvent(
|
|
549
|
+
payloadVersion = "1.0.0", // Must be semver format
|
|
550
|
+
configVersion = 1,
|
|
551
|
+
browserTimestamp = sessionManager.getCurrentTimestamp(),
|
|
552
|
+
sessionId = sessionManager.sessionId,
|
|
553
|
+
userId = sessionManager.userId,
|
|
554
|
+
propertyId = propertyId,
|
|
555
|
+
newUser = sessionManager.isNewUser,
|
|
556
|
+
pageId = sessionManager.getOrCreatePageId(),
|
|
557
|
+
sessionDepth = sessionManager.sessionDepth,
|
|
558
|
+
pageUrl = "", // page_url must be valid URL or empty - mobile doesn't have URLs
|
|
559
|
+
pageSearch = "",
|
|
560
|
+
pageReferrer = "",
|
|
561
|
+
browser = common.browser,
|
|
562
|
+
device = common.device,
|
|
563
|
+
os = common.os,
|
|
564
|
+
country = common.country,
|
|
565
|
+
region = regionCode,
|
|
566
|
+
sessionSource = common.sessionSource,
|
|
567
|
+
sessionMedium = common.sessionMedium,
|
|
568
|
+
utmSource = common.utmSource ?: "",
|
|
569
|
+
utmMedium = common.utmMedium ?: "",
|
|
570
|
+
utmCampaign = common.utmCampaign ?: "",
|
|
571
|
+
utmTerm = common.utmTerm ?: "",
|
|
572
|
+
utmContent = common.utmContent ?: "",
|
|
573
|
+
gclid = "",
|
|
574
|
+
fbclid = "",
|
|
575
|
+
acctType = "anonymous",
|
|
576
|
+
diiSource = "",
|
|
577
|
+
gamNetworkCode = getGamNetworkCode(),
|
|
578
|
+
amznPubId = NIL_UUID, // amzn_pub_id must be valid UUID
|
|
579
|
+
customDimensions = emptyMap(),
|
|
580
|
+
viewability = listOf(viewabilityData)
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
batchViewabilityEvent(event)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Track ad viewability (simple version without metrics)
|
|
588
|
+
*/
|
|
589
|
+
fun trackAdViewable(placementId: String, format: String) {
|
|
590
|
+
trackAdViewable("", placementId, format, 0, 0)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// MARK: - Engagement Tracking
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Track user engagement with content
|
|
597
|
+
*
|
|
598
|
+
* @param engagedTime Time actively engaged in seconds
|
|
599
|
+
* @param timeOnPage Total time on page/screen in seconds
|
|
600
|
+
* @param scrollDepth Maximum scroll depth percentage (0-100)
|
|
601
|
+
*/
|
|
602
|
+
fun trackEngagement(engagedTime: Int, timeOnPage: Int, scrollDepth: Int = 0) {
|
|
603
|
+
// Get common event fields
|
|
604
|
+
val common = getCommonEventFields()
|
|
605
|
+
|
|
606
|
+
// region must be 2 chars or empty per backend validation
|
|
607
|
+
val regionCode = common.region.takeIf { it.length == 2 } ?: ""
|
|
608
|
+
|
|
609
|
+
val event = EngagementEvent(
|
|
610
|
+
payloadVersion = "1.0.0", // Must be semver format
|
|
611
|
+
configVersion = 1,
|
|
612
|
+
browserTimestamp = sessionManager.getCurrentTimestamp(),
|
|
613
|
+
sessionId = sessionManager.sessionId,
|
|
614
|
+
userId = sessionManager.userId,
|
|
615
|
+
propertyId = propertyId,
|
|
616
|
+
newUser = sessionManager.isNewUser,
|
|
617
|
+
pageId = sessionManager.getOrCreatePageId(),
|
|
618
|
+
sessionDepth = sessionManager.sessionDepth,
|
|
619
|
+
pageUrl = "", // page_url must be valid URL or empty - mobile doesn't have URLs
|
|
620
|
+
pageSearch = "",
|
|
621
|
+
pageReferrer = "",
|
|
622
|
+
browser = common.browser,
|
|
623
|
+
device = common.device,
|
|
624
|
+
os = common.os,
|
|
625
|
+
country = common.country,
|
|
626
|
+
region = regionCode,
|
|
627
|
+
sessionSource = common.sessionSource,
|
|
628
|
+
sessionMedium = common.sessionMedium,
|
|
629
|
+
utmSource = common.utmSource ?: "",
|
|
630
|
+
utmMedium = common.utmMedium ?: "",
|
|
631
|
+
utmCampaign = common.utmCampaign ?: "",
|
|
632
|
+
utmTerm = common.utmTerm ?: "",
|
|
633
|
+
utmContent = common.utmContent ?: "",
|
|
634
|
+
gclid = "",
|
|
635
|
+
fbclid = "",
|
|
636
|
+
acctType = "anonymous",
|
|
637
|
+
diiSource = "",
|
|
638
|
+
gamNetworkCode = getGamNetworkCode(),
|
|
639
|
+
amznPubId = NIL_UUID, // amzn_pub_id must be valid UUID
|
|
640
|
+
customDimensions = emptyMap(),
|
|
641
|
+
engagedTime = engagedTime,
|
|
642
|
+
timeOnPage = timeOnPage,
|
|
643
|
+
scrollDepth = scrollDepth
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
sendEvent(event, engagementAdapter, "engagement")
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Track an ad request
|
|
651
|
+
*
|
|
652
|
+
* @param placementId The placement ID being requested
|
|
653
|
+
* @param format The ad format (banner, interstitial, etc.)
|
|
654
|
+
*/
|
|
655
|
+
fun trackAdRequest(placementId: String, format: String) {
|
|
656
|
+
SessionManager.incrementAdRequestCount()
|
|
657
|
+
BCLogger.d("AnalyticsClient", "Tracked ad request: $placementId ($format)")
|
|
658
|
+
// TODO: Send actual event to backend when endpoint is available
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// NOTE: trackAdRevenue() removed - revenue now tracked via ImpressionEvent.bidPriceCpm
|
|
662
|
+
|
|
663
|
+
// MARK: - Cleanup
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Remove an impression context (call when ad is destroyed)
|
|
667
|
+
*/
|
|
668
|
+
fun removeImpressionContext(impressionId: String) {
|
|
669
|
+
impressionContexts.remove(impressionId)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Clear all impression contexts
|
|
674
|
+
*/
|
|
675
|
+
fun clearImpressionContexts() {
|
|
676
|
+
impressionContexts.clear()
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// MARK: - Batching Helpers
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Batch a viewability event (250ms delay)
|
|
683
|
+
*/
|
|
684
|
+
private fun batchViewabilityEvent(event: ViewabilityEvent) {
|
|
685
|
+
synchronized(batchLock) {
|
|
686
|
+
viewabilityBatch.add(event)
|
|
687
|
+
|
|
688
|
+
// Cancel existing timer
|
|
689
|
+
viewabilityBatchHandler?.removeCallbacksAndMessages(null)
|
|
690
|
+
|
|
691
|
+
// Create new handler if needed
|
|
692
|
+
if (viewabilityBatchHandler == null) {
|
|
693
|
+
viewabilityBatchHandler = android.os.Handler(android.os.Looper.getMainLooper())
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Schedule flush after 250ms
|
|
697
|
+
viewabilityBatchHandler?.postDelayed({
|
|
698
|
+
flushViewabilityBatch()
|
|
699
|
+
}, BATCH_DELAY_MS)
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Flush viewability batch to backend
|
|
705
|
+
*/
|
|
706
|
+
private fun flushViewabilityBatch() {
|
|
707
|
+
synchronized(batchLock) {
|
|
708
|
+
if (viewabilityBatch.isEmpty()) return
|
|
709
|
+
|
|
710
|
+
val eventsToSend = viewabilityBatch.toList()
|
|
711
|
+
viewabilityBatch.clear()
|
|
712
|
+
|
|
713
|
+
BCLogger.d(TAG, "Flushing ${eventsToSend.size} viewability events")
|
|
714
|
+
|
|
715
|
+
// Send events as a batch array
|
|
716
|
+
sendEventBatch(eventsToSend, "viewability")
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|