expo-observe 56.0.4 → 56.0.6

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 (104) hide show
  1. package/android/src/main/java/expo/modules/observe/Event.kt +37 -1
  2. package/android/src/main/java/expo/modules/observe/EventDispatcher.kt +62 -27
  3. package/android/src/main/java/expo/modules/observe/OTAnyValueSerializer.kt +111 -0
  4. package/android/src/main/java/expo/modules/observe/ObservabilityBackgroundWorker.kt +5 -1
  5. package/android/src/main/java/expo/modules/observe/ObservabilityManager.kt +58 -2
  6. package/android/src/main/java/expo/modules/observe/ObserveModule.kt +4 -1
  7. package/android/src/main/java/expo/modules/observe/OpenTelemetry.kt +215 -14
  8. package/android/src/main/java/expo/modules/observe/storage/ObserveDatabase.kt +25 -1
  9. package/android/src/main/java/expo/modules/observe/storage/PendingLogsManager.kt +38 -0
  10. package/build/ObserveProvider.d.ts +3 -0
  11. package/build/ObserveProvider.d.ts.map +1 -0
  12. package/build/ObserveProvider.js +6 -0
  13. package/build/ObserveProvider.js.map +1 -0
  14. package/build/ObserveRoot.d.ts +11 -0
  15. package/build/ObserveRoot.d.ts.map +1 -0
  16. package/build/ObserveRoot.js +12 -0
  17. package/build/ObserveRoot.js.map +1 -0
  18. package/build/index.d.ts +3 -1
  19. package/build/index.d.ts.map +1 -1
  20. package/build/index.js +3 -1
  21. package/build/index.js.map +1 -1
  22. package/build/integrations/expo-router/index.d.ts +4 -0
  23. package/build/integrations/expo-router/index.d.ts.map +1 -0
  24. package/build/integrations/expo-router/index.js +4 -0
  25. package/build/integrations/expo-router/index.js.map +1 -0
  26. package/build/integrations/expo-router/init.d.ts +3 -0
  27. package/build/integrations/expo-router/init.d.ts.map +1 -0
  28. package/build/integrations/expo-router/init.js +6 -0
  29. package/build/integrations/expo-router/init.js.map +1 -0
  30. package/build/integrations/expo-router/router.d.ts +4 -0
  31. package/build/integrations/expo-router/router.d.ts.map +1 -0
  32. package/build/integrations/expo-router/router.js +12 -0
  33. package/build/integrations/expo-router/router.js.map +1 -0
  34. package/build/integrations/expo-router/useObserveForRouter.d.ts +5 -0
  35. package/build/integrations/expo-router/useObserveForRouter.d.ts.map +1 -0
  36. package/build/integrations/expo-router/useObserveForRouter.js +48 -0
  37. package/build/integrations/expo-router/useObserveForRouter.js.map +1 -0
  38. package/build/module.d.ts +2 -2
  39. package/build/module.d.ts.map +1 -1
  40. package/build/module.js +18 -1
  41. package/build/module.js.map +1 -1
  42. package/build/types.d.ts +8 -0
  43. package/build/types.d.ts.map +1 -1
  44. package/build/types.js.map +1 -1
  45. package/build/useObserve.d.ts +4 -0
  46. package/build/useObserve.d.ts.map +1 -0
  47. package/build/useObserve.js +9 -0
  48. package/build/useObserve.js.map +1 -0
  49. package/expo-module.config.json +1 -1
  50. package/jest.config.js +2 -0
  51. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6-sources.jar +0 -0
  52. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6-sources.jar.md5 +1 -0
  53. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6-sources.jar.sha1 +1 -0
  54. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6-sources.jar.sha256 +1 -0
  55. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6-sources.jar.sha512 +1 -0
  56. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.aar +0 -0
  57. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.aar.md5 +1 -0
  58. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.aar.sha1 +1 -0
  59. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.aar.sha256 +1 -0
  60. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.aar.sha512 +1 -0
  61. package/local-maven-repo/expo/modules/observe/expo.modules.observe/{56.0.4/expo.modules.observe-56.0.4.module → 56.0.6/expo.modules.observe-56.0.6.module} +23 -23
  62. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.module.md5 +1 -0
  63. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.module.sha1 +1 -0
  64. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.module.sha256 +1 -0
  65. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.module.sha512 +1 -0
  66. package/local-maven-repo/expo/modules/observe/expo.modules.observe/{56.0.4/expo.modules.observe-56.0.4.pom → 56.0.6/expo.modules.observe-56.0.6.pom} +2 -2
  67. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.pom.md5 +1 -0
  68. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.pom.sha1 +1 -0
  69. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.pom.sha256 +1 -0
  70. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.6/expo.modules.observe-56.0.6.pom.sha512 +1 -0
  71. package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml +4 -4
  72. package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.md5 +1 -1
  73. package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.sha1 +1 -1
  74. package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.sha256 +1 -1
  75. package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.sha512 +1 -1
  76. package/package.json +14 -5
  77. package/src/ObserveProvider.tsx +5 -0
  78. package/src/ObserveRoot.tsx +26 -0
  79. package/src/index.ts +3 -1
  80. package/src/integrations/expo-router/index.ts +3 -0
  81. package/src/integrations/expo-router/init.ts +7 -0
  82. package/src/integrations/expo-router/router.ts +11 -0
  83. package/src/integrations/expo-router/useObserveForRouter.ts +65 -0
  84. package/src/module.ts +21 -2
  85. package/src/types.ts +8 -0
  86. package/src/useObserve.ts +10 -0
  87. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.4/expo.modules.observe-56.0.4-sources.jar +0 -0
  88. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.4/expo.modules.observe-56.0.4-sources.jar.md5 +0 -1
  89. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.4/expo.modules.observe-56.0.4-sources.jar.sha1 +0 -1
  90. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.4/expo.modules.observe-56.0.4-sources.jar.sha256 +0 -1
  91. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.4/expo.modules.observe-56.0.4-sources.jar.sha512 +0 -1
  92. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.4/expo.modules.observe-56.0.4.aar +0 -0
  93. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.4/expo.modules.observe-56.0.4.aar.md5 +0 -1
  94. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.4/expo.modules.observe-56.0.4.aar.sha1 +0 -1
  95. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.4/expo.modules.observe-56.0.4.aar.sha256 +0 -1
  96. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.4/expo.modules.observe-56.0.4.aar.sha512 +0 -1
  97. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.4/expo.modules.observe-56.0.4.module.md5 +0 -1
  98. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.4/expo.modules.observe-56.0.4.module.sha1 +0 -1
  99. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.4/expo.modules.observe-56.0.4.module.sha256 +0 -1
  100. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.4/expo.modules.observe-56.0.4.module.sha512 +0 -1
  101. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.4/expo.modules.observe-56.0.4.pom.md5 +0 -1
  102. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.4/expo.modules.observe-56.0.4.pom.sha1 +0 -1
  103. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.4/expo.modules.observe-56.0.4.pom.sha256 +0 -1
  104. package/local-maven-repo/expo/modules/observe/expo.modules.observe/56.0.4/expo.modules.observe-56.0.4.pom.sha512 +0 -1
