@apex-inc/capacitor-plugin 0.3.8 → 2.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.
Files changed (72) hide show
  1. package/android/build.gradle +4 -2
  2. package/android/src/main/AndroidManifest.xml +35 -3
  3. package/android/src/main/java/inc/apex/capacitor/ApexCapacitorPlugin.kt +403 -6
  4. package/android/src/main/java/inc/apex/capacitor/ApexEvents.kt +122 -0
  5. package/android/src/main/java/inc/apex/capacitor/ApexFirebaseMessagingService.kt +99 -0
  6. package/android/src/main/java/inc/apex/capacitor/NativeBatchSender.kt +260 -0
  7. package/dist/batch-sender.d.ts +12 -0
  8. package/dist/batch-sender.d.ts.map +1 -1
  9. package/dist/batch-sender.js +25 -0
  10. package/dist/batch-sender.js.map +1 -1
  11. package/dist/cart-helpers.d.ts +63 -0
  12. package/dist/cart-helpers.d.ts.map +1 -0
  13. package/dist/cart-helpers.js +50 -0
  14. package/dist/cart-helpers.js.map +1 -0
  15. package/dist/definitions.d.ts +166 -2
  16. package/dist/definitions.d.ts.map +1 -1
  17. package/dist/esm/batch-sender.d.ts +12 -0
  18. package/dist/esm/batch-sender.d.ts.map +1 -1
  19. package/dist/esm/batch-sender.js +25 -0
  20. package/dist/esm/batch-sender.js.map +1 -1
  21. package/dist/esm/cart-helpers.d.ts +63 -0
  22. package/dist/esm/cart-helpers.d.ts.map +1 -0
  23. package/dist/esm/cart-helpers.js +44 -0
  24. package/dist/esm/cart-helpers.js.map +1 -0
  25. package/dist/esm/definitions.d.ts +166 -2
  26. package/dist/esm/definitions.d.ts.map +1 -1
  27. package/dist/esm/events.d.ts +85 -0
  28. package/dist/esm/events.d.ts.map +1 -0
  29. package/dist/esm/events.js +96 -0
  30. package/dist/esm/events.js.map +1 -0
  31. package/dist/esm/index.d.ts +4 -5
  32. package/dist/esm/index.d.ts.map +1 -1
  33. package/dist/esm/index.js +19 -1
  34. package/dist/esm/index.js.map +1 -1
  35. package/dist/esm/offline-queue.d.ts +15 -0
  36. package/dist/esm/offline-queue.d.ts.map +1 -1
  37. package/dist/esm/offline-queue.js +35 -0
  38. package/dist/esm/offline-queue.js.map +1 -1
  39. package/dist/esm/screen-view.d.ts +18 -0
  40. package/dist/esm/screen-view.d.ts.map +1 -0
  41. package/dist/esm/screen-view.js +28 -0
  42. package/dist/esm/screen-view.js.map +1 -0
  43. package/dist/esm/web.d.ts +29 -1
  44. package/dist/esm/web.d.ts.map +1 -1
  45. package/dist/esm/web.js +182 -3
  46. package/dist/esm/web.js.map +1 -1
  47. package/dist/events.d.ts +85 -0
  48. package/dist/events.d.ts.map +1 -0
  49. package/dist/events.js +99 -0
  50. package/dist/events.js.map +1 -0
  51. package/dist/index.d.ts +4 -5
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +27 -2
  54. package/dist/index.js.map +1 -1
  55. package/dist/offline-queue.d.ts +15 -0
  56. package/dist/offline-queue.d.ts.map +1 -1
  57. package/dist/offline-queue.js +35 -0
  58. package/dist/offline-queue.js.map +1 -1
  59. package/dist/screen-view.d.ts +18 -0
  60. package/dist/screen-view.d.ts.map +1 -0
  61. package/dist/screen-view.js +31 -0
  62. package/dist/screen-view.js.map +1 -0
  63. package/dist/web.d.ts +29 -1
  64. package/dist/web.d.ts.map +1 -1
  65. package/dist/web.js +182 -3
  66. package/dist/web.js.map +1 -1
  67. package/ios/Sources/ApexCapacitorPlugin/ApexEvents.swift +124 -0
  68. package/ios/Sources/ApexCapacitorPlugin/BatchSender.swift +18 -0
  69. package/ios/Sources/ApexCapacitorPlugin/OfflineQueue.swift +19 -0
  70. package/ios/Sources/ApexCapacitorPlugin/PushNotificationManager.swift +47 -0
  71. package/ios/Sources/ApexCapacitorPluginBridge/ApexCapacitorPlugin.swift +280 -20
  72. package/package.json +1 -1
