expo-observe 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/expo/modules/observe/OpenTelemetry.kt +17 -9
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/ios/Encodable+JSON.swift +16 -0
- package/ios/Observability.swift +5 -3
- package/ios/OpenTelemetry.swift +19 -23
- package/ios/RequestBody.swift +1 -17
- package/ios/Tests/OpenTelemetryTests.swift +67 -4
- package/package.json +4 -5
- package/src/index.ts +2 -0
|
@@ -28,7 +28,7 @@ data class OTAttribute(
|
|
|
28
28
|
@Serializable
|
|
29
29
|
data class OTDataPoint(
|
|
30
30
|
val timeUnixNano: Long,
|
|
31
|
-
val
|
|
31
|
+
val asDouble: Double,
|
|
32
32
|
val attributes: List<OTAttribute>
|
|
33
33
|
)
|
|
34
34
|
|
|
@@ -96,6 +96,16 @@ private val metricNameMap = mapOf(
|
|
|
96
96
|
)
|
|
97
97
|
|
|
98
98
|
fun EASMetric.toOTMetric(): OTMetric {
|
|
99
|
+
val attributes = mutableListOf(
|
|
100
|
+
OTAttribute.of(key = "session.id", rawValue = sessionId)
|
|
101
|
+
)
|
|
102
|
+
routeName?.let {
|
|
103
|
+
attributes.add(OTAttribute.of(key = "expo.route_name", rawValue = it))
|
|
104
|
+
}
|
|
105
|
+
customParams?.let {
|
|
106
|
+
attributes.add(OTAttribute.of(key = "expo.custom_params", rawValue = it.toString()))
|
|
107
|
+
}
|
|
108
|
+
|
|
99
109
|
return OTMetric(
|
|
100
110
|
unit = "s",
|
|
101
111
|
name = metricNameMap[name] ?: "expo.app_startup.$name",
|
|
@@ -103,13 +113,8 @@ fun EASMetric.toOTMetric(): OTMetric {
|
|
|
103
113
|
dataPoints = listOf(
|
|
104
114
|
OTDataPoint(
|
|
105
115
|
timeUnixNano = timestampToDateNS(timestamp),
|
|
106
|
-
|
|
107
|
-
attributes =
|
|
108
|
-
OTAttribute.of(
|
|
109
|
-
key = "session.id",
|
|
110
|
-
rawValue = sessionId
|
|
111
|
-
)
|
|
112
|
-
)
|
|
116
|
+
asDouble = value,
|
|
117
|
+
attributes = attributes
|
|
113
118
|
)
|
|
114
119
|
)
|
|
115
120
|
)
|
|
@@ -126,6 +131,7 @@ fun Event.toOTMetadata(easClientId: String): OTMetadata {
|
|
|
126
131
|
OTAttribute.of("os.version", metadata.deviceOsVersion ?: ""),
|
|
127
132
|
OTAttribute.of("device.model.name", metadata.deviceName ?: ""),
|
|
128
133
|
OTAttribute.of("device.model.identifier", metadata.deviceModel ?: ""),
|
|
134
|
+
OTAttribute.of("browser.language", metadata.languageTag ?: ""),
|
|
129
135
|
OTAttribute.of("telemetry.sdk.name", "expo-observe"),
|
|
130
136
|
OTAttribute.of("telemetry.sdk.version", metadata.clientVersion ?: ""),
|
|
131
137
|
OTAttribute.of("telemetry.sdk.language", "kotlin"),
|
|
@@ -133,8 +139,10 @@ fun Event.toOTMetadata(easClientId: String): OTMetadata {
|
|
|
133
139
|
OTAttribute.of("expo.app.build_number", metadata.appBuildNumber ?: ""),
|
|
134
140
|
OTAttribute.of("expo.app.update_id", metadata.appUpdateId ?: ""),
|
|
135
141
|
OTAttribute.of("expo.sdk.version", metadata.expoSdkVersion),
|
|
142
|
+
OTAttribute.of("expo.environment", metadata.environment ?: ""),
|
|
136
143
|
OTAttribute.of("expo.react_native.version", metadata.reactNativeVersion),
|
|
137
|
-
OTAttribute.of("expo.eas_client.id", easClientId)
|
|
144
|
+
OTAttribute.of("expo.eas_client.id", easClientId),
|
|
145
|
+
OTAttribute.of("expo.eas_build.id", metadata.appEasBuildId ?: "")
|
|
138
146
|
)
|
|
139
147
|
)
|
|
140
148
|
}
|
package/build/index.d.ts
CHANGED
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,cAAc,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEzE,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,cAAc,SAAS,CAAC"}
|
package/build/index.js
CHANGED
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,cAAc,SAAS,CAAC","sourcesContent":["export { default } from './module';\nexport * from './types';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEzE,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,cAAc,SAAS,CAAC","sourcesContent":["export { default as AppMetrics, AppMetricsRoot } from 'expo-app-metrics';\n\nexport { default } from './module';\nexport * from './types';\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Copyright 2025-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
extension Encodable {
|
|
6
|
+
func toJSONData(_ formatting: JSONEncoder.OutputFormatting = []) throws -> Data {
|
|
7
|
+
let encoder = JSONEncoder()
|
|
8
|
+
encoder.outputFormatting = formatting
|
|
9
|
+
return try encoder.encode(self)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
func toJSONString(_ formatting: JSONEncoder.OutputFormatting = []) throws -> String {
|
|
13
|
+
let data = try toJSONData(formatting)
|
|
14
|
+
return String(data: data, encoding: .utf8) ?? ""
|
|
15
|
+
}
|
|
16
|
+
}
|
package/ios/Observability.swift
CHANGED
|
@@ -56,7 +56,7 @@ internal struct ObservabilityManager {
|
|
|
56
56
|
return
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
let body: any
|
|
59
|
+
let body: any Encodable
|
|
60
60
|
if useOpenTelemetry {
|
|
61
61
|
body = OTRequestBody(resourceMetrics: events.map { $0.toOTEvent(easClientId)})
|
|
62
62
|
} else {
|
|
@@ -65,9 +65,11 @@ internal struct ObservabilityManager {
|
|
|
65
65
|
var request = URLRequest(url: endpointUrl)
|
|
66
66
|
request.httpMethod = "POST"
|
|
67
67
|
request.allHTTPHeaderFields = ["Content-Type": "application/json"]
|
|
68
|
-
request.httpBody = try body.
|
|
68
|
+
request.httpBody = try body.toJSONData([])
|
|
69
69
|
|
|
70
|
-
observeLogger.debug("[EAS Observe] Sending the request to \(endpointUrl) with body:
|
|
70
|
+
observeLogger.debug("[EAS Observe] Sending the request to \(endpointUrl) with body:")
|
|
71
|
+
// Use `print` so the JSON can be copied without including the log level emojis.
|
|
72
|
+
print(try body.toJSONString(.prettyPrinted))
|
|
71
73
|
|
|
72
74
|
let (responseData, urlResponse) = try await URLSession.shared.data(for: request)
|
|
73
75
|
|
package/ios/OpenTelemetry.swift
CHANGED
|
@@ -18,7 +18,7 @@ struct OTAttribute: Codable, Sendable {
|
|
|
18
18
|
|
|
19
19
|
struct OTDataPoint: Codable, Sendable {
|
|
20
20
|
let timeUnixNano: UInt64
|
|
21
|
-
let
|
|
21
|
+
let asDouble: Double
|
|
22
22
|
let attributes: [OTAttribute]
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -79,19 +79,24 @@ extension Event.Metric {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
func toOTMetric() -> OTMetric {
|
|
82
|
-
|
|
82
|
+
var attributes: [OTAttribute] = [
|
|
83
|
+
OTAttribute(key: "session.id", rawValue: sessionId)
|
|
84
|
+
]
|
|
85
|
+
if let routeName {
|
|
86
|
+
attributes.append(OTAttribute(key: "expo.route_name", rawValue: routeName))
|
|
87
|
+
}
|
|
88
|
+
if let customParamsString = try? customParams?.toJSONString() {
|
|
89
|
+
attributes.append(OTAttribute(key: "expo.custom_params", rawValue: customParamsString))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return OTMetric(
|
|
83
93
|
unit: "s",
|
|
84
94
|
name: metricNameMap[self.name] ?? "expo.app_startup.\(self.name)",
|
|
85
95
|
gauge: OTGauge(dataPoints: [
|
|
86
96
|
OTDataPoint(
|
|
87
97
|
timeUnixNano: nsFromISODateString(),
|
|
88
|
-
|
|
89
|
-
attributes:
|
|
90
|
-
OTAttribute(
|
|
91
|
-
key: "session.id",
|
|
92
|
-
rawValue: self.sessionId
|
|
93
|
-
)
|
|
94
|
-
]
|
|
98
|
+
asDouble: self.value,
|
|
99
|
+
attributes: attributes
|
|
95
100
|
)
|
|
96
101
|
])
|
|
97
102
|
)
|
|
@@ -108,6 +113,7 @@ extension Event {
|
|
|
108
113
|
OTAttribute(key: "os.version", rawValue: metadata.deviceOsVersion),
|
|
109
114
|
OTAttribute(key: "device.model.name", rawValue: metadata.deviceName),
|
|
110
115
|
OTAttribute(key: "device.model.identifier", rawValue: metadata.deviceModel),
|
|
116
|
+
OTAttribute(key: "browser.language", rawValue: metadata.languageTag),
|
|
111
117
|
OTAttribute(key: "telemetry.sdk.name", rawValue: "expo-observe"),
|
|
112
118
|
OTAttribute(key: "telemetry.sdk.version", rawValue: ObserveVersions.clientVersion),
|
|
113
119
|
OTAttribute(key: "telemetry.sdk.language", rawValue: "swift"),
|
|
@@ -115,9 +121,10 @@ extension Event {
|
|
|
115
121
|
OTAttribute(key: "expo.app.build_number", rawValue: metadata.appBuildNumber ?? ""),
|
|
116
122
|
OTAttribute(key: "expo.app.update_id", rawValue: metadata.appUpdateId ?? ""),
|
|
117
123
|
OTAttribute(key: "expo.sdk.version", rawValue: metadata.expoSdkVersion),
|
|
124
|
+
OTAttribute(key: "expo.environment", rawValue: metadata.environment ?? ""),
|
|
118
125
|
OTAttribute(key: "expo.react_native.version", rawValue: metadata.reactNativeVersion),
|
|
119
|
-
OTAttribute(key: "expo.eas_client.id", rawValue: easClientId)
|
|
120
|
-
|
|
126
|
+
OTAttribute(key: "expo.eas_client.id", rawValue: easClientId),
|
|
127
|
+
OTAttribute(key: "expo.eas_build.id", rawValue: metadata.appEasBuildId ?? ""),
|
|
121
128
|
])
|
|
122
129
|
}
|
|
123
130
|
|
|
@@ -136,17 +143,6 @@ extension Event {
|
|
|
136
143
|
|
|
137
144
|
// MARK: -- Request body for Open Telemetry events
|
|
138
145
|
|
|
139
|
-
internal struct OTRequestBody: Codable, Sendable
|
|
146
|
+
internal struct OTRequestBody: Codable, Sendable {
|
|
140
147
|
let resourceMetrics: [OTEvent]
|
|
141
|
-
|
|
142
|
-
func toData(_ formatting: JSONEncoder.OutputFormatting = []) throws -> Data {
|
|
143
|
-
let encoder = JSONEncoder()
|
|
144
|
-
encoder.outputFormatting = formatting
|
|
145
|
-
return try encoder.encode(self)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
func toString(_ formatting: JSONEncoder.OutputFormatting = []) throws -> String {
|
|
149
|
-
let data = try toData(formatting)
|
|
150
|
-
return String(data: data, encoding: .utf8) ?? ""
|
|
151
|
-
}
|
|
152
148
|
}
|
package/ios/RequestBody.swift
CHANGED
|
@@ -1,25 +1,9 @@
|
|
|
1
1
|
// Copyright 2025-present 650 Industries. All rights reserved.
|
|
2
2
|
|
|
3
|
-
internal protocol RequestBodyProtocol {
|
|
4
|
-
func toData(_ formatting: JSONEncoder.OutputFormatting) throws -> Data
|
|
5
|
-
func toString(_ formatting: JSONEncoder.OutputFormatting) throws -> String
|
|
6
|
-
}
|
|
7
|
-
|
|
8
3
|
/**
|
|
9
4
|
Body of the request containing events with metrics for the EAS server.
|
|
10
5
|
*/
|
|
11
|
-
internal struct RequestBody: Codable, Sendable
|
|
6
|
+
internal struct RequestBody: Codable, Sendable {
|
|
12
7
|
let easClientId: String
|
|
13
8
|
let events: [Event]
|
|
14
|
-
|
|
15
|
-
func toData(_ formatting: JSONEncoder.OutputFormatting = []) throws -> Data {
|
|
16
|
-
let encoder = JSONEncoder()
|
|
17
|
-
encoder.outputFormatting = formatting
|
|
18
|
-
return try encoder.encode(self)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
func toString(_ formatting: JSONEncoder.OutputFormatting = []) throws -> String {
|
|
22
|
-
let data = try toData(formatting)
|
|
23
|
-
return String(data: data, encoding: .utf8) ?? ""
|
|
24
|
-
}
|
|
25
9
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Testing
|
|
2
2
|
|
|
3
3
|
@testable import ExpoObserve
|
|
4
|
+
@testable import ExpoAppMetrics
|
|
4
5
|
|
|
5
6
|
@Suite("OpenTelemetry conversion")
|
|
6
7
|
struct OpenTelemetryTests {
|
|
@@ -96,7 +97,7 @@ struct OpenTelemetryTests {
|
|
|
96
97
|
#expect(otMetric.gauge.dataPoints.count == 1)
|
|
97
98
|
|
|
98
99
|
let dataPoint = otMetric.gauge.dataPoints[0]
|
|
99
|
-
#expect(dataPoint.
|
|
100
|
+
#expect(dataPoint.asDouble == 3.14)
|
|
100
101
|
#expect(dataPoint.attributes.count == 1)
|
|
101
102
|
#expect(dataPoint.attributes[0].key == "session.id")
|
|
102
103
|
#expect(dataPoint.attributes[0].value.stringValue == testSessionId)
|
|
@@ -135,6 +136,7 @@ struct OpenTelemetryTests {
|
|
|
135
136
|
#expect(attrs["expo.sdk.version"] == "55.0.0")
|
|
136
137
|
#expect(attrs["expo.react_native.version"] == "0.83.1")
|
|
137
138
|
#expect(attrs["expo.eas_client.id"] == testEasClientId)
|
|
139
|
+
#expect(attrs["expo.eas_build.id"] == "")
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
// MARK: - Full OTEvent
|
|
@@ -181,11 +183,11 @@ struct OpenTelemetryTests {
|
|
|
181
183
|
let otEvent = event.toOTEvent(testEasClientId)
|
|
182
184
|
let requestBody = OTRequestBody(resourceMetrics: [otEvent])
|
|
183
185
|
|
|
184
|
-
let jsonString = try requestBody.
|
|
186
|
+
let jsonString = try requestBody.toJSONString()
|
|
185
187
|
#expect(!jsonString.isEmpty)
|
|
186
188
|
|
|
187
189
|
// Verify it can be round-tripped
|
|
188
|
-
let data = try requestBody.
|
|
190
|
+
let data = try requestBody.toJSONData()
|
|
189
191
|
let decoded = try JSONDecoder().decode(OTRequestBody.self, from: data)
|
|
190
192
|
#expect(decoded.resourceMetrics.count == 1)
|
|
191
193
|
#expect(decoded.resourceMetrics[0].scopeMetrics[0].metrics.count == 4)
|
|
@@ -197,7 +199,7 @@ struct OpenTelemetryTests {
|
|
|
197
199
|
let otEvent = event.toOTEvent(testEasClientId)
|
|
198
200
|
let requestBody = OTRequestBody(resourceMetrics: [otEvent])
|
|
199
201
|
|
|
200
|
-
let data = try requestBody.
|
|
202
|
+
let data = try requestBody.toJSONData()
|
|
201
203
|
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
|
|
202
204
|
|
|
203
205
|
// Top level should have "resourceMetrics" array
|
|
@@ -221,6 +223,67 @@ struct OpenTelemetryTests {
|
|
|
221
223
|
#expect(attrValue["stringValue"] != nil)
|
|
222
224
|
}
|
|
223
225
|
|
|
226
|
+
// MARK: - Metric attributes
|
|
227
|
+
|
|
228
|
+
@Test
|
|
229
|
+
func `toOTMetric includes route name attribute when present`() {
|
|
230
|
+
let metric = Event.Metric(
|
|
231
|
+
category: "appStartup",
|
|
232
|
+
name: "bundleLoadTime",
|
|
233
|
+
value: 1.0,
|
|
234
|
+
timestamp: "2026-01-01T00:00:00Z",
|
|
235
|
+
sessionId: testSessionId,
|
|
236
|
+
parentSessionId: nil,
|
|
237
|
+
routeName: "/home",
|
|
238
|
+
customParams: nil
|
|
239
|
+
)
|
|
240
|
+
let otMetric = metric.toOTMetric()
|
|
241
|
+
let attrs = Dictionary(uniqueKeysWithValues: otMetric.gauge.dataPoints[0].attributes.map { ($0.key, $0.value.stringValue) })
|
|
242
|
+
|
|
243
|
+
#expect(attrs["expo.route_name"] == "/home")
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
@Test
|
|
247
|
+
func `toOTMetric excludes route name attribute when nil`() {
|
|
248
|
+
let metric = makeMetric(name: "bundleLoadTime", value: 1.0, timestamp: "2026-01-01T00:00:00Z")
|
|
249
|
+
let otMetric = metric.toOTMetric()
|
|
250
|
+
let keys = otMetric.gauge.dataPoints[0].attributes.map { $0.key }
|
|
251
|
+
|
|
252
|
+
#expect(keys.contains("expo.route_name") == false)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
@Test
|
|
256
|
+
func `toOTMetric includes custom params as JSON string`() {
|
|
257
|
+
let metric = Event.Metric(
|
|
258
|
+
category: "appStartup",
|
|
259
|
+
name: "bundleLoadTime",
|
|
260
|
+
value: 1.0,
|
|
261
|
+
timestamp: "2026-01-01T00:00:00Z",
|
|
262
|
+
sessionId: testSessionId,
|
|
263
|
+
parentSessionId: nil,
|
|
264
|
+
routeName: nil,
|
|
265
|
+
customParams: AnyCodable(["screen": "dashboard", "variant": "A"] as [String: Any])
|
|
266
|
+
)
|
|
267
|
+
let otMetric = metric.toOTMetric()
|
|
268
|
+
let attrs = Dictionary(uniqueKeysWithValues: otMetric.gauge.dataPoints[0].attributes.map { ($0.key, $0.value.stringValue) })
|
|
269
|
+
|
|
270
|
+
let jsonString = attrs["expo.custom_params"]!
|
|
271
|
+
let parsed = try! JSONSerialization.jsonObject(with: jsonString.data(using: .utf8)!) as! [String: String]
|
|
272
|
+
#expect(parsed["screen"] == "dashboard")
|
|
273
|
+
#expect(parsed["variant"] == "A")
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
@Test
|
|
277
|
+
func `toOTMetric excludes custom params attribute when nil`() {
|
|
278
|
+
let metric = makeMetric(name: "bundleLoadTime", value: 1.0, timestamp: "2026-01-01T00:00:00Z")
|
|
279
|
+
let otMetric = metric.toOTMetric()
|
|
280
|
+
let keys = otMetric.gauge.dataPoints[0].attributes.map { $0.key }
|
|
281
|
+
|
|
282
|
+
#expect(keys.contains("expo.custom_params") == false)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// MARK: - Multiple events
|
|
286
|
+
|
|
224
287
|
@Test
|
|
225
288
|
func `multiple events produce multiple resourceMetrics entries`() {
|
|
226
289
|
let event1 = Event(
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-observe",
|
|
3
3
|
"title": "Expo Observe",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.3",
|
|
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",
|
|
@@ -33,14 +33,13 @@
|
|
|
33
33
|
"author": "650 Industries, Inc.",
|
|
34
34
|
"license": "MIT",
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"expo-
|
|
37
|
-
"expo-
|
|
36
|
+
"expo-app-metrics": "~0.1.3",
|
|
37
|
+
"expo-eas-client": "~55.0.3"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"expo-module-scripts": "^5.0.7"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"expo": "*"
|
|
44
|
-
"expo-app-metrics": "*"
|
|
43
|
+
"expo": "*"
|
|
45
44
|
}
|
|
46
45
|
}
|
package/src/index.ts
CHANGED