@apex-inc/capacitor-plugin 0.3.9 → 2.2.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 (73) hide show
  1. package/README.md +5 -5
  2. package/android/build.gradle +4 -2
  3. package/android/src/main/AndroidManifest.xml +35 -3
  4. package/android/src/main/java/inc/apex/capacitor/ApexCapacitorPlugin.kt +404 -7
  5. package/android/src/main/java/inc/apex/capacitor/ApexEvents.kt +122 -0
  6. package/android/src/main/java/inc/apex/capacitor/ApexFirebaseMessagingService.kt +99 -0
  7. package/android/src/main/java/inc/apex/capacitor/NativeBatchSender.kt +260 -0
  8. package/dist/batch-sender.d.ts +16 -4
  9. package/dist/batch-sender.d.ts.map +1 -1
  10. package/dist/batch-sender.js +29 -4
  11. package/dist/batch-sender.js.map +1 -1
  12. package/dist/cart-helpers.d.ts +63 -0
  13. package/dist/cart-helpers.d.ts.map +1 -0
  14. package/dist/cart-helpers.js +50 -0
  15. package/dist/cart-helpers.js.map +1 -0
  16. package/dist/definitions.d.ts +150 -5
  17. package/dist/definitions.d.ts.map +1 -1
  18. package/dist/esm/batch-sender.d.ts +16 -4
  19. package/dist/esm/batch-sender.d.ts.map +1 -1
  20. package/dist/esm/batch-sender.js +29 -4
  21. package/dist/esm/batch-sender.js.map +1 -1
  22. package/dist/esm/cart-helpers.d.ts +63 -0
  23. package/dist/esm/cart-helpers.d.ts.map +1 -0
  24. package/dist/esm/cart-helpers.js +44 -0
  25. package/dist/esm/cart-helpers.js.map +1 -0
  26. package/dist/esm/definitions.d.ts +150 -5
  27. package/dist/esm/definitions.d.ts.map +1 -1
  28. package/dist/esm/events.d.ts +85 -0
  29. package/dist/esm/events.d.ts.map +1 -0
  30. package/dist/esm/events.js +96 -0
  31. package/dist/esm/events.js.map +1 -0
  32. package/dist/esm/index.d.ts +5 -6
  33. package/dist/esm/index.d.ts.map +1 -1
  34. package/dist/esm/index.js +20 -2
  35. package/dist/esm/index.js.map +1 -1
  36. package/dist/esm/offline-queue.d.ts +15 -0
  37. package/dist/esm/offline-queue.d.ts.map +1 -1
  38. package/dist/esm/offline-queue.js +35 -0
  39. package/dist/esm/offline-queue.js.map +1 -1
  40. package/dist/esm/screen-view.d.ts +18 -0
  41. package/dist/esm/screen-view.d.ts.map +1 -0
  42. package/dist/esm/screen-view.js +28 -0
  43. package/dist/esm/screen-view.js.map +1 -0
  44. package/dist/esm/web.d.ts +29 -1
  45. package/dist/esm/web.d.ts.map +1 -1
  46. package/dist/esm/web.js +167 -2
  47. package/dist/esm/web.js.map +1 -1
  48. package/dist/events.d.ts +85 -0
  49. package/dist/events.d.ts.map +1 -0
  50. package/dist/events.js +99 -0
  51. package/dist/events.js.map +1 -0
  52. package/dist/index.d.ts +5 -6
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +28 -3
  55. package/dist/index.js.map +1 -1
  56. package/dist/offline-queue.d.ts +15 -0
  57. package/dist/offline-queue.d.ts.map +1 -1
  58. package/dist/offline-queue.js +35 -0
  59. package/dist/offline-queue.js.map +1 -1
  60. package/dist/screen-view.d.ts +18 -0
  61. package/dist/screen-view.d.ts.map +1 -0
  62. package/dist/screen-view.js +31 -0
  63. package/dist/screen-view.js.map +1 -0
  64. package/dist/web.d.ts +29 -1
  65. package/dist/web.d.ts.map +1 -1
  66. package/dist/web.js +167 -2
  67. package/dist/web.js.map +1 -1
  68. package/ios/Sources/ApexCapacitorPlugin/ApexEvents.swift +124 -0
  69. package/ios/Sources/ApexCapacitorPlugin/BatchSender.swift +24 -6
  70. package/ios/Sources/ApexCapacitorPlugin/OfflineQueue.swift +19 -0
  71. package/ios/Sources/ApexCapacitorPlugin/PushNotificationManager.swift +47 -0
  72. package/ios/Sources/ApexCapacitorPluginBridge/ApexCapacitorPlugin.swift +288 -28
  73. package/package.json +1 -1
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Apex Spec — canonical event name constants for the Android SDK.
3
+ *
4
+ * Hand-authored from `app/src/lib/core/apex-spec/events.ts`. Kept in sync
5
+ * via the parity test at
6
+ * `app/src/lib/core/apex-spec/__tests__/sdk-parity.test.ts` — CI fails if a
7
+ * canonical name is added/renamed/removed in the registry without a matching
8
+ * update here.
9
+ *
10
+ * Usage from Kotlin:
11
+ *
12
+ * plugin.track(type = ApexEvents.ADD_TO_CART, data = mapOf("product_id" to "sku_123"))
13
+ *
14
+ * Each constant is the unversioned canonical name as the developer writes it
15
+ * in `track()` calls. Versions are an internal storage primitive; the SDK
16
+ * never carries a version field.
17
+ */
18
+
19
+ package inc.apex.capacitor
20
+
21
+ object ApexEvents {
22
+ // ─── Commerce ───────────────────────────────────────────────────────
23
+ const val ADD_TO_CART = "add_to_cart"
24
+ const val REMOVE_FROM_CART = "remove_from_cart"
25
+ const val PRODUCT_VIEW = "product_view"
26
+ const val ADD_TO_WISHLIST = "add_to_wishlist"
27
+ const val CHECKOUT_STARTED = "checkout_started"
28
+ const val IN_APP_PURCHASE = "in_app_purchase"
29
+ const val PURCHASE_REFUNDED = "purchase_refunded"
30
+ const val SUBSCRIPTION_EVENT = "subscription_event"
31
+ const val ORDER_PLACED = "order_placed"
32
+ const val FIRST_SALE_COMPLETED = "first_sale_completed"
33
+
34
+ // ─── Marketing (merchant-fired triggers) ────────────────────────────
35
+ const val PRICE_DROPPED = "price_dropped"
36
+ const val RESTOCK_DUE = "restock_due"
37
+ const val REVIEW_REQUEST_DUE = "review_request_due"
38
+
39
+ // ─── Identity ───────────────────────────────────────────────────────
40
+ const val USER_SIGNED_UP = "user_signed_up"
41
+ const val USER_SIGNED_IN = "user_signed_in"
42
+ const val USER_SIGNED_OUT = "user_signed_out"
43
+ const val USER_IDENTIFIED = "user_identified"
44
+
45
+ // ─── Lifecycle ──────────────────────────────────────────────────────
46
+ const val APP_INSTALL = "app_install"
47
+ const val APP_OPEN = "app_open"
48
+ const val APP_BACKGROUND = "app_background"
49
+ const val APP_UNINSTALL = "app_uninstall"
50
+ const val APP_REINSTALL = "app_reinstall"
51
+
52
+ // ─── Session ────────────────────────────────────────────────────────
53
+ const val SESSION_START = "session_start"
54
+ const val SESSION_END = "session_end"
55
+
56
+ // ─── Engagement ─────────────────────────────────────────────────────
57
+ const val PAGE_VIEW = "page_view"
58
+ const val PAGEVIEW = "pageview"
59
+ const val SCREEN_VIEW = "screen_view"
60
+ const val SEARCH = "search"
61
+ const val SHARE = "share"
62
+ const val CONTENT_VIEW = "content_view"
63
+ const val FORM_SUBMIT = "form_submit"
64
+ const val CLICK = "click"
65
+ const val ENGAGEMENT = "engagement"
66
+ const val HEARTBEAT = "heartbeat"
67
+ const val RAGE_CLICK = "rage_click"
68
+ const val DEAD_CLICK = "dead_click"
69
+ const val GOAL_CONVERSION = "goal_conversion"
70
+ const val ONBOARDING_STEP_COMPLETED = "onboarding_step_completed"
71
+
72
+ // ─── Product (PLG lifecycle outcomes) ───────────────────────────────
73
+ const val ONBOARDING_COMPLETED = "onboarding_completed"
74
+ const val ACTIVATED = "activated"
75
+
76
+ // ─── Communication ──────────────────────────────────────────────────
77
+ const val PUSH_RECEIVED = "push_received"
78
+ const val PUSH_OPENED = "push_opened"
79
+ const val EMAIL_OPENED = "email_opened"
80
+ const val EMAIL_CLICKED = "email_clicked"
81
+ const val IN_APP_MESSAGE_SEEN = "in_app_message_seen"
82
+ const val IN_APP_MESSAGE_CLICKED = "in_app_message_clicked"
83
+
84
+ // ─── Deep link ──────────────────────────────────────────────────────
85
+ const val DEEP_LINK_OPEN = "deep_link_open"
86
+
87
+ // ─── Attribution ────────────────────────────────────────────────────
88
+ const val REATTRIBUTION = "reattribution"
89
+ const val REENGAGEMENT = "reengagement"
90
+
91
+ // ─── Smart banner ───────────────────────────────────────────────────
92
+ const val SMART_BANNER_IMPRESSION = "smart_banner_impression"
93
+ const val SMART_BANNER_CLICK = "smart_banner_click"
94
+ const val SMART_BANNER_DISMISS = "smart_banner_dismiss"
95
+
96
+ // ─── System ─────────────────────────────────────────────────────────
97
+ const val CUSTOM = "custom"
98
+ const val LIFECYCLE_TRANSITION = "lifecycle_transition"
99
+ const val JS_ERROR = "js_error"
100
+
101
+ /** Apex Spec semver this SDK implements. */
102
+ const val SPEC_VERSION = "1.1.0"
103
+
104
+ /** Complete list of every canonical event name. */
105
+ val ALL: List<String> = listOf(
106
+ ADD_TO_CART, REMOVE_FROM_CART, PRODUCT_VIEW, ADD_TO_WISHLIST, CHECKOUT_STARTED,
107
+ IN_APP_PURCHASE, PURCHASE_REFUNDED, SUBSCRIPTION_EVENT,
108
+ USER_SIGNED_UP, USER_SIGNED_IN, USER_SIGNED_OUT, USER_IDENTIFIED,
109
+ APP_INSTALL, APP_OPEN, APP_BACKGROUND, APP_UNINSTALL, APP_REINSTALL,
110
+ SESSION_START, SESSION_END,
111
+ PAGE_VIEW, PAGEVIEW, SCREEN_VIEW, SEARCH, SHARE, CONTENT_VIEW, FORM_SUBMIT,
112
+ CLICK, ENGAGEMENT, HEARTBEAT, RAGE_CLICK, DEAD_CLICK, GOAL_CONVERSION,
113
+ ONBOARDING_STEP_COMPLETED,
114
+ ONBOARDING_COMPLETED, ACTIVATED,
115
+ PUSH_RECEIVED, PUSH_OPENED, EMAIL_OPENED, EMAIL_CLICKED,
116
+ IN_APP_MESSAGE_SEEN, IN_APP_MESSAGE_CLICKED,
117
+ DEEP_LINK_OPEN,
118
+ REATTRIBUTION, REENGAGEMENT,
119
+ SMART_BANNER_IMPRESSION, SMART_BANNER_CLICK, SMART_BANNER_DISMISS,
120
+ CUSTOM, LIFECYCLE_TRANSITION, JS_ERROR,
121
+ )
122
+ }
@@ -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 workspaceKey: 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-Workspace-Key" to workspaceKey,
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 `{ workspaceKey, 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("workspaceKey", workspaceKey)
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
+ }
@@ -12,10 +12,16 @@
12
12
  */
13
13
  import type { OfflineQueue } from "./offline-queue";
14
14
  export interface BatchSenderOptions {
15
- /** API base URL. Default `https://api.apex.inc`. */
15
+ /** API base URL. Default `https://app.apex.inc`. */
16
16
  apiUrl?: string;
