@advenue/react-native 1.0.1 → 1.1.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/android/build.gradle +8 -2
- package/android/src/main/java/expo/modules/advenue/AdvenueAndroidModule.kt +4 -0
- package/android/src/main/kotlin/io/advenue/Advenue.kt +49 -9
- package/android/src/main/kotlin/io/advenue/AdvenueConfig.kt +9 -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 +28 -2
- package/android/src/main/kotlin/io/advenue/core/Engine.kt +73 -3
- package/android/src/main/kotlin/io/advenue/core/EventQueue.kt +12 -0
- package/android/src/main/kotlin/io/advenue/core/SessionTracker.kt +109 -33
- 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/LifecycleBridge.kt +23 -1
- package/dist/index.cjs +18 -2
- package/dist/index.d.cts +13 -3
- package/dist/index.d.ts +13 -3
- package/dist/index.js +18 -2
- package/ios/AdvenueIosModule.swift +4 -1
- package/ios/vendor/Advenue/Advenue.swift +53 -18
- package/ios/vendor/Advenue/AdvenueConfig.swift +17 -3
- 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 +58 -4
- package/ios/vendor/AdvenueCore/EventQueue.swift +12 -0
- package/ios/vendor/AdvenueCore/SessionTracker.swift +89 -32
- package/ios/vendor/AdvenuePlatform/ChallengeFetcher.swift +14 -2
- package/ios/vendor/AdvenuePlatform/ConversionFetcher.swift +14 -3
- package/ios/vendor/AdvenuePlatform/DeviceInfo.swift +19 -0
- package/ios/vendor/AdvenuePlatform/HttpTransport.swift +28 -4
- package/ios/vendor/AdvenuePlatform/InstallEnrichment.swift +4 -0
- package/ios/vendor/AdvenuePlatform/SkanConfigFetcher.swift +14 -2
- package/package.json +18 -9
- package/src/index.ts +11 -1
- package/src/types.ts +13 -3
|
@@ -17,17 +17,29 @@ public struct HttpChallengeFetcher: ChallengeSource {
|
|
|
17
17
|
self.session = session
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
/// The exact request this fetcher would send. Public so a test can assert
|
|
21
|
+
/// the wire shape without a network round trip.
|
|
22
|
+
///
|
|
23
|
+
/// Sends `?platform=ios`: this fetcher is iOS-only by construction, and a
|
|
24
|
+
/// project-scoped key otherwise cannot resolve a listing on a normal
|
|
25
|
+
/// two-listing (iOS + Android) product — see `resolveProjectListing` in
|
|
26
|
+
/// apps/ingestion/src/app.ts, which 400s an ambiguous project key rather
|
|
27
|
+
/// than guess.
|
|
28
|
+
public func buildRequest(deviceId: String) -> URLRequest {
|
|
21
29
|
let escaped =
|
|
22
30
|
deviceId.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? deviceId
|
|
23
31
|
var request = URLRequest(
|
|
24
|
-
url: URL(string: "\(endpoint)/v1/attest/challenge?deviceId=\(escaped)")!)
|
|
32
|
+
url: URL(string: "\(endpoint)/v1/attest/challenge?deviceId=\(escaped)&platform=ios")!)
|
|
25
33
|
request.httpMethod = "GET"
|
|
26
34
|
request.setValue(apiKey, forHTTPHeaderField: "x-api-key")
|
|
27
35
|
// Matches the deadline the RN SDK uses. Attestation is enrichment: it never
|
|
28
36
|
// holds the install.
|
|
29
37
|
request.timeoutInterval = 5
|
|
38
|
+
return request
|
|
39
|
+
}
|
|
30
40
|
|
|
41
|
+
public func challenge(deviceId: String) async throws -> String {
|
|
42
|
+
let request = buildRequest(deviceId: deviceId)
|
|
31
43
|
let (payload, response) = try await session.data(for: request)
|
|
32
44
|
guard let http = response as? HTTPURLResponse,
|
|
33
45
|
(200..<300).contains(http.statusCode)
|
|
@@ -23,13 +23,24 @@ public struct HttpConversionFetcher: ConversionFetcher {
|
|
|
23
23
|
self.session = session
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
/// Sends `?platform=ios`: this fetcher is iOS-only by construction, and a
|
|
27
|
+
/// project key spans listings, so the server needs the platform to pick the
|
|
28
|
+
/// one this install was filed under (Y-LINKS-1). Without it the lookup
|
|
29
|
+
/// refused the project key the dashboard mints, and the deferred deep link
|
|
30
|
+
/// never arrived.
|
|
31
|
+
func buildRequest() -> URLRequest {
|
|
27
32
|
let escaped =
|
|
28
33
|
deviceId.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? deviceId
|
|
29
|
-
var request = URLRequest(
|
|
34
|
+
var request = URLRequest(
|
|
35
|
+
url: URL(string: "\(endpoint)/sdk/conversion-data?deviceId=\(escaped)&platform=ios")!)
|
|
30
36
|
request.httpMethod = "GET"
|
|
31
37
|
request.setValue(apiKey, forHTTPHeaderField: "x-api-key")
|
|
32
38
|
request.timeoutInterval = 15
|
|
39
|
+
return request
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public func fetch() async throws -> ConversionResult {
|
|
43
|
+
let request = buildRequest()
|
|
33
44
|
|
|
34
45
|
let payload: Data
|
|
35
46
|
let response: URLResponse
|
|
@@ -37,7 +48,7 @@ public struct HttpConversionFetcher: ConversionFetcher {
|
|
|
37
48
|
(payload, response) = try await session.data(for: request)
|
|
38
49
|
} catch {
|
|
39
50
|
// No status to reason about; 408 marks it retryable, as sdk-core does.
|
|
40
|
-
throw IngestError(status: 408)
|
|
51
|
+
throw IngestError(status: 408, networkCause: describeNetworkFailure(error))
|
|
41
52
|
}
|
|
42
53
|
guard let http = response as? HTTPURLResponse else { throw IngestError(status: 408) }
|
|
43
54
|
guard (200..<300).contains(http.statusCode) else {
|
|
@@ -16,6 +16,25 @@ public func readAppVersion() -> String? {
|
|
|
16
16
|
return short
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/// Whether this process was launched into the background — background fetch,
|
|
20
|
+
/// a silent push, a location relaunch — rather than opened by the user. Read
|
|
21
|
+
/// through the runtime, not `UIApplication.shared`: that symbol is unavailable
|
|
22
|
+
/// in an app extension and merely referencing it there fails to link. False
|
|
23
|
+
/// wherever it cannot be known (an extension, off the main thread, macOS),
|
|
24
|
+
/// which keeps the pre-existing behaviour: the launch is a foreground.
|
|
25
|
+
public func readLaunchedInBackground() -> Bool {
|
|
26
|
+
#if canImport(UIKit) && !os(watchOS)
|
|
27
|
+
guard Thread.isMainThread, !Bundle.main.bundlePath.hasSuffix(".appex") else { return false }
|
|
28
|
+
let selector = NSSelectorFromString("sharedApplication")
|
|
29
|
+
guard UIApplication.responds(to: selector),
|
|
30
|
+
let app = UIApplication.perform(selector)?.takeUnretainedValue() as? UIApplication
|
|
31
|
+
else { return false }
|
|
32
|
+
return app.applicationState == .background
|
|
33
|
+
#else
|
|
34
|
+
return false
|
|
35
|
+
#endif
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
/// Device metadata for Meta CAPI `extinfo`.
|
|
20
39
|
///
|
|
21
40
|
/// Best-effort per field: a field is **omitted**, never defaulted, when its
|
|
@@ -5,6 +5,10 @@ import Foundation
|
|
|
5
5
|
struct EventBatch: Encodable {
|
|
6
6
|
let apiKey: String
|
|
7
7
|
let events: [ClientEvent]
|
|
8
|
+
/// The device clock at THIS send attempt (spec 2026-10-01-data-fidelity D3).
|
|
9
|
+
/// The server compares it with its receive time to measure the clock offset,
|
|
10
|
+
/// so it is stamped on every attempt and never persisted with the queue.
|
|
11
|
+
let sentAt: String
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
public let DEFAULT_ENDPOINT = "https://ingest.advenue.io"
|
|
@@ -49,8 +53,12 @@ public struct HttpTransport: EventTransport, Sendable {
|
|
|
49
53
|
/// the wire shape — endpoint, headers, body, signature — without a network
|
|
50
54
|
/// round trip, which is the part that has to be right.
|
|
51
55
|
public func buildRequest(_ events: [ClientEvent]) throws -> URLRequest {
|
|
56
|
+
// One reading for both `sentAt` and the signed timestamp: they describe
|
|
57
|
+
// the same instant, and two reads could straddle a clock change.
|
|
58
|
+
let nowMs = clock.nowMs()
|
|
52
59
|
let body = try EventEncoding.canonicalEncoder()
|
|
53
|
-
.encode(
|
|
60
|
+
.encode(
|
|
61
|
+
EventBatch(apiKey: apiKey, events: events, sentAt: EventEncoding.iso8601(ms: nowMs)))
|
|
54
62
|
var request = URLRequest(url: URL(string: "\(endpoint)/v1/events")!)
|
|
55
63
|
request.httpMethod = "POST"
|
|
56
64
|
request.setValue("application/json", forHTTPHeaderField: "content-type")
|
|
@@ -58,7 +66,7 @@ public struct HttpTransport: EventTransport, Sendable {
|
|
|
58
66
|
request.timeoutInterval = 15
|
|
59
67
|
|
|
60
68
|
if let secret = signingSecret {
|
|
61
|
-
let timestamp = String(
|
|
69
|
+
let timestamp = String(nowMs)
|
|
62
70
|
request.setValue(timestamp, forHTTPHeaderField: "X-Advenue-Timestamp")
|
|
63
71
|
request.setValue(
|
|
64
72
|
signRequest(
|
|
@@ -77,8 +85,8 @@ public struct HttpTransport: EventTransport, Sendable {
|
|
|
77
85
|
(payload, response) = try await session.data(for: request)
|
|
78
86
|
} catch {
|
|
79
87
|
// No status to reason about. 408 marks it retryable, which is what
|
|
80
|
-
// sdk-core does for the same case.
|
|
81
|
-
throw IngestError(status: 408)
|
|
88
|
+
// sdk-core does for the same case; the cause says it was the network.
|
|
89
|
+
throw IngestError(status: 408, networkCause: describeNetworkFailure(error))
|
|
82
90
|
}
|
|
83
91
|
guard let http = response as? HTTPURLResponse else { throw IngestError(status: 408) }
|
|
84
92
|
guard (200..<300).contains(http.statusCode) else {
|
|
@@ -94,3 +102,19 @@ public struct HttpTransport: EventTransport, Sendable {
|
|
|
94
102
|
}
|
|
95
103
|
}
|
|
96
104
|
}
|
|
105
|
+
|
|
106
|
+
/// A short, identifier-free name for a failure that produced no response.
|
|
107
|
+
func describeNetworkFailure(_ error: any Error) -> String {
|
|
108
|
+
guard let urlError = error as? URLError else { return String(describing: type(of: error)) }
|
|
109
|
+
switch urlError.code {
|
|
110
|
+
case .notConnectedToInternet: return "not connected to the internet"
|
|
111
|
+
case .timedOut: return "timed out"
|
|
112
|
+
case .cannotFindHost, .dnsLookupFailed: return "host not found"
|
|
113
|
+
case .cannotConnectToHost: return "connection refused"
|
|
114
|
+
case .networkConnectionLost: return "connection lost"
|
|
115
|
+
case .secureConnectionFailed, .serverCertificateUntrusted, .serverCertificateHasBadDate,
|
|
116
|
+
.serverCertificateNotYetValid, .serverCertificateHasUnknownRoot:
|
|
117
|
+
return "TLS failure"
|
|
118
|
+
default: return "URLError \(urlError.code.rawValue)"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -4,6 +4,10 @@ import Foundation
|
|
|
4
4
|
/// Spec §5: the install waits this long for enrichment and no longer. Matches
|
|
5
5
|
/// the deadlines the RN SDK already uses (SKAN config 3000 ms).
|
|
6
6
|
public let INSTALL_WINDOW_MS = 3_000
|
|
7
|
+
/// How long past `INSTALL_WINDOW_MS` flushes stay held for a pending install —
|
|
8
|
+
/// the enrichment deadline is checked by a task that still has to be
|
|
9
|
+
/// scheduled and then hop through the command pipe.
|
|
10
|
+
public let INSTALL_HOLD_MARGIN_MS = 2_000
|
|
7
11
|
|
|
8
12
|
/// What the install event carries beyond the envelope every event has.
|
|
9
13
|
public struct Enrichment: Sendable, Equatable {
|
|
@@ -22,12 +22,24 @@ public struct HttpSkanConfigFetcher: SkanConfigSource {
|
|
|
22
22
|
self.session = session
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
/// The exact request this fetcher would send. Public so a test can assert
|
|
26
|
+
/// the wire shape without a network round trip.
|
|
27
|
+
///
|
|
28
|
+
/// Sends `?platform=ios`: this fetcher is iOS-only by construction, and a
|
|
29
|
+
/// project-scoped key otherwise cannot resolve a listing on a normal
|
|
30
|
+
/// two-listing (iOS + Android) product — see `resolveProjectListing` in
|
|
31
|
+
/// apps/ingestion/src/app.ts, which 400s an ambiguous project key rather
|
|
32
|
+
/// than guess.
|
|
33
|
+
public func buildRequest(etag: String?) -> URLRequest {
|
|
34
|
+
var request = URLRequest(url: URL(string: "\(endpoint)/v1/sdk-config?platform=ios")!)
|
|
27
35
|
request.setValue(apiKey, forHTTPHeaderField: "x-api-key")
|
|
28
36
|
if let etag { request.setValue(etag, forHTTPHeaderField: "if-none-match") }
|
|
29
37
|
request.timeoutInterval = Double(SKAN_CONFIG_TIMEOUT_MS) / 1000
|
|
38
|
+
return request
|
|
39
|
+
}
|
|
30
40
|
|
|
41
|
+
public func fetch(etag: String?) async throws -> SkanConfigResult {
|
|
42
|
+
let request = buildRequest(etag: etag)
|
|
31
43
|
let (payload, response) = try await session.data(for: request)
|
|
32
44
|
guard let http = response as? HTTPURLResponse else { throw IngestError(status: 408) }
|
|
33
45
|
if http.statusCode == 304 { return .notModified }
|
package/package.json
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@advenue/react-native",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Advenue MMP React Native SDK — attribution, install tracking, and deep links (bundled iOS/Android Expo native modules)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/broverse/advenue.git",
|
|
10
|
+
"directory": "packages/sdk-react-native"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://advenue.io",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/broverse/advenue/issues"
|
|
15
|
+
},
|
|
7
16
|
"main": "./dist/index.js",
|
|
8
17
|
"types": "./dist/index.d.ts",
|
|
9
18
|
"exports": {
|
|
@@ -30,12 +39,6 @@
|
|
|
30
39
|
"publishConfig": {
|
|
31
40
|
"access": "public"
|
|
32
41
|
},
|
|
33
|
-
"scripts": {
|
|
34
|
-
"build": "node scripts/vendor-natives.mjs && node scripts/check-vendored-swift.mjs && tsup && node scripts/check-dist.mjs",
|
|
35
|
-
"typecheck": "tsc --noEmit",
|
|
36
|
-
"test": "vitest run --minWorkers=1 --maxWorkers=2 && node --test scripts/vendor-natives.test.mjs && node scripts/vendor-natives.mjs && node scripts/check-vendored-swift.mjs",
|
|
37
|
-
"vendor": "node scripts/vendor-natives.mjs"
|
|
38
|
-
},
|
|
39
42
|
"dependencies": {},
|
|
40
43
|
"peerDependencies": {
|
|
41
44
|
"@expo/config-plugins": ">=8",
|
|
@@ -56,5 +59,11 @@
|
|
|
56
59
|
"typescript": "^5.7.2",
|
|
57
60
|
"vitest": "^2.1.8"
|
|
58
61
|
},
|
|
59
|
-
"react-native": "./dist/index.cjs"
|
|
60
|
-
|
|
62
|
+
"react-native": "./dist/index.cjs",
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "node scripts/vendor-natives.mjs && node scripts/check-vendored-swift.mjs && tsup && node scripts/check-dist.mjs",
|
|
65
|
+
"typecheck": "tsc --noEmit",
|
|
66
|
+
"test": "vitest run --minWorkers=1 --maxWorkers=2 && node --test scripts/vendor-natives.test.mjs && node scripts/vendor-natives.mjs && node scripts/check-vendored-swift.mjs",
|
|
67
|
+
"vendor": "node scripts/vendor-natives.mjs"
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -63,6 +63,8 @@ let started = false;
|
|
|
63
63
|
let deepLinkSub: DeepLinkHandle | null = null;
|
|
64
64
|
let errorSub: { remove(): void } | null = null;
|
|
65
65
|
let onErrorHook: ((context: string, cause: unknown) => void) | undefined;
|
|
66
|
+
/** `config.debug`: failures are also written to the JS console. */
|
|
67
|
+
let debugLogging = false;
|
|
66
68
|
|
|
67
69
|
/**
|
|
68
70
|
* `getTrackingConsent` is synchronous in JavaScript and the value lives in the
|
|
@@ -105,6 +107,9 @@ function report(context: string, cause: unknown): void {
|
|
|
105
107
|
} catch {
|
|
106
108
|
// A throwing diagnostics hook must never break the SDK.
|
|
107
109
|
}
|
|
110
|
+
// Native failures come back through `onAdvenueError`, so this covers both
|
|
111
|
+
// sides. The native SDKs log their start and sent batches themselves.
|
|
112
|
+
if (debugLogging) console.warn(`[Advenue] ${context}`, cause);
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
function requireStarted(): boolean {
|
|
@@ -122,6 +127,7 @@ export const Advenue = {
|
|
|
122
127
|
*/
|
|
123
128
|
async initialize(config: RNAdvenueConfig): Promise<void> {
|
|
124
129
|
onErrorHook = config.onError;
|
|
130
|
+
debugLogging = config.debug === true;
|
|
125
131
|
|
|
126
132
|
for (const option of IGNORED_OPTIONS) {
|
|
127
133
|
if ((config as unknown as Record<string, unknown>)[option] !== undefined) {
|
|
@@ -145,7 +151,11 @@ export const Advenue = {
|
|
|
145
151
|
signingSecret: config.signingSecret,
|
|
146
152
|
fbAppId: config.fbAppId,
|
|
147
153
|
autoTrackInstall: config.autoTrackInstall !== false,
|
|
148
|
-
|
|
154
|
+
// Names the wrapper, like Flutter's `advenue_sdk/…`: a bare version reads
|
|
155
|
+
// like the native SDK's own stamp.
|
|
156
|
+
sdkVersion: `react-native/${pkg.version}`,
|
|
157
|
+
debug: config.debug === true,
|
|
158
|
+
allowInsecureHttp: config.allowInsecureHttp === true,
|
|
149
159
|
});
|
|
150
160
|
started = true;
|
|
151
161
|
|
package/src/types.ts
CHANGED
|
@@ -59,14 +59,12 @@ export interface TrackOptions {
|
|
|
59
59
|
export interface AdvenueConfig {
|
|
60
60
|
apiKey: string;
|
|
61
61
|
endpoint?: string;
|
|
62
|
-
platform?: 'ios' | 'android' | 'web' | string;
|
|
63
62
|
/**
|
|
64
63
|
* @deprecated Ignored: the native SDK resolves the app version from the
|
|
65
64
|
* package/bundle. Passing it reports `config.ignored:appVersion` via
|
|
66
65
|
* `onError`. Removed in the next major.
|
|
67
66
|
*/
|
|
68
67
|
appVersion?: string;
|
|
69
|
-
osVersion?: string;
|
|
70
68
|
/** Gates every event on `setTrackingConsent(true)`. */
|
|
71
69
|
requireConsent?: boolean;
|
|
72
70
|
/** Events per request. */
|
|
@@ -79,8 +77,20 @@ export interface AdvenueConfig {
|
|
|
79
77
|
* extracted from it.
|
|
80
78
|
*/
|
|
81
79
|
signingSecret?: string;
|
|
82
|
-
|
|
80
|
+
/**
|
|
81
|
+
* **Test only.** Allows an `http://` endpoint — a local self-hosted ingest,
|
|
82
|
+
* or a test server — which the native SDKs otherwise refuse at initialize.
|
|
83
|
+
* Never set it in a production build: everything, the API key included,
|
|
84
|
+
* then travels unencrypted.
|
|
85
|
+
*/
|
|
86
|
+
allowInsecureHttp?: boolean;
|
|
83
87
|
/** Called when the SDK swallows a best-effort failure. Never receives PII. */
|
|
84
88
|
onError?: (context: string, cause: unknown) => void;
|
|
89
|
+
/**
|
|
90
|
+
* Development aid: the native SDKs log their start, every swallowed failure
|
|
91
|
+
* and each sent batch to Xcode's console / logcat, and every failure is also
|
|
92
|
+
* written to the JS console as `[Advenue] <context>`. Never logs an
|
|
93
|
+
* identifier or a payload. Leave off in production; use `onError`.
|
|
94
|
+
*/
|
|
85
95
|
debug?: boolean;
|
|
86
96
|
}
|