expo-observe 0.1.2 → 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.
@@ -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",
@@ -104,12 +114,7 @@ fun EASMetric.toOTMetric(): OTMetric {
104
114
  OTDataPoint(
105
115
  timeUnixNano = timestampToDateNS(timestamp),
106
116
  asDouble = value,
107
- attributes = listOf(
108
- OTAttribute.of(
109
- key = "session.id",
110
- rawValue = sessionId
111
- )
112
- )
117
+ attributes = attributes
113
118
  )
114
119
  )
115
120
  )
@@ -136,7 +141,8 @@ fun Event.toOTMetadata(easClientId: String): OTMetadata {
136
141
  OTAttribute.of("expo.sdk.version", metadata.expoSdkVersion),
137
142
  OTAttribute.of("expo.environment", metadata.environment ?: ""),
138
143
  OTAttribute.of("expo.react_native.version", metadata.reactNativeVersion),
139
- OTAttribute.of("expo.eas_client.id", easClientId)
144
+ OTAttribute.of("expo.eas_client.id", easClientId),
145
+ OTAttribute.of("expo.eas_build.id", metadata.appEasBuildId ?: "")
140
146
  )
141
147
  )
142
148
  }
@@ -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,11 +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
70
  observeLogger.debug("[EAS Observe] Sending the request to \(endpointUrl) with body:")
71
71
  // Use `print` so the JSON can be copied without including the log level emojis.
72
- print(try body.toString(.prettyPrinted))
72
+ print(try body.toJSONString(.prettyPrinted))
73
73
 
74
74
  let (responseData, urlResponse) = try await URLSession.shared.data(for: request)
75
75
 
@@ -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
98
  asDouble: self.value,
89
- attributes: [
90
- OTAttribute(
91
- key: "session.id",
92
- rawValue: self.sessionId
93
- )
94
- ]
99
+ attributes: attributes
95
100
  )
96
101
  ])
97
102
  )
@@ -119,6 +124,7 @@ extension Event {
119
124
  OTAttribute(key: "expo.environment", rawValue: metadata.environment ?? ""),
120
125
  OTAttribute(key: "expo.react_native.version", rawValue: metadata.reactNativeVersion),
121
126
  OTAttribute(key: "expo.eas_client.id", rawValue: easClientId),
127
+ OTAttribute(key: "expo.eas_build.id", rawValue: metadata.appEasBuildId ?? ""),
122
128
  ])
123
129
  }
124
130
 
@@ -137,17 +143,6 @@ extension Event {
137
143
 
138
144
  // MARK: -- Request body for Open Telemetry events
139
145
 
140
- internal struct OTRequestBody: Codable, Sendable, RequestBodyProtocol {
146
+ internal struct OTRequestBody: Codable, Sendable {
141
147
  let resourceMetrics: [OTEvent]
142
-
143
- func toData(_ formatting: JSONEncoder.OutputFormatting = []) throws -> Data {
144
- let encoder = JSONEncoder()
145
- encoder.outputFormatting = formatting
146
- return try encoder.encode(self)
147
- }
148
-
149
- func toString(_ formatting: JSONEncoder.OutputFormatting = []) throws -> String {
150
- let data = try toData(formatting)
151
- return String(data: data, encoding: .utf8) ?? ""
152
- }
153
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 {
@@ -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.2",
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,7 +33,7 @@
33
33
  "author": "650 Industries, Inc.",
34
34
  "license": "MIT",
35
35
  "dependencies": {
36
- "expo-app-metrics": "~0.1.2",
36
+ "expo-app-metrics": "~0.1.3",
37
37
  "expo-eas-client": "~55.0.3"
38
38
  },
39
39
  "devDependencies": {