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.
@@ -28,7 +28,7 @@ data class OTAttribute(
28
28
  @Serializable
29
29
  data class OTDataPoint(
30
30
  val timeUnixNano: Long,
31
- val value: Double,
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
- value = value,
107
- attributes = listOf(
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
@@ -1,3 +1,4 @@
1
+ export { default as AppMetrics, AppMetricsRoot } from 'expo-app-metrics';
1
2
  export { default } from './module';
2
3
  export * from './types';
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -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
@@ -1,3 +1,4 @@
1
+ export { default as AppMetrics, AppMetricsRoot } from 'expo-app-metrics';
1
2
  export { default } from './module';
2
3
  export * from './types';
3
4
  //# sourceMappingURL=index.js.map
@@ -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
+ }
@@ -56,7 +56,7 @@ internal struct ObservabilityManager {
56
56
  return
57
57
  }
58
58
 
59
- let body: any RequestBodyProtocol
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.toData([])
68
+ request.httpBody = try body.toJSONData([])
69
69
 
70
- observeLogger.debug("[EAS Observe] Sending the request to \(endpointUrl) with body: \(try body.toString(.prettyPrinted))")
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
 
@@ -18,7 +18,7 @@ struct OTAttribute: Codable, Sendable {
18
18
 
19
19
  struct OTDataPoint: Codable, Sendable {
20
20
  let timeUnixNano: UInt64
21
- let value: Double
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
- OTMetric(
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
- value: self.value,
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, RequestBodyProtocol {
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
  }
@@ -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, RequestBodyProtocol {
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.value == 3.14)
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.toString()
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.toData()
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.toData()
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.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-eas-client": "^55.0.0",
37
- "expo-updates-interface": "^55.1.3"
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
@@ -1,2 +1,4 @@
1
+ export { default as AppMetrics, AppMetricsRoot } from 'expo-app-metrics';
2
+
1
3
  export { default } from './module';
2
4
  export * from './types';