@advenue/react-native 1.0.0 → 1.1.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/src/main/java/expo/modules/advenue/AdvenueAndroidModule.kt +4 -1
- package/android/src/main/kotlin/io/advenue/Advenue.kt +76 -11
- package/android/src/main/kotlin/io/advenue/AdvenueConfig.kt +36 -1
- package/android/src/main/kotlin/io/advenue/DebugLog.kt +51 -0
- package/android/src/main/kotlin/io/advenue/core/Backoff.kt +14 -1
- package/android/src/main/kotlin/io/advenue/core/CommandPipe.kt +36 -3
- package/android/src/main/kotlin/io/advenue/core/Engine.kt +96 -4
- package/android/src/main/kotlin/io/advenue/core/EventQueue.kt +12 -0
- package/android/src/main/kotlin/io/advenue/core/Limits.kt +3 -0
- package/android/src/main/kotlin/io/advenue/core/PiiScrub.kt +77 -0
- package/android/src/main/kotlin/io/advenue/core/SessionTracker.kt +109 -33
- package/android/src/main/kotlin/io/advenue/platform/Collectors.kt +14 -0
- package/android/src/main/kotlin/io/advenue/platform/ConversionFetcher.kt +24 -10
- package/android/src/main/kotlin/io/advenue/platform/ForegroundTracker.kt +9 -2
- package/android/src/main/kotlin/io/advenue/platform/HttpUrlTransport.kt +21 -8
- package/android/src/main/kotlin/io/advenue/platform/InstallEnrichment.kt +7 -0
- package/android/src/main/kotlin/io/advenue/platform/InstallScopedStore.kt +5 -0
- package/android/src/main/kotlin/io/advenue/platform/LifecycleBridge.kt +23 -1
- package/dist/index.cjs +16 -4
- package/dist/index.d.cts +18 -4
- package/dist/index.d.ts +18 -4
- package/dist/index.js +16 -4
- package/ios/AdvenueIosModule.swift +4 -2
- package/ios/vendor/Advenue/Advenue.swift +88 -17
- package/ios/vendor/Advenue/AdvenueConfig.swift +22 -2
- package/ios/vendor/Advenue/AdvenueSetupError.swift +24 -0
- package/ios/vendor/Advenue/DebugLog.swift +46 -0
- package/ios/vendor/AdvenueCore/Backoff.swift +16 -2
- package/ios/vendor/AdvenueCore/Engine.swift +83 -9
- package/ios/vendor/AdvenueCore/EventQueue.swift +12 -0
- package/ios/vendor/AdvenueCore/SessionTracker.swift +89 -32
- package/ios/vendor/AdvenuePlatform/CacheFileStore.swift +90 -0
- package/ios/vendor/AdvenuePlatform/ChallengeFetcher.swift +14 -2
- package/ios/vendor/AdvenuePlatform/ConversionFetcher.swift +14 -3
- package/ios/vendor/AdvenuePlatform/DeviceInfo.swift +31 -0
- package/ios/vendor/AdvenuePlatform/HttpTransport.swift +28 -4
- package/ios/vendor/AdvenuePlatform/InstallEnrichment.swift +20 -4
- package/ios/vendor/AdvenuePlatform/SkanConfigFetcher.swift +14 -2
- package/package.json +1 -1
- package/src/index.ts +20 -3
- package/src/native-types.ts +0 -1
- package/src/types.ts +18 -3
|
@@ -44,7 +44,6 @@ class AdvenueAndroidModule : Module() {
|
|
|
44
44
|
AdvenueConfig(
|
|
45
45
|
apiKey = apiKey,
|
|
46
46
|
endpoint = (options["endpoint"] as? String) ?: DEFAULT_ENDPOINT,
|
|
47
|
-
appVersion = options["appVersion"] as? String,
|
|
48
47
|
requireConsent = (options["requireConsent"] as? Boolean) ?: false,
|
|
49
48
|
batchSize = (options["batchSize"] as? Number)?.toInt() ?: 20,
|
|
50
49
|
flushIntervalMs = (options["flushIntervalMs"] as? Number)?.toLong() ?: 15_000L,
|
|
@@ -58,6 +57,10 @@ class AdvenueAndroidModule : Module() {
|
|
|
58
57
|
// The JavaScript has sent this since before the inversion; nothing
|
|
59
58
|
// read it, so Meta install-referrer attribution never ran.
|
|
60
59
|
fbAppId = options["fbAppId"] as? String,
|
|
60
|
+
// Logs the start, failures and sent batches to logcat (tag Advenue).
|
|
61
|
+
debug = (options["debug"] as? Boolean) ?: false,
|
|
62
|
+
// Test only: an http:// endpoint is refused without it.
|
|
63
|
+
allowInsecureHttp = (options["allowInsecureHttp"] as? Boolean) ?: false,
|
|
61
64
|
),
|
|
62
65
|
)
|
|
63
66
|
}
|
|
@@ -19,6 +19,8 @@ import io.advenue.core.SystemClock
|
|
|
19
19
|
import io.advenue.core.SystemUuids
|
|
20
20
|
import io.advenue.core.TimerScheduler
|
|
21
21
|
import io.advenue.platform.HttpConversionFetcher
|
|
22
|
+
import io.advenue.platform.INSTALL_HOLD_MARGIN_MS
|
|
23
|
+
import io.advenue.platform.REFERRER_DEADLINE_MS
|
|
22
24
|
import io.advenue.platform.HttpUrlTransport
|
|
23
25
|
import io.advenue.platform.IdentityResolution
|
|
24
26
|
import io.advenue.platform.EnrichmentSources
|
|
@@ -30,6 +32,8 @@ import io.advenue.core.extractAemCampaignIds
|
|
|
30
32
|
import io.advenue.core.tcfToConsent
|
|
31
33
|
import io.advenue.platform.collectDeviceInfo
|
|
32
34
|
import io.advenue.platform.readTcf
|
|
35
|
+
import io.advenue.platform.readAppVersion
|
|
36
|
+
import io.advenue.platform.readLaunchedInForeground
|
|
33
37
|
import io.advenue.platform.collectEnrichment
|
|
34
38
|
import io.advenue.platform.sha256Hex
|
|
35
39
|
import io.advenue.platform.resolveIdentity
|
|
@@ -249,13 +253,27 @@ internal class FacadeState {
|
|
|
249
253
|
*/
|
|
250
254
|
fun start(
|
|
251
255
|
application: Application,
|
|
252
|
-
|
|
256
|
+
requested: AdvenueConfig,
|
|
253
257
|
transport: EventTransport? = null,
|
|
254
258
|
sources: EnrichmentSources? = null,
|
|
259
|
+
launchedInForeground: () -> Boolean = ::readLaunchedInForeground,
|
|
260
|
+
logSink: AdvenueLogSink? = null,
|
|
255
261
|
) {
|
|
256
262
|
// Replace-and-shut-down, never add.
|
|
257
263
|
stop()
|
|
258
264
|
|
|
265
|
+
// `debug`: every swallowed failure goes through onError, so logging there
|
|
266
|
+
// covers all of them — wrapped once, before anything below captures it.
|
|
267
|
+
val (config, log) = withDebugLogging(requested, logSink)
|
|
268
|
+
|
|
269
|
+
// The SDK resolves the app version itself (readAppVersion); a config value
|
|
270
|
+
// is ignored — and said so, rather than looking like it took effect.
|
|
271
|
+
if (config.appVersion != null) {
|
|
272
|
+
config.onError(
|
|
273
|
+
"config.ignored:appVersion",
|
|
274
|
+
IllegalArgumentException("appVersion is resolved by the SDK and has no effect"),
|
|
275
|
+
)
|
|
276
|
+
}
|
|
259
277
|
val scoped = InstallScopedStore(application)
|
|
260
278
|
val prefs = PreferencesStore(application)
|
|
261
279
|
val uuid = SystemUuids()
|
|
@@ -266,12 +284,21 @@ internal class FacadeState {
|
|
|
266
284
|
// and nothing starts, so no event carries an invented identifier. The
|
|
267
285
|
// receiver below is the exit — without it the deferral would trade a
|
|
268
286
|
// phantom id for permanent silence.
|
|
269
|
-
|
|
270
|
-
|
|
287
|
+
// Reported as what it is: an IngestError(0) read as an HTTP failure
|
|
288
|
+
// that never happened, naming no cause.
|
|
289
|
+
config.onError(
|
|
290
|
+
"identity.deferred",
|
|
291
|
+
IllegalStateException(
|
|
292
|
+
"credential-encrypted storage is locked (not unlocked since boot); " +
|
|
293
|
+
"the SDK starts when the user unlocks the device",
|
|
294
|
+
),
|
|
295
|
+
)
|
|
296
|
+
// The unwrapped config: the retry wraps it again on its own start.
|
|
297
|
+
registerUnlockRetry(application, requested, transport, sources)
|
|
271
298
|
return
|
|
272
299
|
}
|
|
273
300
|
|
|
274
|
-
val
|
|
301
|
+
val baseTransport =
|
|
275
302
|
transport
|
|
276
303
|
?: HttpUrlTransport(
|
|
277
304
|
endpoint = config.endpoint,
|
|
@@ -280,8 +307,14 @@ internal class FacadeState {
|
|
|
280
307
|
clock = SystemClock(),
|
|
281
308
|
// Diagnostics only: it runs after the batch is already accepted, so
|
|
282
309
|
// nothing it does can turn a successful ingest into a failure.
|
|
283
|
-
onAccepted = { appId ->
|
|
310
|
+
onAccepted = { appId ->
|
|
311
|
+
// A key pasted from the wrong app is otherwise silent.
|
|
312
|
+
if (resolvedAppId != appId) log("[Advenue] ingesting into app $appId")
|
|
313
|
+
resolvedAppId = appId
|
|
314
|
+
},
|
|
284
315
|
)
|
|
316
|
+
val eventTransport =
|
|
317
|
+
if (config.debug) LoggingTransport(baseTransport, log) else baseTransport
|
|
285
318
|
|
|
286
319
|
val scheduler = TimerScheduler()
|
|
287
320
|
val engine =
|
|
@@ -292,12 +325,13 @@ internal class FacadeState {
|
|
|
292
325
|
platform = "android",
|
|
293
326
|
deviceId = identity.deviceId,
|
|
294
327
|
installationId = identity.installationId,
|
|
295
|
-
appVersion =
|
|
328
|
+
appVersion = readAppVersion(application),
|
|
296
329
|
osVersion = Build.VERSION.RELEASE,
|
|
297
330
|
sdkVersion = (config.sdkVersion ?: AdvenueVersion.CURRENT).take(32),
|
|
298
331
|
requireConsent = config.requireConsent,
|
|
299
332
|
sessionWindowMs = config.sessionWindowMs,
|
|
300
333
|
batchSize = config.batchSize,
|
|
334
|
+
piiScrubEnabled = config.piiScrubEnabled,
|
|
301
335
|
),
|
|
302
336
|
store = CompositeStore(prefs, scoped),
|
|
303
337
|
clock = SystemClock(),
|
|
@@ -329,10 +363,19 @@ internal class FacadeState {
|
|
|
329
363
|
pendingDeepLinks = mutableListOf()
|
|
330
364
|
}
|
|
331
365
|
|
|
366
|
+
// Before anything else is recorded: a launch that still owes its install
|
|
367
|
+
// stamps it now and holds every flush until the install is enqueued, so
|
|
368
|
+
// the server never sees this launch's session or events ahead of it. The
|
|
369
|
+
// hold outlives the referrer deadline by a margin and then lapses.
|
|
370
|
+
pipe.submit(Command.BeginInstall(REFERRER_DEADLINE_MS + INSTALL_HOLD_MARGIN_MS))
|
|
371
|
+
|
|
332
372
|
// Replayed in arrival order, before the first session, so a deferred deep
|
|
333
373
|
// link is attributed to the launch it belongs to.
|
|
334
374
|
buffered.forEach { sendDeepLink(it) }
|
|
335
375
|
|
|
376
|
+
// A process woken with no activity (a push, a WorkManager job, a
|
|
377
|
+
// broadcast) is not a session: the first activity start opens it instead.
|
|
378
|
+
val inForeground = launchedInForeground()
|
|
336
379
|
val bridge =
|
|
337
380
|
LifecycleBridge(
|
|
338
381
|
application,
|
|
@@ -343,12 +386,19 @@ internal class FacadeState {
|
|
|
343
386
|
// for hours.
|
|
344
387
|
pipe.submit(Command.Flush)
|
|
345
388
|
},
|
|
389
|
+
// Seeded to match: the Foreground below IS this launch's session, so
|
|
390
|
+
// the launcher activity's start that follows must not open a second
|
|
391
|
+
// one — the engine would read the open session as a killed app and
|
|
392
|
+
// split it with a synthetic session_end.
|
|
393
|
+
seededInForeground = inForeground,
|
|
346
394
|
)
|
|
347
395
|
bridge.start()
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
396
|
+
if (inForeground) pipe.submit(Command.Foreground)
|
|
397
|
+
log(
|
|
398
|
+
"[Advenue] initialized — endpoint ${config.endpoint}, " +
|
|
399
|
+
"sdk ${config.sdkVersion ?: AdvenueVersion.CURRENT}, " +
|
|
400
|
+
if (inForeground) "foreground launch" else "background launch, no session yet",
|
|
401
|
+
)
|
|
352
402
|
|
|
353
403
|
// Enrichment then install, off the caller's thread. initialize() returns
|
|
354
404
|
// synchronously — an SDK that blocks Application.onCreate for five seconds
|
|
@@ -368,6 +418,8 @@ internal class FacadeState {
|
|
|
368
418
|
gaid = enrichment.gaid,
|
|
369
419
|
androidId = null,
|
|
370
420
|
appInstanceId = enrichment.appInstanceId,
|
|
421
|
+
// B2: bayrak artık düşürülmüyor — olaya işlenir.
|
|
422
|
+
limitAdTracking = enrichment.limitAdTracking,
|
|
371
423
|
),
|
|
372
424
|
)
|
|
373
425
|
// Device metadata rides the install, where Meta CAPI reads it.
|
|
@@ -458,7 +510,10 @@ internal class FacadeState {
|
|
|
458
510
|
}
|
|
459
511
|
|
|
460
512
|
private fun sendDeepLink(url: String) {
|
|
461
|
-
|
|
513
|
+
// B3: ham query gönderilmez — token/e-posta query'de taşınır. AEM
|
|
514
|
+
// ayrıştırması ve sourceUrlHash TAM url'den yapılır (aşağıda), yalnızca
|
|
515
|
+
// gönderilen `url` özelliği scheme+host+path'e indirgenir.
|
|
516
|
+
submit(Command.Track("deep_link", mapOf("url" to stripUrlQuery(url)), "custom"))
|
|
462
517
|
|
|
463
518
|
// Meta AEM: a link from Meta carries al_applink_data with an opaque,
|
|
464
519
|
// Meta-encrypted campaign_ids blob. Emitted at most once per URL — the
|
|
@@ -482,6 +537,7 @@ internal class FacadeState {
|
|
|
482
537
|
private fun queryValue(url: String, name: String): String? =
|
|
483
538
|
runCatching { android.net.Uri.parse(url).getQueryParameter(name) }.getOrNull()
|
|
484
539
|
|
|
540
|
+
|
|
485
541
|
/** Test surface: how many links are waiting for `initialize`. */
|
|
486
542
|
val bufferedDeepLinkCount: Int
|
|
487
543
|
get() = synchronized(lock) { pendingDeepLinks.size }
|
|
@@ -547,3 +603,12 @@ internal class FacadeState {
|
|
|
547
603
|
val app: Application?,
|
|
548
604
|
)
|
|
549
605
|
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* B3: deep-link URL'sinden query+fragment atılır (token/e-posta taşınır).
|
|
609
|
+
* Saf dize işlemi — Android çalıştırıcısı gerekmez, JVM testinde de çalışır.
|
|
610
|
+
*/
|
|
611
|
+
internal fun stripUrlQuery(url: String): String {
|
|
612
|
+
val cut = url.indexOfAny(charArrayOf('?', '#'))
|
|
613
|
+
return if (cut >= 0) url.substring(0, cut) else url
|
|
614
|
+
}
|
|
@@ -12,6 +12,7 @@ public data class AdvenueConfig
|
|
|
12
12
|
constructor(
|
|
13
13
|
val apiKey: String,
|
|
14
14
|
val endpoint: String = DEFAULT_ENDPOINT,
|
|
15
|
+
/** Ignored: the SDK reads the app version from the package itself. Setting it reports `config.ignored:appVersion` through [onError]. Removed in the next major. */
|
|
15
16
|
val appVersion: String? = null,
|
|
16
17
|
val requireConsent: Boolean = false,
|
|
17
18
|
val sessionWindowMs: Long = DEFAULT_SESSION_WINDOW_MS,
|
|
@@ -53,7 +54,30 @@ constructor(
|
|
|
53
54
|
* iOS has no equivalent. A field on the Swift config would do nothing.
|
|
54
55
|
*/
|
|
55
56
|
val fbAppId: String? = null,
|
|
57
|
+
/**
|
|
58
|
+
* B1: track properties PII scrub'u. Varsayılan-açık; yalnızca açıkça
|
|
59
|
+
* kapatın (dokümante risk — ham PII ingest'e gider). Swift ile aynı.
|
|
60
|
+
*/
|
|
61
|
+
val piiScrubEnabled: Boolean = true,
|
|
62
|
+
/**
|
|
63
|
+
* B5: testler için `http://` izni. Üretimde kapalı tutun — açık `http://`
|
|
64
|
+
* API anahtarı dahil tüm olayları şifresiz taşır.
|
|
65
|
+
*/
|
|
66
|
+
val allowInsecureHttp: Boolean = false,
|
|
67
|
+
/**
|
|
68
|
+
* Development aid: logs the SDK's start, every failure [onError] sees and each
|
|
69
|
+
* accepted batch to logcat (tag `Advenue`). Never logs an identifier or a
|
|
70
|
+
* payload. Leave off in production; route failures through [onError] instead.
|
|
71
|
+
*/
|
|
72
|
+
val debug: Boolean = false,
|
|
56
73
|
) {
|
|
74
|
+
init {
|
|
75
|
+
// B5: şemasız endpoint fail-fast — geçersiz şema transportta değil,
|
|
76
|
+
// kurulumda yakalanır.
|
|
77
|
+
require(allowInsecureHttp || endpoint.startsWith("https://")) {
|
|
78
|
+
"Advenue endpoint must use https:// (set allowInsecureHttp only for tests)"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
57
81
|
/**
|
|
58
82
|
* Java-friendly builder. Kotlin's named and default arguments are invisible
|
|
59
83
|
* from Java, and both incumbents are Java-first — a Kotlin SDK that is painful
|
|
@@ -64,6 +88,7 @@ constructor(
|
|
|
64
88
|
|
|
65
89
|
public fun endpoint(value: String): Builder = apply { config = config.copy(endpoint = value) }
|
|
66
90
|
|
|
91
|
+
/** Ignored — see [AdvenueConfig.appVersion]. */
|
|
67
92
|
public fun appVersion(value: String?): Builder = apply {
|
|
68
93
|
config = config.copy(appVersion = value)
|
|
69
94
|
}
|
|
@@ -98,6 +123,16 @@ constructor(
|
|
|
98
123
|
config = config.copy(fbAppId = value)
|
|
99
124
|
}
|
|
100
125
|
|
|
126
|
+
public fun piiScrubEnabled(value: Boolean): Builder = apply {
|
|
127
|
+
config = config.copy(piiScrubEnabled = value)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public fun allowInsecureHttp(value: Boolean): Builder = apply {
|
|
131
|
+
config = config.copy(allowInsecureHttp = value)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public fun debug(value: Boolean): Builder = apply { config = config.copy(debug = value) }
|
|
135
|
+
|
|
101
136
|
public fun build(): AdvenueConfig = config
|
|
102
137
|
}
|
|
103
138
|
}
|
|
@@ -110,5 +145,5 @@ constructor(
|
|
|
110
145
|
* the event.
|
|
111
146
|
*/
|
|
112
147
|
public object AdvenueVersion {
|
|
113
|
-
public const val CURRENT: String = "
|
|
148
|
+
public const val CURRENT: String = "1.1.0"
|
|
114
149
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
package io.advenue
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import io.advenue.core.ClientEvent
|
|
5
|
+
import io.advenue.core.EventTransport
|
|
6
|
+
|
|
7
|
+
/** Where [AdvenueConfig.debug] writes: logcat in an app, a recorder in tests. */
|
|
8
|
+
internal fun interface AdvenueLogSink {
|
|
9
|
+
fun log(message: String)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Logcat, tag `Advenue`. The lines carry failure contexts, counts and the app
|
|
13
|
+
* id the server resolved — never an identifier or a payload. */
|
|
14
|
+
internal object LogcatSink : AdvenueLogSink {
|
|
15
|
+
override fun log(message: String) {
|
|
16
|
+
Log.i("Advenue", message)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Logs each accepted batch; failures already reach the log through the
|
|
21
|
+
* wrapped `onError` (`flush.transport`, `flush.poison`). */
|
|
22
|
+
internal class LoggingTransport(
|
|
23
|
+
private val inner: EventTransport,
|
|
24
|
+
private val log: (String) -> Unit,
|
|
25
|
+
) : EventTransport {
|
|
26
|
+
override fun send(events: List<ClientEvent>) {
|
|
27
|
+
inner.send(events)
|
|
28
|
+
log("[Advenue] sent ${events.size} event(s)")
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* With `debug` on: the config with `onError` also writing to the log, and the
|
|
34
|
+
* log itself. Off: the config unchanged and a log that discards.
|
|
35
|
+
*/
|
|
36
|
+
internal fun withDebugLogging(
|
|
37
|
+
config: AdvenueConfig,
|
|
38
|
+
sink: AdvenueLogSink?,
|
|
39
|
+
): Pair<AdvenueConfig, (String) -> Unit> {
|
|
40
|
+
if (!config.debug) return config to {}
|
|
41
|
+
val out = sink ?: LogcatSink
|
|
42
|
+
val report = config.onError
|
|
43
|
+
val logged =
|
|
44
|
+
config.copy(
|
|
45
|
+
onError = { context, error ->
|
|
46
|
+
out.log("[Advenue] $context: $error")
|
|
47
|
+
report(context, error)
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
return logged to { message -> out.log(message) }
|
|
51
|
+
}
|
|
@@ -5,7 +5,20 @@ package io.advenue.core
|
|
|
5
5
|
* poison payload: retrying a 400 forever would block the queue head, and
|
|
6
6
|
* dropping a 429 would discard events over a throttle.
|
|
7
7
|
*/
|
|
8
|
-
internal class IngestError(
|
|
8
|
+
internal class IngestError(
|
|
9
|
+
public val status: Int,
|
|
10
|
+
/**
|
|
11
|
+
* Set when no response arrived at all — DNS, refused connection, timeout, no
|
|
12
|
+
* network. [status] is then 408 so the backoff retries it, but that 408 was
|
|
13
|
+
* never sent by a server, and logging it as one sends whoever debugs it
|
|
14
|
+
* looking at the wrong end (F-SDK-10).
|
|
15
|
+
*/
|
|
16
|
+
public val networkCause: String? = null,
|
|
17
|
+
) :
|
|
18
|
+
Exception(
|
|
19
|
+
if (networkCause != null) "advenue ingest failed: no response (network error: $networkCause)"
|
|
20
|
+
else "advenue ingest failed: $status",
|
|
21
|
+
) {
|
|
9
22
|
public val isRetryable: Boolean
|
|
10
23
|
get() = status == 408 || status == 429 || status >= 500
|
|
11
24
|
}
|
|
@@ -19,12 +19,17 @@ internal sealed class Command {
|
|
|
19
19
|
val gaid: String?,
|
|
20
20
|
val androidId: String?,
|
|
21
21
|
val appInstanceId: String?,
|
|
22
|
+
/** B2: reklam kimliği rıza bayrağı — toplanıp düşürülüyordu. */
|
|
23
|
+
val limitAdTracking: Boolean? = null,
|
|
22
24
|
) : Command()
|
|
23
25
|
|
|
24
26
|
internal data class SetAppInstanceId(val id: String?) : Command()
|
|
25
27
|
|
|
26
28
|
public data class TrackInstall(val properties: Map<String, Any?>?) : Command()
|
|
27
29
|
|
|
30
|
+
/** A launch that still owes its install: hold flushes for at most [holdMs]. */
|
|
31
|
+
internal data class BeginInstall(val holdMs: Long) : Command()
|
|
32
|
+
|
|
28
33
|
internal data class SetConsentData(val consent: Consent?) : Command()
|
|
29
34
|
|
|
30
35
|
internal data class SetPushToken(val token: String?, val provider: String?) : Command()
|
|
@@ -121,26 +126,54 @@ internal class CommandPipe(
|
|
|
121
126
|
is Command.SetConsent -> engine.setConsent(command.granted)
|
|
122
127
|
is Command.SetUserId -> engine.setUserId(command.id)
|
|
123
128
|
is Command.SetIdentity ->
|
|
124
|
-
engine.setIdentity(
|
|
129
|
+
engine.setIdentity(
|
|
130
|
+
command.gaid,
|
|
131
|
+
command.androidId,
|
|
132
|
+
command.appInstanceId,
|
|
133
|
+
limitAdTracking = command.limitAdTracking,
|
|
134
|
+
)
|
|
125
135
|
is Command.SetAppInstanceId -> engine.setAppInstanceId(command.id)
|
|
126
136
|
is Command.TrackInstall -> engine.trackInstall(command.properties)
|
|
137
|
+
is Command.BeginInstall -> engine.beginInstall(command.holdMs)
|
|
127
138
|
is Command.SetConsentData -> engine.setConsentData(command.consent)
|
|
128
139
|
is Command.SetPushToken -> engine.setPushToken(command.token, command.provider)
|
|
129
140
|
Command.ForgetMe -> engine.forgetMe()
|
|
130
141
|
Command.Foreground -> engine.notifyForeground()
|
|
131
142
|
Command.Background -> engine.notifyBackground()
|
|
132
|
-
Command.Flush ->
|
|
143
|
+
Command.Flush -> {
|
|
144
|
+
val batch = engine.beginFlush()
|
|
145
|
+
if (batch != null) {
|
|
146
|
+
sends.put(Send(batch, single = null))
|
|
147
|
+
} else if (engine.isFlushing()) {
|
|
148
|
+
// Asked for while a batch is on the wire: run it when that send
|
|
149
|
+
// finishes rather than drop it — the events tracked meanwhile would
|
|
150
|
+
// otherwise wait a whole auto-flush interval.
|
|
151
|
+
flushRequested = true
|
|
152
|
+
}
|
|
153
|
+
}
|
|
133
154
|
is Command.BatchOutcome ->
|
|
134
155
|
engine.onBatchOutcome(command.error)?.let { sends.put(Send(listOf(it), single = it)) }
|
|
156
|
+
?: flushIfRequested()
|
|
135
157
|
is Command.SingleOutcome ->
|
|
136
158
|
engine.onSingleOutcome(command.event, command.error)?.let {
|
|
137
159
|
sends.put(Send(listOf(it), single = it))
|
|
138
|
-
}
|
|
160
|
+
} ?: flushIfRequested()
|
|
139
161
|
is Command.RunScheduled -> command.work()
|
|
140
162
|
Command.Shutdown -> Unit
|
|
141
163
|
}
|
|
142
164
|
}
|
|
143
165
|
|
|
166
|
+
/** Core-thread only, like everything [handle] touches. */
|
|
167
|
+
private var flushRequested = false
|
|
168
|
+
|
|
169
|
+
/** The flush a send in flight deferred, once that send is over. After a
|
|
170
|
+
* transient failure the backoff makes this a no-op: the timer retries. */
|
|
171
|
+
private fun flushIfRequested() {
|
|
172
|
+
if (!flushRequested || engine.isFlushing()) return
|
|
173
|
+
flushRequested = false
|
|
174
|
+
engine.beginFlush()?.let { sends.put(Send(it, single = null)) }
|
|
175
|
+
}
|
|
176
|
+
|
|
144
177
|
/**
|
|
145
178
|
* Synchronous, non-blocking and **ordered** — the entire reason the ingress
|
|
146
179
|
* is a queue.
|
|
@@ -39,6 +39,9 @@ internal data class EngineConfig(
|
|
|
39
39
|
val batchSize: Int = 20,
|
|
40
40
|
val retryBaseMs: Double = 1_000.0,
|
|
41
41
|
val retryCapMs: Double = 60_000.0,
|
|
42
|
+
/** B1: track properties PII scrub'u. Varsayılan-açık; yalnızca açık bayrakla
|
|
43
|
+
* kapatılır (opt-out) ve bu dokümante risklidir. Swift EngineConfig ile aynı. */
|
|
44
|
+
val piiScrubEnabled: Boolean = true,
|
|
42
45
|
)
|
|
43
46
|
|
|
44
47
|
/**
|
|
@@ -72,9 +75,24 @@ internal class AdvenueEngine(
|
|
|
72
75
|
private var gaid: String? = null
|
|
73
76
|
private var androidId: String? = null
|
|
74
77
|
private var appInstanceId: String? = null
|
|
78
|
+
/** B2: rıza bayrağı — her olaya işlenir, persist edilmez (kimlik gibi). */
|
|
79
|
+
private var limitAdTracking: Boolean? = null
|
|
75
80
|
private var consentData: Consent? = readConsentData()
|
|
76
81
|
private var pushToken: String? = null
|
|
77
82
|
private var pushProvider: String? = null
|
|
83
|
+
/**
|
|
84
|
+
* F-SDK-3: when this launch began an install that is not recorded yet. The
|
|
85
|
+
* install is stamped with it — the first open, not the moment the referrer
|
|
86
|
+
* read that preceded it finished.
|
|
87
|
+
*/
|
|
88
|
+
private var installStartedAtMs: Long? = null
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Flushes are held until the install is enqueued, so nothing reaches the
|
|
92
|
+
* server ahead of it. Bounded: past this instant the hold lapses, because a
|
|
93
|
+
* lost enrichment thread must never strand the queue.
|
|
94
|
+
*/
|
|
95
|
+
private var installHoldUntilMs: Long? = null
|
|
78
96
|
private var flushing = false
|
|
79
97
|
private var consecutiveFailures = 0
|
|
80
98
|
private var backoffUntilMs = 0L
|
|
@@ -90,7 +108,12 @@ internal class AdvenueEngine(
|
|
|
90
108
|
onError("track.rejected:$rejection", IngestError(400))
|
|
91
109
|
return false
|
|
92
110
|
}
|
|
93
|
-
|
|
111
|
+
// B1: PII scrub (Swift PIIScrub ile aynı sözleşme).
|
|
112
|
+
val scrubbed = if (config.piiScrubEnabled) PiiScrub.scrub(properties) else properties
|
|
113
|
+
queue.enqueue(newEvent(type = type, name = name, properties = scrubbed))
|
|
114
|
+
// An event is proof of life: a relaunch after an OS kill measures the gap
|
|
115
|
+
// from the last one (F-SDK-7).
|
|
116
|
+
sessions.heartbeat()
|
|
94
117
|
return true
|
|
95
118
|
}
|
|
96
119
|
|
|
@@ -146,6 +169,12 @@ internal class AdvenueEngine(
|
|
|
146
169
|
(parseJson(store.getString(CONSENT_DATA_KEY) ?: "") as? Map<*, *>)?.let(Consent::fromMap)
|
|
147
170
|
|
|
148
171
|
public fun setUserId(id: String?) {
|
|
172
|
+
// B6: 128 üstü id tüm batch'i 400'e düşürür (ingest bütün-batch ayrıştırır).
|
|
173
|
+
// Reddet, persist etme, onError ile bildir (Limits.kt deseni).
|
|
174
|
+
if (id != null && id.length > MAX_USER_ID_LENGTH) {
|
|
175
|
+
onError("account.setUserId:id_too_long", IngestError(400))
|
|
176
|
+
return
|
|
177
|
+
}
|
|
149
178
|
customerUserId = id
|
|
150
179
|
if (id != null) store.setString(USER_ID_KEY, id) else store.remove(USER_ID_KEY)
|
|
151
180
|
}
|
|
@@ -160,11 +189,13 @@ internal class AdvenueEngine(
|
|
|
160
189
|
androidId: String?,
|
|
161
190
|
appInstanceId: String?,
|
|
162
191
|
idfa: String? = null,
|
|
192
|
+
limitAdTracking: Boolean? = null,
|
|
163
193
|
) {
|
|
164
194
|
this.gaid = gaid
|
|
165
195
|
this.androidId = androidId
|
|
166
196
|
this.appInstanceId = appInstanceId
|
|
167
197
|
this.idfa = idfa
|
|
198
|
+
this.limitAdTracking = limitAdTracking
|
|
168
199
|
}
|
|
169
200
|
|
|
170
201
|
/**
|
|
@@ -187,13 +218,55 @@ internal class AdvenueEngine(
|
|
|
187
218
|
* failure.
|
|
188
219
|
*/
|
|
189
220
|
public fun trackInstall(properties: Map<String, Any?>? = null): Boolean {
|
|
221
|
+
val startedAtMs = installStartedAtMs
|
|
222
|
+
// Every outcome below ends the wait: recorded, refused or already sent,
|
|
223
|
+
// nothing is coming that the held events should still wait for.
|
|
224
|
+
releaseInstallHold()
|
|
190
225
|
if (forgotten || (config.requireConsent && !consent)) return false
|
|
191
226
|
if (store.getString(INSTALL_SENT_KEY) == "1") return false
|
|
192
|
-
|
|
227
|
+
// B1: install properties de scrub kapsamındadır.
|
|
228
|
+
val scrubbed = if (config.piiScrubEnabled) PiiScrub.scrub(properties) else properties
|
|
229
|
+
// At the head: the session and custom events recorded while enrichment ran
|
|
230
|
+
// are already queued, and the install must reach the server before them.
|
|
231
|
+
queue.enqueueFirst(
|
|
232
|
+
newEvent(
|
|
233
|
+
type = "install",
|
|
234
|
+
name = "install",
|
|
235
|
+
properties = scrubbed,
|
|
236
|
+
timestampMs = startedAtMs ?: clock.nowMs(),
|
|
237
|
+
),
|
|
238
|
+
)
|
|
193
239
|
store.setString(INSTALL_SENT_KEY, "1")
|
|
194
240
|
return true
|
|
195
241
|
}
|
|
196
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Marks the start of a launch that still owes its install. Called by the
|
|
245
|
+
* facade BEFORE the launch's foreground, while enrichment (Play referrer,
|
|
246
|
+
* advertising id, attestation) runs. Adjust and AppsFlyer both hold every
|
|
247
|
+
* later package behind the first one; this does the same, for at most
|
|
248
|
+
* [holdMs].
|
|
249
|
+
*/
|
|
250
|
+
public fun beginInstall(holdMs: Long) {
|
|
251
|
+
if (forgotten || store.getString(INSTALL_SENT_KEY) == "1") return
|
|
252
|
+
val now = clock.nowMs()
|
|
253
|
+
installStartedAtMs = now
|
|
254
|
+
installHoldUntilMs = now + holdMs
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Whether a pending install still holds flushes back. */
|
|
258
|
+
private fun heldForInstall(): Boolean {
|
|
259
|
+
val until = installHoldUntilMs ?: return false
|
|
260
|
+
if (clock.nowMs() < until) return true
|
|
261
|
+
installHoldUntilMs = null
|
|
262
|
+
return false
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private fun releaseInstallHold() {
|
|
266
|
+
installStartedAtMs = null
|
|
267
|
+
installHoldUntilMs = null
|
|
268
|
+
}
|
|
269
|
+
|
|
197
270
|
/**
|
|
198
271
|
* Erasure, including the install-scoped identifiers.
|
|
199
272
|
*
|
|
@@ -211,6 +284,7 @@ internal class AdvenueEngine(
|
|
|
211
284
|
*/
|
|
212
285
|
public fun forgetMe() {
|
|
213
286
|
forgotten = true
|
|
287
|
+
releaseInstallHold()
|
|
214
288
|
queue.clear()
|
|
215
289
|
sessions.reset()
|
|
216
290
|
consent = false
|
|
@@ -231,6 +305,10 @@ internal class AdvenueEngine(
|
|
|
231
305
|
DEVICE_ID_KEY,
|
|
232
306
|
INSTALLATION_ID_KEY,
|
|
233
307
|
INSTALL_SENT_KEY,
|
|
308
|
+
// queue.clear() above persists an empty blob synchronously; remove it
|
|
309
|
+
// so erasure leaves no queue file behind either. Safe to do last:
|
|
310
|
+
// `forgotten` stops every later enqueue, so nothing rewrites it.
|
|
311
|
+
QUEUE_KEY,
|
|
234
312
|
)) {
|
|
235
313
|
store.remove(key)
|
|
236
314
|
}
|
|
@@ -269,9 +347,21 @@ internal class AdvenueEngine(
|
|
|
269
347
|
/** Whether a transport is wired at all. */
|
|
270
348
|
internal fun canFlush(): Boolean = transport != null
|
|
271
349
|
|
|
350
|
+
/** Whether a batch is on the wire (its outcome not yet consumed). */
|
|
351
|
+
internal fun isFlushing(): Boolean = flushing
|
|
352
|
+
|
|
272
353
|
/** Decides what to send. Null means there is nothing to do. */
|
|
273
354
|
internal fun beginFlush(): List<ClientEvent>? {
|
|
274
|
-
|
|
355
|
+
// The auto-flush timer is the foreground tick: it keeps the open
|
|
356
|
+
// sub-session's last activity fresh even when the app records nothing.
|
|
357
|
+
if (!forgotten) sessions.heartbeat()
|
|
358
|
+
if (
|
|
359
|
+
transport == null ||
|
|
360
|
+
flushing ||
|
|
361
|
+
queue.size == 0 ||
|
|
362
|
+
clock.nowMs() < backoffUntilMs ||
|
|
363
|
+
heldForInstall()
|
|
364
|
+
) {
|
|
275
365
|
return null
|
|
276
366
|
}
|
|
277
367
|
// Clamped to the wire's limit, not trusted. The server answers 400 for a
|
|
@@ -387,13 +477,14 @@ internal class AdvenueEngine(
|
|
|
387
477
|
type: String,
|
|
388
478
|
name: String,
|
|
389
479
|
properties: Map<String, Any?>?,
|
|
480
|
+
timestampMs: Long = clock.nowMs(),
|
|
390
481
|
): ClientEvent =
|
|
391
482
|
ClientEvent(
|
|
392
483
|
id = uuid.next(),
|
|
393
484
|
deviceId = config.deviceId,
|
|
394
485
|
type = type,
|
|
395
486
|
name = name,
|
|
396
|
-
timestamp = iso8601(
|
|
487
|
+
timestamp = iso8601(timestampMs),
|
|
397
488
|
platform = config.platform,
|
|
398
489
|
installationId = config.installationId,
|
|
399
490
|
appVersion = config.appVersion,
|
|
@@ -402,6 +493,7 @@ internal class AdvenueEngine(
|
|
|
402
493
|
customerUserId = customerUserId,
|
|
403
494
|
idfa = idfa,
|
|
404
495
|
gaid = gaid,
|
|
496
|
+
limitAdTracking = limitAdTracking,
|
|
405
497
|
androidId = androidId,
|
|
406
498
|
appInstanceId = appInstanceId,
|
|
407
499
|
consent = consentData?.toMap(),
|
|
@@ -35,6 +35,18 @@ internal class EventQueue(
|
|
|
35
35
|
schedulePersist()
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Puts an event at the HEAD of the queue — the install, which is the first
|
|
40
|
+
* package of an installation and must not be preceded by the session and
|
|
41
|
+
* custom events recorded while enrichment ran. Over the cap, the oldest
|
|
42
|
+
* events behind it are dropped, never the event just placed.
|
|
43
|
+
*/
|
|
44
|
+
public fun enqueueFirst(event: ClientEvent) {
|
|
45
|
+
events.add(0, event)
|
|
46
|
+
while (events.size > maxSize) events.removeAt(1)
|
|
47
|
+
schedulePersist()
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
public fun peek(max: Int): List<ClientEvent> = events.take(max)
|
|
39
51
|
|
|
40
52
|
public fun ack(sent: List<ClientEvent>) {
|
|
@@ -22,6 +22,9 @@ package io.advenue.core
|
|
|
22
22
|
/** `clientEventSchema.name` — z.string().min(1).max(128). */
|
|
23
23
|
internal const val MAX_NAME_LENGTH: Int = 128
|
|
24
24
|
|
|
25
|
+
/** `customerUserId` — z.string().max(128). Üstü batch'i 400'e düşürür. */
|
|
26
|
+
internal const val MAX_USER_ID_LENGTH: Int = 128
|
|
27
|
+
|
|
25
28
|
/** `propertiesSchema` — at most this many keys. */
|
|
26
29
|
internal const val MAX_PROPERTIES_KEYS: Int = 50
|
|
27
30
|
|