@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.
Files changed (37) hide show
  1. package/android/build.gradle +8 -2
  2. package/android/src/main/java/expo/modules/advenue/AdvenueAndroidModule.kt +4 -0
  3. package/android/src/main/kotlin/io/advenue/Advenue.kt +49 -9
  4. package/android/src/main/kotlin/io/advenue/AdvenueConfig.kt +9 -1
  5. package/android/src/main/kotlin/io/advenue/DebugLog.kt +51 -0
  6. package/android/src/main/kotlin/io/advenue/core/Backoff.kt +14 -1
  7. package/android/src/main/kotlin/io/advenue/core/CommandPipe.kt +28 -2
  8. package/android/src/main/kotlin/io/advenue/core/Engine.kt +73 -3
  9. package/android/src/main/kotlin/io/advenue/core/EventQueue.kt +12 -0
  10. package/android/src/main/kotlin/io/advenue/core/SessionTracker.kt +109 -33
  11. package/android/src/main/kotlin/io/advenue/platform/ConversionFetcher.kt +24 -10
  12. package/android/src/main/kotlin/io/advenue/platform/ForegroundTracker.kt +9 -2
  13. package/android/src/main/kotlin/io/advenue/platform/HttpUrlTransport.kt +21 -8
  14. package/android/src/main/kotlin/io/advenue/platform/InstallEnrichment.kt +7 -0
  15. package/android/src/main/kotlin/io/advenue/platform/LifecycleBridge.kt +23 -1
  16. package/dist/index.cjs +18 -2
  17. package/dist/index.d.cts +13 -3
  18. package/dist/index.d.ts +13 -3
  19. package/dist/index.js +18 -2
  20. package/ios/AdvenueIosModule.swift +4 -1
  21. package/ios/vendor/Advenue/Advenue.swift +53 -18
  22. package/ios/vendor/Advenue/AdvenueConfig.swift +17 -3
  23. package/ios/vendor/Advenue/AdvenueSetupError.swift +24 -0
  24. package/ios/vendor/Advenue/DebugLog.swift +46 -0
  25. package/ios/vendor/AdvenueCore/Backoff.swift +16 -2
  26. package/ios/vendor/AdvenueCore/Engine.swift +58 -4
  27. package/ios/vendor/AdvenueCore/EventQueue.swift +12 -0
  28. package/ios/vendor/AdvenueCore/SessionTracker.swift +89 -32
  29. package/ios/vendor/AdvenuePlatform/ChallengeFetcher.swift +14 -2
  30. package/ios/vendor/AdvenuePlatform/ConversionFetcher.swift +14 -3
  31. package/ios/vendor/AdvenuePlatform/DeviceInfo.swift +19 -0
  32. package/ios/vendor/AdvenuePlatform/HttpTransport.swift +28 -4
  33. package/ios/vendor/AdvenuePlatform/InstallEnrichment.swift +4 -0
  34. package/ios/vendor/AdvenuePlatform/SkanConfigFetcher.swift +14 -2
  35. package/package.json +18 -9
  36. package/src/index.ts +11 -1
  37. package/src/types.ts +13 -3
@@ -5,6 +5,12 @@ internal const val SESSION_STATE_KEY: String = "advenue.session"
5
5
  /** 30 minutes. */
6
6
  internal const val DEFAULT_SESSION_WINDOW_MS: Long = 1_800_000L
7
7
 
8
+ /**
9
+ * The heartbeat persists at most this often: it runs on every recorded event,
10
+ * and a chatty app must not turn that into a storage write per event.
11
+ */
12
+ internal const val SESSION_HEARTBEAT_MIN_INTERVAL_MS: Long = 1_000L
13
+
8
14
  /** A session lifecycle event, wrapped by the engine as a `type: "session"` event. */
9
15
  internal data class SessionEvent(val name: String, val properties: Map<String, Any?>)
10
16
 
