expo-observe 57.0.20 → 57.0.22

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 (57) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/android/build.gradle +0 -25
  3. package/android/src/main/java/expo/modules/observe/Constants.kt +0 -1
  4. package/android/src/main/java/expo/modules/observe/CursorRepair.kt +58 -0
  5. package/android/src/main/java/expo/modules/observe/DispatchUtils.kt +0 -22
  6. package/android/src/main/java/expo/modules/observe/Event.kt +1 -1
  7. package/android/src/main/java/expo/modules/observe/ObservabilityBackgroundWorker.kt +1 -12
  8. package/android/src/main/java/expo/modules/observe/ObservabilityManager.kt +105 -142
  9. package/android/src/main/java/expo/modules/observe/ObservePreferences.kt +22 -0
  10. package/expo-module.config.json +1 -1
  11. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.22/expo.modules.observe-57.0.22-sources.jar +0 -0
  12. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.22/expo.modules.observe-57.0.22-sources.jar.md5 +1 -0
  13. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.22/expo.modules.observe-57.0.22-sources.jar.sha1 +1 -0
  14. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.22/expo.modules.observe-57.0.22-sources.jar.sha256 +1 -0
  15. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.22/expo.modules.observe-57.0.22-sources.jar.sha512 +1 -0
  16. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.22/expo.modules.observe-57.0.22.aar +0 -0
  17. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.22/expo.modules.observe-57.0.22.aar.md5 +1 -0
  18. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.22/expo.modules.observe-57.0.22.aar.sha1 +1 -0
  19. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.22/expo.modules.observe-57.0.22.aar.sha256 +1 -0
  20. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.22/expo.modules.observe-57.0.22.aar.sha512 +1 -0
  21. package/local-maven-repo/expo/modules/observe/expo.modules.observe/{57.0.20/expo.modules.observe-57.0.20.module → 57.0.22/expo.modules.observe-57.0.22.module} +25 -39
  22. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.22/expo.modules.observe-57.0.22.module.md5 +1 -0
  23. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.22/expo.modules.observe-57.0.22.module.sha1 +1 -0
  24. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.22/expo.modules.observe-57.0.22.module.sha256 +1 -0
  25. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.22/expo.modules.observe-57.0.22.module.sha512 +1 -0
  26. package/local-maven-repo/expo/modules/observe/expo.modules.observe/{57.0.20/expo.modules.observe-57.0.20.pom → 57.0.22/expo.modules.observe-57.0.22.pom} +4 -16
  27. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.22/expo.modules.observe-57.0.22.pom.md5 +1 -0
  28. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.22/expo.modules.observe-57.0.22.pom.sha1 +1 -0
  29. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.22/expo.modules.observe-57.0.22.pom.sha256 +1 -0
  30. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.22/expo.modules.observe-57.0.22.pom.sha512 +1 -0
  31. package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml +4 -4
  32. package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.md5 +1 -1
  33. package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.sha1 +1 -1
  34. package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.sha256 +1 -1
  35. package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.sha512 +1 -1
  36. package/package.json +5 -5
  37. package/android/src/main/java/expo/modules/observe/storage/ObserveDatabase.kt +0 -94
  38. package/android/src/main/java/expo/modules/observe/storage/PendingLogsManager.kt +0 -42
  39. package/android/src/main/java/expo/modules/observe/storage/PendingMetricsManager.kt +0 -42
  40. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.20/expo.modules.observe-57.0.20-sources.jar +0 -0
  41. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.20/expo.modules.observe-57.0.20-sources.jar.md5 +0 -1
  42. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.20/expo.modules.observe-57.0.20-sources.jar.sha1 +0 -1
  43. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.20/expo.modules.observe-57.0.20-sources.jar.sha256 +0 -1
  44. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.20/expo.modules.observe-57.0.20-sources.jar.sha512 +0 -1
  45. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.20/expo.modules.observe-57.0.20.aar +0 -0
  46. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.20/expo.modules.observe-57.0.20.aar.md5 +0 -1
  47. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.20/expo.modules.observe-57.0.20.aar.sha1 +0 -1
  48. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.20/expo.modules.observe-57.0.20.aar.sha256 +0 -1
  49. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.20/expo.modules.observe-57.0.20.aar.sha512 +0 -1
  50. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.20/expo.modules.observe-57.0.20.module.md5 +0 -1
  51. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.20/expo.modules.observe-57.0.20.module.sha1 +0 -1
  52. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.20/expo.modules.observe-57.0.20.module.sha256 +0 -1
  53. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.20/expo.modules.observe-57.0.20.module.sha512 +0 -1
  54. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.20/expo.modules.observe-57.0.20.pom.md5 +0 -1
  55. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.20/expo.modules.observe-57.0.20.pom.sha1 +0 -1
  56. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.20/expo.modules.observe-57.0.20.pom.sha256 +0 -1
  57. package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.20/expo.modules.observe-57.0.20.pom.sha512 +0 -1