@@ -0,0 +1,99 @@
1
+ package inc.apex.capacitor
2
+
3
+ import android.app.NotificationChannel
4
+ import android.app.NotificationManager
5
+ import android.app.PendingIntent
6
+ import android.content.Context
7
+ import android.content.Intent
8
+ import android.os.Build
9
+ import androidx.core.app.NotificationCompat
10
+ import com.google.firebase.messaging.FirebaseMessagingService
11
+ import com.google.firebase.messaging.RemoteMessage
12
+
13
+ /**
14
+ * FCM messaging service for Android push.
15
+ *
16
+ * - `onNewToken` — registers the (refreshed) device token with
17
+ * Apex via `ApexCapacitorPlugin.registerTokenFromService`.
18
+ * - `onMessageReceived` — for foreground/data messages, posts a system
19
+ * notification whose tap PendingIntent re-opens
20
+ * the launcher activity carrying the apex push
21
+ * data (`apex_url`, `apex_messageId`,
22
+ * `apex_apexToken`) as extras. The plugin reads
23
+ * those on launch and fires `pushClicked`, which
24
+ * the app uses to report engagement + route.
25
+ *
26
+ * For background "notification" messages the system tray renders the
27
+ * banner directly and FCM copies the `data` bag into the launcher
28
+ * intent extras on tap — same `apex_*` keys, same plugin handling.
29
+ */
30
+ class ApexFirebaseMessagingService : FirebaseMessagingService() {
31
+
32
+ override fun onNewToken(token: String) {
33
+ super.onNewToken(token)
34
+ try {
35
+ ApexCapacitorPlugin.registerTokenFromService(applicationContext, token)
36
+ } catch (_: Throwable) {
37
+ // best-effort — a registration failure must not crash the service
38
+ }
39
+ }
40
+
41
+ override fun onMessageReceived(message: RemoteMessage) {
42
+ super.onMessageReceived(message)
43
+ val data = message.data
44
+ val title = message.notification?.title ?: data["title"] ?: "Notification"
45
+ val body = message.notification?.body ?: data["body"] ?: ""
46
+
47
+ val launch = packageManager.getLaunchIntentForPackage(packageName)
48
+ if (launch != null) {
49
+ launch.flags =
50
+ Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
51
+ data["url"]?.let { launch.putExtra("apex_url", it) }
52
+ data["messageId"]?.let { launch.putExtra("apex_messageId", it) }
53
+ data["apexToken"]?.let { launch.putExtra("apex_apexToken", it) }
54
+ }
55
+
56
+ val flags =
57
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
58
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
59
+ } else {
60
+ PendingIntent.FLAG_UPDATE_CURRENT
61
+ }
62
+ val contentIntent =
63
+ launch?.let {
64
+ PendingIntent.getActivity(this, message.messageId?.hashCode() ?: 0, it, flags)
65
+ }
66
+
67
+ ensureChannel()
68
+ val notification = NotificationCompat.Builder(this, CHANNEL_ID)
69
+ .setContentTitle(title)
70
+ .setContentText(body)
71
+ .setSmallIcon(applicationInfo.icon)
72
+ .setAutoCancel(true)
73
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
74
+ .apply { if (contentIntent != null) setContentIntent(contentIntent) }
75
+ .build()
76
+
77
+ val mgr = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
78
+ mgr.notify(message.messageId?.hashCode() ?: System.currentTimeMillis().toInt(), notification)
79
+ }
80
+
81
+ private fun ensureChannel() {
82
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
83
+ val mgr = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
84
+ if (mgr.getNotificationChannel(CHANNEL_ID) == null) {
85
+ mgr.createNotificationChannel(
86
+ NotificationChannel(
87
+ CHANNEL_ID,
88
+ "Notifications",
89
+ NotificationManager.IMPORTANCE_HIGH,
90
+ ),
91
+ )
92
+ }
93
+ }
94
+ }
95
+
96
+ companion object {
97
+ private const val CHANNEL_ID = "apex_default"
98
+ }
99
+ }
@@ -0,0 +1,260 @@
1
+ package inc.apex.capacitor
2
+
3
+ import org.json.JSONArray
4
+ import org.json.JSONObject
5
+ import java.net.HttpURLConnection
6
+ import java.net.URL
7
+ import java.util.concurrent.Executor
8
+ import java.util.concurrent.Executors
9
+
10
+ /**
11
+ * MMP-215 — Drains [NativeOfflineQueue] by POSTing batches of events to
12
+ * `${apiBaseUrl}/api/events`. Mirrors the iOS `BatchSender.swift` so the
13
+ * on-disk Android queue isn't stranded.
14
+ *
15
+ * Before this class shipped, `flushQueue()` on Android was a no-op —
16
+ * `track()` enqueued to the native file-backed queue and nothing ever
17
+ * drained it, so any event captured while the Capacitor webview wasn't
18
+ * mounted was lost forever. iOS shipped its sender in MMP-205; this is
19
+ * the parity port.
20
+ *
21
+ * Concurrency model:
22
+ * - Serial single-threaded executor gates send attempts so two flushes
23
+ * don't race over the same batch (parity with Swift's serial
24
+ * DispatchQueue).
25
+ * - HTTP is synchronous via `HttpURLConnection` (no OkHttp / coroutines
26
+ * dep — keeps APK delta small). The whole drain runs on the executor.
27
+ * - Retry backoff sleeps on the executor thread; since callers fire
28
+ * `flush()` and observe via the completion callback, blocking the
29
+ * serial thread for a retry doesn't surface as a UI freeze.
30
+ */
31
+ class NativeBatchSender(
32
+ apiBaseUrl: String,
33
+ private val projectKey: String,
34
+ private val platformHeader: String = "android",
35
+ private val apiKey: String? = null,
36
+ batchSize: Int = 50,
37
+ maxRetries: Int = 3,
38
+ baseBackoffMs: Long = 1000L,
39
+ private val debug: Boolean = false,
40
+ private val httpClient: HttpClient = DefaultHttpClient(),
41
+ private val executor: Executor = Executors.newSingleThreadExecutor { r ->
42
+ Thread(r, "inc.apex.batch-sender").apply { isDaemon = true }
43
+ },
44
+ private val sleeper: (Long) -> Unit = { ms -> Thread.sleep(ms) },
45
+ ) {
46
+
47
+ private val apiBaseUrl: String = apiBaseUrl.trimEnd('/')
48
+ private val batchSize: Int = maxOf(1, batchSize)
49
+ private val maxRetries: Int = maxOf(1, maxRetries)
50
+ private val baseBackoffMs: Long = maxOf(0L, baseBackoffMs)
51
+
52
+ @Volatile
53
+ private var inFlight: Boolean = false
54
+ private val inFlightLock = Any()
55
+
56
+ /**
57
+ * Drains the queue, one batch at a time, with exponential backoff on
58
+ * retryable failures. Safe to call concurrently — only one flush runs
59
+ * at a time; concurrent callers receive `(0, currentSize)` and the
60
+ * in-flight drain keeps going.
61
+ *
62
+ * @param callback fires exactly once with (flushed, remaining).
63
+ */
64
+ fun flush(queue: NativeOfflineQueue, callback: (flushed: Int, remaining: Int) -> Unit) {
65
+ executor.execute {
66
+ synchronized(inFlightLock) {
67
+ if (inFlight) {
68
+ if (debug) log("flush called while drain in flight — no-op")
69
+ callback(0, queue.size())
70
+ return@execute
71
+ }
72
+ inFlight = true
73
+ }
74
+ val flushed = try {
75
+ drainLoop(queue, accumulated = 0)
76
+ } catch (t: Throwable) {
77
+ if (debug) log("drainLoop unexpected throw: ${t.message}")
78
+ 0
79
+ } finally {
80
+ synchronized(inFlightLock) { inFlight = false }
81
+ }
82
+ callback(flushed, queue.size())
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Recursive drain. Returns the running total of successfully flushed
88
+ * events. Stops at the first non-retryable error or exhausted retry.
89
+ */
90
+ private fun drainLoop(queue: NativeOfflineQueue, accumulated: Int): Int {
91
+ val batch = try {
92
+ queue.peek(batchSize)
93
+ } catch (e: Exception) {
94
+ if (debug) log("peek failed: ${e.message}")
95
+ return accumulated
96
+ }
97
+ if (batch.isEmpty()) return accumulated
98
+
99
+ return when (val outcome = sendBatchWithRetry(batch, attempt = 0)) {
100
+ SendOutcome.Success -> {
101
+ queue.markSent(batch.map { it.id })
102
+ if (debug) log("flushed batch of ${batch.size}")
103
+ drainLoop(queue, accumulated + batch.size)
104
+ }
105
+ is SendOutcome.NonRetryable -> {
106
+ if (debug) log("non-retryable HTTP ${outcome.statusCode}; leaving events queued")
107
+ accumulated
108
+ }
109
+ is SendOutcome.RetryableExhausted -> {
110
+ queue.markFailed(batch.map { it.id })
111
+ if (debug) log("retries exhausted: ${outcome.errorMessage}")
112
+ accumulated
113
+ }
114
+ }
115
+ }
116
+
117
+ private fun sendBatchWithRetry(
118
+ batch: List<NativeQueuedEvent>,
119
+ attempt: Int,
120
+ ): SendOutcome {
121
+ if (batch.isEmpty()) return SendOutcome.Success
122
+
123
+ val url = "$apiBaseUrl/api/events"
124
+ val body = buildRequestBody(batch)
125
+ val headers = mutableMapOf(
126
+ "Content-Type" to "application/json",
127
+ "X-Apex-Project-Key" to projectKey,
128
+ "X-Apex-Platform" to platformHeader,
129
+ )
130
+ apiKey?.let { headers["x-apex-api-key"] = it }
131
+
132
+ if (debug) log("POST $url (${batch.size} events, attempt ${attempt + 1}/$maxRetries)")
133
+
134
+ val response = try {
135
+ httpClient.send(url, headers, body, timeoutMs = 20_000)
136
+ } catch (e: Exception) {
137
+ return retryOrFail(batch, attempt, e.message ?: "network error")
138
+ }
139
+
140
+ return when {
141
+ response.statusCode in 200..299 -> SendOutcome.Success
142
+ response.statusCode in 400..499 ->
143
+ // Client error — don't retry; events stay queued for
144
+ // inspection but we stop hammering the server.
145
+ SendOutcome.NonRetryable(response.statusCode)
146
+ else ->
147
+ retryOrFail(
148
+ batch,
149
+ attempt,
150
+ response.errorMessage ?: "HTTP ${response.statusCode}",
151
+ )
152
+ }
153
+ }
154
+
155
+ private fun retryOrFail(
156
+ batch: List<NativeQueuedEvent>,
157
+ attempt: Int,
158
+ errorMessage: String,
159
+ ): SendOutcome {
160
+ if (attempt + 1 >= maxRetries) {
161
+ return SendOutcome.RetryableExhausted(errorMessage)
162
+ }
163
+ val backoffMs = baseBackoffMs shl attempt
164
+ if (debug) log("retry in ${backoffMs}ms ($errorMessage)")
165
+ sleeper(backoffMs)
166
+ return sendBatchWithRetry(batch, attempt + 1)
167
+ }
168
+
169
+ private fun buildRequestBody(batch: List<NativeQueuedEvent>): ByteArray {
170
+ // Server accepts `{ projectKey, events: [{ id, ...payload }] }`.
171
+ // Unwrap the queued envelope so `id` lives at the top level of
172
+ // each event (matches /api/events expectations and the JS-side
173
+ // BatchSender shape).
174
+ //
175
+ // MMP-081 — stamp `clientType="native-android"` if the queued
176
+ // payload didn't already carry one (caller-supplied values win,
177
+ // which preserves test fixtures and unusual workflows).
178
+ val eventsArray = JSONArray()
179
+ for (event in batch) {
180
+ val flat = JSONObject(event.payload.toMap())
181
+ flat.put("id", event.id)
182
+ if (!flat.has("clientType") || flat.isNull("clientType")) {
183
+ flat.put("clientType", "native-android")
184
+ }
185
+ eventsArray.put(flat)
186
+ }
187
+ val payload = JSONObject()
188
+ .put("projectKey", projectKey)
189
+ .put("events", eventsArray)
190
+ return payload.toString().toByteArray(Charsets.UTF_8)
191
+ }
192
+
193
+ private fun log(msg: String) {
194
+ println("[apex-capacitor] $msg")
195
+ }
196
+
197
+ // ── Result types ──────────────────────────────────────────────────
198
+
199
+ sealed class SendOutcome {
200
+ object Success : SendOutcome()
201
+ data class NonRetryable(val statusCode: Int) : SendOutcome()
202
+ data class RetryableExhausted(val errorMessage: String) : SendOutcome()
203
+ }
204
+
205
+ // ── HTTP abstraction (test-swappable) ─────────────────────────────
206
+
207
+ /**
208
+ * Minimal HTTP transport. Tests inject a fake; production uses
209
+ * [DefaultHttpClient]. Synchronous by design — the caller is already
210
+ * on the executor thread.
211
+ */
212
+ interface HttpClient {
213
+ fun send(
214
+ url: String,
215
+ headers: Map<String, String>,
216
+ body: ByteArray,
217
+ timeoutMs: Int,
218
+ ): HttpResponse
219
+ }
220
+
221
+ data class HttpResponse(
222
+ val statusCode: Int,
223
+ val errorMessage: String? = null,
224
+ )
225
+
226
+ /**
227
+ * Production HTTP client. Uses [HttpURLConnection] — same approach
228
+ * as the rest of the plugin (no OkHttp dependency).
229
+ */
230
+ class DefaultHttpClient : HttpClient {
231
+ override fun send(
232
+ url: String,
233
+ headers: Map<String, String>,
234
+ body: ByteArray,
235
+ timeoutMs: Int,
236
+ ): HttpResponse {
237
+ val conn = (URL(url).openConnection() as HttpURLConnection).apply {
238
+ requestMethod = "POST"
239
+ doOutput = true
240
+ connectTimeout = timeoutMs
241
+ readTimeout = timeoutMs
242
+ useCaches = false
243
+ for ((k, v) in headers) setRequestProperty(k, v)
244
+ }
245
+ try {
246
+ conn.outputStream.use { it.write(body) }
247
+ val code = conn.responseCode
248
+ return HttpResponse(statusCode = code)
249
+ } catch (e: Exception) {
250
+ return HttpResponse(statusCode = 0, errorMessage = e.message ?: "io error")
251
+ } finally {
252
+ try {
253
+ conn.disconnect()
254
+ } catch (_: Exception) {
255
+ // best-effort
256
+ }
257
+ }
258
+ }
259
+ }
260
+ }
@@ -16,6 +16,12 @@ export interface BatchSenderOptions {
16
16
  apiUrl?: string;
17
17
  /** Project key attached to each request. Required. */
18
18
  projectKey: string;
19
+ /**
20
+ * Optional workspace-bound SDK key (`apex_uk_*` / `apex_sk_*`).
21
+ * Forwarded as `x-apex-api-key` on every batch so the server can
22
+ * authorize quarantine-mode auto-stitch.
23
+ */
24
+ apiKey?: string;
19
25
  /** Max events sent per batch. Default 50. */
20
26
  batchSize?: number;
21
27
  /** Max retry attempts before giving up on a batch. Default 3. */
@@ -40,6 +46,7 @@ export interface FlushResult {
40
46
  export declare class BatchSender {
41
47
  private readonly apiUrl;
42
48
  private readonly projectKey;
49
+ private readonly apiKey?;
43
50
  /** Exposed for callers (e.g. getVariant) that need to address the same server. */
44
51
  getApiUrl(): string;
45
52
  private readonly batchSize;
@@ -54,6 +61,11 @@ export declare class BatchSender {
54
61
  * Flushes as many events from the queue as possible, one batch at a time,
55
62
  * with exponential backoff on retryable failures. Stops early on
56
63
  * non-retryable errors (400/401/403) and leaves remaining events queued.
64
+ *
65
+ * MOBX-005 (post-mortem #5) — every flush starts with queue hygiene:
66
+ * events older than 7 days or with more than 20 failed delivery
67
+ * attempts are evicted so a poisoned head can never block fresh
68
+ * events indefinitely.
57
69
  */
58
70
  flush(queue: OfflineQueue): Promise<FlushResult>;
59
71
  private sendBatchWithRetry;
@@ -1 +1 @@
1
- {"version":3,"file":"batch-sender.d.ts","sourceRoot":"","sources":["../src/batch-sender.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,iBAAiB,CAAC;AAEjE,MAAM,WAAW,kBAAkB;IACjC,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,0EAA0E;IAC1E,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,sDAAsD;IACtD,cAAc,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,KAAK,CAAC;IAC3C,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IAEpC,kFAAkF;IAClF,SAAS,IAAI,MAAM;IAInB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgC;IACxD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAA4B;IAC5D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAU;gBAEpB,OAAO,EAAE,kBAAkB;IAiBvC;;;;OAIG;IACG,KAAK,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;YAgCxC,kBAAkB;YAyClB,SAAS;CAoBxB"}
1
+ {"version":3,"file":"batch-sender.d.ts","sourceRoot":"","sources":["../src/batch-sender.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,iBAAiB,CAAC;AAEjE,MAAM,WAAW,kBAAkB;IACjC,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,0EAA0E;IAC1E,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,sDAAsD;IACtD,cAAc,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,KAAK,CAAC;IAC3C,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAS;IAEjC,kFAAkF;IAClF,SAAS,IAAI,MAAM;IAInB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgC;IACxD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAA4B;IAC5D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAU;gBAEpB,OAAO,EAAE,kBAAkB;IAkBvC;;;;;;;;;OASG;IACG,KAAK,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;YA8CxC,kBAAkB;YAyClB,SAAS;CA2BxB"}
@@ -21,6 +21,7 @@ class BatchSender {
21
21
  constructor(options) {
22
22
  this.apiUrl = (options.apiUrl ?? "https://api.apex.inc").replace(/\/+$/, "");
23
23
  this.projectKey = options.projectKey;
24
+ this.apiKey = options.apiKey;
24
25
  this.batchSize = options.batchSize ?? 50;
25
26
  this.maxRetries = options.maxRetries ?? 3;
26
27
  this.baseBackoffMs = options.baseBackoffMs ?? 1000;
@@ -37,11 +38,28 @@ class BatchSender {
37
38
  * Flushes as many events from the queue as possible, one batch at a time,
38
39
  * with exponential backoff on retryable failures. Stops early on
39
40
  * non-retryable errors (400/401/403) and leaves remaining events queued.
41
+ *
42
+ * MOBX-005 (post-mortem #5) — every flush starts with queue hygiene:
43
+ * events older than 7 days or with more than 20 failed delivery
44
+ * attempts are evicted so a poisoned head can never block fresh
45
+ * events indefinitely.
40
46
  */
41
47
  async flush(queue) {
42
48
  let flushed = 0;
43
49
  let attemptedBatches = 0;
44
50
  let lastError;
51
+ try {
52
+ const evicted = await queue.evictStale({
53
+ maxAttempts: 20,
54
+ maxAgeMs: 7 * 24 * 60 * 60 * 1000,
55
+ });
56
+ if (evicted > 0 && this.debug) {
57
+ console.warn(`[apex-capacitor] evicted ${evicted} stale event(s) from the offline queue (TTL 7d / 20 attempts)`);
58
+ }
59
+ }
60
+ catch {
61
+ /* hygiene is best-effort — never block the flush */
62
+ }
45
63
  while (true) {
46
64
  const batch = await queue.peek(this.batchSize);
47
65
  if (batch.length === 0)
@@ -108,6 +126,13 @@ class BatchSender {
108
126
  if (this.platformHeader) {
109
127
  headers["X-Apex-Platform"] = this.platformHeader;
110
128
  }
129
+ if (this.apiKey) {
130
+ // Phase 2-quarantine — workspace-bound SDK key. The server uses
131
+ // this to authorize auto-stitch on `user_signed_up` /
132
+ // `user_identified`. Missing key means events still flow but
133
+ // auto-stitch is suppressed (the fail-closed default).
134
+ headers["x-apex-api-key"] = this.apiKey;
135
+ }
111
136
  return this.fetchFn(`${this.apiUrl}/api/events`, {
112
137
  method: "POST",
113
138
  headers,
@@ -1 +1 @@
1
- {"version":3,"file":"batch-sender.js","sourceRoot":"","sources":["../src/batch-sender.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"batch-sender.js","sourceRoot":"","sources":["../src/batch-sender.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Journey Exit Semantics — SDK catch-up.
3
+ *
4
+ * Typed cart helpers. Operators who reach for "track an add-to-cart"
5
+ * with the raw `Apex.track({ type: "add_to_cart", data: { ... } })`
6
+ * have to invent the data shape — which means the server-side cart
7
+ * rollup writer (Phase 2) reads inconsistent SKUs/qtys/IDs and the
8
+ * `Contact.cart` state drifts.
9
+ *
10
+ * These helpers emit the canonical shape the rollup expects so
11
+ * branches on `cart.itemCount` / `cart.valueCents` light up
12
+ * automatically with zero downstream configuration.
13
+ *
14
+ * All helpers delegate to `Apex.track()` so the existing offline
15
+ * queue / batch-sender / session-manager paths stay unchanged.
16
+ */
17
+ /**
18
+ * Canonical line shape consumed by the server-side cart rollup. Mirrors
19
+ * the same interface in `@apex-inc/sdk` for documentation parity.
20
+ */
21
+ export interface CartLine {
22
+ /** Merchant SKU. Required for stable identity. */
23
+ sku: string;
24
+ /** Items in the line. Negative deltas clamp at 0 server-side. */
25
+ qty: number;
26
+ priceCents?: number;
27
+ currency?: string;
28
+ /** Display name for the Contact-drawer rendering. */
29
+ name?: string;
30
+ /**
31
+ * Stable line identifier when one SKU can have multiple cart lines
32
+ * (e.g. configurable products with different variants). Defaults
33
+ * to `sku` server-side when omitted.
34
+ */
35
+ stableLineId?: string;
36
+ }
37
+ /** Add an item to the cart. */
38
+ export declare function trackCartAdd(line: CartLine): Promise<void>;
39
+ /** Remove an item from the cart. */
40
+ export declare function trackCartRemove(opts: {
41
+ sku: string;
42
+ qty: number;
43
+ stableLineId?: string;
44
+ cartValueCents?: number;
45
+ currency?: string;
46
+ }): Promise<void>;
47
+ /**
48
+ * Full-cart reconciliation. Use whenever the app loads the source-of-
49
+ * truth cart (login restore, foreground refresh, cart screen mount).
50
+ * The rollup writer replaces stored lines wholesale on this event,
51
+ * which heals drift caused by missed add/remove deltas.
52
+ */
53
+ export declare function trackCartSnapshot(opts: {
54
+ lines: CartLine[];
55
+ currency?: string;
56
+ }): Promise<void>;
57
+ /** Clear the cart on successful checkout. */
58
+ export declare function trackCheckoutCompleted(opts: {
59
+ orderId: string;
60
+ totalCents: number;
61
+ currency?: string;
62
+ }): Promise<void>;
63
+ //# sourceMappingURL=cart-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cart-helpers.d.ts","sourceRoot":"","sources":["../src/cart-helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH;;;GAGG;AACH,MAAM,WAAW,QAAQ;IACvB,kDAAkD;IAClD,GAAG,EAAE,MAAM,CAAC;IACZ,iEAAiE;IACjE,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,+BAA+B;AAC/B,wBAAsB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhE;AAED,oCAAoC;AACpC,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhB;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAMhB;AAED,6CAA6C;AAC7C,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhB"}
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ /**
3
+ * Journey Exit Semantics — SDK catch-up.
4
+ *
5
+ * Typed cart helpers. Operators who reach for "track an add-to-cart"
6
+ * with the raw `Apex.track({ type: "add_to_cart", data: { ... } })`
7
+ * have to invent the data shape — which means the server-side cart
8
+ * rollup writer (Phase 2) reads inconsistent SKUs/qtys/IDs and the
9
+ * `Contact.cart` state drifts.
10
+ *
11
+ * These helpers emit the canonical shape the rollup expects so
12
+ * branches on `cart.itemCount` / `cart.valueCents` light up
13
+ * automatically with zero downstream configuration.
14
+ *
15
+ * All helpers delegate to `Apex.track()` so the existing offline
16
+ * queue / batch-sender / session-manager paths stay unchanged.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.trackCartAdd = trackCartAdd;
20
+ exports.trackCartRemove = trackCartRemove;
21
+ exports.trackCartSnapshot = trackCartSnapshot;
22
+ exports.trackCheckoutCompleted = trackCheckoutCompleted;
23
+ const index_1 = require("./index");
24
+ const events_1 = require("./events");
25
+ /** Add an item to the cart. */
26
+ async function trackCartAdd(line) {
27
+ await index_1.Apex.track({ type: events_1.ApexEvents.AddToCart, data: { ...line } });
28
+ }
29
+ /** Remove an item from the cart. */
30
+ async function trackCartRemove(opts) {
31
+ await index_1.Apex.track({ type: events_1.ApexEvents.RemoveFromCart, data: { ...opts } });
32
+ }
33
+ /**
34
+ * Full-cart reconciliation. Use whenever the app loads the source-of-
35
+ * truth cart (login restore, foreground refresh, cart screen mount).
36
+ * The rollup writer replaces stored lines wholesale on this event,
37
+ * which heals drift caused by missed add/remove deltas.
38
+ */
39
+ async function trackCartSnapshot(opts) {
40
+ // `cart_snapshot` isn't in the published ApexEvents enum yet (it's
41
+ // a Phase-2 addition). Passing the canonical string lets older
42
+ // plugin versions still emit it cleanly; the server route accepts
43
+ // arbitrary string event types.
44
+ await index_1.Apex.track({ type: "cart_snapshot", data: { ...opts } });
45
+ }
46
+ /** Clear the cart on successful checkout. */
47
+ async function trackCheckoutCompleted(opts) {
48
+ await index_1.Apex.track({ type: "checkout_completed", data: { ...opts } });
49
+ }
50
+ //# sourceMappingURL=cart-helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cart-helpers.js","sourceRoot":"","sources":["../src/cart-helpers.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;AA2BH,oCAEC;AAGD,0CAQC;AAQD,8CASC;AAGD,wDAMC;AAhED,mCAA+B;AAC/B,qCAAsC;AAuBtC,+BAA+B;AACxB,KAAK,UAAU,YAAY,CAAC,IAAc;IAC/C,MAAM,YAAI,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,mBAAU,CAAC,SAAS,EAAE,IAAI,EAAE,EAAE,GAAG,IAAI,EAAE,EAAE,CAAC,CAAC;AACtE,CAAC;AAED,oCAAoC;AAC7B,KAAK,UAAU,eAAe,CAAC,IAMrC;IACC,MAAM,YAAI,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,mBAAU,CAAC,cAAc,EAAE,IAAI,EAAE,EAAE,GAAG,IAAI,EAAE,EAAE,CAAC,CAAC;AAC3E,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,iBAAiB,CAAC,IAGvC;IACC,mEAAmE;IACnE,+DAA+D;IAC/D,kEAAkE;IAClE,gCAAgC;IAChC,MAAM,YAAI,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,EAAE,GAAG,IAAI,EAAE,EAAE,CAAC,CAAC;AACjE,CAAC;AAED,6CAA6C;AACtC,KAAK,UAAU,sBAAsB,CAAC,IAI5C;IACC,MAAM,YAAI,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,EAAE,GAAG,IAAI,EAAE,EAAE,CAAC,CAAC;AACtE,CAAC"}