@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
package/dist/index.d.ts
CHANGED
|
@@ -54,9 +54,12 @@ interface TrackOptions {
|
|
|
54
54
|
interface AdvenueConfig {
|
|
55
55
|
apiKey: string;
|
|
56
56
|
endpoint?: string;
|
|
57
|
-
|
|
57
|
+
/**
|
|
58
|
+
* @deprecated Ignored: the native SDK resolves the app version from the
|
|
59
|
+
* package/bundle. Passing it reports `config.ignored:appVersion` via
|
|
60
|
+
* `onError`. Removed in the next major.
|
|
61
|
+
*/
|
|
58
62
|
appVersion?: string;
|
|
59
|
-
osVersion?: string;
|
|
60
63
|
/** Gates every event on `setTrackingConsent(true)`. */
|
|
61
64
|
requireConsent?: boolean;
|
|
62
65
|
/** Events per request. */
|
|
@@ -69,9 +72,21 @@ interface AdvenueConfig {
|
|
|
69
72
|
* extracted from it.
|
|
70
73
|
*/
|
|
71
74
|
signingSecret?: string;
|
|
72
|
-
|
|
75
|
+
/**
|
|
76
|
+
* **Test only.** Allows an `http://` endpoint — a local self-hosted ingest,
|
|
77
|
+
* or a test server — which the native SDKs otherwise refuse at initialize.
|
|
78
|
+
* Never set it in a production build: everything, the API key included,
|
|
79
|
+
* then travels unencrypted.
|
|
80
|
+
*/
|
|
81
|
+
allowInsecureHttp?: boolean;
|
|
73
82
|
/** Called when the SDK swallows a best-effort failure. Never receives PII. */
|
|
74
83
|
onError?: (context: string, cause: unknown) => void;
|
|
84
|
+
/**
|
|
85
|
+
* Development aid: the native SDKs log their start, every swallowed failure
|
|
86
|
+
* and each sent batch to Xcode's console / logcat, and every failure is also
|
|
87
|
+
* written to the JS console as `[Advenue] <context>`. Never logs an
|
|
88
|
+
* identifier or a payload. Leave off in production; use `onError`.
|
|
89
|
+
*/
|
|
75
90
|
debug?: boolean;
|
|
76
91
|
}
|
|
77
92
|
|
|
@@ -80,7 +95,6 @@ type TrackingAuthorizationStatus = 'authorized' | 'denied' | 'restricted' | 'not
|
|
|
80
95
|
interface BridgeInitOptions {
|
|
81
96
|
apiKey: string;
|
|
82
97
|
endpoint?: string;
|
|
83
|
-
appVersion?: string;
|
|
84
98
|
requireConsent: boolean;
|
|
85
99
|
batchSize?: number;
|
|
86
100
|
flushIntervalMs?: number;
|
package/dist/index.js
CHANGED
|
@@ -54,7 +54,7 @@ var require_native_require = __commonJS({
|
|
|
54
54
|
// package.json
|
|
55
55
|
var package_default = {
|
|
56
56
|
name: "@advenue/react-native",
|
|
57
|
-
version: "1.
|
|
57
|
+
version: "1.1.0",
|
|
58
58
|
description: "Advenue MMP React Native SDK \u2014 attribution, install tracking, and deep links (bundled iOS/Android Expo native modules)",
|
|
59
59
|
type: "module",
|
|
60
60
|
license: "MIT",
|
|
@@ -203,11 +203,18 @@ function getLinking() {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
// src/index.ts
|
|
206
|
-
var IGNORED_OPTIONS = [
|
|
206
|
+
var IGNORED_OPTIONS = [
|
|
207
|
+
"mmkv",
|
|
208
|
+
"secureStore",
|
|
209
|
+
"sdkConfigFetcher",
|
|
210
|
+
"tcfDataCollection",
|
|
211
|
+
"appVersion"
|
|
212
|
+
];
|
|
207
213
|
var started = false;
|
|
208
214
|
var deepLinkSub = null;
|
|
209
215
|
var errorSub = null;
|
|
210
216
|
var onErrorHook;
|
|
217
|
+
var debugLogging = false;
|
|
211
218
|
var consentCache = false;
|
|
212
219
|
function bridge() {
|
|
213
220
|
return { ios: getIosNative(), android: getAndroidNative() };
|
|
@@ -232,6 +239,7 @@ function report(context, cause) {
|
|
|
232
239
|
onErrorHook?.(context, cause);
|
|
233
240
|
} catch {
|
|
234
241
|
}
|
|
242
|
+
if (debugLogging) console.warn(`[Advenue] ${context}`, cause);
|
|
235
243
|
}
|
|
236
244
|
function requireStarted() {
|
|
237
245
|
if (started) return true;
|
|
@@ -245,6 +253,7 @@ var Advenue = {
|
|
|
245
253
|
*/
|
|
246
254
|
async initialize(config) {
|
|
247
255
|
onErrorHook = config.onError;
|
|
256
|
+
debugLogging = config.debug === true;
|
|
248
257
|
for (const option of IGNORED_OPTIONS) {
|
|
249
258
|
if (config[option] !== void 0) {
|
|
250
259
|
report(
|
|
@@ -257,14 +266,17 @@ var Advenue = {
|
|
|
257
266
|
call("bridgeInitialize", {
|
|
258
267
|
apiKey: config.apiKey,
|
|
259
268
|
endpoint: config.endpoint,
|
|
260
|
-
appVersion: config.appVersion,
|
|
261
269
|
requireConsent: config.requireConsent ?? false,
|
|
262
270
|
batchSize: config.batchSize,
|
|
263
271
|
flushIntervalMs: config.flushIntervalMs,
|
|
264
272
|
signingSecret: config.signingSecret,
|
|
265
273
|
fbAppId: config.fbAppId,
|
|
266
274
|
autoTrackInstall: config.autoTrackInstall !== false,
|
|
267
|
-
|
|
275
|
+
// Names the wrapper, like Flutter's `advenue_sdk/…`: a bare version reads
|
|
276
|
+
// like the native SDK's own stamp.
|
|
277
|
+
sdkVersion: `react-native/${package_default.version}`,
|
|
278
|
+
debug: config.debug === true,
|
|
279
|
+
allowInsecureHttp: config.allowInsecureHttp === true
|
|
268
280
|
});
|
|
269
281
|
started = true;
|
|
270
282
|
const { ios: iosForErrors, android: androidForErrors } = bridge();
|
|
@@ -37,7 +37,6 @@ public class AdvenueIosModule: Module {
|
|
|
37
37
|
var config = AdvenueConfig(
|
|
38
38
|
apiKey: app,
|
|
39
39
|
endpoint: (options["endpoint"] as? String) ?? DEFAULT_ENDPOINT,
|
|
40
|
-
appVersion: options["appVersion"] as? String,
|
|
41
40
|
requireConsent: (options["requireConsent"] as? Bool) ?? false,
|
|
42
41
|
signingSecret: options["signingSecret"] as? String,
|
|
43
42
|
onError: { [weak self] context, error in
|
|
@@ -45,11 +44,14 @@ public class AdvenueIosModule: Module {
|
|
|
45
44
|
},
|
|
46
45
|
// The wrapper's version, not this SDK's: a React Native release pins
|
|
47
46
|
// the native snapshot inside it, so its version identifies both.
|
|
48
|
-
sdkVersion: options["sdkVersion"] as? String
|
|
47
|
+
sdkVersion: options["sdkVersion"] as? String,
|
|
48
|
+
// Test only; decided at init, where the https precondition runs.
|
|
49
|
+
allowInsecureHttp: (options["allowInsecureHttp"] as? Bool) ?? false)
|
|
49
50
|
if let flushInterval = options["flushIntervalMs"] as? Int {
|
|
50
51
|
config.flushIntervalMs = flushInterval
|
|
51
52
|
}
|
|
52
53
|
if let batchSize = options["batchSize"] as? Int { config.batchSize = batchSize }
|
|
54
|
+
config.debug = (options["debug"] as? Bool) ?? false
|
|
53
55
|
Advenue.initialize(config)
|
|
54
56
|
// Firebase App Instance ID, when the host app already ships Firebase.
|
|
55
57
|
// `AdvenueFirebase` guards its import with `#if canImport`, which is
|
|
@@ -129,14 +129,11 @@ public enum Advenue {
|
|
|
129
129
|
/// is empty or a backoff window is open.
|
|
130
130
|
public static func flush() { state.submit(.flush) }
|
|
131
131
|
|
|
132
|
-
public static func notifyForeground() { state.
|
|
132
|
+
public static func notifyForeground() { state.notifyForeground() }
|
|
133
133
|
|
|
134
134
|
/// Backgrounding both closes the session and flushes: a batch stranded at the
|
|
135
135
|
/// moment the app leaves the foreground may not be sent for hours.
|
|
136
|
-
public static func notifyBackground() {
|
|
137
|
-
state.submit(.background)
|
|
138
|
-
state.submit(.flush)
|
|
139
|
-
}
|
|
136
|
+
public static func notifyBackground() { state.notifyBackground() }
|
|
140
137
|
|
|
141
138
|
/// Presents the ATT prompt. The app decides when; the SDK never prompts on
|
|
142
139
|
/// its own.
|
|
@@ -167,10 +164,14 @@ final class AcceptedAppId: @unchecked Sendable {
|
|
|
167
164
|
return value
|
|
168
165
|
}
|
|
169
166
|
|
|
170
|
-
|
|
167
|
+
/// True the first time this app id is seen.
|
|
168
|
+
@discardableResult
|
|
169
|
+
func set(_ appId: String) -> Bool {
|
|
171
170
|
lock.lock()
|
|
171
|
+
defer { lock.unlock() }
|
|
172
|
+
let changed = value != appId
|
|
172
173
|
value = appId
|
|
173
|
-
|
|
174
|
+
return changed
|
|
174
175
|
}
|
|
175
176
|
}
|
|
176
177
|
|
|
@@ -208,12 +209,34 @@ final class FacadeState: @unchecked Sendable {
|
|
|
208
209
|
_ config: AdvenueConfig,
|
|
209
210
|
transport: (any EventTransport)? = nil,
|
|
210
211
|
sources: EnrichmentSources? = nil,
|
|
211
|
-
skan skanReporter: (any SkanReporter)? = nil
|
|
212
|
+
skan skanReporter: (any SkanReporter)? = nil,
|
|
213
|
+
appVersion readVersion: () -> String? = readAppVersion,
|
|
214
|
+
launchedInBackground: () -> Bool = readLaunchedInBackground,
|
|
215
|
+
logSink: (any AdvenueLogSink)? = nil
|
|
212
216
|
) {
|
|
213
217
|
// Replace-and-shut-down, never add.
|
|
214
218
|
stop()
|
|
215
219
|
|
|
216
|
-
|
|
220
|
+
// `debug`: every swallowed failure goes through onError, so logging there
|
|
221
|
+
// covers all of them — wrapped once, before anything below captures it.
|
|
222
|
+
let (config, log) = withDebugLogging(config, sink: logSink)
|
|
223
|
+
|
|
224
|
+
// The SDK resolves the app version itself (readAppVersion); a config value
|
|
225
|
+
// is ignored — and said so, rather than looking like it took effect.
|
|
226
|
+
if config.appVersion != nil {
|
|
227
|
+
config.onError("config.ignored:appVersion", AdvenueSetupError.appVersionIgnored)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// B4: kuyruk blob'u Caches dosyalarına (yedek dışı); UserDefaults'ta
|
|
231
|
+
// kalan eski blob ilk okumada migrate edilir. Caches yoksa UserDefaults
|
|
232
|
+
// geri dönüşüdür — yedek kapsamı açılır ama SDK çalışır (fail-open, loglanır).
|
|
233
|
+
let store: any KeyValueStore
|
|
234
|
+
if let files = CacheFileStore.caches() {
|
|
235
|
+
store = CompositeStore(files: files)
|
|
236
|
+
} else {
|
|
237
|
+
store = UserDefaultsStore()
|
|
238
|
+
config.onError("store.cache_unavailable", AdvenueSetupError.cacheUnavailable)
|
|
239
|
+
}
|
|
217
240
|
let secure = KeychainStore()
|
|
218
241
|
let uuid = SystemUUIDs()
|
|
219
242
|
|
|
@@ -222,7 +245,7 @@ final class FacadeState: @unchecked Sendable {
|
|
|
222
245
|
// Deferred: the Keychain could not be read. Nothing is minted and
|
|
223
246
|
// nothing starts, so no event carries an invented identifier. The next
|
|
224
247
|
// launch after first unlock resolves it.
|
|
225
|
-
config.onError("identity.deferred",
|
|
248
|
+
config.onError("identity.deferred", AdvenueSetupError.identityDeferred)
|
|
226
249
|
return
|
|
227
250
|
}
|
|
228
251
|
|
|
@@ -233,19 +256,24 @@ final class FacadeState: @unchecked Sendable {
|
|
|
233
256
|
osVersion = nil
|
|
234
257
|
#endif
|
|
235
258
|
|
|
236
|
-
let
|
|
259
|
+
let baseTransport =
|
|
237
260
|
transport
|
|
238
261
|
?? HttpTransport(
|
|
239
262
|
endpoint: config.endpoint, apiKey: config.apiKey,
|
|
240
263
|
signingSecret: config.signingSecret,
|
|
241
264
|
// Diagnostics only: it runs after the batch is already accepted, so
|
|
242
265
|
// nothing it does can turn a successful ingest into a failure.
|
|
243
|
-
onAccepted: { [acceptedAppId] appId in
|
|
266
|
+
onAccepted: { [acceptedAppId] appId in
|
|
267
|
+
// A key pasted from the wrong app is otherwise silent.
|
|
268
|
+
if acceptedAppId.set(appId) { log("[Advenue] ingesting into app \(appId)") }
|
|
269
|
+
})
|
|
270
|
+
let eventTransport: any EventTransport =
|
|
271
|
+
config.debug ? LoggingTransport(inner: baseTransport, log: log) : baseTransport
|
|
244
272
|
|
|
245
273
|
let engine = AdvenueEngine(
|
|
246
274
|
config: EngineConfig(
|
|
247
275
|
apiKey: config.apiKey, platform: "ios", deviceId: deviceId,
|
|
248
|
-
installationId: installationId, appVersion:
|
|
276
|
+
installationId: installationId, appVersion: readVersion(),
|
|
249
277
|
osVersion: osVersion,
|
|
250
278
|
sdkVersion: String((config.sdkVersion ?? AdvenueVersion.current).prefix(32)),
|
|
251
279
|
requireConsent: config.requireConsent, sessionWindowMs: config.sessionWindowMs,
|
|
@@ -312,13 +340,32 @@ final class FacadeState: @unchecked Sendable {
|
|
|
312
340
|
pendingDeepLinks = []
|
|
313
341
|
lock.unlock()
|
|
314
342
|
|
|
343
|
+
// Before anything else is recorded: a launch that still owes its install
|
|
344
|
+
// stamps it now and holds every flush until the install is enqueued, so
|
|
345
|
+
// the server never sees this launch's session or events ahead of it. The
|
|
346
|
+
// hold outlives the enrichment deadline by a margin and then lapses.
|
|
347
|
+
pipe.submit(.beginInstall(holdMs: INSTALL_WINDOW_MS + INSTALL_HOLD_MARGIN_MS))
|
|
348
|
+
|
|
315
349
|
// Replayed in arrival order, before the first session, so a deferred deep
|
|
316
350
|
// link is attributed to the launch it belongs to.
|
|
317
351
|
for url in buffered { send(url) }
|
|
318
|
-
|
|
352
|
+
// A launch into the background (fetch, silent push, location) is not a
|
|
353
|
+
// session: the first didBecomeActive opens it instead. iOS posts no
|
|
354
|
+
// didEnterBackground for an app that never left the background, so a
|
|
355
|
+
// tracker seeded as foregrounded would swallow the user's real open.
|
|
356
|
+
let inForeground = !launchedInBackground()
|
|
357
|
+
if inForeground { pipe.submit(.foreground) }
|
|
319
358
|
// Seeded to match: the line above IS this launch's foreground, so the
|
|
320
359
|
// activation notification that follows must not open a second session.
|
|
321
|
-
observeLifecycle(seededInForeground:
|
|
360
|
+
observeLifecycle(seededInForeground: inForeground)
|
|
361
|
+
|
|
362
|
+
log(
|
|
363
|
+
"[Advenue] initialized — endpoint \(config.endpoint), "
|
|
364
|
+
+ "sdk \(config.sdkVersion ?? AdvenueVersion.current), "
|
|
365
|
+
// Not "background launch": a scene-based (SwiftUI) app is still
|
|
366
|
+
// reported in the background during didFinishLaunching even when the
|
|
367
|
+
// user opened it. Either way the session opens on activation.
|
|
368
|
+
+ (inForeground ? "launched active" : "not active yet, the session opens on activation"))
|
|
322
369
|
|
|
323
370
|
if config.flushIntervalMs > 0 {
|
|
324
371
|
let timer = DispatchSource.makeTimerSource(queue: .global(qos: .utility))
|
|
@@ -357,7 +404,8 @@ final class FacadeState: @unchecked Sendable {
|
|
|
357
404
|
self?.submit(
|
|
358
405
|
.setIdentity(
|
|
359
406
|
idfa: enrichment.idfa, vendorId: enrichment.vendorId,
|
|
360
|
-
appInstanceId: enrichment.appInstanceId
|
|
407
|
+
appInstanceId: enrichment.appInstanceId,
|
|
408
|
+
limitAdTracking: enrichment.limitAdTracking))
|
|
361
409
|
self?.submit(.setDeviceInfo(collectDeviceInfo()))
|
|
362
410
|
// The CMP writes TCF to the standard defaults, and reading it is the
|
|
363
411
|
// difference between shipping a real consent signal and shipping none.
|
|
@@ -487,10 +535,27 @@ final class FacadeState: @unchecked Sendable {
|
|
|
487
535
|
return pendingDeepLinks.count
|
|
488
536
|
}
|
|
489
537
|
|
|
538
|
+
/// B3: deep-link URL'sinden query+fragment atılır. URLComponents
|
|
539
|
+
/// çözümlenemezse ham dizenin `?`/`#` öncesi alınır (asla ham query sızmaz).
|
|
540
|
+
func stripUrlQuery(_ url: URL) -> String {
|
|
541
|
+
if var components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
|
|
542
|
+
components.query = nil
|
|
543
|
+
components.fragment = nil
|
|
544
|
+
if let stripped = components.string { return stripped }
|
|
545
|
+
}
|
|
546
|
+
let raw = url.absoluteString
|
|
547
|
+
if let cut = raw.firstIndex(where: { $0 == "?" || $0 == "#" }) {
|
|
548
|
+
return String(raw[..<cut])
|
|
549
|
+
}
|
|
550
|
+
return raw
|
|
551
|
+
}
|
|
552
|
+
|
|
490
553
|
private func send(_ url: URL) {
|
|
554
|
+
// B3: ham query gönderilmez — token/e-posta query'de taşınır. AEM
|
|
555
|
+
// ayrıştırması ve hash TAM url'den yapılır (aşağıda).
|
|
491
556
|
submit(
|
|
492
557
|
.track(
|
|
493
|
-
name: "deep_link", properties: ["url": .string(url
|
|
558
|
+
name: "deep_link", properties: ["url": .string(stripUrlQuery(url))], type: "custom"))
|
|
494
559
|
|
|
495
560
|
// Meta AEM: a link from Meta carries al_applink_data with an opaque,
|
|
496
561
|
// Meta-encrypted campaign_ids blob. Emitted at most once per URL — the same
|
|
@@ -557,6 +622,12 @@ final class FacadeState: @unchecked Sendable {
|
|
|
557
622
|
#endif
|
|
558
623
|
}
|
|
559
624
|
|
|
625
|
+
/// The public lifecycle calls go through the same tracker as UIKit's signals,
|
|
626
|
+
/// so one forwarded while already foregrounded (or already backgrounded) is a
|
|
627
|
+
/// no-op instead of a session split.
|
|
628
|
+
func notifyForeground() { handle(.didBecomeActive) }
|
|
629
|
+
func notifyBackground() { handle(.didEnterBackground) }
|
|
630
|
+
|
|
560
631
|
/// Applies one lifecycle signal. Public routing lives here rather than in the
|
|
561
632
|
/// observer closure so a test can drive it without UIKit.
|
|
562
633
|
func handle(_ signal: LifecycleSignal) {
|
|
@@ -3,6 +3,8 @@ import Foundation
|
|
|
3
3
|
public struct AdvenueConfig: Sendable {
|
|
4
4
|
public var apiKey: String
|
|
5
5
|
public var endpoint: String
|
|
6
|
+
/// Ignored: the SDK reads the app version from the bundle itself. Setting it
|
|
7
|
+
/// reports `config.ignored:appVersion` through `onError`. Removed in the next major.
|
|
6
8
|
public var appVersion: String?
|
|
7
9
|
public var requireConsent: Bool
|
|
8
10
|
public var sessionWindowMs: Int64
|
|
@@ -27,6 +29,12 @@ public struct AdvenueConfig: Sendable {
|
|
|
27
29
|
/// Called when the SDK swallows a best-effort failure. Never receives PII.
|
|
28
30
|
public var onError: @Sendable (String, any Error) -> Void
|
|
29
31
|
|
|
32
|
+
/// Development aid: logs the SDK's start, every failure `onError` sees and
|
|
33
|
+
/// each accepted batch to the unified log (subsystem `io.advenue.sdk`) —
|
|
34
|
+
/// Xcode's console and Console.app. Never logs an identifier or a payload.
|
|
35
|
+
/// Leave off in production; route failures through `onError` instead.
|
|
36
|
+
public var debug: Bool
|
|
37
|
+
|
|
30
38
|
/// Overrides the version stamped on every event. Set by a WRAPPER SDK, never
|
|
31
39
|
/// by an app.
|
|
32
40
|
///
|
|
@@ -41,6 +49,12 @@ public struct AdvenueConfig: Sendable {
|
|
|
41
49
|
/// the whole batch down with a 400, so it is truncated rather than sent.
|
|
42
50
|
public var sdkVersion: String?
|
|
43
51
|
|
|
52
|
+
/// B5: `https://` şema kuralı. Fail-fast `precondition` ile kurulumda
|
|
53
|
+
/// yakalanır; kuralın kendisi test edilebilir saf fonksiyondur.
|
|
54
|
+
public static func isSecureEndpoint(_ endpoint: String) -> Bool {
|
|
55
|
+
endpoint.lowercased().hasPrefix("https://")
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
public init(
|
|
45
59
|
apiKey: String,
|
|
46
60
|
endpoint: String = DEFAULT_ENDPOINT,
|
|
@@ -52,12 +66,18 @@ public struct AdvenueConfig: Sendable {
|
|
|
52
66
|
signingSecret: String? = nil,
|
|
53
67
|
conversionValues: ConversionValueConfig? = nil,
|
|
54
68
|
onError: @escaping @Sendable (String, any Error) -> Void = { _, _ in },
|
|
55
|
-
sdkVersion: String? = nil
|
|
69
|
+
sdkVersion: String? = nil,
|
|
70
|
+
allowInsecureHttp: Bool = false,
|
|
71
|
+
debug: Bool = false
|
|
56
72
|
) {
|
|
73
|
+
precondition(
|
|
74
|
+
allowInsecureHttp || Self.isSecureEndpoint(endpoint),
|
|
75
|
+
"Advenue endpoint must use https:// (allowInsecureHttp is test-only)")
|
|
57
76
|
self.apiKey = apiKey
|
|
58
77
|
self.endpoint = endpoint
|
|
59
78
|
self.appVersion = appVersion
|
|
60
79
|
self.requireConsent = requireConsent
|
|
80
|
+
self.debug = debug
|
|
61
81
|
self.sessionWindowMs = sessionWindowMs
|
|
62
82
|
self.batchSize = batchSize
|
|
63
83
|
self.flushIntervalMs = flushIntervalMs
|
|
@@ -73,5 +93,5 @@ public struct AdvenueConfig: Sendable {
|
|
|
73
93
|
/// release tag unless something checks. CI does, because the first question
|
|
74
94
|
/// every field report raises is which build produced the event.
|
|
75
95
|
public enum AdvenueVersion {
|
|
76
|
-
public static let current = "1.
|
|
96
|
+
public static let current = "1.1.0"
|
|
77
97
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/// A condition met while starting the SDK, reported through `onError` under
|
|
2
|
+
/// the context in parentheses. Not a transport failure — these used to arrive
|
|
3
|
+
/// as `IngestError(status: 0)`, which read as an HTTP error that never
|
|
4
|
+
/// happened and named no cause.
|
|
5
|
+
public enum AdvenueSetupError: Error, Equatable, Sendable, CustomStringConvertible {
|
|
6
|
+
/// `config.ignored:appVersion`
|
|
7
|
+
case appVersionIgnored
|
|
8
|
+
/// `store.cache_unavailable`
|
|
9
|
+
case cacheUnavailable
|
|
10
|
+
/// `identity.deferred`
|
|
11
|
+
case identityDeferred
|
|
12
|
+
|
|
13
|
+
public var description: String {
|
|
14
|
+
switch self {
|
|
15
|
+
case .appVersionIgnored:
|
|
16
|
+
return "appVersion is read from the bundle; the config value has no effect"
|
|
17
|
+
case .cacheUnavailable:
|
|
18
|
+
return "the Caches directory is unavailable; the event queue falls back to UserDefaults"
|
|
19
|
+
case .identityDeferred:
|
|
20
|
+
return "the Keychain is unavailable (not unlocked since boot, or the app is not "
|
|
21
|
+
+ "code-signed); nothing starts until the next launch that can read it"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
/// Where `AdvenueConfig.debug` writes. The unified logging system in an app;
|
|
5
|
+
/// a recorder in tests.
|
|
6
|
+
protocol AdvenueLogSink: Sendable {
|
|
7
|
+
func log(_ message: String)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/// `os.Logger`, public privacy: the lines carry failure contexts, counts and
|
|
11
|
+
/// the app id the server resolved — never an identifier or a payload — and a
|
|
12
|
+
/// debug aid that Console.app shows as `<private>` would not be one.
|
|
13
|
+
struct UnifiedLogSink: AdvenueLogSink {
|
|
14
|
+
private let logger = Logger(subsystem: "io.advenue.sdk", category: "Advenue")
|
|
15
|
+
func log(_ message: String) {
|
|
16
|
+
logger.notice("\(message, privacy: .public)")
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Logs each accepted batch; failures already reach the log through the
|
|
21
|
+
/// wrapped `onError` (`flush.transport`, `flush.poison`).
|
|
22
|
+
struct LoggingTransport: EventTransport {
|
|
23
|
+
let inner: any EventTransport
|
|
24
|
+
let log: @Sendable (String) -> Void
|
|
25
|
+
|
|
26
|
+
func send(_ events: [ClientEvent]) async throws {
|
|
27
|
+
try await inner.send(events)
|
|
28
|
+
log("[Advenue] sent \(events.count) event(s)")
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// With `debug` on: the config with `onError` also writing to the log, and the
|
|
33
|
+
/// log itself. Off: the config unchanged and a log that discards.
|
|
34
|
+
func withDebugLogging(
|
|
35
|
+
_ config: AdvenueConfig, sink: (any AdvenueLogSink)?
|
|
36
|
+
) -> (AdvenueConfig, @Sendable (String) -> Void) {
|
|
37
|
+
guard config.debug else { return (config, { _ in }) }
|
|
38
|
+
let sink = sink ?? UnifiedLogSink()
|
|
39
|
+
var logged = config
|
|
40
|
+
let report = config.onError
|
|
41
|
+
logged.onError = { context, error in
|
|
42
|
+
sink.log("[Advenue] \(context): \(error)")
|
|
43
|
+
report(context, error)
|
|
44
|
+
}
|
|
45
|
+
return (logged, { sink.log($0) })
|
|
46
|
+
}
|
|
@@ -3,9 +3,23 @@ import Foundation
|
|
|
3
3
|
/// A non-2xx ingest response. `isRetryable` separates a transient failure from
|
|
4
4
|
/// a poison payload: retrying a 400 forever would block the queue head, and
|
|
5
5
|
/// dropping a 429 would discard events over a throttle.
|
|
6
|
-
public struct IngestError: Error, Equatable, Sendable {
|
|
6
|
+
public struct IngestError: Error, Equatable, Sendable, CustomStringConvertible {
|
|
7
7
|
public let status: Int
|
|
8
|
-
|
|
8
|
+
/// Set when no response arrived at all — DNS, refused connection, timeout,
|
|
9
|
+
/// no network. `status` is then 408 so the backoff retries it, but that 408
|
|
10
|
+
/// was never sent by a server, and logging it as one sends whoever debugs it
|
|
11
|
+
/// looking at the wrong end (F-SDK-10).
|
|
12
|
+
public let networkCause: String?
|
|
13
|
+
|
|
14
|
+
public init(status: Int, networkCause: String? = nil) {
|
|
15
|
+
self.status = status
|
|
16
|
+
self.networkCause = networkCause
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public var description: String {
|
|
20
|
+
if let networkCause { return "IngestError: no response (network error: \(networkCause))" }
|
|
21
|
+
return "IngestError(status: \(status))"
|
|
22
|
+
}
|
|
9
23
|
|
|
10
24
|
public var isRetryable: Bool {
|
|
11
25
|
status == 408 || status == 429 || status >= 500
|