package/CHANGELOG.md CHANGED
@@ -10,6 +10,16 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 57.0.22 — 2026-09-15
14
+
15
+ _This version does not introduce any user-facing changes._
16
+
17
+ ## 57.0.21 — 2026-09-11
18
+
19
+ ### 💡 Others
20
+
21
+ - [Android] Replace pending telemetry queues with persisted row-id cursors. ([#49547](https://github.com/expo/expo/pull/49547) by [@Ubax](https://github.com/Ubax))
22
+
13
23
  ## 57.0.20 — 2026-09-08
14
24
 
15
25
  _This version does not introduce any user-facing changes._
@@ -1,22 +1,4 @@
1
1
  buildscript {
2
- ext {
3
- getKspVersion = {
4
- if (rootProject.hasProperty("kspVersion")) {
5
- return rootProject["kspVersion"]
6
- }
7
-
8
- def kotlinVersion = rootProject.hasProperty("kotlinVersion") ? rootProject["kotlinVersion"] : "2.0.21"
9
- if (kotlinVersion == "2.1.20") {
10
- return "2.1.20-2.0.1"
11
- } else if (kotlinVersion == "2.0.21") {
12
- return "2.0.21-1.0.28"
13
- } else if (kotlinVersion == "1.9.25") {
14
- return "1.9.25-1.0.20"
15
- }
16
- return "1.9.24-1.0.20"
17
- }
18
- }
19
-
20
2
  repositories
21
3
  {
22
4
  google()
@@ -26,13 +8,11 @@ buildscript {
26
8
 
27
9
  dependencies {
28
10
  classpath("org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin:${kotlinVersion}")
29
- classpath "com.google.devtools.ksp:symbol-processing-gradle-plugin:${getKspVersion()}"
30
11
  }
31
12
  }
32
13
  apply plugin: 'com.android.library'
33
14
  apply plugin: 'expo-module-gradle-plugin'
34
15
  apply plugin: 'org.jetbrains.kotlin.plugin.serialization'
35
- apply plugin: 'com.google.devtools.ksp'
36
16
 
37
17
  group = 'expo.modules.observe'
38
18
 
@@ -65,11 +45,6 @@ dependencies {
65
45
  // This is a workaround for IDE warning - https://youtrack.jetbrains.com/issue/KTIJ-31549
66
46
  implementation "androidx.annotation:annotation-experimental:1.5.1"
67
47
 
68
- def room_version = "2.8.3"
69
- implementation "androidx.room:room-runtime:$room_version"
70
- implementation "androidx.room:room-ktx:$room_version"
71
- ksp "androidx.room:room-compiler:$room_version"
72
-
73
48
  implementation 'androidx.work:work-runtime-ktx:2.9.1'
74
49
 
75
50
  testImplementation 'junit:junit:4.13.2'
@@ -4,4 +4,3 @@ internal const val OBSERVE_TAG = "EasObserve"
4
4
  internal const val OBSERVE_DEFAULT_BASE_URL = "https://o.expo.dev/"
5
5
 
6
6
  internal const val DISPATCH_CHUNK_SIZE = 200
7
- internal const val SQLITE_MAX_BIND_VARIABLES = 900
@@ -0,0 +1,58 @@
1
+ // Copyright 2025-present 650 Industries. All rights reserved.
2
+
3
+ package expo.modules.observe
4
+
5
+ import android.content.Context
6
+ import android.util.Log
7
+ import expo.modules.appmetrics.storage.SessionManager
8
+ import kotlinx.coroutines.CancellationException
9
+
10
+ internal suspend fun repairCursorIfStale(
11
+ signalName: String,
12
+ readCursor: () -> Long,
13
+ writeCursor: (Long) -> Unit,
14
+ readMaxId: suspend () -> Long?
15
+ ) {
16
+ val cursor = readCursor()
17
+ if (cursor < 0) return
18
+
19
+ val maxId = try {
20
+ readMaxId()
21
+ } catch (error: CancellationException) {
22
+ throw error
23
+ } catch (error: Exception) {
24
+ Log.w(OBSERVE_TAG, "Failed to read max $signalName id while repairing cursor: ${error.message}")
25
+ return
26
+ }
27
+ if (cursor > (maxId ?: -1)) {
28
+ Log.i(
29
+ OBSERVE_TAG,
30
+ "Resetting stale $signalName dispatch cursor (was $cursor, max id is ${maxId ?: "<empty>"})"
31
+ )
32
+ writeCursor(-1)
33
+ }
34
+ }
35
+
36
+ internal suspend fun repairMetricCursorIfStale(
37
+ context: Context,
38
+ sessionManager: SessionManager
39
+ ) {
40
+ repairCursorIfStale(
41
+ signalName = "metric",
42
+ readCursor = { ObservePreferences.getLastDispatchedMetricId(context) },
43
+ writeCursor = { ObservePreferences.setLastDispatchedMetricId(context, it) },
44
+ readMaxId = sessionManager::getMaxMetricId
45
+ )
46
+ }
47
+
48
+ internal suspend fun repairLogCursorIfStale(
49
+ context: Context,
50
+ sessionManager: SessionManager
51
+ ) {
52
+ repairCursorIfStale(
53
+ signalName = "log",
54
+ readCursor = { ObservePreferences.getLastDispatchedLogId(context) },
55
+ writeCursor = { ObservePreferences.setLastDispatchedLogId(context, it) },
56
+ readMaxId = sessionManager::getMaxLogId
57
+ )
58
+ }
@@ -88,28 +88,6 @@ object DispatchUtils {
88
88
  }
89
89
  }
90
90
 
91
- /**
92
- * Whether the dispatch caller should remove the just-sent pending IDs from the queue.
93
- *
94
- * - `Success` and `PartialSuccess` remove them — the rows have been accepted by the server
95
- * (partial success rejects a subset server-side, but the bytes still landed; re-sending
96
- * them would just re-trip the same rejection).
97
- * - `NonRetryableFailure` ALSO removes them — the server has refused these rows
98
- * permanently, so retrying would produce the same answer; removing them drops the batch
99
- * so it can't wedge subsequent rounds. This is the acceptance-criterion behavior: a
100
- * 400/403 must not be re-sent on the next cycle.
101
- * - `PayloadTooLarge` removes the pending ID because multi-record batches are retried with
102
- * smaller chunks before this check, so reaching it means a single record exceeded the limit.
103
- * - `RetryableFailure` keeps them so they can be retried.
104
- */
105
- fun shouldRemovePending(result: DispatchResult): Boolean = when (result) {
106
- is DispatchResult.Success,
107
- is DispatchResult.PartialSuccess,
108
- is DispatchResult.NonRetryableFailure,
109
- is DispatchResult.PayloadTooLarge -> true
110
- is DispatchResult.RetryableFailure -> false
111
- }
112
-
113
91
  /**
114
92
  * Parses an HTTP `Retry-After` header into a delay in milliseconds from now. Accepts both
115
93
  * formats permitted by RFC 7231: an integer delta-seconds, or an HTTP-date.
@@ -109,7 +109,7 @@ data class EASMetric(
109
109
  * Wire shape of a log event ready for dispatch. Distinct from the storage-side
110
110
  * `LogRecord`: this form has the JSON `attributes` blob already parsed back
111
111
  * into a structured object (so the OTel encoder can map values to typed
112
- * `OTAnyValue`s) and drops storage-only columns like `logId`.
112
+ * `OTAnyValue`s) and drops storage-only columns like the row id.
113
113
  */
114
114
  @Serializable
115
115
  data class LogEvent(
@@ -15,16 +15,10 @@ import androidx.work.OneTimeWorkRequestBuilder
15
15
  import androidx.work.WorkManager
16
16
  import androidx.work.WorkerParameters
17
17
  import androidx.work.workDataOf
18
- import expo.modules.observe.storage.PendingLogsManager
19
- import expo.modules.observe.storage.PendingMetricsManager
20
18
  import expo.modules.appmetrics.storage.SessionManager
21
19
 
22
20
  /**
23
- * Background worker that dispatches previously queued metrics to EAS Observe.
24
- *
25
- * This worker intentionally does NOT register a [SessionManager.MetricsInsertListener].
26
- * It only dispatches metrics that were already queued in the pending table by the foreground
27
- * [ObservabilityManager].
21
+ * Background worker that dispatches stored metrics and logs to EAS Observe.
28
22
  */
29
23
  class ObservabilityBackgroundWorker(
30
24
  context: Context,
@@ -42,15 +36,10 @@ class ObservabilityBackgroundWorker(
42
36
  context = context
43
37
  )
44
38
 
45
- val pendingMetricsManager = PendingMetricsManager(context)
46
- val pendingLogsManager = PendingLogsManager(context)
47
-
48
39
  BaseObservabilityManager(
49
40
  context = context,
50
41
  projectId = projectId,
51
42
  sessionManager = sessionManager,
52
- pendingMetricsManager = pendingMetricsManager,
53
- pendingLogsManager = pendingLogsManager,
54
43
  baseUrl = baseUrl,
55
44
  isDebugBuild = BuildConfig.DEBUG
56
45
  )
@@ -3,8 +3,6 @@ package expo.modules.observe
3
3
  import android.content.Context
4
4
  import android.util.Log
5
5
  import expo.modules.easclient.EASClientID
6
- import expo.modules.observe.storage.PendingLogsManager
7
- import expo.modules.observe.storage.PendingMetricsManager
8
6
  import expo.modules.appmetrics.storage.SessionManager
9
7
  import expo.modules.appmetrics.utils.TimeUtils
10
8
  import expo.modules.interfaces.constants.ConstantsInterface
@@ -34,25 +32,13 @@ class ObservabilityManager(
34
32
  }
35
33
  val baseUrl = manifest.baseUrl ?: OBSERVE_DEFAULT_BASE_URL
36
34
 
37
- val pendingMetricsManager = PendingMetricsManager(context)
38
- val pendingLogsManager = PendingLogsManager(context)
39
-
40
35
  baseManager = BaseObservabilityManager(
41
36
  context = context,
42
37
  sessionManager = sessionManager,
43
- pendingMetricsManager = pendingMetricsManager,
44
- pendingLogsManager = pendingLogsManager,
45
38
  projectId = projectId,
46
39
  baseUrl = baseUrl,
47
40
  isDebugBuild = BuildConfig.DEBUG
48
41
  )
49
-
50
- sessionManager.addMetricsInsertListener { metricIds ->
51
- pendingMetricsManager.addPendingMetrics(metricIds)
52
- }
53
- sessionManager.addLogsInsertListener { logIds ->
54
- pendingLogsManager.addPendingLogs(logIds)
55
- }
56
42
  }
57
43
 
58
44
  suspend fun dispatchUnsentMetrics() {
@@ -75,8 +61,6 @@ class ObservabilityManager(
75
61
  class BaseObservabilityManager(
76
62
  private val context: Context,
77
63
  private val sessionManager: SessionManager,
78
- private val pendingMetricsManager: PendingMetricsManager,
79
- private val pendingLogsManager: PendingLogsManager,
80
64
  val projectId: String,
81
65
  val baseUrl: String,
82
66
  private val isDebugBuild: Boolean = false,
@@ -108,20 +92,6 @@ class BaseObservabilityManager(
108
92
  private var metricsRetryGate: DispatchUtils.RetryGateState = DispatchUtils.RetryGateState.initial
109
93
  private var logsRetryGate: DispatchUtils.RetryGateState = DispatchUtils.RetryGateState.initial
110
94
 
111
- /**
112
- * Per-signal mutexes that serialize same-signal dispatch calls. Two `dispatchEvents`
113
- * invocations from JS can otherwise land on the same `BaseObservabilityManager` instance
114
- * concurrently and race on the gate's read-modify-write (and double-POST the same pending
115
- * rows). The metrics and logs paths take separate mutexes so they can still run in
116
- * parallel — only same-signal calls serialize.
117
- *
118
- * Worst case without these would be benign (a dropped gate update or a duplicate dispatch),
119
- * since the pending-ID stores are already DB-backed and telemetry is best-effort — but the
120
- * gate is in-memory state with no other synchronization, so close the race explicitly.
121
- */
122
- private val metricsDispatchMutex = Mutex()
123
- private val logsDispatchMutex = Mutex()
124
-
125
95
  /**
126
96
  * Returns true and logs when an active retry gate suppresses this dispatch round. Called
127
97
  * inside each per-signal dispatch method rather than at a shared entry point, so a backoff
@@ -152,169 +122,156 @@ class BaseObservabilityManager(
152
122
  )
153
123
 
154
124
  suspend fun dispatchUnsentMetrics(): Unit = metricsDispatchMutex.withLock {
155
- if (!pendingMetricsManager.hasPendingMetrics()) {
156
- return
157
- }
158
-
159
125
  if (retryGateBlocks(metricsRetryGate, "metrics")) {
160
126
  return
161
127
  }
162
128
 
129
+ repairMetricCursorIfStale(context, sessionManager)
163
130
  if (!shouldDispatch()) {
164
- pendingMetricsManager.removeAllPendingMetrics()
131
+ val maxId = sessionManager.getMaxMetricId() ?: -1
132
+ if (ObservePreferences.getLastDispatchedMetricId(context) != maxId) {
133
+ ObservePreferences.setLastDispatchedMetricId(context, maxId)
134
+ }
165
135
  return
166
136
  }
167
137
 
138
+ var cursor = ObservePreferences.getLastDispatchedMetricId(context)
168
139
  var chunkSize = dispatchChunkSize
169
140
  while (currentCoroutineContext().isActive) {
170
- val pendingIds = pendingMetricsManager.getPendingMetricIds(chunkSize)
171
- // Use the default for the next batch unless a 413 below overrides it. Re-discovering
172
- // the limit each batch is fine: the server accepts payloads over 1 MB, so the default
173
- // chunk stays far below the limit and a 413 is exceptional.
141
+ val metrics = sessionManager.getMetrics(cursor, chunkSize)
174
142
  chunkSize = dispatchChunkSize
175
- if (pendingIds.isEmpty()) {
143
+ if (metrics.isEmpty()) {
176
144
  break
177
145
  }
178
146
 
179
- val sessionsWithPendingMetrics = sessionManager.getSessionsWithMetrics(pendingIds)
180
-
181
- // Clean up orphaned pending IDs (metrics deleted from MetricsDatabase but still in pending table)
182
- val resolvedMetricIds = sessionsWithPendingMetrics.flatMap { it.metrics }.map { it.metricId }.toSet()
183
- val orphanedIds = pendingIds.filter { it !in resolvedMetricIds }
184
- if (orphanedIds.isNotEmpty()) {
185
- pendingMetricsManager.removePendingMetrics(orphanedIds)
186
- }
187
-
188
- if (sessionsWithPendingMetrics.isNotEmpty()) {
189
- val events = sessionsWithPendingMetrics.map { sessionWithMetrics ->
147
+ val highestId = metrics.last().id
148
+ val metricsBySessionId = metrics.groupBy { it.sessionId }
149
+ val sessions = sessionManager.getSessions(metricsBySessionId.keys).associateBy { it.id }
150
+ val events = metricsBySessionId.mapNotNull { (sessionId, sessionMetrics) ->
151
+ sessions[sessionId]?.let { session ->
190
152
  Event(
191
- metadata = Metadata.fromSessionMetadata(sessionWithMetrics.session),
192
- metrics = sessionWithMetrics.metrics.map { EASMetric.fromMetric(it) }
153
+ metadata = Metadata.fromSessionMetadata(session),
154
+ metrics = sessionMetrics.map(EASMetric::fromMetric)
193
155
  )
194
156
  }
157
+ }
158
+ if (events.isEmpty()) {
159
+ cursor = highestId
160
+ ObservePreferences.setLastDispatchedMetricId(context, cursor)
161
+ continue
162
+ }
195
163
 
196
- val result = eventDispatcher.dispatch(events)
197
- metricsRetryGate = nextGate(metricsRetryGate, result)
198
- val dispatchedMetricIds = sessionsWithPendingMetrics.flatMap { it.metrics }.map { it.metricId }
199
- when (result) {
200
- is DispatchResult.PartialSuccess ->
201
- Log.w(
202
- OBSERVE_TAG,
203
- "Partial success on batch of ${dispatchedMetricIds.size} metric event(s): " +
204
- "server rejected ${result.partial.rejectedCount} " +
205
- "(${result.partial.errorMessage ?: "no error message"})"
206
- )
207
- is DispatchResult.NonRetryableFailure ->
208
- Log.w(
209
- OBSERVE_TAG,
210
- "Dropping batch of ${dispatchedMetricIds.size} metric event(s): ${result.reason}"
211
- )
212
- is DispatchResult.PayloadTooLarge ->
213
- if (dispatchedMetricIds.size == 1) {
214
- Log.w(OBSERVE_TAG, "Dropping metric event that exceeds the server's payload limit")
215
- }
216
- is DispatchResult.Success, is DispatchResult.RetryableFailure -> Unit
217
- }
218
- if (result is DispatchResult.PayloadTooLarge && dispatchedMetricIds.size > 1) {
219
- chunkSize = dispatchedMetricIds.size / 2
220
- // Keep the pending metrics, but retry immediately with a smaller chunk.
221
- continue
164
+ val result = eventDispatcher.dispatch(events)
165
+ metricsRetryGate = nextGate(metricsRetryGate, result)
166
+ when (result) {
167
+ is DispatchResult.PartialSuccess ->
168
+ Log.w(
169
+ OBSERVE_TAG,
170
+ "Partial success on batch of ${metrics.size} metric event(s): " +
171
+ "server rejected ${result.partial.rejectedCount} " +
172
+ "(${result.partial.errorMessage ?: "no error message"})"
173
+ )
174
+ is DispatchResult.NonRetryableFailure ->
175
+ Log.w(OBSERVE_TAG, "Dropping batch of ${metrics.size} metric event(s): ${result.reason}")
176
+ DispatchResult.PayloadTooLarge -> if (metrics.size == 1) {
177
+ Log.w(OBSERVE_TAG, "Dropping metric event that exceeds the server's payload limit")
222
178
  }
223
- if (!DispatchUtils.shouldRemovePending(result)) {
224
- break
179
+ DispatchResult.Success, is DispatchResult.RetryableFailure -> Unit
180
+ }
181
+ if (result is DispatchResult.PayloadTooLarge && metrics.size > 1) {
182
+ chunkSize = metrics.size / 2
183
+ continue
184
+ }
185
+ when (result) {
186
+ DispatchResult.Success, is DispatchResult.PartialSuccess -> {
187
+ cursor = highestId
188
+ ObservePreferences.setLastDispatchedMetricId(context, cursor)
225
189
  }
226
- pendingMetricsManager.removePendingMetrics(dispatchedMetricIds)
227
- // A systematic rejection or an oversized record: leave the rest for the next run.
228
- if (result is DispatchResult.NonRetryableFailure || result is DispatchResult.PayloadTooLarge) {
190
+ is DispatchResult.NonRetryableFailure, DispatchResult.PayloadTooLarge -> {
191
+ ObservePreferences.setLastDispatchedMetricId(context, highestId)
229
192
  break
230
193
  }
194
+ is DispatchResult.RetryableFailure -> break
231
195
  }
232
196
  }
233
197
  }
234
198
 
235
199
  /**
236
200
  * Dispatches log events to `/v1/logs`. Independent from the metrics path —
237
- * a logs failure doesn't affect the metrics pending table and vice versa.
201
+ * a logs failure doesn't affect the metrics cursor and vice versa.
238
202
  */
239
203
  suspend fun dispatchUnsentLogs(): Unit = logsDispatchMutex.withLock {
240
- if (!pendingLogsManager.hasPendingLogs()) {
241
- return
242
- }
243
-
244
204
  if (retryGateBlocks(logsRetryGate, "logs")) {
245
205
  return
246
206
  }
247
207
 
208
+ repairLogCursorIfStale(context, sessionManager)
248
209
  if (!shouldDispatch()) {
249
- pendingLogsManager.removeAllPendingLogs()
210
+ val maxId = sessionManager.getMaxLogId() ?: -1
211
+ if (ObservePreferences.getLastDispatchedLogId(context) != maxId) {
212
+ ObservePreferences.setLastDispatchedLogId(context, maxId)
213
+ }
250
214
  return
251
215
  }
252
216
 
217
+ var cursor = ObservePreferences.getLastDispatchedLogId(context)
253
218
  var chunkSize = dispatchChunkSize
254
219
  while (currentCoroutineContext().isActive) {
255
- val pendingIds = pendingLogsManager.getPendingLogIds(chunkSize)
256
- // Use the default for the next batch unless a 413 below overrides it. Re-discovering
257
- // the limit each batch is fine: the server accepts payloads over 1 MB, so the default
258
- // chunk stays far below the limit and a 413 is exceptional.
220
+ val logs = sessionManager.getLogs(cursor, chunkSize)
259
221
  chunkSize = dispatchChunkSize
260
- if (pendingIds.isEmpty()) {
222
+ if (logs.isEmpty()) {
261
223
  break
262
224
  }
263
225
 
264
- val sessionsWithPendingLogs = sessionManager.getSessionsWithLogs(pendingIds)
265
-
266
- // Clean up orphaned pending IDs (logs deleted from the `logs` table but
267
- // still tracked in `pending_logs`).
268
- val resolvedLogIds = sessionsWithPendingLogs.flatMap { it.logs }.map { it.logId }.toSet()
269
- val orphanedIds = pendingIds.filter { it !in resolvedLogIds }
270
- if (orphanedIds.isNotEmpty()) {
271
- pendingLogsManager.removePendingLogs(orphanedIds)
272
- }
273
-
274
- if (sessionsWithPendingLogs.isNotEmpty()) {
275
- val events = sessionsWithPendingLogs.map { sessionWithLogs ->
226
+ val highestId = logs.last().id
227
+ val logsBySessionId = logs.groupBy { it.sessionId }
228
+ val sessions = sessionManager.getSessions(logsBySessionId.keys).associateBy { it.id }
229
+ val events = logsBySessionId.mapNotNull { (sessionId, sessionLogs) ->
230
+ sessions[sessionId]?.let { session ->
276
231
  Event(
277
- metadata = Metadata.fromSessionMetadata(sessionWithLogs.session),
232
+ metadata = Metadata.fromSessionMetadata(session),
278
233
  metrics = emptyList(),
279
- logs = sessionWithLogs.logs.map { LogEvent.fromLogRecord(it) }
234
+ logs = sessionLogs.map(LogEvent::fromLogRecord)
280
235
  )
281
236
  }
237
+ }
238
+ if (events.isEmpty()) {
239
+ cursor = highestId
240
+ ObservePreferences.setLastDispatchedLogId(context, cursor)
241
+ continue
242
+ }
282
243
 
283
- val result = eventDispatcher.dispatchLogs(events)
284
- logsRetryGate = nextGate(logsRetryGate, result)
285
- val dispatchedLogIds = sessionsWithPendingLogs.flatMap { it.logs }.map { it.logId }
286
- when (result) {
287
- is DispatchResult.PartialSuccess ->
288
- Log.w(
289
- OBSERVE_TAG,
290
- "Partial success on batch of ${dispatchedLogIds.size} log event(s): " +
291
- "server rejected ${result.partial.rejectedCount} " +
292
- "(${result.partial.errorMessage ?: "no error message"})"
293
- )
294
- is DispatchResult.NonRetryableFailure ->
295
- Log.w(
296
- OBSERVE_TAG,
297
- "Dropping batch of ${dispatchedLogIds.size} log event(s): ${result.reason}"
298
- )
299
- is DispatchResult.PayloadTooLarge ->
300
- if (dispatchedLogIds.size == 1) {
301
- Log.w(OBSERVE_TAG, "Dropping log event that exceeds the server's payload limit")
302
- }
303
- is DispatchResult.Success, is DispatchResult.RetryableFailure -> Unit
304
- }
305
- if (result is DispatchResult.PayloadTooLarge && dispatchedLogIds.size > 1) {
306
- chunkSize = dispatchedLogIds.size / 2
307
- // Keep the pending logs, but retry immediately with a smaller chunk.
308
- continue
244
+ val result = eventDispatcher.dispatchLogs(events)
245
+ logsRetryGate = nextGate(logsRetryGate, result)
246
+ when (result) {
247
+ is DispatchResult.PartialSuccess ->
248
+ Log.w(
249
+ OBSERVE_TAG,
250
+ "Partial success on batch of ${logs.size} log event(s): " +
251
+ "server rejected ${result.partial.rejectedCount} " +
252
+ "(${result.partial.errorMessage ?: "no error message"})"
253
+ )
254
+ is DispatchResult.NonRetryableFailure ->
255
+ Log.w(OBSERVE_TAG, "Dropping batch of ${logs.size} log event(s): ${result.reason}")
256
+ DispatchResult.PayloadTooLarge -> if (logs.size == 1) {
257
+ Log.w(OBSERVE_TAG, "Dropping log event that exceeds the server's payload limit")
309
258
  }
310
- if (!DispatchUtils.shouldRemovePending(result)) {
311
- break
259
+ DispatchResult.Success, is DispatchResult.RetryableFailure -> Unit
260
+ }
261
+ if (result is DispatchResult.PayloadTooLarge && logs.size > 1) {
262
+ chunkSize = logs.size / 2
263
+ continue
264
+ }
265
+ when (result) {
266
+ DispatchResult.Success, is DispatchResult.PartialSuccess -> {
267
+ cursor = highestId
268
+ ObservePreferences.setLastDispatchedLogId(context, cursor)
312
269
  }
313
- pendingLogsManager.removePendingLogs(dispatchedLogIds)
314
- // A systematic rejection or an oversized record: leave the rest for the next run.
315
- if (result is DispatchResult.NonRetryableFailure || result is DispatchResult.PayloadTooLarge) {
270
+ is DispatchResult.NonRetryableFailure, DispatchResult.PayloadTooLarge -> {
271
+ ObservePreferences.setLastDispatchedLogId(context, highestId)
316
272
  break
317
273
  }
274
+ is DispatchResult.RetryableFailure -> break
318
275
  }
319
276
  }
320
277
  }
@@ -338,10 +295,16 @@ class BaseObservabilityManager(
338
295
  }
339
296
 
340
297
  suspend fun cleanup() {
341
- pendingMetricsManager.cleanupOldPendingMetrics()
342
- pendingLogsManager.cleanupOldPendingLogs()
343
298
  // TODO(@ubax): Move sessionManager.cleanupOldSessions out of eas observe
344
299
  sessionManager.cleanupOldSessions()
300
+ // Remove the database used by the old pending telemetry queues.
301
+ context.deleteDatabase("eas_observe")
345
302
  sessionManager.cleanupOldLogs()
346
303
  }
304
+
305
+ companion object {
306
+ // Serialize foreground and background dispatches without blocking the other signal.
307
+ private val metricsDispatchMutex = Mutex()
308
+ private val logsDispatchMutex = Mutex()
309
+ }
347
310
  }
@@ -8,6 +8,8 @@ import kotlinx.serialization.json.Json
8
8
  private const val PREFS_NAME = "dev.expo.observe"
9
9
  private const val KEY_CONFIG = "config"
10
10
  private const val KEY_BUNDLE_DEFAULTS = "bundleDefaults"
11
+ private const val KEY_LAST_DISPATCHED_METRIC_ID = "lastDispatchedMetricId"
12
+ private const val KEY_LAST_DISPATCHED_LOG_ID = "lastDispatchedLogId"
11
13
 
12
14
  /**
13
15
  * Snapshot of the last `configure(...)` payload
@@ -54,4 +56,24 @@ object ObservePreferences {
54
56
  putString(KEY_BUNDLE_DEFAULTS, Json.encodeToString(defaults))
55
57
  }
56
58
  }
59
+
60
+ fun getLastDispatchedMetricId(context: Context): Long =
61
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
62
+ .getLong(KEY_LAST_DISPATCHED_METRIC_ID, -1)
63
+
64
+ fun setLastDispatchedMetricId(context: Context, id: Long) {
65
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit(commit = true) {
66
+ putLong(KEY_LAST_DISPATCHED_METRIC_ID, id)
67
+ }
68
+ }
69
+
70
+ fun getLastDispatchedLogId(context: Context): Long =
71
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
72
+ .getLong(KEY_LAST_DISPATCHED_LOG_ID, -1)
73
+
74
+ fun setLastDispatchedLogId(context: Context, id: Long) {
75
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit(commit = true) {
76
+ putLong(KEY_LAST_DISPATCHED_LOG_ID, id)
77
+ }
78
+ }
57
79
  }
@@ -9,7 +9,7 @@
9
9
  "publication": {
10
10
  "groupId": "expo.modules.observe",
11
11
  "artifactId": "expo.modules.observe",
12
- "version": "57.0.20",
12
+ "version": "57.0.22",
13
13
  "repository": "local-maven-repo"
14
14
  }
15
15
  }
@@ -0,0 +1 @@
1
+ 2804e38922d1ce46c8ce12ee4672f58555a2aa5a8750aa40f87a647b40e44909
@@ -0,0 +1 @@
1
+ d0530049f5f76be03f57de762aafba77081039857864f6db1a920ddf9f42ec216d1bee6fca44783b8f5324f695b4fe3faed935befdd08614ef20e954a9906a72
@@ -0,0 +1 @@
1
+ 204f8f1b8fc9a619e9e556a596a55f1b06eb0761346d06857d6ccf8dd40b3499
@@ -0,0 +1 @@
1
+ 441de4b1b747af8031431b6b7a5538a9b36f781d3d41a1cbb7d4c8a2161e2ebfe31ac3aaa82a592ad5dae9abe6d34e41511f80a07570ccc38c2b23e8c22ddbbf
@@ -3,7 +3,7 @@
3
3
  "component": {
4
4
  "group": "expo.modules.observe",
5
5
  "module": "expo.modules.observe",
6
- "version": "57.0.20",
6
+ "version": "57.0.22",
7
7
  "attributes": {
8
8
  "org.gradle.status": "release"
9
9
  }
@@ -24,13 +24,13 @@
24
24
  },
25
25
  "files": [
26
26
  {
27
- "name": "expo.modules.observe-57.0.20.aar",
28
- "url": "expo.modules.observe-57.0.20.aar",
29
- "size": 279118,
30
- "sha512": "1faa3721b4a32ff829d9c3b090c0395259dbfc7a4cf8824734cacecbcb97c5cbf6808d8aa00594ef8b39b8b575738245b76b950e01e71d2e970d0ae4762e9391",
31
- "sha256": "338b5ae19b1db326442caf9d501bf5e2a0fa9ccdb0b659c014a08a330bfd2d2b",
32
- "sha1": "64c582d3dce97b3992ee5787a183a3548edfbda8",
33
- "md5": "6f1ebe2d017e285613ebf412c0534170"
27
+ "name": "expo.modules.observe-57.0.22.aar",
28
+ "url": "expo.modules.observe-57.0.22.aar",
29
+ "size": 249243,
30
+ "sha512": "441de4b1b747af8031431b6b7a5538a9b36f781d3d41a1cbb7d4c8a2161e2ebfe31ac3aaa82a592ad5dae9abe6d34e41511f80a07570ccc38c2b23e8c22ddbbf",
31
+ "sha256": "204f8f1b8fc9a619e9e556a596a55f1b06eb0761346d06857d6ccf8dd40b3499",
32
+ "sha1": "6cb935926fb92b62d3454565ea5ffe8478e90225",
33
+ "md5": "d7092ee67bd0503b26e954ad059c8047"
34
34
  }
35
35
  ]
36
36
  },
@@ -61,21 +61,21 @@
61
61
  "group": "expo.modules.appmetrics",
62
62
  "module": "expo.modules.appmetrics",
63
63
  "version": {
64
- "requires": "57.0.17"
64
+ "requires": "57.0.19"
65
65
  }
66
66
  },
67
67
  {
68
68
  "group": "host.exp.exponent",
69
69
  "module": "expo-eas-client",
70
70
  "version": {
71
- "requires": "57.0.3"
71
+ "requires": "57.0.4"
72
72
  }
73
73
  },
74
74
  {
75
75
  "group": "host.exp.exponent",
76
76
  "module": "expo-updates-interface",
77
77
  "version": {
78
- "requires": "57.0.1"
78
+ "requires": "57.0.2"
79
79
  }
80
80
  },
81
81
  {
@@ -103,20 +103,6 @@
103
103
  "requires": "1.5.1"
104
104
  }
105
105
  },
106
- {
107
- "group": "androidx.room",
108
- "module": "room-runtime",
109
- "version": {
110
- "requires": "2.8.3"
111
- }
112
- },
113
- {
114
- "group": "androidx.room",
115
- "module": "room-ktx",
116
- "version": {
117
- "requires": "2.8.3"
118
- }
119
- },
120
106
  {
121
107
  "group": "androidx.work",
122
108
  "module": "work-runtime-ktx",
@@ -134,13 +120,13 @@
134
120
  ],
135
121
  "files": [
136
122
  {
137
- "name": "expo.modules.observe-57.0.20.aar",
138
- "url": "expo.modules.observe-57.0.20.aar",
139
- "size": 279118,
140
- "sha512": "1faa3721b4a32ff829d9c3b090c0395259dbfc7a4cf8824734cacecbcb97c5cbf6808d8aa00594ef8b39b8b575738245b76b950e01e71d2e970d0ae4762e9391",
141
- "sha256": "338b5ae19b1db326442caf9d501bf5e2a0fa9ccdb0b659c014a08a330bfd2d2b",
142
- "sha1": "64c582d3dce97b3992ee5787a183a3548edfbda8",
143
- "md5": "6f1ebe2d017e285613ebf412c0534170"
123
+ "name": "expo.modules.observe-57.0.22.aar",
124
+ "url": "expo.modules.observe-57.0.22.aar",
125
+ "size": 249243,
126
+ "sha512": "441de4b1b747af8031431b6b7a5538a9b36f781d3d41a1cbb7d4c8a2161e2ebfe31ac3aaa82a592ad5dae9abe6d34e41511f80a07570ccc38c2b23e8c22ddbbf",
127
+ "sha256": "204f8f1b8fc9a619e9e556a596a55f1b06eb0761346d06857d6ccf8dd40b3499",
128
+ "sha1": "6cb935926fb92b62d3454565ea5ffe8478e90225",
129
+ "md5": "d7092ee67bd0503b26e954ad059c8047"
144
130
  }
145
131
  ]
146
132
  },
@@ -154,13 +140,13 @@
154
140
  },
155
141
  "files": [
156
142
  {
157
- "name": "expo.modules.observe-57.0.20-sources.jar",
158
- "url": "expo.modules.observe-57.0.20-sources.jar",
159
- "size": 26681,
160
- "sha512": "dc1e5b0d7d55d2541259696a0c80fc9027f870bc6098795c406318200ee133e96c9f1530e497b0071814e97cbbf2f030888753805c5f872ce88a0ba6bb2d886e",
161
- "sha256": "72bc4b79e95af73fb199265326ab3aad5ec145d8d3d5b74e710fbad2a51ffdbc",
162
- "sha1": "66178e77bb77fab970066d8e7d3007278d5d42e7",
163
- "md5": "101a43989137ebb70aabefaf9a2dc10f"
143
+ "name": "expo.modules.observe-57.0.22-sources.jar",
144
+ "url": "expo.modules.observe-57.0.22-sources.jar",
145
+ "size": 23585,
146
+ "sha512": "d0530049f5f76be03f57de762aafba77081039857864f6db1a920ddf9f42ec216d1bee6fca44783b8f5324f695b4fe3faed935befdd08614ef20e954a9906a72",
147
+ "sha256": "2804e38922d1ce46c8ce12ee4672f58555a2aa5a8750aa40f87a647b40e44909",
148
+ "sha1": "86160bf79b776ee12b1d3ee5148d9c5fa8be615f",
149
+ "md5": "a23b4461bae18b20e1963631bbf5a6ad"
164
150
  }
165
151
  ]
166
152
  }
@@ -0,0 +1 @@
1
+ 6532619652923a49f9c380203e0da5e7254a6218ea0e1b4277b9eaac96147499
@@ -0,0 +1 @@
1
+ e7e8cd414113842f7cc719161a9e155bcf9ee8acb77de34cf92f22ac297bda56ebcb14892e299c8895347e14e5a267f547ed34325b055095237303ad0dc614a6
@@ -9,7 +9,7 @@
9
9
  <modelVersion>4.0.0</modelVersion>
10
10
  <groupId>expo.modules.observe</groupId>
11
11
  <artifactId>expo.modules.observe</artifactId>
12
- <version>57.0.20</version>
12
+ <version>57.0.22</version>
13
13
  <packaging>aar</packaging>
14
14
  <name>expo.modules.observe</name>
15
15
  <url>https://github.com/expo/expo</url>
@@ -52,19 +52,19 @@
52
52
  <dependency>
53
53
  <groupId>expo.modules.appmetrics</groupId>
54
54
  <artifactId>expo.modules.appmetrics</artifactId>
55
- <version>57.0.17</version>
55
+ <version>57.0.19</version>
56
56
  <scope>runtime</scope>
57
57
  </dependency>
58
58
  <dependency>
59
59
  <groupId>host.exp.exponent</groupId>
60
60
  <artifactId>expo-eas-client</artifactId>
61
- <version>57.0.3</version>
61
+ <version>57.0.4</version>
62
62
  <scope>runtime</scope>
63
63
  </dependency>
64
64
  <dependency>
65
65
  <groupId>host.exp.exponent</groupId>
66
66
  <artifactId>expo-updates-interface</artifactId>
67
- <version>57.0.1</version>
67
+ <version>57.0.2</version>
68
68
  <scope>runtime</scope>
69
69
  </dependency>
70
70
  <dependency>
@@ -90,18 +90,6 @@
90
90
  <version>1.5.1</version>
91
91
  <scope>runtime</scope>
92
92
  </dependency>
93
- <dependency>
94
- <groupId>androidx.room</groupId>
95
- <artifactId>room-runtime</artifactId>
96
- <version>2.8.3</version>
97
- <scope>runtime</scope>
98
- </dependency>
99
- <dependency>
100
- <groupId>androidx.room</groupId>
101
- <artifactId>room-ktx</artifactId>
102
- <version>2.8.3</version>
103
- <scope>runtime</scope>
104
- </dependency>
105
93
  <dependency>
106
94
  <groupId>androidx.work</groupId>
107
95
  <artifactId>work-runtime-ktx</artifactId>
@@ -0,0 +1 @@
1
+ 96dee9036188d706d86f610388558da3129ae48f5e566475cad117c4b67e1574
@@ -0,0 +1 @@
1
+ c38ef322262ee8053841ff46bd3c7897fd14f3c77053f2680871fb75fd975b00cf6008fc5e16f6376ee0220e40117a621532220a4ba66799d37abfd25fb45103
@@ -3,11 +3,11 @@
3
3
  <groupId>expo.modules.observe</groupId>
4
4
  <artifactId>expo.modules.observe</artifactId>
5
5
  <versioning>
6
- <latest>57.0.20</latest>
7
- <release>57.0.20</release>
6
+ <latest>57.0.22</latest>
7
+ <release>57.0.22</release>
8
8
  <versions>
9
- <version>57.0.20</version>
9
+ <version>57.0.22</version>
10
10
  </versions>
11
- <lastUpdated>20260908134222</lastUpdated>
11
+ <lastUpdated>20260915155138</lastUpdated>
12
12
  </versioning>
13
13
  </metadata>
@@ -1 +1 @@
1
- 0737fccc7cc04049d6e928bc1b60cfa7
1
+ d2cff64db3bc41f15cc4c45e06a45c96
@@ -1 +1 @@
1
- f08b4021302b978e0efb8a57ee1d9b27a85d2649
1
+ 44eae1aadbe4523d38a02a3d149b9d55b7afe9fb
@@ -1 +1 @@
1
- 66c1a69ef85c2fa4df12a8cd1bcc90b9bb9d08811a3053d1d530de75914ac0e6
1
+ e9f1962e0cba5319f80bf9bd1b4f57711837e889c99d30b5b095db6d60d95c5b
@@ -1 +1 @@
1
- a4d14a136f3be0d74042b573f91c66402c76aca4361e3726427f6ca53402f790bb6c06489be027c24d893c96d9b0a280e62498b804122e1d2b48ed54b720736a
1
+ dbd9d144c69cb4642627975633e5d5185c567647b70e6cb7f75fa0b9947a922cf1f5f04d6e3aed716a1b4f6f056b75e7de7d3a86257c3ffdd166dede50085d54
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "expo-observe",
3
3
  "title": "Expo Observe",
4
- "version": "57.0.20",
4
+ "version": "57.0.22",
5
5
  "description": "Expo module that dispatches collected app metrics to EAS Observe",
6
6
  "main": "src/index.ts",
7
7
  "types": "build/index.d.ts",
@@ -35,14 +35,14 @@
35
35
  "author": "650 Industries, Inc.",
36
36
  "license": "MIT",
37
37
  "dependencies": {
38
- "expo-app-metrics": "~57.0.17",
39
- "expo-eas-client": "~57.0.3"
38
+ "expo-app-metrics": "~57.0.19",
39
+ "expo-eas-client": "~57.0.4"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@react-navigation/native": "^7.1.33",
43
43
  "@testing-library/react-native": "^13.3.0",
44
44
  "@types/react": "~19.2.0",
45
- "expo": "57.0.21",
45
+ "expo": "57.0.23",
46
46
  "expo-module-scripts": "56.0.3"
47
47
  },
48
48
  "peerDependencies": {
@@ -60,7 +60,7 @@
60
60
  "optional": true
61
61
  }
62
62
  },
63
- "gitHead": "ee89f7c31877f111cb1705c8e5c2c8078fa80a03",
63
+ "gitHead": "f6f5af06b95cb7d511b52641885b2fb5adad525d",
64
64
  "scripts": {
65
65
  "build": "expo-module build",
66
66
  "clean": "expo-module clean",
@@ -1,94 +0,0 @@
1
- package expo.modules.observe.storage
2
-
3
- import android.content.Context
4
- import androidx.room.Dao
5
- import androidx.room.Database
6
- import androidx.room.Entity
7
- import androidx.room.Insert
8
- import androidx.room.OnConflictStrategy
9
- import androidx.room.PrimaryKey
10
- import androidx.room.Query
11
- import androidx.room.Room
12
- import androidx.room.RoomDatabase
13
-
14
- // TODO: Draining large backlogs from both pending tables reruns `ORDER BY addedAt`
15
- // for every chunk. Consider `Index("addedAt")` and a version bump if this matters in practice.
16
- @Entity(tableName = "pending_metrics")
17
- data class PendingMetric(
18
- @PrimaryKey val metricId: String,
19
- // ISO 8601 timestamp
20
- val addedAt: String
21
- )
22
-
23
- @Entity(tableName = "pending_logs")
24
- data class PendingLog(
25
- @PrimaryKey val logId: String,
26
- // ISO 8601 timestamp
27
- val addedAt: String
28
- )
29
-
30
- @Dao
31
- interface PendingMetricDao {
32
- @Insert(onConflict = OnConflictStrategy.IGNORE)
33
- suspend fun insertAll(metrics: List<PendingMetric>)
34
-
35
- @Query("SELECT metricId FROM pending_metrics ORDER BY addedAt ASC LIMIT :limit")
36
- suspend fun getMetricIds(limit: Int): List<String>
37
-
38
- @Query("SELECT EXISTS(SELECT 1 FROM pending_metrics)")
39
- suspend fun hasMetricIds(): Boolean
40
-
41
- @Query("DELETE FROM pending_metrics")
42
- suspend fun deleteAll()
43
-
44
- @Query("DELETE FROM pending_metrics WHERE metricId IN (:metricIds)")
45
- suspend fun deleteByIds(metricIds: List<String>)
46
-
47
- @Query("DELETE FROM pending_metrics WHERE addedAt < :cutoffTimestamp")
48
- suspend fun deleteOlderThan(cutoffTimestamp: String)
49
- }
50
-
51
- @Dao
52
- interface PendingLogDao {
53
- @Insert(onConflict = OnConflictStrategy.IGNORE)
54
- suspend fun insertAll(logs: List<PendingLog>)
55
-
56
- @Query("SELECT logId FROM pending_logs ORDER BY addedAt ASC LIMIT :limit")
57
- suspend fun getLogIds(limit: Int): List<String>
58
-
59
- @Query("SELECT EXISTS(SELECT 1 FROM pending_logs)")
60
- suspend fun hasLogIds(): Boolean
61
-
62
- @Query("DELETE FROM pending_logs")
63
- suspend fun deleteAll()
64
-
65
- @Query("DELETE FROM pending_logs WHERE logId IN (:logIds)")
66
- suspend fun deleteByIds(logIds: List<String>)
67
-
68
- @Query("DELETE FROM pending_logs WHERE addedAt < :cutoffTimestamp")
69
- suspend fun deleteOlderThan(cutoffTimestamp: String)
70
- }
71
-
72
- @Database(entities = [PendingMetric::class, PendingLog::class], version = 2, exportSchema = false)
73
- abstract class ObserveDatabase : RoomDatabase() {
74
- abstract fun pendingMetricDao(): PendingMetricDao
75
-
76
- abstract fun pendingLogDao(): PendingLogDao
77
-
78
- companion object {
79
- @Volatile
80
- private var INSTANCE: ObserveDatabase? = null
81
-
82
- fun getDatabase(context: Context): ObserveDatabase =
83
- INSTANCE ?: synchronized(this) {
84
- INSTANCE ?: Room
85
- .databaseBuilder(
86
- context.applicationContext,
87
- ObserveDatabase::class.java,
88
- "eas_observe"
89
- ).fallbackToDestructiveMigration()
90
- .build()
91
- .also { INSTANCE = it }
92
- }
93
- }
94
- }
@@ -1,42 +0,0 @@
1
- package expo.modules.observe.storage
2
-
3
- import android.content.Context
4
- import androidx.room.withTransaction
5
- import expo.modules.appmetrics.utils.TimeUtils
6
- import expo.modules.observe.SQLITE_MAX_BIND_VARIABLES
7
-
8
- class PendingLogsManager(
9
- context: Context,
10
- database: ObserveDatabase? = null
11
- ) {
12
- private val database: ObserveDatabase = database ?: ObserveDatabase.getDatabase(context)
13
-
14
- suspend fun addPendingLogs(logIds: List<String>) {
15
- val now = TimeUtils.getCurrentTimestampInISOFormat()
16
- val pendingLogs = logIds.map { PendingLog(logId = it, addedAt = now) }
17
- database.pendingLogDao().insertAll(pendingLogs)
18
- }
19
-
20
- suspend fun getPendingLogIds(limit: Int): List<String> = database.pendingLogDao().getLogIds(limit)
21
-
22
- suspend fun hasPendingLogs(): Boolean = database.pendingLogDao().hasLogIds()
23
-
24
- suspend fun removeAllPendingLogs() = database.pendingLogDao().deleteAll()
25
-
26
- suspend fun removePendingLogs(logIds: List<String>) {
27
- database.withTransaction {
28
- logIds.chunked(SQLITE_MAX_BIND_VARIABLES).forEach { chunk ->
29
- database.pendingLogDao().deleteByIds(chunk)
30
- }
31
- }
32
- }
33
-
34
- suspend fun cleanupOldPendingLogs() {
35
- val cutoffTimestamp = TimeUtils.getTimestampInISOFormatFromPast(SECONDS_TO_REMOVE_OLD_PENDING_LOGS)
36
- database.pendingLogDao().deleteOlderThan(cutoffTimestamp)
37
- }
38
-
39
- companion object {
40
- private const val SECONDS_TO_REMOVE_OLD_PENDING_LOGS: Long = 7 * 24 * 60 * 60 // 7 days in seconds
41
- }
42
- }
@@ -1,42 +0,0 @@
1
- package expo.modules.observe.storage
2
-
3
- import android.content.Context
4
- import androidx.room.withTransaction
5
- import expo.modules.appmetrics.utils.TimeUtils
6
- import expo.modules.observe.SQLITE_MAX_BIND_VARIABLES
7
-
8
- class PendingMetricsManager(
9
- context: Context,
10
- database: ObserveDatabase? = null
11
- ) {
12
- private val database: ObserveDatabase = database ?: ObserveDatabase.getDatabase(context)
13
-
14
- suspend fun addPendingMetrics(metricIds: List<String>) {
15
- val now = TimeUtils.getCurrentTimestampInISOFormat()
16
- val pendingMetrics = metricIds.map { PendingMetric(metricId = it, addedAt = now) }
17
- database.pendingMetricDao().insertAll(pendingMetrics)
18
- }
19
-
20
- suspend fun getPendingMetricIds(limit: Int): List<String> = database.pendingMetricDao().getMetricIds(limit)
21
-
22
- suspend fun hasPendingMetrics(): Boolean = database.pendingMetricDao().hasMetricIds()
23
-
24
- suspend fun removeAllPendingMetrics() = database.pendingMetricDao().deleteAll()
25
-
26
- suspend fun removePendingMetrics(metricIds: List<String>) {
27
- database.withTransaction {
28
- metricIds.chunked(SQLITE_MAX_BIND_VARIABLES).forEach { chunk ->
29
- database.pendingMetricDao().deleteByIds(chunk)
30
- }
31
- }
32
- }
33
-
34
- suspend fun cleanupOldPendingMetrics() {
35
- val cutoffTimestamp = TimeUtils.getTimestampInISOFormatFromPast(SECONDS_TO_REMOVE_OLD_PENDING_METRICS)
36
- database.pendingMetricDao().deleteOlderThan(cutoffTimestamp)
37
- }
38
-
39
- companion object {
40
- private const val SECONDS_TO_REMOVE_OLD_PENDING_METRICS: Long = 7 * 24 * 60 * 60 // 7 days in seconds
41
- }
42
- }
@@ -1 +0,0 @@
1
- 72bc4b79e95af73fb199265326ab3aad5ec145d8d3d5b74e710fbad2a51ffdbc
@@ -1 +0,0 @@
1
- dc1e5b0d7d55d2541259696a0c80fc9027f870bc6098795c406318200ee133e96c9f1530e497b0071814e97cbbf2f030888753805c5f872ce88a0ba6bb2d886e
@@ -1 +0,0 @@
1
- 338b5ae19b1db326442caf9d501bf5e2a0fa9ccdb0b659c014a08a330bfd2d2b
@@ -1 +0,0 @@
1
- 1faa3721b4a32ff829d9c3b090c0395259dbfc7a4cf8824734cacecbcb97c5cbf6808d8aa00594ef8b39b8b575738245b76b950e01e71d2e970d0ae4762e9391
@@ -1 +0,0 @@
1
- 2d3233b298debd978bf33448f54ba7ae4024e0197b2cd842d98d211d04d08b75
@@ -1 +0,0 @@
1
- 6e994053444daf21580be5ac0955fe89ab8fde987ea21c169c0e8b6b8fe4a85e5e2e1b8647553189a673f21312a15cc72f03031da5bb5fd30b926e5f66098c36
@@ -1 +0,0 @@
1
- 487e57c4e1833058c62e49eef905b4ddb6f835fc56a513ff7361fcc84e2507da
@@ -1 +0,0 @@
1
- a886ff4bf5ce99c1f726c2b4ca0872a3a58cf65137e36c57882034c969be53f34d7baf27ed7b7ef696c3aeb749bb6476254efaaf948589808f040ebe41676558