@@ -1,5 +1,6 @@
1
1
  package expo.modules.observe
2
2
 
3
+ import expo.modules.appmetrics.storage.LogRecord
3
4
  import expo.modules.appmetrics.storage.Metric
4
5
  import expo.modules.appmetrics.storage.Session
5
6
  import kotlinx.serialization.Serializable
@@ -104,8 +105,43 @@ data class EASMetric(
104
105
  }
105
106
  }
106
107
 
108
+ /**
109
+ * Wire shape of a log event ready for dispatch. Distinct from the storage-side
110
+ * `LogRecord`: this form has the JSON `attributes` blob already parsed back
111
+ * into a structured object (so the OTel encoder can map values to typed
112
+ * `OTAnyValue`s) and drops storage-only columns like `logId`.
113
+ */
114
+ @Serializable
115
+ data class LogEvent(
116
+ val sessionId: String,
117
+ val timestamp: String,
118
+ val name: String,
119
+ val body: String? = null,
120
+ val severity: String,
121
+ val attributes: JsonObject? = null,
122
+ val droppedAttributesCount: Int = 0
123
+ ) {
124
+ companion object {
125
+ fun fromLogRecord(log: LogRecord): LogEvent =
126
+ LogEvent(
127
+ sessionId = log.sessionId,
128
+ timestamp = log.timestamp,
129
+ name = log.name,
130
+ body = log.body,
131
+ severity = log.severity,
132
+ // Stored as a JSON string; parse defensively, falling back to no
133
+ // attributes if the blob is somehow malformed.
134
+ attributes = log.attributes?.let {
135
+ runCatching { Json.decodeFromString<JsonObject>(it) }.getOrNull()
136
+ },
137
+ droppedAttributesCount = log.droppedAttributesCount
138
+ )
139
+ }
140
+ }
141
+
107
142
  @Serializable
