expo-observe 57.0.4 → 57.0.5
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/CHANGELOG.md +14 -0
- package/android/src/main/java/expo/modules/observe/DispatchUtils.kt +246 -0
- package/android/src/main/java/expo/modules/observe/EventDispatcher.kt +59 -31
- package/android/src/main/java/expo/modules/observe/ObservabilityManager.kt +113 -7
- package/android/src/main/java/expo/modules/observe/ObserveModule.kt +14 -1
- package/android/src/main/java/expo/modules/observe/OpenTelemetry.kt +28 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/integrations/expo-router/emitTTI.d.ts +2 -0
- package/build/integrations/expo-router/emitTTI.d.ts.map +1 -1
- package/build/integrations/expo-router/init.d.ts +3 -1
- package/build/integrations/expo-router/init.d.ts.map +1 -1
- package/build/integrations/expo-router/useObserveForRouter.d.ts.map +1 -1
- package/build/integrations/navigationConfig.d.ts +19 -0
- package/build/integrations/navigationConfig.d.ts.map +1 -0
- package/build/integrations/react-navigation/emitTTI.d.ts +1 -0
- package/build/integrations/react-navigation/emitTTI.d.ts.map +1 -1
- package/build/integrations/react-navigation/handleStateChange.d.ts.map +1 -1
- package/build/integrations/react-navigation/init.d.ts +3 -1
- package/build/integrations/react-navigation/init.d.ts.map +1 -1
- package/build/integrations/react-navigation/useObserveForReactNavigation.d.ts.map +1 -1
- package/build/module.d.ts.map +1 -1
- package/build/module.web.d.ts +3 -2
- package/build/module.web.d.ts.map +1 -1
- package/build/types.d.ts +34 -4
- package/build/types.d.ts.map +1 -1
- package/expo-module.config.json +1 -1
- package/ios/DispatchUtils.swift +317 -0
- package/ios/Observability.swift +100 -57
- package/ios/ObserveModule.swift +13 -0
- package/ios/OpenTelemetry.swift +28 -0
- package/ios/Tests/DispatchUtilsBackoffTests.swift +100 -0
- package/ios/Tests/DispatchUtilsNextCursorTests.swift +96 -0
- package/ios/Tests/DispatchUtilsRetryGateTests.swift +182 -0
- package/ios/Tests/ObservabilityClassifyResponseTests.swift +369 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.5/expo.modules.observe-57.0.5-sources.jar +0 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.5/expo.modules.observe-57.0.5-sources.jar.md5 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.5/expo.modules.observe-57.0.5-sources.jar.sha1 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.5/expo.modules.observe-57.0.5-sources.jar.sha256 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.5/expo.modules.observe-57.0.5-sources.jar.sha512 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.5/expo.modules.observe-57.0.5.aar +0 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.5/expo.modules.observe-57.0.5.aar.md5 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.5/expo.modules.observe-57.0.5.aar.sha1 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.5/expo.modules.observe-57.0.5.aar.sha256 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.5/expo.modules.observe-57.0.5.aar.sha512 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/{57.0.4/expo.modules.observe-57.0.4.module → 57.0.5/expo.modules.observe-57.0.5.module} +23 -23
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.5/expo.modules.observe-57.0.5.module.md5 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.5/expo.modules.observe-57.0.5.module.sha1 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.5/expo.modules.observe-57.0.5.module.sha256 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.5/expo.modules.observe-57.0.5.module.sha512 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/{57.0.4/expo.modules.observe-57.0.4.pom → 57.0.5/expo.modules.observe-57.0.5.pom} +2 -2
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.5/expo.modules.observe-57.0.5.pom.md5 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.5/expo.modules.observe-57.0.5.pom.sha1 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.5/expo.modules.observe-57.0.5.pom.sha256 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.5/expo.modules.observe-57.0.5.pom.sha512 +1 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml +4 -4
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.md5 +1 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.sha1 +1 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.sha256 +1 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/maven-metadata.xml.sha512 +1 -1
- package/package.json +5 -5
- package/src/index.ts +2 -0
- package/src/integrations/expo-router/emitTTI.ts +8 -1
- package/src/integrations/expo-router/init.ts +13 -2
- package/src/integrations/expo-router/useObserveForRouter.ts +18 -3
- package/src/integrations/navigationConfig.ts +75 -0
- package/src/integrations/react-navigation/emitTTI.ts +2 -1
- package/src/integrations/react-navigation/handleStateChange.ts +10 -5
- package/src/integrations/react-navigation/init.ts +8 -1
- package/src/integrations/react-navigation/useObserveForReactNavigation.ts +9 -4
- package/src/module.ts +8 -3
- package/src/module.web.ts +11 -2
- package/src/types.ts +34 -4
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.4/expo.modules.observe-57.0.4-sources.jar +0 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.4/expo.modules.observe-57.0.4-sources.jar.md5 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.4/expo.modules.observe-57.0.4-sources.jar.sha1 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.4/expo.modules.observe-57.0.4-sources.jar.sha256 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.4/expo.modules.observe-57.0.4-sources.jar.sha512 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.4/expo.modules.observe-57.0.4.aar +0 -0
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.4/expo.modules.observe-57.0.4.aar.md5 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.4/expo.modules.observe-57.0.4.aar.sha1 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.4/expo.modules.observe-57.0.4.aar.sha256 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.4/expo.modules.observe-57.0.4.aar.sha512 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.4/expo.modules.observe-57.0.4.module.md5 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.4/expo.modules.observe-57.0.4.module.sha1 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.4/expo.modules.observe-57.0.4.module.sha256 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.4/expo.modules.observe-57.0.4.module.sha512 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.4/expo.modules.observe-57.0.4.pom.md5 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.4/expo.modules.observe-57.0.4.pom.sha1 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.4/expo.modules.observe-57.0.4.pom.sha256 +0 -1
- package/local-maven-repo/expo/modules/observe/expo.modules.observe/57.0.4/expo.modules.observe-57.0.4.pom.sha512 +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,20 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 57.0.5 — 2026-07-15
|
|
14
|
+
|
|
15
|
+
### 🎉 New features
|
|
16
|
+
|
|
17
|
+
- Expose configure event ([#47388](https://github.com/expo/expo/pull/47388) by [@Ubax](https://github.com/Ubax))
|
|
18
|
+
- Add `filteredParams` configuration option to navigation integrations ([#47488](https://github.com/expo/expo/pull/47488) by [@Ubax](https://github.com/Ubax))
|
|
19
|
+
|
|
20
|
+
### 🐛 Bug fixes
|
|
21
|
+
|
|
22
|
+
- Fix non-serializable route params issue ([#47497](https://github.com/expo/expo/pull/47497) by [@Ubax](https://github.com/Ubax))
|
|
23
|
+
- [Android] Fix `logEvent` not being forwarded to the AppMetrics module through the native module proxy. ([@Ubax](https://github.com/Ubax)) ([#47766](https://github.com/expo/expo/pull/47766) by [@Ubax](https://github.com/Ubax))
|
|
24
|
+
- [iOS] Adjust dispatch code to comply with OTLP retry spec. ([#47159](https://github.com/expo/expo/pull/47159) by [@douglowder](https://github.com/douglowder))
|
|
25
|
+
- [Android] Adjust dispatch code to comply with OTLP retry spec. ([@douglowder](https://github.com/douglowder)) ([#47160](https://github.com/expo/expo/pull/47160) by [@douglowder](https://github.com/douglowder))
|
|
26
|
+
|
|
13
27
|
## 57.0.4 — 2026-07-07
|
|
14
28
|
|
|
15
29
|
_This version does not introduce any user-facing changes._
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// Copyright 2025-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
package expo.modules.observe
|
|
4
|
+
|
|
5
|
+
import expo.modules.appmetrics.utils.TimeUtils
|
|
6
|
+
import kotlinx.serialization.json.Json
|
|
7
|
+
import java.text.SimpleDateFormat
|
|
8
|
+
import java.util.Locale
|
|
9
|
+
import java.util.TimeZone
|
|
10
|
+
import kotlin.math.pow
|
|
11
|
+
import kotlin.random.Random
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Outcome of a single dispatch attempt to the OTLP endpoint. Four cases, modeled after the
|
|
15
|
+
* OTLP retry guidance (see https://opentelemetry.io/docs/specs/otlp/#otlphttp-response):
|
|
16
|
+
*
|
|
17
|
+
* - `Success` — server accepted the batch without rejections.
|
|
18
|
+
* - `PartialSuccess` — server accepted the batch but rejected some records (the OTLP
|
|
19
|
+
* `partial_success` body with `rejectedCount > 0`). The rows DID land on the server, so
|
|
20
|
+
* pending-ID removal and counter reset match `Success`; the case stays distinct so the
|
|
21
|
+
* call site can log the rejected count and `errorMessage` clearly rather than describing
|
|
22
|
+
* it as a drop.
|
|
23
|
+
* - `RetryableFailure` — transient failure (429/502/503/504 or transport error); retry
|
|
24
|
+
* the same batch after `retryAfterMs` or a client-computed backoff.
|
|
25
|
+
* - `NonRetryableFailure` — permanent failure (4xx/5xx outside the retryable set, encoding
|
|
26
|
+
* error); drop the batch so it can't wedge the queue.
|
|
27
|
+
*
|
|
28
|
+
* `RetryableFailure.retryAfterMs` carries the parsed `Retry-After` header value in
|
|
29
|
+
* milliseconds (or `null` when the server didn't supply one). `NonRetryableFailure.reason`
|
|
30
|
+
* carries a short diagnostic string for the warn log.
|
|
31
|
+
*/
|
|
32
|
+
sealed class DispatchResult {
|
|
33
|
+
object Success : DispatchResult()
|
|
34
|
+
data class PartialSuccess(val partial: OTPartialSuccess) : DispatchResult()
|
|
35
|
+
data class RetryableFailure(val retryAfterMs: Long? = null) : DispatchResult()
|
|
36
|
+
data class NonRetryableFailure(val reason: String) : DispatchResult()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* HTTP response classification + Retry-After parsing for `EventDispatcher`. Lives in its own
|
|
41
|
+
* file mirroring `DispatchUtils.swift` on the iOS side. All functions are pure so they can be
|
|
42
|
+
* unit-tested without instantiating the dispatcher or hitting the network.
|
|
43
|
+
*/
|
|
44
|
+
object DispatchUtils {
|
|
45
|
+
/**
|
|
46
|
+
* Pure classifier that maps an HTTP response into one of three retry outcomes. Extracted
|
|
47
|
+
* from the dispatch call site so the OTLP-spec rules can be unit-tested without a real
|
|
48
|
+
* network call.
|
|
49
|
+
*
|
|
50
|
+
* `bodyExcerpt` is invoked lazily, only when the result is `NonRetryable` and the reason
|
|
51
|
+
* string benefits from including a peek at the response body. Lets the caller bound how
|
|
52
|
+
* much data we slurp into the log line.
|
|
53
|
+
*/
|
|
54
|
+
fun classifyResponse(
|
|
55
|
+
statusCode: Int,
|
|
56
|
+
retryAfterHeader: String?,
|
|
57
|
+
responseBody: String?,
|
|
58
|
+
bodyExcerpt: () -> String = { "" }
|
|
59
|
+
): DispatchResult {
|
|
60
|
+
val retryAfter = parseRetryAfter(retryAfterHeader)
|
|
61
|
+
|
|
62
|
+
if (statusCode in 200..299) {
|
|
63
|
+
// The OTLP spec allows `partial_success` to carry a warning-only payload —
|
|
64
|
+
// `rejectedCount == 0` with a non-empty `errorMessage`. Treat that as a successful send
|
|
65
|
+
// (the records DID land) so we don't double-drop. When `rejectedCount > 0`, the records
|
|
66
|
+
// still landed server-side but a subset was rejected; surface that as `PartialSuccess`
|
|
67
|
+
// so the caller can log the count and message clearly. Pending-ID removal and gate
|
|
68
|
+
// behavior for `PartialSuccess` match `Success`.
|
|
69
|
+
val partial = responseBody?.takeIf { it.isNotBlank() }?.let { body ->
|
|
70
|
+
runCatching {
|
|
71
|
+
responseJson.decodeFromString(OTServiceResponse.serializer(), body).partialSuccess
|
|
72
|
+
}.getOrNull()
|
|
73
|
+
}
|
|
74
|
+
if (partial != null && partial.rejectedCount > 0) {
|
|
75
|
+
return DispatchResult.PartialSuccess(partial)
|
|
76
|
+
}
|
|
77
|
+
return DispatchResult.Success
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return when (statusCode) {
|
|
81
|
+
429, 502, 503, 504 -> DispatchResult.RetryableFailure(retryAfter)
|
|
82
|
+
else -> {
|
|
83
|
+
val excerpt = bodyExcerpt()
|
|
84
|
+
val suffix = if (excerpt.isEmpty()) "" else ": $excerpt"
|
|
85
|
+
DispatchResult.NonRetryableFailure(reason = "HTTP $statusCode$suffix")
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Whether the dispatch caller should remove the just-sent pending IDs from the queue.
|
|
92
|
+
*
|
|
93
|
+
* - `Success` and `PartialSuccess` remove them — the rows have been accepted by the server
|
|
94
|
+
* (partial success rejects a subset server-side, but the bytes still landed; re-sending
|
|
95
|
+
* them would just re-trip the same rejection).
|
|
96
|
+
* - `NonRetryableFailure` ALSO removes them — the server has refused these rows
|
|
97
|
+
* permanently, so retrying would produce the same answer; removing them drops the batch
|
|
98
|
+
* so it can't wedge subsequent rounds. This is the acceptance-criterion behavior: a
|
|
99
|
+
* 400/403 must not be re-sent on the next cycle.
|
|
100
|
+
* - `RetryableFailure` keeps them so the next dispatch round picks the same rows up again.
|
|
101
|
+
*/
|
|
102
|
+
fun shouldRemovePending(result: DispatchResult): Boolean = when (result) {
|
|
103
|
+
is DispatchResult.Success,
|
|
104
|
+
is DispatchResult.PartialSuccess,
|
|
105
|
+
is DispatchResult.NonRetryableFailure -> true
|
|
106
|
+
is DispatchResult.RetryableFailure -> false
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parses an HTTP `Retry-After` header into a delay in milliseconds from now. Accepts both
|
|
111
|
+
* formats permitted by RFC 7231: an integer delta-seconds, or an HTTP-date.
|
|
112
|
+
* Returns `null` if the header is absent or unparseable, so the caller can fall through to
|
|
113
|
+
* `computeBackoffDelay` for a client-driven delay.
|
|
114
|
+
*
|
|
115
|
+
* A successfully parsed value is clamped to `[base, cap]` so a misbehaving server can't
|
|
116
|
+
* drive us to either extreme: a value below `base` (including `0`, negatives, or HTTP-dates
|
|
117
|
+
* in the past) floors to `base` so we don't hammer; a value above `cap` (or a date far in
|
|
118
|
+
* the future) ceilings to `cap` so we don't snooze for hours. Non-finite floating-point
|
|
119
|
+
* values (`Infinity`, `NaN`) are treated as garbage and return `null`.
|
|
120
|
+
*/
|
|
121
|
+
fun parseRetryAfter(
|
|
122
|
+
header: String?,
|
|
123
|
+
base: Long = backoffBaseMs,
|
|
124
|
+
cap: Long = backoffCapMs
|
|
125
|
+
): Long? {
|
|
126
|
+
val raw = header?.trim() ?: return null
|
|
127
|
+
if (raw.isEmpty()) return null
|
|
128
|
+
|
|
129
|
+
raw.toLongOrNull()?.let { return clampToBounds(it * 1000L, base, cap) }
|
|
130
|
+
raw.toDoubleOrNull()?.let {
|
|
131
|
+
if (!it.isFinite()) return null
|
|
132
|
+
return clampToBounds((it * 1000L).toLong(), base, cap)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// IMF-fixdate only; others fall through to backoff
|
|
136
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Date#syntax
|
|
137
|
+
val formatter = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US)
|
|
138
|
+
formatter.timeZone = TimeZone.getTimeZone("GMT")
|
|
139
|
+
val parsed = runCatching { formatter.parse(raw) }.getOrNull()
|
|
140
|
+
if (parsed != null) {
|
|
141
|
+
return clampToBounds(TimeUtils.millisUntil(parsed), base, cap)
|
|
142
|
+
}
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Clamps a server-supplied retry delay into the client's `[base, cap]` window. Used by
|
|
148
|
+
* `parseRetryAfter` so the gate is bounded regardless of what the server sent.
|
|
149
|
+
*/
|
|
150
|
+
private fun clampToBounds(ms: Long, base: Long, cap: Long): Long = ms.coerceIn(base, cap)
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* First `limit` bytes of the response body, for inclusion in error log lines. Bounded so a
|
|
154
|
+
* giant HTML error page doesn't flood the log; truncated mid-codepoint is acceptable for a
|
|
155
|
+
* diagnostic excerpt.
|
|
156
|
+
*/
|
|
157
|
+
internal fun bodyExcerpt(body: String?, limit: Int = 512): String {
|
|
158
|
+
if (body.isNullOrEmpty()) return ""
|
|
159
|
+
return if (body.length <= limit) body else body.substring(0, limit)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private val responseJson = Json { ignoreUnknownKeys = true }
|
|
163
|
+
|
|
164
|
+
// MARK: -- Retry gate + exponential backoff
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Base delay (in ms) for the exponential backoff when the server doesn't supply a
|
|
168
|
+
* `Retry-After`. The cap (`backoffCapMs`) bounds the worst-case wait so we don't end up
|
|
169
|
+
* snoozing for hours after a long string of failures.
|
|
170
|
+
*/
|
|
171
|
+
const val backoffBaseMs: Long = 60_000L
|
|
172
|
+
const val backoffCapMs: Long = 900_000L
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Computes a backoff delay (in ms) for the next dispatch attempt when the server didn't
|
|
176
|
+
* supply `Retry-After`. Exponential growth (base × 2^(attempt-1)), capped, with full jitter
|
|
177
|
+
* so a fleet of devices recovering from the same transient backend outage doesn't
|
|
178
|
+
* thunder-herd the recovery.
|
|
179
|
+
*
|
|
180
|
+
* https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
|
|
181
|
+
*
|
|
182
|
+
* `attempt` is 1-based; the helper is defensive for `0` / negative inputs and returns 0
|
|
183
|
+
* rather than producing a negative exponent.
|
|
184
|
+
*/
|
|
185
|
+
fun computeBackoffDelay(
|
|
186
|
+
attempt: Int,
|
|
187
|
+
base: Long = backoffBaseMs,
|
|
188
|
+
cap: Long = backoffCapMs,
|
|
189
|
+
random: () -> Double = { Random.nextDouble() }
|
|
190
|
+
): Long {
|
|
191
|
+
if (attempt < 1) return 0L
|
|
192
|
+
val unjittered = minOf((base.toDouble() * 2.0.pow(attempt - 1)).toLong(), cap)
|
|
193
|
+
return (unjittered.toDouble() * random()).toLong()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Snapshot of the retry-gate state carried across dispatch rounds. `dispatchAfterMs` is the
|
|
198
|
+
* wall-clock deadline (ms since epoch) before which the dispatch entry point should
|
|
199
|
+
* short-circuit (set by a retryable response, naturally expires); `consecutiveRetryableFailures`
|
|
200
|
+
* is the counter that drives `computeBackoffDelay` when the server didn't supply a
|
|
201
|
+
* `Retry-After`.
|
|
202
|
+
*/
|
|
203
|
+
data class RetryGateState(
|
|
204
|
+
val dispatchAfterMs: Long?,
|
|
205
|
+
val consecutiveRetryableFailures: Int
|
|
206
|
+
) {
|
|
207
|
+
companion object {
|
|
208
|
+
val initial = RetryGateState(dispatchAfterMs = null, consecutiveRetryableFailures = 0)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Pure helper that computes the next `RetryGateState` after a single dispatch result.
|
|
214
|
+
*
|
|
215
|
+
* - `Success` and `PartialSuccess` reset the counter to 0 and leave the gate alone. The
|
|
216
|
+
* gate either already expired (we wouldn't have dispatched otherwise) or was never set
|
|
217
|
+
* — either way, a server response that ACCEPTED the bytes (even if it rejected a subset
|
|
218
|
+
* server-side) doesn't introduce a new pause.
|
|
219
|
+
* - `NonRetryableFailure` also resets the counter. A permanent drop isn't a sign that the
|
|
220
|
+
* server is unhealthy and shouldn't pause subsequent rounds.
|
|
221
|
+
* - `RetryableFailure` increments the counter and sets the gate to `now + delay`, where
|
|
222
|
+
* `delay` is the server-supplied `retryAfterMs` if present, otherwise `backoff(nextCount)`.
|
|
223
|
+
*
|
|
224
|
+
* `backoff` is injected so tests can drive it deterministically without going through
|
|
225
|
+
* `computeBackoffDelay`'s random source.
|
|
226
|
+
*/
|
|
227
|
+
fun nextRetryGateState(
|
|
228
|
+
result: DispatchResult,
|
|
229
|
+
currentState: RetryGateState,
|
|
230
|
+
now: Long,
|
|
231
|
+
backoff: (Int) -> Long
|
|
232
|
+
): RetryGateState = when (result) {
|
|
233
|
+
is DispatchResult.Success,
|
|
234
|
+
is DispatchResult.PartialSuccess,
|
|
235
|
+
is DispatchResult.NonRetryableFailure ->
|
|
236
|
+
currentState.copy(consecutiveRetryableFailures = 0)
|
|
237
|
+
is DispatchResult.RetryableFailure -> {
|
|
238
|
+
val nextCount = currentState.consecutiveRetryableFailures + 1
|
|
239
|
+
val delay = result.retryAfterMs ?: backoff(nextCount)
|
|
240
|
+
RetryGateState(
|
|
241
|
+
dispatchAfterMs = now + delay,
|
|
242
|
+
consecutiveRetryableFailures = nextCount
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -9,7 +9,6 @@ import okhttp3.OkHttpClient
|
|
|
9
9
|
import okhttp3.Request
|
|
10
10
|
import okhttp3.RequestBody.Companion.toRequestBody
|
|
11
11
|
import kotlin.coroutines.resume
|
|
12
|
-
import kotlin.coroutines.resumeWithException
|
|
13
12
|
|
|
14
13
|
// TODO: Make this safe for concurrent usage
|
|
15
14
|
class EventDispatcher(
|
|
@@ -28,60 +27,61 @@ class EventDispatcher(
|
|
|
28
27
|
|
|
29
28
|
private fun logsEndpointUrl(): String = "$baseProjectUrl/v1/logs"
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* POSTs `events` to the metrics endpoint and classifies the response per the OTLP retry
|
|
32
|
+
* spec. Non-throwing: payload-encoding errors collapse to `NonRetryable` (same bytes will
|
|
33
|
+
* fail again), transport errors collapse to `Retryable(null)` (transient by definition).
|
|
34
|
+
*/
|
|
35
|
+
suspend fun dispatch(events: List<Event>): DispatchResult =
|
|
32
36
|
suspendCancellableCoroutine { continuation ->
|
|
33
37
|
if (events.isEmpty()) {
|
|
34
|
-
|
|
38
|
+
// Empty input isn't a server response - signal success.
|
|
39
|
+
continuation.resume(DispatchResult.Success)
|
|
35
40
|
return@suspendCancellableCoroutine Unit
|
|
36
41
|
}
|
|
37
42
|
val easId = EASClientID(context).uuid.toString()
|
|
38
|
-
try {
|
|
39
|
-
|
|
43
|
+
val body = try {
|
|
44
|
+
OTRequestBody(
|
|
40
45
|
resourceMetrics = events.map { it.toOTEvent(easId) }
|
|
41
46
|
).toJson(prettyPrint = true)
|
|
42
|
-
|
|
43
|
-
executePost(continuation, metricsEndpointUrl(), body)
|
|
44
47
|
} catch (e: Exception) {
|
|
45
|
-
Log.w(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
)
|
|
49
|
-
continuation.resumeWithException(e)
|
|
48
|
+
Log.w(OBSERVE_TAG, "Encoding metrics request body failed: ${e.message}")
|
|
49
|
+
continuation.resume(DispatchResult.NonRetryableFailure("encoding error: ${e.message}"))
|
|
50
|
+
return@suspendCancellableCoroutine Unit
|
|
50
51
|
}
|
|
52
|
+
executePost(continuation, metricsEndpointUrl(), body)
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
56
|
* Dispatches log records to `{baseUrl}/{projectId}/v1/logs`. Always uses the
|
|
55
57
|
* OTLP wire shape — there is no legacy logs endpoint.
|
|
56
58
|
*/
|
|
57
|
-
suspend fun dispatchLogs(events: List<Event>):
|
|
59
|
+
suspend fun dispatchLogs(events: List<Event>): DispatchResult =
|
|
58
60
|
suspendCancellableCoroutine { continuation ->
|
|
59
61
|
val easId = EASClientID(context).uuid.toString()
|
|
60
62
|
val resourceLogs = events
|
|
61
63
|
.filter { it.logs.isNotEmpty() }
|
|
62
64
|
.map { it.toOTResourceLogs(easId) }
|
|
63
65
|
if (resourceLogs.isEmpty()) {
|
|
64
|
-
continuation.resume(
|
|
66
|
+
continuation.resume(DispatchResult.Success)
|
|
65
67
|
return@suspendCancellableCoroutine Unit
|
|
66
68
|
}
|
|
67
|
-
try {
|
|
68
|
-
|
|
69
|
-
executePost(continuation, logsEndpointUrl(), body)
|
|
69
|
+
val body = try {
|
|
70
|
+
OTLogsRequestBody(resourceLogs = resourceLogs).toJson(prettyPrint = true)
|
|
70
71
|
} catch (e: Exception) {
|
|
71
|
-
Log.w(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
)
|
|
75
|
-
continuation.resumeWithException(e)
|
|
72
|
+
Log.w(OBSERVE_TAG, "Encoding logs request body failed: ${e.message}")
|
|
73
|
+
continuation.resume(DispatchResult.NonRetryableFailure("encoding error: ${e.message}"))
|
|
74
|
+
return@suspendCancellableCoroutine Unit
|
|
76
75
|
}
|
|
76
|
+
executePost(continuation, logsEndpointUrl(), body)
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
private fun executePost(
|
|
80
|
-
continuation: kotlinx.coroutines.CancellableContinuation<
|
|
80
|
+
continuation: kotlinx.coroutines.CancellableContinuation<DispatchResult>,
|
|
81
81
|
endpointUrl: String,
|
|
82
82
|
body: String
|
|
83
83
|
) {
|
|
84
|
-
Log.d(
|
|
84
|
+
Log.d(OBSERVE_TAG, "Sending events to $endpointUrl")
|
|
85
85
|
|
|
86
86
|
val request = Request
|
|
87
87
|
.Builder()
|
|
@@ -96,7 +96,7 @@ class EventDispatcher(
|
|
|
96
96
|
.post(body.toRequestBody("application/json".toMediaType()))
|
|
97
97
|
.build()
|
|
98
98
|
|
|
99
|
-
Log.d(
|
|
99
|
+
Log.d(OBSERVE_TAG, body)
|
|
100
100
|
|
|
101
101
|
val call = httpClient.newCall(request)
|
|
102
102
|
|
|
@@ -104,13 +104,41 @@ class EventDispatcher(
|
|
|
104
104
|
call.cancel()
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
val response =
|
|
108
|
-
|
|
107
|
+
val response = try {
|
|
108
|
+
call.execute()
|
|
109
|
+
} catch (e: Exception) {
|
|
110
|
+
Log.w(OBSERVE_TAG, "Transport error talking to $endpointUrl: ${e.message}")
|
|
111
|
+
continuation.resume(DispatchResult.RetryableFailure(retryAfterMs = null))
|
|
112
|
+
return
|
|
113
|
+
}
|
|
109
114
|
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
val responseBody = response.body?.string()
|
|
116
|
+
val retryAfterHeader = response.header("Retry-After")
|
|
117
|
+
val result = DispatchUtils.classifyResponse(
|
|
118
|
+
statusCode = response.code,
|
|
119
|
+
retryAfterHeader = retryAfterHeader,
|
|
120
|
+
responseBody = responseBody,
|
|
121
|
+
bodyExcerpt = { DispatchUtils.bodyExcerpt(responseBody) }
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
when (result) {
|
|
125
|
+
is DispatchResult.Success ->
|
|
126
|
+
Log.d(OBSERVE_TAG, "Server responded successfully with ${response.code} and data: $responseBody")
|
|
127
|
+
is DispatchResult.PartialSuccess ->
|
|
128
|
+
Log.w(
|
|
129
|
+
OBSERVE_TAG,
|
|
130
|
+
"Server responded with ${response.code} (partial success, rejected " +
|
|
131
|
+
"${result.partial.rejectedCount}: ${result.partial.errorMessage ?: "no error message"}) "
|
|
132
|
+
)
|
|
133
|
+
is DispatchResult.RetryableFailure ->
|
|
134
|
+
Log.w(OBSERVE_TAG, "Server responded with ${response.code} (retryable) and data: $responseBody")
|
|
135
|
+
is DispatchResult.NonRetryableFailure ->
|
|
136
|
+
Log.w(
|
|
137
|
+
OBSERVE_TAG,
|
|
138
|
+
"Server responded with ${response.code} (non-retryable, ${result.reason}) and data: $responseBody"
|
|
139
|
+
)
|
|
140
|
+
}
|
|
112
141
|
|
|
113
|
-
|
|
114
|
-
private const val TAG = "EasObserve"
|
|
142
|
+
continuation.resume(result)
|
|
115
143
|
}
|
|
116
144
|
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
package expo.modules.observe
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.util.Log
|
|
4
5
|
import expo.modules.easclient.EASClientID
|
|
5
6
|
import expo.modules.observe.storage.PendingLogsManager
|
|
6
7
|
import expo.modules.observe.storage.PendingMetricsManager
|
|
7
8
|
import expo.modules.appmetrics.storage.SessionManager
|
|
9
|
+
import expo.modules.appmetrics.utils.TimeUtils
|
|
8
10
|
import expo.modules.interfaces.constants.ConstantsInterface
|
|
11
|
+
import kotlinx.coroutines.sync.Mutex
|
|
12
|
+
import kotlinx.coroutines.sync.withLock
|
|
9
13
|
|
|
10
14
|
class ObservabilityManager(
|
|
11
15
|
// TODO(@lukmccall): Consider saving context as weak reference to avoid potential memory leaks
|
|
@@ -76,7 +80,8 @@ class BaseObservabilityManager(
|
|
|
76
80
|
private val isDebugBuild: Boolean = false,
|
|
77
81
|
private val deterministicUniformValueProvider: () -> Double = {
|
|
78
82
|
EASClientID.deterministicUniformValue(EASClientID(context).uuid)
|
|
79
|
-
}
|
|
83
|
+
},
|
|
84
|
+
private val currentTimeMs: () -> Long = { TimeUtils.getWallClockMillis() }
|
|
80
85
|
) {
|
|
81
86
|
private val eventDispatcher = EventDispatcher(
|
|
82
87
|
context = context,
|
|
@@ -84,12 +89,75 @@ class BaseObservabilityManager(
|
|
|
84
89
|
baseUrl = baseUrl
|
|
85
90
|
)
|
|
86
91
|
|
|
87
|
-
|
|
92
|
+
/**
|
|
93
|
+
* In-memory retry-gate state, kept independently per OTLP endpoint. The `/v1/metrics` and
|
|
94
|
+
* `/v1/logs` endpoints fail independently in practice (one schema validation disagreement
|
|
95
|
+
* on the metrics side shouldn't suppress a healthy logs stream), so each signal carries
|
|
96
|
+
* its own consecutive-failure counter and dispatch-after deadline. A single shared field
|
|
97
|
+
* would conflate the two: a recovering signal would reset the other's counter on success,
|
|
98
|
+
* and a server's `Retry-After` on one endpoint would silently overwrite a longer backoff
|
|
99
|
+
* computed for the other.
|
|
100
|
+
*
|
|
101
|
+
* State is reset implicitly when the process restarts — a relaunch usually means enough
|
|
102
|
+
* time passed that the transient cause has cleared anyway, and persisting the gates would
|
|
103
|
+
* mean a disk write per retryable response.
|
|
104
|
+
*/
|
|
105
|
+
private var metricsRetryGate: DispatchUtils.RetryGateState = DispatchUtils.RetryGateState.initial
|
|
106
|
+
private var logsRetryGate: DispatchUtils.RetryGateState = DispatchUtils.RetryGateState.initial
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Per-signal mutexes that serialize same-signal dispatch calls. Two `dispatchEvents`
|
|
110
|
+
* invocations from JS can otherwise land on the same `BaseObservabilityManager` instance
|
|
111
|
+
* concurrently and race on the gate's read-modify-write (and double-POST the same pending
|
|
112
|
+
* rows). The metrics and logs paths take separate mutexes so they can still run in
|
|
113
|
+
* parallel — only same-signal calls serialize.
|
|
114
|
+
*
|
|
115
|
+
* Worst case without these would be benign (a dropped gate update or a duplicate dispatch),
|
|
116
|
+
* since the pending-ID stores are already DB-backed and telemetry is best-effort — but the
|
|
117
|
+
* gate is in-memory state with no other synchronization, so close the race explicitly.
|
|
118
|
+
*/
|
|
119
|
+
private val metricsDispatchMutex = Mutex()
|
|
120
|
+
private val logsDispatchMutex = Mutex()
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Returns true and logs when an active retry gate suppresses this dispatch round. Called
|
|
124
|
+
* inside each per-signal dispatch method rather than at a shared entry point, so a backoff
|
|
125
|
+
* on one endpoint doesn't suppress traffic on the other.
|
|
126
|
+
*/
|
|
127
|
+
private fun retryGateBlocks(state: DispatchUtils.RetryGateState, signal: String): Boolean {
|
|
128
|
+
val until = state.dispatchAfterMs ?: return false
|
|
129
|
+
val now = currentTimeMs()
|
|
130
|
+
if (until <= now) return false
|
|
131
|
+
Log.d(OBSERVE_TAG, "$signal dispatch suppressed by retry gate until $until (now $now)")
|
|
132
|
+
return true
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Computes the next gate state for a given current state and dispatch result. Each per-
|
|
137
|
+
* signal call site assigns the return value back to its own field — the manager doesn't
|
|
138
|
+
* share a single mutable state across signals, so the metrics and logs gates can't drift
|
|
139
|
+
* out of sync from cross-signal updates.
|
|
140
|
+
*/
|
|
141
|
+
private fun nextGate(
|
|
142
|
+
current: DispatchUtils.RetryGateState,
|
|
143
|
+
result: DispatchResult
|
|
144
|
+
): DispatchUtils.RetryGateState = DispatchUtils.nextRetryGateState(
|
|
145
|
+
result = result,
|
|
146
|
+
currentState = current,
|
|
147
|
+
now = currentTimeMs(),
|
|
148
|
+
backoff = { DispatchUtils.computeBackoffDelay(it) }
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
suspend fun dispatchUnsentMetrics(): Unit = metricsDispatchMutex.withLock {
|
|
88
152
|
val pendingIds = pendingMetricsManager.getAllPendingMetricIds()
|
|
89
153
|
if (pendingIds.isEmpty()) {
|
|
90
154
|
return
|
|
91
155
|
}
|
|
92
156
|
|
|
157
|
+
if (retryGateBlocks(metricsRetryGate, "metrics")) {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
93
161
|
if (!shouldDispatch()) {
|
|
94
162
|
pendingMetricsManager.removePendingMetrics(pendingIds)
|
|
95
163
|
return
|
|
@@ -115,22 +183,43 @@ class BaseObservabilityManager(
|
|
|
115
183
|
)
|
|
116
184
|
}
|
|
117
185
|
|
|
118
|
-
|
|
119
|
-
|
|
186
|
+
val result = eventDispatcher.dispatch(events)
|
|
187
|
+
metricsRetryGate = nextGate(metricsRetryGate, result)
|
|
188
|
+
val dispatchedMetricIds = sessionsWithPendingMetrics.flatMap { it.metrics }.map { it.metricId }
|
|
189
|
+
if (DispatchUtils.shouldRemovePending(result)) {
|
|
120
190
|
pendingMetricsManager.removePendingMetrics(dispatchedMetricIds)
|
|
121
191
|
}
|
|
192
|
+
when (result) {
|
|
193
|
+
is DispatchResult.PartialSuccess ->
|
|
194
|
+
Log.w(
|
|
195
|
+
OBSERVE_TAG,
|
|
196
|
+
"Partial success on batch of ${dispatchedMetricIds.size} metric event(s): " +
|
|
197
|
+
"server rejected ${result.partial.rejectedCount} " +
|
|
198
|
+
"(${result.partial.errorMessage ?: "no error message"})"
|
|
199
|
+
)
|
|
200
|
+
is DispatchResult.NonRetryableFailure ->
|
|
201
|
+
Log.w(
|
|
202
|
+
OBSERVE_TAG,
|
|
203
|
+
"Dropping batch of ${dispatchedMetricIds.size} metric event(s): ${result.reason}"
|
|
204
|
+
)
|
|
205
|
+
is DispatchResult.Success, is DispatchResult.RetryableFailure -> Unit
|
|
206
|
+
}
|
|
122
207
|
}
|
|
123
208
|
|
|
124
209
|
/**
|
|
125
210
|
* Dispatches log events to `/v1/logs`. Independent from the metrics path —
|
|
126
211
|
* a logs failure doesn't affect the metrics pending table and vice versa.
|
|
127
212
|
*/
|
|
128
|
-
suspend fun dispatchUnsentLogs() {
|
|
213
|
+
suspend fun dispatchUnsentLogs(): Unit = logsDispatchMutex.withLock {
|
|
129
214
|
val pendingIds = pendingLogsManager.getAllPendingLogIds()
|
|
130
215
|
if (pendingIds.isEmpty()) {
|
|
131
216
|
return
|
|
132
217
|
}
|
|
133
218
|
|
|
219
|
+
if (retryGateBlocks(logsRetryGate, "logs")) {
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
134
223
|
if (!shouldDispatch()) {
|
|
135
224
|
pendingLogsManager.removePendingLogs(pendingIds)
|
|
136
225
|
return
|
|
@@ -158,10 +247,27 @@ class BaseObservabilityManager(
|
|
|
158
247
|
)
|
|
159
248
|
}
|
|
160
249
|
|
|
161
|
-
|
|
162
|
-
|
|
250
|
+
val result = eventDispatcher.dispatchLogs(events)
|
|
251
|
+
logsRetryGate = nextGate(logsRetryGate, result)
|
|
252
|
+
val dispatchedLogIds = sessionsWithPendingLogs.flatMap { it.logs }.map { it.logId }
|
|
253
|
+
if (DispatchUtils.shouldRemovePending(result)) {
|
|
163
254
|
pendingLogsManager.removePendingLogs(dispatchedLogIds)
|
|
164
255
|
}
|
|
256
|
+
when (result) {
|
|
257
|
+
is DispatchResult.PartialSuccess ->
|
|
258
|
+
Log.w(
|
|
259
|
+
OBSERVE_TAG,
|
|
260
|
+
"Partial success on batch of ${dispatchedLogIds.size} log event(s): " +
|
|
261
|
+
"server rejected ${result.partial.rejectedCount} " +
|
|
262
|
+
"(${result.partial.errorMessage ?: "no error message"})"
|
|
263
|
+
)
|
|
264
|
+
is DispatchResult.NonRetryableFailure ->
|
|
265
|
+
Log.w(
|
|
266
|
+
OBSERVE_TAG,
|
|
267
|
+
"Dropping batch of ${dispatchedLogIds.size} log event(s): ${result.reason}"
|
|
268
|
+
)
|
|
269
|
+
is DispatchResult.Success, is DispatchResult.RetryableFailure -> Unit
|
|
270
|
+
}
|
|
165
271
|
}
|
|
166
272
|
|
|
167
273
|
private fun isInSample(): Boolean {
|
|
@@ -17,7 +17,8 @@ class Config(
|
|
|
17
17
|
@Field val environment: String? = null,
|
|
18
18
|
@Field val dispatchingEnabled: Boolean? = null,
|
|
19
19
|
@Field val dispatchInDebug: Boolean? = null,
|
|
20
|
-
@Field val sampleRate: Double? = null
|
|
20
|
+
@Field val sampleRate: Double? = null,
|
|
21
|
+
@Field val integrations: Map<String, Any?>? = null
|
|
21
22
|
) : Record
|
|
22
23
|
|
|
23
24
|
@OptimizedRecord
|
|
@@ -33,10 +34,14 @@ class ObserveModule : Module() {
|
|
|
33
34
|
private lateinit var observabilityManager: ObservabilityManager
|
|
34
35
|
private lateinit var appMetricsModule: AppMetricsModule
|
|
35
36
|
|
|
37
|
+
private var lastIntegrations: Map<String, Any?> = emptyMap()
|
|
38
|
+
|
|
36
39
|
override fun definition() =
|
|
37
40
|
ModuleDefinition {
|
|
38
41
|
Name("ExpoObserve")
|
|
39
42
|
|
|
43
|
+
Events("configure")
|
|
44
|
+
|
|
40
45
|
OnCreate {
|
|
41
46
|
appMetricsModule = checkNotNull(appContext.registry.getModule<AppMetricsModule>()) {
|
|
42
47
|
"AppMetricsModule is required by ObserveModule. Make sure expo-app-metrics is installed."
|
|
@@ -72,6 +77,14 @@ class ObserveModule : Module() {
|
|
|
72
77
|
val resolvedEnvironment = config.environment
|
|
73
78
|
?: ObservePreferences.getBundleDefaults(context)?.environment
|
|
74
79
|
resolvedEnvironment?.let { appMetricsModule.setEnvironment(it) }
|
|
80
|
+
|
|
81
|
+
// Broadcast the integrations config so integration libraries (e.g. expo-image) can activate.
|
|
82
|
+
lastIntegrations = config.integrations ?: emptyMap()
|
|
83
|
+
this@ObserveModule.sendEvent("configure", mapOf("integrations" to lastIntegrations))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
Function("getIntegrations") {
|
|
87
|
+
lastIntegrations
|
|
75
88
|
}
|
|
76
89
|
|
|
77
90
|
Function("setBundleDefaults") { defaults: BundleDefaults ->
|
|
@@ -160,6 +160,34 @@ data class OTLogsRequestBody(
|
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
// MARK: -- Response shapes
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* `partial_success` body emitted by the collector when records were accepted but some were
|
|
167
|
+
* rejected. Mirrors the wire shape the server actually sends (camelCase keys; the metrics
|
|
168
|
+
* endpoint populates `rejectedDataPoints`, the logs endpoint populates `rejectedLogRecords`).
|
|
169
|
+
* The server only emits this block when at least one record was rejected — a fully successful
|
|
170
|
+
* dispatch returns no body.
|
|
171
|
+
*
|
|
172
|
+
* The OTLP spec also allows a warning-only variant where `rejectedCount == 0` and
|
|
173
|
+
* `errorMessage` carries an advisory note (e.g. deprecation warnings). The classifier handles
|
|
174
|
+
* that case as a success since the records did land.
|
|
175
|
+
*/
|
|
176
|
+
@Serializable
|
|
177
|
+
data class OTPartialSuccess(
|
|
178
|
+
val rejectedDataPoints: Int? = null,
|
|
179
|
+
val rejectedLogRecords: Int? = null,
|
|
180
|
+
val errorMessage: String? = null
|
|
181
|
+
) {
|
|
182
|
+
val rejectedCount: Int
|
|
183
|
+
get() = (rejectedDataPoints ?: 0) + (rejectedLogRecords ?: 0)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@Serializable
|
|
187
|
+
data class OTServiceResponse(
|
|
188
|
+
val partialSuccess: OTPartialSuccess? = null
|
|
189
|
+
)
|
|
190
|
+
|
|
163
191
|
/**
|
|
164
192
|
* OpenTelemetry Semantic Conventions schema URL referenced by the resource on
|
|
165
193
|
* every dispatched payload. Bumping this constant signals that our attribute
|
package/build/index.d.ts
CHANGED
|
@@ -6,6 +6,6 @@ export { ObserveRoot } from './ObserveRoot';
|
|
|
6
6
|
export { Observe };
|
|
7
7
|
/** @deprecated Import the named `Observe` export instead. */
|
|
8
8
|
export default Observe;
|
|
9
|
-
export type { ObserveAttribute, ObserveAttributes, ObserveConfig, ObserveIntegrationsConfig, } from './types';
|
|
9
|
+
export type { ObserveAttribute, ObserveAttributes, ObserveConfig, ObserveIntegrationsConfig, ObserveModule, ObserveModuleEvents, } from './types';
|
|
10
10
|
export { useObserve } from './useObserve';
|
|
11
11
|
//# sourceMappingURL=index.d.ts.map
|