@@ -16,6 +22,12 @@ private data class SessionState(
16
22
  val activeStart: Long?,
17
23
  val firstForegroundAt: Long,
18
24
  val timeSpentMs: Long,
25
+ /**
26
+ * The last moment the open sub-session was known to be alive (F-SDK-7).
27
+ * Absent in state written before it existed, which reads as "alive at
28
+ * activeStart".
29
+ */
30
+ val lastActiveAt: Long? = null,
19
31
  ) {
20
32
  /**
21
33
  * The persisted blob. Field names are frozen: a rename would make RN-written
@@ -30,6 +42,7 @@ private data class SessionState(
30
42
  "activeStart" to activeStart,
31
43
  "firstForegroundAt" to firstForegroundAt,
32
44
  "timeSpentMs" to timeSpentMs,
45
+ "lastActiveAt" to lastActiveAt,
33
46
  )
34
47
  }
35
48
 
@@ -46,21 +59,88 @@ internal class SessionTracker(
46
59
  ) {
47
60
  private var lastState: SessionState? = null
48
61
 
62
+ /**
63
+ * True once THIS process opened the current sub-session (X-SDK-1). Never
64
+ * persisted: a relaunched process starts false, so its tracks and flushes
65
+ * before [handleForeground] cannot keep the dead process's sub-session alive
66
+ * and move its kill time to the relaunch.
67
+ */
68
+ private var ownsOpenSubSession = false
69
+
49
70
  /** Drops the cached state (erasure) so a wiped store cannot be resurrected. */
50
71
  public fun reset() {
51
72
  lastState = null
73
+ ownsOpenSubSession = false
52
74
  }
53
75
 
54
76
  /**
55
- * Foreground transition, including cold start. Returns:
56
- * - empty — a sub-session inside the window;
57
- * - `[session_start]` — a new session;
58
- * - `[session_end, session_start]` — kill recovery: the prior session was
59
- * still open when the OS killed the app, so its end is synthesised first.
77
+ * Refreshes the open sub-session's last known activity. Called by the engine
78
+ * on every recorded event and every flush — the auto-flush timer is the
79
+ * foreground tick. It is what a relaunch after an OS kill measures the gap
80
+ * from, the role Adjust's `lastActivity` plays.
81
+ *
82
+ * A no-op until this process has run [handleForeground]: only the process
83
+ * that opened the sub-session may extend it. Otherwise an event or flush in a
84
+ * relaunched process (a background wake from a broadcast, push or WorkManager
85
+ * before the activity resumes) would stamp the killed sub-session alive "now"
86
+ * and count the dead time as active (X-SDK-1).
87
+ */
88
+ public fun heartbeat() {
89
+ if (!ownsOpenSubSession) return
90
+ val state = load() ?: return
91
+ val activeStart = state.activeStart ?: return
92
+ val now = clock.nowMs()
93
+ if (now - (state.lastActiveAt ?: activeStart) < SESSION_HEARTBEAT_MIN_INTERVAL_MS) return
94
+ save(state.copy(lastActiveAt = now))
95
+ }
96
+
97
+ /**
98
+ * Foreground transition, including cold start. Returns the events to emit, in
99
+ * order:
100
+ * - a synthetic `session_end` first when a sub-session was still open — the
101
+ * OS killed the app before it could background. It is closed at its last
102
+ * heartbeat, not at the relaunch: the time the app was dead is not active
103
+ * time. A consumer must never see two sessions open, so it comes first.
104
+ * - then, measured from the background (or that kill time): nothing for a
105
+ * sub-session inside the window, or a `session_start` for a new session.
106
+ *
107
+ * A kill is not a session boundary by itself (F-SDK-7). Adjust's rule — a new
108
+ * session only after the session interval of inactivity — applies to a
109
+ * relaunch exactly as it does to a return from the background.
60
110
  */
61
111
  public fun handleForeground(): List<SessionEvent> {
62
- val state = load()
63
112
  val now = clock.nowMs()
113
+ val state = load()
114
+ val activeStart = state?.activeStart ?: return resume(state, now)
115
+
116
+ val killTime = minOf(now, maxOf(activeStart, state.lastActiveAt ?: activeStart))
117
+ val activeMs = Math.max(0L, killTime - activeStart)
118
+ val timeSpentMs = state.timeSpentMs + activeMs
119
+ val sessionEnd =
120
+ SessionEvent(
121
+ "session_end",
122
+ mapOf(
123
+ "sessionId" to state.sessionId,
124
+ "sessionNumber" to state.sessionNumber,
125
+ "subSession" to state.subSessionCount,
126
+ "activeMs" to activeMs,
127
+ "timeSpentMs" to timeSpentMs,
128
+ "sessionLengthMs" to Math.max(0L, killTime - state.firstForegroundAt),
129
+ "synthetic" to true,
130
+ ),
131
+ )
132
+ val closed =
133
+ state.copy(
134
+ lastBackgroundAt = killTime,
135
+ activeStart = null,
136
+ lastActiveAt = null,
137
+ timeSpentMs = timeSpentMs,
138
+ )
139
+ return listOf(sessionEnd) + resume(closed, now)
140
+ }
141
+
142
+ /** The foreground decision once no sub-session is open. */
143
+ private fun resume(state: SessionState?, now: Long): List<SessionEvent> {
64
144
  val gap = if (state?.lastBackgroundAt != null) now - state.lastBackgroundAt else null
65
145
 
66
146
  if (state == null || gap == null || gap >= windowMs) {
@@ -77,8 +157,8 @@ internal class SessionTracker(
77
157
  timeSpentMs = 0L,
78
158
  ),
79
159
  )
80
-
81
- val sessionStart =
160
+ ownsOpenSubSession = true
161
+ return listOf(
82
162
  SessionEvent(
83
163
  "session_start",
84
164
  mapOf(
@@ -89,32 +169,18 @@ internal class SessionTracker(
89
169
  // 0 rather than infinity, which would not survive JSON.
90
170
  "timeSinceLastSessionMs" to (gap ?: 0L),
91
171
  ),
92
- )
93
-
94
- if (state?.activeStart != null) {
95
- val killTime = state.lastBackgroundAt ?: now
96
- val activeMs = Math.max(0L, killTime - state.activeStart)
97
- val timeSpentMs = state.timeSpentMs + activeMs
98
- val sessionEnd =
99
- SessionEvent(
100
- "session_end",
101
- mapOf(
102
- "sessionId" to state.sessionId,
103
- "sessionNumber" to state.sessionNumber,
104
- "subSession" to state.subSessionCount,
105
- "activeMs" to activeMs,
106
- "timeSpentMs" to timeSpentMs,
107
- "sessionLengthMs" to Math.max(0L, killTime - state.firstForegroundAt),
108
- "synthetic" to true,
109
- ),
110
- )
111
- return listOf(sessionEnd, sessionStart)
112
- }
113
-
114
- return listOf(sessionStart)
172
+ ),
173
+ )
115
174
  }
116
175
 
117
- save(state.copy(subSessionCount = state.subSessionCount + 1L, activeStart = now))
176
+ save(
177
+ state.copy(
178
+ subSessionCount = state.subSessionCount + 1L,
179
+ activeStart = now,
180
+ lastActiveAt = null,
181
+ ),
182
+ )
183
+ ownsOpenSubSession = true
118
184
  return emptyList()
119
185
  }
