@advenue/react-native 0.9.0 → 1.0.1
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 +8 -7
- package/android/src/main/java/expo/modules/advenue/AdvenueAndroidModule.kt +117 -368
- package/android/src/main/kotlin/io/advenue/Advenue.kt +574 -0
- package/android/src/main/kotlin/io/advenue/AdvenueConfig.kt +141 -0
- package/android/src/main/kotlin/io/advenue/core/Backoff.kt +28 -0
- package/android/src/main/kotlin/io/advenue/core/ClientEvent.kt +136 -0
- package/android/src/main/kotlin/io/advenue/core/CommandPipe.kt +193 -0
- package/android/src/main/kotlin/io/advenue/core/Consent.kt +52 -0
- package/android/src/main/kotlin/io/advenue/core/Contracts.kt +80 -0
- package/android/src/main/kotlin/io/advenue/core/Conversion.kt +69 -0
- package/android/src/main/kotlin/io/advenue/core/Engine.kt +436 -0
- package/android/src/main/kotlin/io/advenue/core/EventQueue.kt +89 -0
- package/android/src/main/kotlin/io/advenue/core/HmacSigner.kt +31 -0
- package/android/src/main/kotlin/io/advenue/core/InstallReferrer.kt +146 -0
- package/android/src/main/kotlin/io/advenue/core/Json.kt +272 -0
- package/android/src/main/kotlin/io/advenue/core/Limits.kt +50 -0
- package/android/src/main/kotlin/io/advenue/core/MetaReferrer.kt +80 -0
- package/android/src/main/kotlin/io/advenue/core/PiiScrub.kt +77 -0
- package/android/src/main/kotlin/io/advenue/core/SessionTracker.kt +189 -0
- package/android/src/main/kotlin/io/advenue/core/SystemServices.kt +58 -0
- package/android/src/main/kotlin/io/advenue/core/Tcf.kt +48 -0
- package/android/src/main/kotlin/io/advenue/core/Time.kt +56 -0
- package/android/src/main/kotlin/io/advenue/platform/Collectors.kt +163 -0
- package/android/src/main/kotlin/io/advenue/platform/CompositeStore.kt +33 -0
- package/android/src/main/kotlin/io/advenue/platform/ConversionFetcher.kt +68 -0
- package/android/src/main/kotlin/io/advenue/platform/ForegroundTracker.kt +90 -0
- package/android/src/main/kotlin/io/advenue/platform/HttpUrlTransport.kt +101 -0
- package/android/src/main/kotlin/io/advenue/platform/Identity.kt +79 -0
- package/android/src/main/kotlin/io/advenue/platform/InstallEnrichment.kt +197 -0
- package/android/src/main/kotlin/io/advenue/platform/InstallScopedStore.kt +96 -0
- package/android/src/main/kotlin/io/advenue/platform/LifecycleBridge.kt +72 -0
- package/android/src/main/kotlin/io/advenue/platform/PreferencesStore.kt +32 -0
- package/android/src/main/kotlin/io/advenue/plugin/Contracts.kt +67 -0
- package/android/src/main/kotlin/io/advenue/plugin/FirebaseAppInstanceIdSource.kt +73 -0
- package/android/src/main/kotlin/io/advenue/plugin/PlayAdvertisingIdSource.kt +47 -0
- package/android/src/main/kotlin/io/advenue/plugin/PlayInstallReferrerSource.kt +85 -0
- package/android/src/main/kotlin/io/advenue/plugin/PlayIntegritySource.kt +86 -0
- package/android/src/main/kotlin/io/advenue/plugin/PluginRegistry.kt +61 -0
- package/dist/index.cjs +188 -675
- package/dist/index.d.cts +203 -336
- package/dist/index.d.ts +203 -336
- package/dist/index.js +187 -681
- package/ios/AdvenueIosModule.swift +143 -446
- package/ios/vendor/Advenue/Advenue.swift +632 -0
- package/ios/vendor/Advenue/AdvenueConfig.swift +89 -0
- package/ios/vendor/AdvenueCore/AdvenueValue.swift +103 -0
- package/ios/vendor/AdvenueCore/Attestation.swift +52 -0
- package/ios/vendor/AdvenueCore/Backoff.swift +28 -0
- package/ios/vendor/AdvenueCore/ClientEvent.swift +154 -0
- package/ios/vendor/AdvenueCore/Consent.swift +40 -0
- package/ios/vendor/AdvenueCore/Contracts.swift +59 -0
- package/ios/vendor/AdvenueCore/Conversion.swift +74 -0
- package/ios/vendor/AdvenueCore/ConversionValue.swift +217 -0
- package/ios/vendor/AdvenueCore/Engine.swift +607 -0
- package/ios/vendor/AdvenueCore/EventQueue.swift +89 -0
- package/ios/vendor/AdvenueCore/Limits.swift +41 -0
- package/ios/vendor/AdvenueCore/PIIScrub.swift +102 -0
- package/ios/vendor/AdvenueCore/SessionTracker.swift +139 -0
- package/ios/vendor/AdvenueCore/SkanConfig.swift +76 -0
- package/ios/vendor/AdvenueCore/SkanReporter.swift +25 -0
- package/ios/vendor/AdvenueCore/SkanState.swift +258 -0
- package/ios/vendor/AdvenueCore/Tcf.swift +48 -0
- package/ios/vendor/AdvenueCore/Transport.swift +14 -0
- package/ios/vendor/AdvenueFirebase/FirebaseAppInstanceId.swift +33 -0
- package/ios/vendor/AdvenuePlatform/AdvertisingIdentity.swift +87 -0
- package/ios/vendor/AdvenuePlatform/CacheFileStore.swift +90 -0
- package/ios/vendor/AdvenuePlatform/ChallengeFetcher.swift +42 -0
- package/ios/vendor/AdvenuePlatform/ConversionFetcher.swift +63 -0
- package/ios/vendor/AdvenuePlatform/CryptoKitSigner.swift +19 -0
- package/ios/vendor/AdvenuePlatform/DeviceCheckAttestation.swift +91 -0
- package/ios/vendor/AdvenuePlatform/DeviceInfo.swift +77 -0
- package/ios/vendor/AdvenuePlatform/ForegroundTracker.swift +55 -0
- package/ios/vendor/AdvenuePlatform/HttpTransport.swift +96 -0
- package/ios/vendor/AdvenuePlatform/Identity.swift +52 -0
- package/ios/vendor/AdvenuePlatform/InstallEnrichment.swift +155 -0
- package/ios/vendor/AdvenuePlatform/KeychainStore.swift +95 -0
- package/ios/vendor/AdvenuePlatform/SearchAdsToken.swift +76 -0
- package/ios/vendor/AdvenuePlatform/SkanConfigFetcher.swift +70 -0
- package/ios/vendor/AdvenuePlatform/StoreKitSkanReporter.swift +140 -0
- package/ios/vendor/AdvenuePlatform/SystemServices.swift +55 -0
- package/ios/vendor/AdvenuePlatform/TcfReader.swift +18 -0
- package/ios/vendor/AdvenuePlatform/UserDefaultsStore.swift +26 -0
- package/package.json +13 -15
- package/scripts/check-dist.mjs +17 -0
- package/scripts/check-vendored-swift.mjs +148 -0
- package/scripts/vendor-natives.mjs +149 -0
- package/scripts/vendor-natives.test.mjs +112 -0
- package/src/deep-links.ts +19 -1
- package/src/index.ts +273 -830
- package/src/native-types.ts +73 -181
- package/src/native.ts +0 -23
- package/src/types.ts +86 -0
- package/src/aem.ts +0 -33
- package/src/mmkv-storage.ts +0 -21
- package/src/native-storage.ts +0 -63
- package/src/secure-store.ts +0 -26
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
package io.advenue
|
|
2
|
+
|
|
3
|
+
import io.advenue.core.DEFAULT_SESSION_WINDOW_MS
|
|
4
|
+
import io.advenue.platform.DEFAULT_ENDPOINT
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SDK configuration. Defaults match sdk-core exactly, so an app moving from the
|
|
8
|
+
* React Native SDK gets the same behaviour without restating anything.
|
|
9
|
+
*/
|
|
10
|
+
public data class AdvenueConfig
|
|
11
|
+
@JvmOverloads
|
|
12
|
+
constructor(
|
|
13
|
+
val apiKey: String,
|
|
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. */
|
|
16
|
+
val appVersion: String? = null,
|
|
17
|
+
val requireConsent: Boolean = false,
|
|
18
|
+
val sessionWindowMs: Long = DEFAULT_SESSION_WINDOW_MS,
|
|
19
|
+
val batchSize: Int = 20,
|
|
20
|
+
/** Auto-flush period. Zero disables the timer, which is what tests want and no
|
|
21
|
+
* shipping app does. */
|
|
22
|
+
val flushIntervalMs: Long = 15_000,
|
|
23
|
+
/**
|
|
24
|
+
* Per-key HMAC secret. **Read this before setting it.** The signature provides
|
|
25
|
+
* integrity and replay protection, not authentication: anything shipped inside
|
|
26
|
+
* an app binary can be extracted from it, exactly as it can from a JavaScript
|
|
27
|
+
* bundle. Leaving it null sends unsigned requests, which the server accepts
|
|
28
|
+
* unless the app enforces signatures.
|
|
29
|
+
*/
|
|
30
|
+
val signingSecret: String? = null,
|
|
31
|
+
/** Called when the SDK swallows a best-effort failure. Never receives PII. */
|
|
32
|
+
val onError: (String, Throwable) -> Unit = { _, _ -> },
|
|
33
|
+
/**
|
|
34
|
+
* Overrides the version stamped on every event. Set by a WRAPPER SDK, never
|
|
35
|
+
* by an app.
|
|
36
|
+
*
|
|
37
|
+
* An event stamped with this SDK's own version says the same thing for every
|
|
38
|
+
* install and answers nothing. The useful answer is which wrapper produced it
|
|
39
|
+
* — a React Native or Flutter release pins the native snapshot inside it, so
|
|
40
|
+
* the wrapper's version identifies both. Adjust and AppsFlyer report the
|
|
41
|
+
* wrapper for the same reason.
|
|
42
|
+
*
|
|
43
|
+
* Capped at 32 characters by the ingest schema; a longer value would take the
|
|
44
|
+
* whole batch down with a 400, so it is truncated rather than sent.
|
|
45
|
+
*/
|
|
46
|
+
val sdkVersion: String? = null,
|
|
47
|
+
/**
|
|
48
|
+
* Meta app id from the Meta App Dashboard, enabling the Meta install
|
|
49
|
+
* referrer. Not a secret — the decryption key stays server-side and the
|
|
50
|
+
* device only ever carries ciphertext. Leave it null and no Meta content
|
|
51
|
+
* provider is queried at all.
|
|
52
|
+
*
|
|
53
|
+
* Android only: the Meta install referrer is a content-provider read, and
|
|
54
|
+
* iOS has no equivalent. A field on the Swift config would do nothing.
|
|
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
|
+
init {
|
|
69
|
+
// B5: şemasız endpoint fail-fast — geçersiz şema transportta değil,
|
|
70
|
+
// kurulumda yakalanır.
|
|
71
|
+
require(allowInsecureHttp || endpoint.startsWith("https://")) {
|
|
72
|
+
"Advenue endpoint must use https:// (set allowInsecureHttp only for tests)"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Java-friendly builder. Kotlin's named and default arguments are invisible
|
|
77
|
+
* from Java, and both incumbents are Java-first — a Kotlin SDK that is painful
|
|
78
|
+
* from Java excludes a large share of Android apps.
|
|
79
|
+
*/
|
|
80
|
+
public class Builder(private val apiKey: String) {
|
|
81
|
+
private var config = AdvenueConfig(apiKey)
|
|
82
|
+
|
|
83
|
+
public fun endpoint(value: String): Builder = apply { config = config.copy(endpoint = value) }
|
|
84
|
+
|
|
85
|
+
/** Ignored — see [AdvenueConfig.appVersion]. */
|
|
86
|
+
public fun appVersion(value: String?): Builder = apply {
|
|
87
|
+
config = config.copy(appVersion = value)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public fun requireConsent(value: Boolean): Builder = apply {
|
|
91
|
+
config = config.copy(requireConsent = value)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public fun sessionWindowMs(value: Long): Builder = apply {
|
|
95
|
+
config = config.copy(sessionWindowMs = value)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public fun batchSize(value: Int): Builder = apply { config = config.copy(batchSize = value) }
|
|
99
|
+
|
|
100
|
+
public fun flushIntervalMs(value: Long): Builder = apply {
|
|
101
|
+
config = config.copy(flushIntervalMs = value)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public fun signingSecret(value: String?): Builder = apply {
|
|
105
|
+
config = config.copy(signingSecret = value)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public fun onError(value: (String, Throwable) -> Unit): Builder = apply {
|
|
109
|
+
config = config.copy(onError = value)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public fun sdkVersion(value: String?): Builder = apply {
|
|
113
|
+
config = config.copy(sdkVersion = value)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public fun fbAppId(value: String?): Builder = apply {
|
|
117
|
+
config = config.copy(fbAppId = value)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public fun piiScrubEnabled(value: Boolean): Builder = apply {
|
|
121
|
+
config = config.copy(piiScrubEnabled = value)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public fun allowInsecureHttp(value: Boolean): Builder = apply {
|
|
125
|
+
config = config.copy(allowInsecureHttp = value)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public fun build(): AdvenueConfig = config
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Stamped on every event as `sdkVersion`. An Android library has no runtime
|
|
134
|
+
* access to its own Gradle version without reflection, so this is a constant —
|
|
135
|
+
* and a constant drifts from the release tag unless something checks. CI does,
|
|
136
|
+
* because the first question every field report raises is which build produced
|
|
137
|
+
* the event.
|
|
138
|
+
*/
|
|
139
|
+
public object AdvenueVersion {
|
|
140
|
+
public const val CURRENT: String = "0.1.0"
|
|
141
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package io.advenue.core
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A non-2xx ingest response. [isRetryable] separates a transient failure from a
|
|
5
|
+
* poison payload: retrying a 400 forever would block the queue head, and
|
|
6
|
+
* dropping a 429 would discard events over a throttle.
|
|
7
|
+
*/
|
|
8
|
+
internal class IngestError(public val status: Int) : Exception("advenue ingest failed: $status") {
|
|
9
|
+
public val isRetryable: Boolean
|
|
10
|
+
get() = status == 408 || status == 429 || status >= 500
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The delay armed after the Nth consecutive transient failure.
|
|
15
|
+
*
|
|
16
|
+
* Mirrors sdk-core's `armBackoff`: the cap applies to the exponential term
|
|
17
|
+
* BEFORE the jitter is added, so the true maximum is `capMs * 1.2`, not
|
|
18
|
+
* `capMs`. [random] is injected so the schedule is a pure function.
|
|
19
|
+
*/
|
|
20
|
+
internal fun backoffDelayMs(
|
|
21
|
+
failures: Int,
|
|
22
|
+
baseMs: Double,
|
|
23
|
+
capMs: Double,
|
|
24
|
+
random: Double,
|
|
25
|
+
): Double {
|
|
26
|
+
val exponential = Math.min(baseMs * Math.pow(2.0, (failures - 1).toDouble()), capMs)
|
|
27
|
+
return exponential + exponential * 0.2 * random
|
|
28
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
package io.advenue.core
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The wire envelope, mirroring `clientEventSchema` in
|
|
5
|
+
* `packages/shared/src/events.ts` and `ClientEvent` in the Swift port.
|
|
6
|
+
*
|
|
7
|
+
* Every optional is genuinely optional on the wire: the server uses zod
|
|
8
|
+
* `.optional()`, which accepts a missing key and **rejects** an explicit
|
|
9
|
+
* `null`. [writeCanonicalJson] omits null object entries, which is exactly the
|
|
10
|
+
* behaviour required — a serialiser emitting `null` would 400 the whole batch,
|
|
11
|
+
* every event in it.
|
|
12
|
+
*/
|
|
13
|
+
internal data class ClientEvent(
|
|
14
|
+
public val id: String,
|
|
15
|
+
public val deviceId: String,
|
|
16
|
+
public val type: String,
|
|
17
|
+
public val name: String,
|
|
18
|
+
public val timestamp: String,
|
|
19
|
+
public val platform: String,
|
|
20
|
+
public val installationId: String? = null,
|
|
21
|
+
public val osVersion: String? = null,
|
|
22
|
+
public val appVersion: String? = null,
|
|
23
|
+
public val sdkVersion: String? = null,
|
|
24
|
+
public val network: String? = null,
|
|
25
|
+
public val campaign: String? = null,
|
|
26
|
+
public val country: String? = null,
|
|
27
|
+
public val idfa: String? = null,
|
|
28
|
+
public val gaid: String? = null,
|
|
29
|
+
public val limitAdTracking: Boolean? = null,
|
|
30
|
+
public val vendorId: String? = null,
|
|
31
|
+
public val androidId: String? = null,
|
|
32
|
+
public val customerUserId: String? = null,
|
|
33
|
+
public val appInstanceId: String? = null,
|
|
34
|
+
public val consent: Map<String, Any?>? = null,
|
|
35
|
+
public val properties: Map<String, Any?>? = null,
|
|
36
|
+
public val pushToken: String? = null,
|
|
37
|
+
public val pushProvider: String? = null,
|
|
38
|
+
public val attestationToken: String? = null,
|
|
39
|
+
public val attestationType: String? = null,
|
|
40
|
+
public val attestationChallenge: String? = null,
|
|
41
|
+
public val adservicesToken: String? = null,
|
|
42
|
+
) {
|
|
43
|
+
internal fun toMap(): Map<String, Any?> =
|
|
44
|
+
mapOf(
|
|
45
|
+
"id" to id,
|
|
46
|
+
"deviceId" to deviceId,
|
|
47
|
+
"type" to type,
|
|
48
|
+
"name" to name,
|
|
49
|
+
"timestamp" to timestamp,
|
|
50
|
+
"platform" to platform,
|
|
51
|
+
"installationId" to installationId,
|
|
52
|
+
"osVersion" to osVersion,
|
|
53
|
+
"appVersion" to appVersion,
|
|
54
|
+
"sdkVersion" to sdkVersion,
|
|
55
|
+
"network" to network,
|
|
56
|
+
"campaign" to campaign,
|
|
57
|
+
"country" to country,
|
|
58
|
+
"idfa" to idfa,
|
|
59
|
+
"gaid" to gaid,
|
|
60
|
+
"limitAdTracking" to limitAdTracking,
|
|
61
|
+
"vendorId" to vendorId,
|
|
62
|
+
"androidId" to androidId,
|
|
63
|
+
"customerUserId" to customerUserId,
|
|
64
|
+
"appInstanceId" to appInstanceId,
|
|
65
|
+
"consent" to consent,
|
|
66
|
+
"properties" to properties,
|
|
67
|
+
"pushToken" to pushToken,
|
|
68
|
+
"pushProvider" to pushProvider,
|
|
69
|
+
"attestationToken" to attestationToken,
|
|
70
|
+
"attestationType" to attestationType,
|
|
71
|
+
"attestationChallenge" to attestationChallenge,
|
|
72
|
+
"adservicesToken" to adservicesToken,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
/** The exact bytes this event puts on the wire. */
|
|
76
|
+
public fun encodeCanonical(): String = writeCanonicalJson(toMap())
|
|
77
|
+
|
|
78
|
+
public companion object {
|
|
79
|
+
/**
|
|
80
|
+
* The queue's reload path — and `properties` is restored with everything
|
|
81
|
+
* else, which it was not.
|
|
82
|
+
*
|
|
83
|
+
* It used to be dropped, on the premise that "a reloaded event is only ever
|
|
84
|
+
* re-sent as the bytes already written". The bytes are not re-sent: the
|
|
85
|
+
* object is. `EventQueue` assigns `events = load(store)` and `peek()`
|
|
86
|
+
* returns from `events`, so a reloaded event is exactly what the transport
|
|
87
|
+
* re-encodes. An app killed before flushing therefore sent every buffered
|
|
88
|
+
* event with no payload — no revenue, no currency, no sku. The writer had
|
|
89
|
+
* always written them; only this refused to read them.
|
|
90
|
+
*
|
|
91
|
+
* A queue conformance vector now pins it in both SDKs at once.
|
|
92
|
+
*/
|
|
93
|
+
internal fun fromMap(map: Map<*, *>): ClientEvent? {
|
|
94
|
+
fun str(key: String): String? = map[key] as? String
|
|
95
|
+
val id = str("id") ?: return null
|
|
96
|
+
val deviceId = str("deviceId") ?: return null
|
|
97
|
+
val type = str("type") ?: return null
|
|
98
|
+
val name = str("name") ?: return null
|
|
99
|
+
val timestamp = str("timestamp") ?: return null
|
|
100
|
+
val platform = str("platform") ?: return null
|
|
101
|
+
return ClientEvent(
|
|
102
|
+
id = id,
|
|
103
|
+
deviceId = deviceId,
|
|
104
|
+
type = type,
|
|
105
|
+
name = name,
|
|
106
|
+
timestamp = timestamp,
|
|
107
|
+
platform = platform,
|
|
108
|
+
installationId = str("installationId"),
|
|
109
|
+
osVersion = str("osVersion"),
|
|
110
|
+
appVersion = str("appVersion"),
|
|
111
|
+
sdkVersion = str("sdkVersion"),
|
|
112
|
+
network = str("network"),
|
|
113
|
+
campaign = str("campaign"),
|
|
114
|
+
country = str("country"),
|
|
115
|
+
idfa = str("idfa"),
|
|
116
|
+
gaid = str("gaid"),
|
|
117
|
+
limitAdTracking = map["limitAdTracking"] as? Boolean,
|
|
118
|
+
vendorId = str("vendorId"),
|
|
119
|
+
androidId = str("androidId"),
|
|
120
|
+
customerUserId = str("customerUserId"),
|
|
121
|
+
appInstanceId = str("appInstanceId"),
|
|
122
|
+
// parseJson already yields the nested Map/List shape this field holds,
|
|
123
|
+
// so there is nothing to convert — only to stop discarding.
|
|
124
|
+
properties = (map["properties"] as? Map<*, *>)?.entries?.associate {
|
|
125
|
+
(it.key as String) to it.value
|
|
126
|
+
},
|
|
127
|
+
pushToken = str("pushToken"),
|
|
128
|
+
pushProvider = str("pushProvider"),
|
|
129
|
+
attestationToken = str("attestationToken"),
|
|
130
|
+
attestationType = str("attestationType"),
|
|
131
|
+
attestationChallenge = str("attestationChallenge"),
|
|
132
|
+
adservicesToken = str("adservicesToken"),
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
package io.advenue.core
|
|
2
|
+
|
|
3
|
+
import java.util.concurrent.LinkedBlockingQueue
|
|
4
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
5
|
+
|
|
6
|
+
/** A command submitted to the engine through the ordered ingress. */
|
|
7
|
+
internal sealed class Command {
|
|
8
|
+
public data class Track(
|
|
9
|
+
val name: String,
|
|
10
|
+
val properties: Map<String, Any?>?,
|
|
11
|
+
val type: String,
|
|
12
|
+
) : Command()
|
|
13
|
+
|
|
14
|
+
public data class SetConsent(val granted: Boolean) : Command()
|
|
15
|
+
|
|
16
|
+
public data class SetUserId(val id: String?) : Command()
|
|
17
|
+
|
|
18
|
+
public data class SetIdentity(
|
|
19
|
+
val gaid: String?,
|
|
20
|
+
val androidId: String?,
|
|
21
|
+
val appInstanceId: String?,
|
|
22
|
+
/** B2: reklam kimliği rıza bayrağı — toplanıp düşürülüyordu. */
|
|
23
|
+
val limitAdTracking: Boolean? = null,
|
|
24
|
+
) : Command()
|
|
25
|
+
|
|
26
|
+
internal data class SetAppInstanceId(val id: String?) : Command()
|
|
27
|
+
|
|
28
|
+
public data class TrackInstall(val properties: Map<String, Any?>?) : Command()
|
|
29
|
+
|
|
30
|
+
internal data class SetConsentData(val consent: Consent?) : Command()
|
|
31
|
+
|
|
32
|
+
internal data class SetPushToken(val token: String?, val provider: String?) : Command()
|
|
33
|
+
|
|
34
|
+
public object ForgetMe : Command()
|
|
35
|
+
|
|
36
|
+
public object Foreground : Command()
|
|
37
|
+
|
|
38
|
+
public object Background : Command()
|
|
39
|
+
|
|
40
|
+
public object Flush : Command()
|
|
41
|
+
|
|
42
|
+
/** Posted back by the io thread; carries what the network said. */
|
|
43
|
+
internal data class BatchOutcome(val error: Throwable?) : Command()
|
|
44
|
+
|
|
45
|
+
internal data class SingleOutcome(val event: ClientEvent, val error: Throwable?) : Command()
|
|
46
|
+
|
|
47
|
+
/** Deferred work hopped back onto the engine's thread — see TimerScheduler. */
|
|
48
|
+
internal data class RunScheduled(val work: () -> Unit) : Command()
|
|
49
|
+
|
|
50
|
+
internal object Shutdown : Command()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The ordered ingress: a synchronous, non-blocking [submit] feeding one
|
|
55
|
+
* consumer thread.
|
|
56
|
+
*
|
|
57
|
+
* Why a queue and not a thread per call: a thread pool does not preserve
|
|
58
|
+
* submission order, so an event could overtake its own `session_start` and be
|
|
59
|
+
* attributed to the wrong session. Adding to a `LinkedBlockingQueue` is
|
|
60
|
+
* non-blocking and ordered, which is exactly the property `AsyncStream.yield`
|
|
61
|
+
* gives the Swift port.
|
|
62
|
+
*
|
|
63
|
+
* **Two threads, on purpose.** `advenue-core` owns all state; `advenue-io`
|
|
64
|
+
* performs the transport call. In Swift, `flush` awaited inside an actor and
|
|
65
|
+
* reentrancy let other work proceed. A single blocking thread has no such
|
|
66
|
+
* property: a 15-second HTTP timeout would block every lifecycle command queued
|
|
67
|
+
* behind it.
|
|
68
|
+
*/
|
|
69
|
+
internal class CommandPipe(
|
|
70
|
+
private val engine: AdvenueEngine,
|
|
71
|
+
private val onError: (String, Throwable) -> Unit = { _, _ -> },
|
|
72
|
+
) {
|
|
73
|
+
/** A send the io thread must perform, and the events it covers. */
|
|
74
|
+
private class Send(val events: List<ClientEvent>, val single: ClientEvent?)
|
|
75
|
+
|
|
76
|
+
private val commands = LinkedBlockingQueue<Command>()
|
|
77
|
+
private val sends = LinkedBlockingQueue<Send>()
|
|
78
|
+
private val running = AtomicBoolean(true)
|
|
79
|
+
|
|
80
|
+
private val core =
|
|
81
|
+
thread("advenue-core") {
|
|
82
|
+
while (running.get()) {
|
|
83
|
+
val command = commands.take()
|
|
84
|
+
if (command === Command.Shutdown) break
|
|
85
|
+
// Each command is handled inside its own try/catch. An uncaught throw
|
|
86
|
+
// would END this thread: the queue would stop draining, submit() would
|
|
87
|
+
// keep accepting, nothing would send, nothing would error, and the app
|
|
88
|
+
// would not crash — attribution would simply stop. Silent total failure
|
|
89
|
+
// is the worst mode available, so this loop must be unkillable.
|
|
90
|
+
try {
|
|
91
|
+
handle(command)
|
|
92
|
+
} catch (error: Throwable) {
|
|
93
|
+
onError("engine.command", error)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Performs network calls and nothing else. It never touches engine state: the
|
|
100
|
+
* decision of what to send and what to do with the answer stays on the core
|
|
101
|
+
* thread, which is what keeps the queue confined to one thread while still
|
|
102
|
+
* letting a 15-second request run without blocking anything.
|
|
103
|
+
*/
|
|
104
|
+
private val io =
|
|
105
|
+
thread("advenue-io") {
|
|
106
|
+
while (running.get()) {
|
|
107
|
+
val send = sends.take()
|
|
108
|
+
if (!running.get()) break
|
|
109
|
+
val error = engine.send(send.events)
|
|
110
|
+
commands.put(
|
|
111
|
+
if (send.single != null) {
|
|
112
|
+
Command.SingleOutcome(send.single, error)
|
|
113
|
+
} else {
|
|
114
|
+
Command.BatchOutcome(error)
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private fun handle(command: Command) {
|
|
121
|
+
when (command) {
|
|
122
|
+
is Command.Track -> engine.track(command.name, command.properties, command.type)
|
|
123
|
+
is Command.SetConsent -> engine.setConsent(command.granted)
|
|
124
|
+
is Command.SetUserId -> engine.setUserId(command.id)
|
|
125
|
+
is Command.SetIdentity ->
|
|
126
|
+
engine.setIdentity(
|
|
127
|
+
command.gaid,
|
|
128
|
+
command.androidId,
|
|
129
|
+
command.appInstanceId,
|
|
130
|
+
limitAdTracking = command.limitAdTracking,
|
|
131
|
+
)
|
|
132
|
+
is Command.SetAppInstanceId -> engine.setAppInstanceId(command.id)
|
|
133
|
+
is Command.TrackInstall -> engine.trackInstall(command.properties)
|
|
134
|
+
is Command.SetConsentData -> engine.setConsentData(command.consent)
|
|
135
|
+
is Command.SetPushToken -> engine.setPushToken(command.token, command.provider)
|
|
136
|
+
Command.ForgetMe -> engine.forgetMe()
|
|
137
|
+
Command.Foreground -> engine.notifyForeground()
|
|
138
|
+
Command.Background -> engine.notifyBackground()
|
|
139
|
+
Command.Flush -> engine.beginFlush()?.let { sends.put(Send(it, single = null)) }
|
|
140
|
+
is Command.BatchOutcome ->
|
|
141
|
+
engine.onBatchOutcome(command.error)?.let { sends.put(Send(listOf(it), single = it)) }
|
|
142
|
+
is Command.SingleOutcome ->
|
|
143
|
+
engine.onSingleOutcome(command.event, command.error)?.let {
|
|
144
|
+
sends.put(Send(listOf(it), single = it))
|
|
145
|
+
}
|
|
146
|
+
is Command.RunScheduled -> command.work()
|
|
147
|
+
Command.Shutdown -> Unit
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Synchronous, non-blocking and **ordered** — the entire reason the ingress
|
|
153
|
+
* is a queue.
|
|
154
|
+
*/
|
|
155
|
+
public fun submit(command: Command) {
|
|
156
|
+
if (!running.get()) return
|
|
157
|
+
commands.put(command)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public fun shutdown() {
|
|
161
|
+
if (!running.compareAndSet(true, false)) return
|
|
162
|
+
commands.put(Command.Shutdown)
|
|
163
|
+
sends.put(Send(emptyList(), single = null))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Test surface: block until both queues are empty or the deadline passes. */
|
|
167
|
+
internal fun drain(timeoutMs: Long = 2_000) {
|
|
168
|
+
val deadline = System.currentTimeMillis() + timeoutMs
|
|
169
|
+
while (
|
|
170
|
+
(commands.isNotEmpty() || sends.isNotEmpty()) && System.currentTimeMillis() < deadline
|
|
171
|
+
) {
|
|
172
|
+
Thread.sleep(5)
|
|
173
|
+
}
|
|
174
|
+
Thread.sleep(20)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private fun thread(name: String, body: () -> Unit): Thread =
|
|
178
|
+
Thread(
|
|
179
|
+
{
|
|
180
|
+
try {
|
|
181
|
+
body()
|
|
182
|
+
} catch (_: InterruptedException) {
|
|
183
|
+
// shutdown
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
name,
|
|
187
|
+
)
|
|
188
|
+
.apply {
|
|
189
|
+
// Daemon: the SDK must never hold a process open.
|
|
190
|
+
isDaemon = true
|
|
191
|
+
start()
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
package io.advenue.core
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Granular ad-platform consent (Google DMA), forwarded to Google and Meta by
|
|
5
|
+
* server-side postbacks as `gdpr_applies`, `ad_user_data`, `ad_personalization`
|
|
6
|
+
* and `ad_storage`.
|
|
7
|
+
*
|
|
8
|
+
* Three of the four fields are optional, and that is the point rather than
|
|
9
|
+
* laxity: **"not stated" is not "denied".** A CMP that has not asked about ad
|
|
10
|
+
* storage yet must not have `false` invented on its behalf, because downstream
|
|
11
|
+
* that is a recorded refusal. The canonical writer omits nulls, which is what
|
|
12
|
+
* keeps the distinction on the wire.
|
|
13
|
+
*/
|
|
14
|
+
// Public because the facade both takes and returns it: an app that calls
|
|
15
|
+
// setConsentData must be able to read back what it set, and the iOS SDK's
|
|
16
|
+
// Consent is public for the same reason.
|
|
17
|
+
public data class Consent(
|
|
18
|
+
val isUserSubjectToGDPR: Boolean,
|
|
19
|
+
val hasConsentForDataUsage: Boolean? = null,
|
|
20
|
+
val hasConsentForAdsPersonalization: Boolean? = null,
|
|
21
|
+
val hasConsentForAdStorage: Boolean? = null,
|
|
22
|
+
) {
|
|
23
|
+
internal fun toMap(): Map<String, Any?> =
|
|
24
|
+
mapOf(
|
|
25
|
+
"isUserSubjectToGDPR" to isUserSubjectToGDPR,
|
|
26
|
+
"hasConsentForDataUsage" to hasConsentForDataUsage,
|
|
27
|
+
"hasConsentForAdsPersonalization" to hasConsentForAdsPersonalization,
|
|
28
|
+
"hasConsentForAdStorage" to hasConsentForAdStorage,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
internal companion object {
|
|
32
|
+
internal fun fromMap(map: Map<*, *>): Consent? {
|
|
33
|
+
val gdpr = map["isUserSubjectToGDPR"] as? Boolean ?: return null
|
|
34
|
+
return Consent(
|
|
35
|
+
isUserSubjectToGDPR = gdpr,
|
|
36
|
+
hasConsentForDataUsage = map["hasConsentForDataUsage"] as? Boolean,
|
|
37
|
+
hasConsentForAdsPersonalization = map["hasConsentForAdsPersonalization"] as? Boolean,
|
|
38
|
+
hasConsentForAdStorage = map["hasConsentForAdStorage"] as? Boolean,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validates a push token against the server's bound and charset.
|
|
46
|
+
*
|
|
47
|
+
* Rejecting here costs one dropped registration; letting it through costs every
|
|
48
|
+
* event in the batch it would have ridden in, because ingest parses a batch as
|
|
49
|
+
* a whole.
|
|
50
|
+
*/
|
|
51
|
+
internal fun isValidPushToken(token: String): Boolean =
|
|
52
|
+
token.isNotEmpty() && token.length <= 512 && token.all { it.isLetterOrDigit() || it in "_:.-" }
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
package io.advenue.core
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Synchronous key/value storage. On Android this is a `SharedPreferences` file;
|
|
5
|
+
* in tests it is a map. Mirrors `StorageAdapter` in sdk-core and `KeyValueStore`
|
|
6
|
+
* in the Swift port.
|
|
7
|
+
*/
|
|
8
|
+
internal interface KeyValueStore {
|
|
9
|
+
public fun getString(key: String): String?
|
|
10
|
+
|
|
11
|
+
public fun setString(key: String, value: String)
|
|
12
|
+
|
|
13
|
+
public fun remove(key: String)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Milliseconds since the Unix epoch. Injected so vectors are deterministic. */
|
|
17
|
+
internal interface Clock {
|
|
18
|
+
public fun nowMs(): Long
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Opaque handle returned by [Scheduler.schedule]. */
|
|
22
|
+
internal typealias CancelToken = Long
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Deferred execution, injected so the queue's persist debounce is observable
|
|
26
|
+
* without waiting for it.
|
|
27
|
+
*
|
|
28
|
+
* **Isolation contract.** The scheduled work touches state confined to the
|
|
29
|
+
* engine's thread, so an implementation must run it on that same thread. The
|
|
30
|
+
* production scheduler satisfies this by posting back onto the command pipe;
|
|
31
|
+
* the test scheduler satisfies it by running inline.
|
|
32
|
+
*/
|
|
33
|
+
internal interface Scheduler {
|
|
34
|
+
public fun schedule(afterMs: Int, work: () -> Unit): CancelToken
|
|
35
|
+
|
|
36
|
+
public fun cancel(token: CancelToken)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Event, session and device id source. Injected for the same reason. */
|
|
40
|
+
internal interface UuidSource {
|
|
41
|
+
public fun next(): String
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* HMAC-SHA256. An interface rather than a bare function so tests can pin the
|
|
46
|
+
* digest, but unlike the Swift port there is only ever one implementation:
|
|
47
|
+
* `javax.crypto` is in every JDK and every Android runtime, so the core can own
|
|
48
|
+
* it without taking a dependency.
|
|
49
|
+
*/
|
|
50
|
+
internal interface Signer {
|
|
51
|
+
/** Lowercase hex digest of [message] under [secret], both UTF-8. */
|
|
52
|
+
public fun hmacSha256Hex(secret: String, message: String): String
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* HMAC over `"{timestamp}.{body}"` — the Stripe-style construction in
|
|
57
|
+
* `packages/signing`. Binding the timestamp is what makes replay detectable, so
|
|
58
|
+
* the separator and the order are part of the contract, not a detail.
|
|
59
|
+
*/
|
|
60
|
+
internal fun signRequest(
|
|
61
|
+
signer: Signer,
|
|
62
|
+
secret: String,
|
|
63
|
+
timestamp: String,
|
|
64
|
+
body: String,
|
|
65
|
+
): String = signer.hmacSha256Hex(secret, "$timestamp.$body")
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* What the core needs from a transport, and no more.
|
|
69
|
+
*
|
|
70
|
+
* Declared in the core rather than the platform layer because the flush loop is
|
|
71
|
+
* behaviour the `flush/` conformance vectors bind, and a rule the vectors cannot
|
|
72
|
+
* reach is a rule three implementations can each interpret differently.
|
|
73
|
+
*/
|
|
74
|
+
internal interface EventTransport {
|
|
75
|
+
/**
|
|
76
|
+
* Delivers one batch. Throws [IngestError] on a non-2xx response, and on a
|
|
77
|
+
* transport failure, which maps to 408.
|
|
78
|
+
*/
|
|
79
|
+
public fun send(events: List<ClientEvent>)
|
|
80
|
+
}
|