@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.
- package/android/build.gradle +4 -2
- package/android/src/main/AndroidManifest.xml +35 -3
- package/android/src/main/java/inc/apex/capacitor/ApexCapacitorPlugin.kt +403 -6
- package/android/src/main/java/inc/apex/capacitor/ApexEvents.kt +122 -0
- package/android/src/main/java/inc/apex/capacitor/ApexFirebaseMessagingService.kt +99 -0
- package/android/src/main/java/inc/apex/capacitor/NativeBatchSender.kt +260 -0
- package/dist/batch-sender.d.ts +12 -0
- package/dist/batch-sender.d.ts.map +1 -1
- package/dist/batch-sender.js +25 -0
- package/dist/batch-sender.js.map +1 -1
- package/dist/cart-helpers.d.ts +63 -0
- package/dist/cart-helpers.d.ts.map +1 -0
- package/dist/cart-helpers.js +50 -0
- package/dist/cart-helpers.js.map +1 -0
- package/dist/definitions.d.ts +166 -2
- package/dist/definitions.d.ts.map +1 -1
- package/dist/esm/batch-sender.d.ts +12 -0
- package/dist/esm/batch-sender.d.ts.map +1 -1
- package/dist/esm/batch-sender.js +25 -0
- package/dist/esm/batch-sender.js.map +1 -1
- package/dist/esm/cart-helpers.d.ts +63 -0
- package/dist/esm/cart-helpers.d.ts.map +1 -0
- package/dist/esm/cart-helpers.js +44 -0
- package/dist/esm/cart-helpers.js.map +1 -0
- package/dist/esm/definitions.d.ts +166 -2
- package/dist/esm/definitions.d.ts.map +1 -1
- package/dist/esm/events.d.ts +85 -0
- package/dist/esm/events.d.ts.map +1 -0
- package/dist/esm/events.js +96 -0
- package/dist/esm/events.js.map +1 -0
- package/dist/esm/index.d.ts +4 -5
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +19 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/offline-queue.d.ts +15 -0
- package/dist/esm/offline-queue.d.ts.map +1 -1
- package/dist/esm/offline-queue.js +35 -0
- package/dist/esm/offline-queue.js.map +1 -1
- package/dist/esm/screen-view.d.ts +18 -0
- package/dist/esm/screen-view.d.ts.map +1 -0
- package/dist/esm/screen-view.js +28 -0
- package/dist/esm/screen-view.js.map +1 -0
- package/dist/esm/web.d.ts +29 -1
- package/dist/esm/web.d.ts.map +1 -1
- package/dist/esm/web.js +182 -3
- package/dist/esm/web.js.map +1 -1
- package/dist/events.d.ts +85 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +99 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -2
- package/dist/index.js.map +1 -1
- package/dist/offline-queue.d.ts +15 -0
- package/dist/offline-queue.d.ts.map +1 -1
- package/dist/offline-queue.js +35 -0
- package/dist/offline-queue.js.map +1 -1
- package/dist/screen-view.d.ts +18 -0
- package/dist/screen-view.d.ts.map +1 -0
- package/dist/screen-view.js +31 -0
- package/dist/screen-view.js.map +1 -0
- package/dist/web.d.ts +29 -1
- package/dist/web.d.ts.map +1 -1
- package/dist/web.js +182 -3
- package/dist/web.js.map +1 -1
- package/ios/Sources/ApexCapacitorPlugin/ApexEvents.swift +124 -0
- package/ios/Sources/ApexCapacitorPlugin/BatchSender.swift +18 -0
- package/ios/Sources/ApexCapacitorPlugin/OfflineQueue.swift +19 -0
- package/ios/Sources/ApexCapacitorPlugin/PushNotificationManager.swift +47 -0
- package/ios/Sources/ApexCapacitorPluginBridge/ApexCapacitorPlugin.swift +280 -20
- 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
|
+
}
|
package/dist/batch-sender.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/batch-sender.js
CHANGED
|
@@ -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,
|
package/dist/batch-sender.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batch-sender.js","sourceRoot":"","sources":["../src/batch-sender.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;
|
|
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"}
|