108
143
  data class Event(
109
144
  val metadata: Metadata,
110
- val metrics: List<EASMetric>
145
+ val metrics: List<EASMetric>,
146
+ val logs: List<LogEvent> = emptyList()
111
147
  )
@@ -21,15 +21,18 @@ class EventDispatcher(
21
21
  private val useOpenTelemetry: Boolean = false,
22
22
  private val httpClient: OkHttpClient = OkHttpClient()
23
23
  ) {
24
- private fun endpointUrl(): String {
25
- val base = when (baseUrl.endsWith("/")) {
24
+ private val baseProjectUrl: String
25
+ get() = when (baseUrl.endsWith("/")) {
26
26
  true -> "${baseUrl}$projectId"
27
27
  else -> "$baseUrl/$projectId"
28
28
  }
29
- return if (useOpenTelemetry) "$base/v1/metrics" else base
30
- }
31
29
 
32
- suspend fun dispatch(events: List<Event>) =
30
+ private fun metricsEndpointUrl(): String =
31
+ if (useOpenTelemetry) "$baseProjectUrl/v1/metrics" else baseProjectUrl
32
+
33
+ private fun logsEndpointUrl(): String = "$baseProjectUrl/v1/logs"
34
+
35
+ suspend fun dispatch(events: List<Event>): Boolean =
33
36
  suspendCancellableCoroutine { continuation ->
34
37
  if (events.isEmpty()) {
35
38
  continuation.resume(false)
@@ -54,28 +57,7 @@ class EventDispatcher(
54
57
  json.encodeToString(Payload.serializer(), payload)
55
58
  }
56
59
 
57
- val endpointUrl = endpointUrl()
58
-
59
- Log.d(TAG, "Sending events to $endpointUrl")
60
-
61
- val request = Request
62
- .Builder()
63
- .url(endpointUrl)
64
- .post(body.toRequestBody("application/json".toMediaType()))
65
- .build()
66
-
67
- Log.d(TAG, body)
68
-
69
- val call = httpClient.newCall(request)
70
-
71
- continuation.invokeOnCancellation {
72
- call.cancel()
73
- }
74
-
75
- val response = call.execute()
76
- Log.d(TAG, "Server responded with: ${response.body?.string()}")
77
-
78
- continuation.resume(response.code in 200..299)
60
+ executePost(continuation, metricsEndpointUrl(), body)
79
61
  } catch (e: Exception) {
80
62
  Log.w(
81
63
  TAG,
@@ -85,6 +67,59 @@ class EventDispatcher(
85
67
  }
86
68
  }
87
69
 
70
+ /**
71
+ * Dispatches log records to `{baseUrl}/{projectId}/v1/logs`. Always uses the
72
+ * OTLP wire shape — there is no legacy logs endpoint.
73
+ */
74
+ suspend fun dispatchLogs(events: List<Event>): Boolean =
75
+ suspendCancellableCoroutine { continuation ->
76
+ val easId = EASClientID(context).uuid.toString()
77
+ val resourceLogs = events
78
+ .filter { it.logs.isNotEmpty() }
79
+ .map { it.toOTResourceLogs(easId) }
80
+ if (resourceLogs.isEmpty()) {
81
+ continuation.resume(false)
82
+ return@suspendCancellableCoroutine Unit
83
+ }
84
+ try {
85
+ val body = OTLogsRequestBody(resourceLogs = resourceLogs).toJson(prettyPrint = true)
86
+ executePost(continuation, logsEndpointUrl(), body)
87
+ } catch (e: Exception) {
88
+ Log.w(
89
+ TAG,
90
+ "Dispatching the logs has thrown an error: ${e.message}"
91
+ )
92
+ continuation.resumeWithException(e)
93
+ }
94
+ }
95
+
96
+ private fun executePost(
97
+ continuation: kotlinx.coroutines.CancellableContinuation<Boolean>,
98
+ endpointUrl: String,
99
+ body: String
100
+ ) {
101
+ Log.d(TAG, "Sending events to $endpointUrl")
102
+
103
+ val request = Request
104
+ .Builder()
105
+ .url(endpointUrl)
106
+ .post(body.toRequestBody("application/json".toMediaType()))
107
+ .build()
108
+
109
+ Log.d(TAG, body)
110
+
111
+ val call = httpClient.newCall(request)
112
+
113
+ continuation.invokeOnCancellation {
114
+ call.cancel()
115
+ }
116
+
117
+ val response = call.execute()
118
+ Log.d(TAG, "Server responded with: ${response.body?.string()}")
119
+
120
+ continuation.resume(response.code in 200..299)
121
+ }
122
+
88
123
  companion object {
89
124
  private const val TAG = "EasObserve"
90
125
  }
@@ -0,0 +1,111 @@
1
+ package expo.modules.observe
2
+
3
+ import kotlinx.serialization.KSerializer
4
+ import kotlinx.serialization.builtins.ListSerializer
5
+ import kotlinx.serialization.descriptors.SerialDescriptor
6
+ import kotlinx.serialization.descriptors.buildClassSerialDescriptor
7
+ import kotlinx.serialization.encoding.Decoder
8
+ import kotlinx.serialization.encoding.Encoder
9
+ import kotlinx.serialization.json.JsonDecoder
10
+ import kotlinx.serialization.json.JsonElement
11
+ import kotlinx.serialization.json.JsonEncoder
12
+ import kotlinx.serialization.json.JsonPrimitive
13
+ import kotlinx.serialization.json.boolean
14
+ import kotlinx.serialization.json.booleanOrNull
15
+ import kotlinx.serialization.json.buildJsonObject
16
+ import kotlinx.serialization.json.contentOrNull
17
+ import kotlinx.serialization.json.doubleOrNull
18
+ import kotlinx.serialization.json.jsonArray
19
+ import kotlinx.serialization.json.jsonObject
20
+ import kotlinx.serialization.json.jsonPrimitive
21
+ import kotlinx.serialization.json.longOrNull
22
+
23
+ /**
24
+ * Custom serializer that emits the OTLP-correct wire shape for `OTAnyValue`.
25
+ * Each variant becomes a single-key JSON object:
26
+ * - Str -> {"stringValue": "..."}
27
+ * - Int64 -> {"intValue": "42"} (string per OTLP int64 mapping)
28
+ * - Dbl -> {"doubleValue": 3.14}
29
+ * - Bln -> {"boolValue": true}
30
+ * - Arr -> {"arrayValue": {"values": [...]}}
31
+ * - KvList -> {"kvlistValue": {"values": [{"key": "...", "value": ...}]}}
32
+ */
33
+ internal object OTAnyValueSerializer : KSerializer<OTAnyValue> {
34
+ override val descriptor: SerialDescriptor =
35
+ buildClassSerialDescriptor("expo.modules.observe.OTAnyValue")
36
+
37
+ override fun serialize(encoder: Encoder, value: OTAnyValue) {
38
+ val jsonEncoder = encoder as? JsonEncoder
39
+ ?: error("OTAnyValue is only serializable to JSON (got ${encoder::class}).")
40
+
41
+ val element: JsonElement = when (value) {
42
+ is OTAnyValue.Str -> buildJsonObject { put("stringValue", JsonPrimitive(value.value)) }
43
+ is OTAnyValue.Int64 -> buildJsonObject { put("intValue", JsonPrimitive(value.value.toString())) }
44
+ is OTAnyValue.Dbl -> buildJsonObject { put("doubleValue", JsonPrimitive(value.value)) }
45
+ is OTAnyValue.Bln -> buildJsonObject { put("boolValue", JsonPrimitive(value.value)) }
46
+ is OTAnyValue.Arr -> buildJsonObject {
47
+ put(
48
+ "arrayValue",
49
+ buildJsonObject {
50
+ put(
51
+ "values",
52
+ jsonEncoder.json.encodeToJsonElement(
53
+ ListSerializer(OTAnyValueSerializer),
54
+ value.values
55
+ )
56
+ )
57
+ }
58
+ )
59
+ }
60
+ is OTAnyValue.KvList -> buildJsonObject {
61
+ put(
62
+ "kvlistValue",
63
+ buildJsonObject {
64
+ put(
65
+ "values",
66
+ jsonEncoder.json.encodeToJsonElement(
67
+ ListSerializer(OTKeyValue.serializer()),
68
+ value.values
69
+ )
70
+ )
71
+ }
72
+ )
73
+ }
74
+ }
75
+
76
+ jsonEncoder.encodeJsonElement(element)
77
+ }
78
+
79
+ override fun deserialize(decoder: Decoder): OTAnyValue {
80
+ val jsonDecoder = decoder as? JsonDecoder
81
+ ?: error("OTAnyValue is only deserializable from JSON (got ${decoder::class}).")
82
+ val obj = jsonDecoder.decodeJsonElement().jsonObject
83
+
84
+ obj["stringValue"]?.let {
85
+ return OTAnyValue.Str(it.jsonPrimitive.contentOrNull ?: "")
86
+ }
87
+ obj["intValue"]?.let {
88
+ val parsed = it.jsonPrimitive.contentOrNull?.toLongOrNull()
89
+ ?: it.jsonPrimitive.longOrNull
90
+ ?: error("OTAnyValue.intValue could not be parsed as Long: $it")
91
+ return OTAnyValue.Int64(parsed)
92
+ }
93
+ obj["doubleValue"]?.let {
94
+ return OTAnyValue.Dbl(it.jsonPrimitive.doubleOrNull ?: error("OTAnyValue.doubleValue not a number: $it"))
95
+ }
96
+ obj["boolValue"]?.let {
97
+ return OTAnyValue.Bln(it.jsonPrimitive.booleanOrNull ?: it.jsonPrimitive.boolean)
98
+ }
99
+ obj["arrayValue"]?.let { arr ->
100
+ val values = arr.jsonObject["values"]?.jsonArray
101
+ ?: error("OTAnyValue.arrayValue is missing `values`")
102
+ return OTAnyValue.Arr(values.map { jsonDecoder.json.decodeFromJsonElement(OTAnyValueSerializer, it) })
103
+ }
104
+ obj["kvlistValue"]?.let { kv ->
105
+ val values = kv.jsonObject["values"]?.jsonArray
106
+ ?: error("OTAnyValue.kvlistValue is missing `values`")
107
+ return OTAnyValue.KvList(values.map { jsonDecoder.json.decodeFromJsonElement(OTKeyValue.serializer(), it) })
108
+ }
109
+ error("OTAnyValue has no recognized variant: $obj")
110
+ }
111
+ }
@@ -15,6 +15,7 @@ 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
18
19
  import expo.modules.observe.storage.PendingMetricsManager
19
20
  import expo.modules.appmetrics.storage.SessionManager
20
21
 
@@ -43,12 +44,14 @@ class ObservabilityBackgroundWorker(
43
44
  )
44
45
 
45
46
  val pendingMetricsManager = PendingMetricsManager(context)
47
+ val pendingLogsManager = PendingLogsManager(context)
46
48
 
47
49
  BaseObservabilityManager(
48
50
  context = context,
49
51
  projectId = projectId,
50
52
  sessionManager = sessionManager,
51
53
  pendingMetricsManager = pendingMetricsManager,
54
+ pendingLogsManager = pendingLogsManager,
52
55
  baseUrl = baseUrl,
53
56
  isDebugBuild = BuildConfig.DEBUG,
54
57
  useOpenTelemetry = useOpenTelemetry
@@ -81,7 +84,8 @@ class ObservabilityBackgroundWorker(
81
84
  // This also adds a side benefit of cleaning up even if dispatch fails
82
85
  observabilityManager.cleanup()
83
86
  observabilityManager.dispatchUnsentMetrics()
84
- Log.d(OBSERVE_TAG, "Successfully dispatched unsent metrics")
87
+ observabilityManager.dispatchUnsentLogs()
88
+ Log.d(OBSERVE_TAG, "Successfully dispatched unsent metrics and logs")
85
89
  Result.success()
86
90
  } catch (e: Exception) {
87
91
  Log.e(OBSERVE_TAG, "Failed to dispatch metrics", e)
@@ -2,6 +2,7 @@ package expo.modules.observe
2
2
 
3
3
  import android.content.Context
4
4
  import expo.modules.easclient.EASClientID
5
+ import expo.modules.observe.storage.PendingLogsManager
5
6
  import expo.modules.observe.storage.PendingMetricsManager
6
7
  import expo.modules.appmetrics.storage.SessionManager
7
8
  import expo.modules.interfaces.constants.ConstantsInterface
@@ -30,11 +31,13 @@ class ObservabilityManager(
30
31
  useOpenTelemetry = manifest.useOpenTelemetry
31
32
 
32
33
  val pendingMetricsManager = PendingMetricsManager(context)
34
+ val pendingLogsManager = PendingLogsManager(context)
33
35
 
34
36
  baseManager = BaseObservabilityManager(
35
37
  context = context,
36
38
  sessionManager = sessionManager,
37
39
  pendingMetricsManager = pendingMetricsManager,
40
+ pendingLogsManager = pendingLogsManager,
38
41
  projectId = projectId,
39
42
  baseUrl = baseUrl,
40
43
  isDebugBuild = BuildConfig.DEBUG,
@@ -44,12 +47,19 @@ class ObservabilityManager(
44
47
  sessionManager.addMetricsInsertListener { metricIds ->
45
48
  pendingMetricsManager.addPendingMetrics(metricIds)
46
49
  }
50
+ sessionManager.addLogsInsertListener { logIds ->
51
+ pendingLogsManager.addPendingLogs(logIds)
52
+ }
47
53
  }
48
54
 
49
55
  suspend fun dispatchUnsentMetrics() {
50
56
  baseManager.dispatchUnsentMetrics()
51
57
  }
52
58
 
59
+ suspend fun dispatchUnsentLogs() {
60
+ baseManager.dispatchUnsentLogs()
61
+ }
62
+
53
63
  fun scheduleBackgroundDispatch() {
54
64
  ObservabilityBackgroundWorker.scheduleBackgroundDispatch(
55
65
  context = context,
@@ -64,6 +74,7 @@ class BaseObservabilityManager(
64
74
  private val context: Context,
65
75
  private val sessionManager: SessionManager,
66
76
  private val pendingMetricsManager: PendingMetricsManager,
77
+ private val pendingLogsManager: PendingLogsManager,
67
78
  val projectId: String,
68
79
  val baseUrl: String,
69
80
  private val isDebugBuild: Boolean = false,
@@ -116,6 +127,49 @@ class BaseObservabilityManager(
116
127
  }
117
128
  }
118
129
 
130
+ /**
131
+ * Dispatches log events to `/v1/logs`. Independent from the metrics path —
132
+ * a logs failure doesn't affect the metrics pending table and vice versa.
133
+ */
134
+ suspend fun dispatchUnsentLogs() {
135
+ val pendingIds = pendingLogsManager.getAllPendingLogIds()
136
+ if (pendingIds.isEmpty()) {
137
+ return
138
+ }
139
+
140
+ if (!shouldDispatch()) {
141
+ pendingLogsManager.removePendingLogs(pendingIds)
142
+ return
143
+ }
144
+
145
+ val sessionsWithPendingLogs = sessionManager.getSessionsWithLogs(pendingIds)
146
+
147
+ // Clean up orphaned pending IDs (logs deleted from the `logs` table but
148
+ // still tracked in `pending_logs`).
149
+ val resolvedLogIds = sessionsWithPendingLogs.flatMap { it.logs }.map { it.logId }.toSet()
150
+ val orphanedIds = pendingIds.filter { it !in resolvedLogIds }
151
+ if (orphanedIds.isNotEmpty()) {
152
+ pendingLogsManager.removePendingLogs(orphanedIds)
153
+ }
154
+
155
+ if (sessionsWithPendingLogs.isEmpty()) {
156
+ return
157
+ }
158
+
159
+ val events = sessionsWithPendingLogs.map { sessionWithLogs ->
160
+ Event(
161
+ metadata = Metadata.fromSessionMetadata(sessionWithLogs.session),
162
+ metrics = emptyList(),
163
+ logs = sessionWithLogs.logs.map { LogEvent.fromLogRecord(it) }
164
+ )
165
+ }
166
+
167
+ if (eventDispatcher.dispatchLogs(events)) {
168
+ val dispatchedLogIds = sessionsWithPendingLogs.flatMap { it.logs }.map { it.logId }
169
+ pendingLogsManager.removePendingLogs(dispatchedLogIds)
170
+ }
171
+ }
172
+
119
173
  private fun isInSample(): Boolean {
120
174
  val rate = ObservePreferences.getConfig(context)?.sampleRate ?: return true
121
175
  val clamped = rate.coerceIn(0.0, 1.0)
@@ -136,7 +190,9 @@ class BaseObservabilityManager(
136
190
 
137
191
  suspend fun cleanup() {
138
192
  pendingMetricsManager.cleanupOldPendingMetrics()
139
- // TODO(@ubax): Move sessionManager.cleanupOldMetrics() out of eas observe
140
- sessionManager.cleanupOldMetrics()
193
+ pendingLogsManager.cleanupOldPendingLogs()
194
+ // TODO(@ubax): Move sessionManager.cleanupOldSessions out of eas observe
195
+ sessionManager.cleanupOldSessions()
196
+ sessionManager.cleanupOldLogs()
141
197
  }
142
198
  }
@@ -50,7 +50,10 @@ class ObserveModule : Module() {
50
50
  observabilityManager.scheduleBackgroundDispatch()
51
51
  }
52
52
 
53
- AsyncFunction("dispatchEvents") Coroutine { -> observabilityManager.dispatchUnsentMetrics() }
53
+ AsyncFunction("dispatchEvents") Coroutine { ->
54
+ observabilityManager.dispatchUnsentMetrics()
55
+ observabilityManager.dispatchUnsentLogs()
56
+ }
54
57
 
55
58
  Function("configure") { config: Config ->
56
59
  ObservePreferences.setConfig(