120
186
 
@@ -125,7 +191,15 @@ internal class SessionTracker(
125
191
  val now = clock.nowMs()
126
192
  val activeMs = Math.max(0L, now - activeStart)
127
193
  val timeSpentMs = state.timeSpentMs + activeMs
128
- save(state.copy(lastBackgroundAt = now, activeStart = null, timeSpentMs = timeSpentMs))
194
+ save(
195
+ state.copy(
196
+ lastBackgroundAt = now,
197
+ activeStart = null,
198
+ lastActiveAt = null,
199
+ timeSpentMs = timeSpentMs,
200
+ ),
201
+ )
202
+ ownsOpenSubSession = false
129
203
  return SessionEvent(
130
204
  "session_end",
131
205
  mapOf(
@@ -157,6 +231,7 @@ internal class SessionTracker(
157
231
  // it, an app updating from an older SDK would fail to load its own session
158
232
  // and restart its numbering.
159
233
  val timeSpentMs = (map["timeSpentMs"] as? Number)?.toLong() ?: 0L
234
+ val lastActiveAt = (map["lastActiveAt"] as? Number)?.toLong()
160
235
  val firstForegroundAt =
161
236
  (map["firstForegroundAt"] as? Number)?.toLong()
162
237
  ?: activeStart
@@ -172,6 +247,7 @@ internal class SessionTracker(
172
247
  activeStart = activeStart,
173
248
  firstForegroundAt = firstForegroundAt,
174
249
  timeSpentMs = timeSpentMs,
250
+ lastActiveAt = lastActiveAt,
175
251
  )
176
252
  lastState = state
177
253
  return state
@@ -24,23 +24,37 @@ public class HttpConversionFetcher(
24
24
  private val timeoutMs: Int = 15_000,
25
25
  ) : ConversionFetcher {
26
26
 
27
- override fun fetch(): ConversionResult {
27
+ internal val requestMethod: String = "GET"
28
+
29
+ /**
30
+ * Sends `?platform=android`: this fetcher is Android-only by construction,
31
+ * and a project key spans listings, so the server needs the platform to pick
32
+ * the one this install was filed under (Y-LINKS-1). Without it the lookup
33
+ * refused the project key the dashboard mints, and the deferred deep link
34
+ * never arrived. No `store`: the SDK does not report one on events either, so
35
+ * both sides fall back to the project's default Android store.
36
+ */
37
+ internal fun requestUrl(): URL {
28
38
  val escaped = URLEncoder.encode(deviceId, "UTF-8")
29
- val connection =
30
- URL("$endpoint/sdk/conversion-data?deviceId=$escaped").openConnection()
31
- as HttpURLConnection
39
+ return URL("$endpoint/sdk/conversion-data?deviceId=$escaped&platform=android")
40
+ }
41
+
42
+ internal fun requestHeaders(): Map<String, String> = mapOf("x-api-key" to apiKey)
43
+
44
+ override fun fetch(): ConversionResult {
45
+ val connection = requestUrl().openConnection() as HttpURLConnection
32
46
  try {
33
- connection.requestMethod = "GET"
47
+ connection.requestMethod = requestMethod
34
48
  connection.connectTimeout = timeoutMs
35
49
  connection.readTimeout = timeoutMs
36
- connection.setRequestProperty("x-api-key", apiKey)
50
+ for ((name, value) in requestHeaders()) connection.setRequestProperty(name, value)
37
51
 
38
52
  val status =
39
53
  try {
40
54
  connection.responseCode
41
- } catch (_: IOException) {
55
+ } catch (error: IOException) {
42
56
  // No status to reason about; 408 marks it retryable, as sdk-core does.
43
- throw IngestError(408)
57
+ throw IngestError(408, networkCause = error.javaClass.simpleName)
44
58
  }
45
59
  if (status < 200 || status >= 300) throw IngestError(status)
46
60
 
@@ -59,8 +73,8 @@ public class HttpConversionFetcher(
59
73
  )
60
74
  } catch (error: IngestError) {
61
75
  throw error
62
- } catch (_: IOException) {
63
- throw IngestError(408)
76
+ } catch (error: IOException) {
77
+ throw IngestError(408, networkCause = error.javaClass.simpleName)
64
78
  } finally {
65
79
  connection.disconnect()
66
80
  }
@@ -38,9 +38,16 @@ internal enum class ForegroundTransition {
38
38
  * `ProcessLifecycleOwner`'s constant. We cannot take the dependency, but we can
39
39
  * take its number.
40
40
  */
41
- internal class ForegroundTracker {
41
+ internal class ForegroundTracker(seededInForeground: Boolean = false) {
42
42
  private var started = 0
43
- private var inForeground = false
43
+
44
+ /**
45
+ * Seeded by initialize to match the session it did or did not open. Seeded
46
+ * as foregrounded, the launcher activity's first start is not a second
47
+ * foreground, and the stop of an activity that started before the bridge
48
+ * was registered still reaches the background.
49
+ */
50
+ private var inForeground = seededInForeground
44
51
  private var backgroundPending = false
45
52
 
46
53
  public fun onSignal(signal: LifecycleSignal): ForegroundTransition =
@@ -5,6 +5,7 @@ import io.advenue.core.ClientEvent
5
5
  import io.advenue.core.EventTransport
6
6
  import io.advenue.core.IngestError
7
7
  import io.advenue.core.JvmHmacSigner
8
+ import io.advenue.core.iso8601
8
9
  import io.advenue.core.Signer
9
10
  import io.advenue.core.signRequest
10
11
  import io.advenue.core.parseJson
@@ -48,8 +49,17 @@ internal class HttpUrlTransport(
48
49
  * wire shape without a network round trip, which is the part that has to be
49
50
  * right.
50
51
  */
51
- internal fun buildBody(events: List<ClientEvent>): String =
52
- writeCanonicalJson(mapOf("apiKey" to apiKey, "events" to events.map { it.toMap() }))
52
+ internal fun buildBody(events: List<ClientEvent>, nowMs: Long = clock.nowMs()): String =
53
+ writeCanonicalJson(
54
+ mapOf(
55
+ "apiKey" to apiKey,
56
+ "events" to events.map { it.toMap() },
57
+ // The device clock at THIS attempt (spec 2026-10-01-data-fidelity D3):
58
+ // the server compares it with its receive time to measure the clock
59
+ // offset, so it is stamped per send and never persisted with the queue.
60
+ "sentAt" to iso8601(nowMs),
61
+ ),
62
+ )
53
63
 
54
64
  /** The headers for [body]; empty of signature headers when no secret is set. */
55
65
  internal fun buildHeaders(body: String, timestamp: String): Map<String, String> {
@@ -61,8 +71,11 @@ internal class HttpUrlTransport(
61
71
  }
62
72
 
63
73
  override fun send(events: List<ClientEvent>) {
64
- val body = buildBody(events)
65
- val headers = buildHeaders(body, clock.nowMs().toString())
74
+ // One reading for both `sentAt` and the signed timestamp: they describe the
75
+ // same instant, and two reads could straddle a clock change.
76
+ val nowMs = clock.nowMs()
77
+ val body = buildBody(events, nowMs)
78
+ val headers = buildHeaders(body, nowMs.toString())
66
79
 
67
80
  val connection = URL("$endpoint/v1/events").openConnection() as HttpURLConnection
68
81
  try {
@@ -77,9 +90,9 @@ internal class HttpUrlTransport(
77
90
  val status =
78
91
  try {
79
92
  connection.responseCode
80
- } catch (_: IOException) {
93
+ } catch (error: IOException) {
81
94
  // No response at all is a transient failure, not a poison payload.
82
- throw IngestError(408)
95
+ throw IngestError(408, networkCause = error.javaClass.simpleName)
83
96
  }
84
97
  if (status < 200 || status >= 300) throw IngestError(status)
85
98
 
@@ -92,8 +105,8 @@ internal class HttpUrlTransport(
92
105
  }
93
106
  } catch (error: IngestError) {
94
107
  throw error
95
- } catch (_: IOException) {
96
- throw IngestError(408)
108
+ } catch (error: IOException) {
109
+ throw IngestError(408, networkCause = error.javaClass.simpleName)
97
110
  } finally {
98
111
  connection.disconnect()
99
112
  }
@@ -28,6 +28,13 @@ import java.util.concurrent.TimeUnit
28
28
  */
29
29
  internal const val REFERRER_DEADLINE_MS: Long = 5_000
30
30
 
31
+ /**
32
+ * How long past [REFERRER_DEADLINE_MS] flushes stay held for a pending install:
33
+ * the enrichment thread still has to submit the install through the pipe after
34
+ * its deadline.
35
+ */
36
+ internal const val INSTALL_HOLD_MARGIN_MS: Long = 2_000
37
+
31
38
  /** A shorter budget for the enrichment that is not attribution-critical. */
32
39
  internal const val ENRICHMENT_DEADLINE_MS: Long = 3_000
33
40
 
@@ -1,6 +1,7 @@
1
1
  package io.advenue.platform
2
2
 
3
3
  import android.app.Activity
4
+ import android.app.ActivityManager
4
5
  import android.app.Application
5
6
  import android.os.Bundle
6
7
  import android.os.Handler
@@ -19,8 +20,9 @@ internal class LifecycleBridge(
19
20
  private val application: Application,
20
21
  private val onForeground: () -> Unit,
21
22
  private val onBackground: () -> Unit,
23
+ seededInForeground: Boolean,
22
24
  ) : Application.ActivityLifecycleCallbacks {
23
- private val tracker = ForegroundTracker()
25
+ private val tracker = ForegroundTracker(seededInForeground)
24
26
  private val handler = Handler(Looper.getMainLooper())
25
27
  private val debounce = Runnable { apply(tracker.onSignal(LifecycleSignal.DebounceElapsed)) }
26
28
 
@@ -70,3 +72,23 @@ internal class LifecycleBridge(
70
72
 
71
73
  override fun onActivityDestroyed(activity: Activity): Unit = Unit
72
74
  }
75
+
76
+ /**
77
+ * Whether this process is running for a user-visible launch, read at
78
+ * initialize. `Application.onCreate` runs for a tapped icon and equally for a
79
+ * push, a WorkManager job or a broadcast that wakes the process with no
80
+ * activity; only the first is a session. During a user launch the system has
81
+ * already made the process top, so its importance is `IMPORTANCE_FOREGROUND`;
82
+ * a background wake reports a service or cached importance. The same check
83
+ * crash reporters use to tell a cold start from a background start. True
84
+ * wherever it cannot be read, which keeps the pre-existing behaviour: the
85
+ * launch is a foreground.
86
+ */
87
+ internal fun readLaunchedInForeground(): Boolean =
88
+ try {
89
+ val info = ActivityManager.RunningAppProcessInfo()
90
+ ActivityManager.getMyMemoryState(info)
91
+ info.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
92
+ } catch (_: Throwable) {
93
+ true
94
+ }
package/dist/index.cjs CHANGED
@@ -65,10 +65,19 @@ module.exports = __toCommonJS(index_exports);
65
65
  // package.json
66
66
  var package_default = {
67
67
  name: "@advenue/react-native",
68
- version: "1.0.1",
68
+ version: "1.1.1",
69
69
  description: "Advenue MMP React Native SDK \u2014 attribution, install tracking, and deep links (bundled iOS/Android Expo native modules)",
70
70
  type: "module",
71
71
  license: "MIT",
72
+ repository: {
73
+ type: "git",
74
+ url: "git+https://github.com/broverse/advenue.git",
75
+ directory: "packages/sdk-react-native"
76
+ },
77
+ homepage: "https://advenue.io",
78
+ bugs: {
79
+ url: "https://github.com/broverse/advenue/issues"
80
+ },
72
81
  main: "./dist/index.js",
73
82
  types: "./dist/index.d.ts",
74
83
  exports: {
@@ -225,6 +234,7 @@ var started = false;
225
234
  var deepLinkSub = null;
226
235
  var errorSub = null;
227
236
  var onErrorHook;
237
+ var debugLogging = false;
228
238
  var consentCache = false;
229
239
  function bridge() {
230
240
  return { ios: getIosNative(), android: getAndroidNative() };
@@ -249,6 +259,7 @@ function report(context, cause) {
249
259
  onErrorHook?.(context, cause);
250
260
  } catch {
251
261
  }
262
+ if (debugLogging) console.warn(`[Advenue] ${context}`, cause);
252
263
  }
253
264
  function requireStarted() {
254
265
  if (started) return true;
@@ -262,6 +273,7 @@ var Advenue = {
262
273
  */
263
274
  async initialize(config) {
264
275
  onErrorHook = config.onError;
276
+ debugLogging = config.debug === true;
265
277
  for (const option of IGNORED_OPTIONS) {
266
278
  if (config[option] !== void 0) {
267
279
  report(
@@ -280,7 +292,11 @@ var Advenue = {
280
292
  signingSecret: config.signingSecret,
281
293
  fbAppId: config.fbAppId,
282
294
  autoTrackInstall: config.autoTrackInstall !== false,
283
- sdkVersion: package_default.version
295
+ // Names the wrapper, like Flutter's `advenue_sdk/…`: a bare version reads
296
+ // like the native SDK's own stamp.
297
+ sdkVersion: `react-native/${package_default.version}`,
298
+ debug: config.debug === true,
299
+ allowInsecureHttp: config.allowInsecureHttp === true
284
300
  });
285
301
  started = true;
286
302
  const { ios: iosForErrors, android: androidForErrors } = bridge();
package/dist/index.d.cts CHANGED
@@ -54,14 +54,12 @@ interface TrackOptions {
54
54
  interface AdvenueConfig {
55
55
  apiKey: string;
56
56
  endpoint?: string;
57
- platform?: 'ios' | 'android' | 'web' | string;
58
57
  /**
59
58
  * @deprecated Ignored: the native SDK resolves the app version from the
60
59
  * package/bundle. Passing it reports `config.ignored:appVersion` via
61
60
  * `onError`. Removed in the next major.
62
61
  */
63
62
  appVersion?: string;
64
- osVersion?: string;
65
63
  /** Gates every event on `setTrackingConsent(true)`. */
66
64
  requireConsent?: boolean;
67
65
  /** Events per request. */
@@ -74,9 +72,21 @@ interface AdvenueConfig {
74
72
  * extracted from it.
75
73
  */
76
74
  signingSecret?: string;
77
- disableAutoSessions?: boolean;
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;
78
82
  /** Called when the SDK swallows a best-effort failure. Never receives PII. */
79
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
+ */
80
90
  debug?: boolean;
81
91
  }
82
92
 
package/dist/index.d.ts CHANGED
@@ -54,14 +54,12 @@ interface TrackOptions {
54
54
  interface AdvenueConfig {
55
55
  apiKey: string;
56
56
  endpoint?: string;
57
- platform?: 'ios' | 'android' | 'web' | string;
58
57
  /**
59
58
  * @deprecated Ignored: the native SDK resolves the app version from the
60
59
  * package/bundle. Passing it reports `config.ignored:appVersion` via
61
60
  * `onError`. Removed in the next major.
62
61
  */
63
62
  appVersion?: string;
64
- osVersion?: string;
65
63
  /** Gates every event on `setTrackingConsent(true)`. */
66
64
  requireConsent?: boolean;
67
65
  /** Events per request. */
@@ -74,9 +72,21 @@ interface AdvenueConfig {
74
72
  * extracted from it.
75
73
  */
76
74
  signingSecret?: string;
77
- disableAutoSessions?: boolean;
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;
78
82
  /** Called when the SDK swallows a best-effort failure. Never receives PII. */
79
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
+ */
80
90
  debug?: boolean;
81
91
  }
82
92
 
package/dist/index.js CHANGED
@@ -54,10 +54,19 @@ var require_native_require = __commonJS({
54
54
  // package.json
55
55
  var package_default = {
56
56
  name: "@advenue/react-native",
57
- version: "1.0.1",
57
+ version: "1.1.1",
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",
61
+ repository: {
62
+ type: "git",
63
+ url: "git+https://github.com/broverse/advenue.git",
64
+ directory: "packages/sdk-react-native"
65
+ },
66
+ homepage: "https://advenue.io",
67
+ bugs: {
68
+ url: "https://github.com/broverse/advenue/issues"
69
+ },
61
70
  main: "./dist/index.js",
62
71
  types: "./dist/index.d.ts",
63
72
  exports: {
@@ -214,6 +223,7 @@ var started = false;
214
223
  var deepLinkSub = null;
215
224
  var errorSub = null;
216
225
  var onErrorHook;
226
+ var debugLogging = false;
217
227
  var consentCache = false;
218
228
  function bridge() {
219
229
  return { ios: getIosNative(), android: getAndroidNative() };
@@ -238,6 +248,7 @@ function report(context, cause) {
238
248
  onErrorHook?.(context, cause);
239
249
  } catch {
240
250
  }
251
+ if (debugLogging) console.warn(`[Advenue] ${context}`, cause);
241
252
  }
242
253
  function requireStarted() {
243
254
  if (started) return true;
@@ -251,6 +262,7 @@ var Advenue = {
251
262
  */
252
263
  async initialize(config) {
253
264
  onErrorHook = config.onError;
265
+ debugLogging = config.debug === true;
254
266
  for (const option of IGNORED_OPTIONS) {
255
267
  if (config[option] !== void 0) {
256
268
  report(
@@ -269,7 +281,11 @@ var Advenue = {
269
281
  signingSecret: config.signingSecret,
270
282
  fbAppId: config.fbAppId,
271
283
  autoTrackInstall: config.autoTrackInstall !== false,
272
- sdkVersion: package_default.version
284
+ // Names the wrapper, like Flutter's `advenue_sdk/…`: a bare version reads
285
+ // like the native SDK's own stamp.
286
+ sdkVersion: `react-native/${package_default.version}`,
287
+ debug: config.debug === true,
288
+ allowInsecureHttp: config.allowInsecureHttp === true
273
289
  });
274
290
  started = true;
275
291
  const { ios: iosForErrors, android: androidForErrors } = bridge();
@@ -44,11 +44,14 @@ public class AdvenueIosModule: Module {
44
44
  },
45
45
  // The wrapper's version, not this SDK's: a React Native release pins
46
46
  // the native snapshot inside it, so its version identifies both.
47
- 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)
48
50
  if let flushInterval = options["flushIntervalMs"] as? Int {
49
51
  config.flushIntervalMs = flushInterval
50
52
  }
51
53
  if let batchSize = options["batchSize"] as? Int { config.batchSize = batchSize }
54
+ config.debug = (options["debug"] as? Bool) ?? false
52
55
  Advenue.initialize(config)
53
56
  // Firebase App Instance ID, when the host app already ships Firebase.
54
57
  // `AdvenueFirebase` guards its import with `#if canImport`, which is