17
- /** Project key attached to each request. Required. */
18
- projectKey: string;
17
+ /** Workspace key attached to each request. Required. */
18
+ workspaceKey: 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. */
@@ -39,7 +45,8 @@ export interface FlushResult {
39
45
  }
40
46
  export declare class BatchSender {
41
47
  private readonly apiUrl;
42
- private readonly projectKey;
48
+ private readonly workspaceKey;
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,wDAAwD;IACxD,YAAY,EAAE,MAAM,CAAC;IACrB;;;;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,YAAY,CAAS;IACtC,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"}
@@ -19,8 +19,9 @@ class BatchSender {
19
19
  return this.apiUrl;
20
20
  }
21
21
  constructor(options) {
22
- this.apiUrl = (options.apiUrl ?? "https://api.apex.inc").replace(/\/+$/, "");
23
- this.projectKey = options.projectKey;
22
+ this.apiUrl = (options.apiUrl ?? "https://app.apex.inc").replace(/\/+$/, "");
23
+ this.workspaceKey = options.workspaceKey;
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)
@@ -98,16 +116,23 @@ class BatchSender {
98
116
  }
99
117
  async postBatch(batch) {
100
118
  const body = {
101
- projectKey: this.projectKey,
119
+ workspaceKey: this.workspaceKey,
102
120
  events: batch.map((q) => q.event),
103
121
  };
104
122
  const headers = {
105
123
  "Content-Type": "application/json",
106
- "X-Apex-Project-Key": this.projectKey,
124
+ "X-Apex-Workspace-Key": this.workspaceKey,
107
125
  };
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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"}