expo-app-metrics 58.0.2 → 58.0.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/android/src/main/java/expo/modules/appmetrics/crashreporting/CrashReport.kt +3 -1
  3. package/expo-module.config.json +1 -1
  4. package/ios/CrashReporting/CrashReport.swift +76 -15
  5. package/ios/CrashReporting/CrashReportSymbolicator.swift +35 -1
  6. package/ios/Tests/CrashReportSymbolicatorTests.swift +37 -0
  7. package/ios/Tests/CrashReportTests.swift +104 -5
  8. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/{58.0.2/expo.modules.appmetrics-58.0.2-sources.jar → 58.0.3/expo.modules.appmetrics-58.0.3-sources.jar} +0 -0
  9. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.3/expo.modules.appmetrics-58.0.3-sources.jar.md5 +1 -0
  10. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.3/expo.modules.appmetrics-58.0.3-sources.jar.sha1 +1 -0
  11. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.3/expo.modules.appmetrics-58.0.3-sources.jar.sha256 +1 -0
  12. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.3/expo.modules.appmetrics-58.0.3-sources.jar.sha512 +1 -0
  13. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.3/expo.modules.appmetrics-58.0.3.aar +0 -0
  14. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.3/expo.modules.appmetrics-58.0.3.aar.md5 +1 -0
  15. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.3/expo.modules.appmetrics-58.0.3.aar.sha1 +1 -0
  16. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.3/expo.modules.appmetrics-58.0.3.aar.sha256 +1 -0
  17. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.3/expo.modules.appmetrics-58.0.3.aar.sha512 +1 -0
  18. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/{58.0.2/expo.modules.appmetrics-58.0.2.module → 58.0.3/expo.modules.appmetrics-58.0.3.module} +22 -22
  19. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.3/expo.modules.appmetrics-58.0.3.module.md5 +1 -0
  20. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.3/expo.modules.appmetrics-58.0.3.module.sha1 +1 -0
  21. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.3/expo.modules.appmetrics-58.0.3.module.sha256 +1 -0
  22. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.3/expo.modules.appmetrics-58.0.3.module.sha512 +1 -0
  23. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/{58.0.2/expo.modules.appmetrics-58.0.2.pom → 58.0.3/expo.modules.appmetrics-58.0.3.pom} +1 -1
  24. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.3/expo.modules.appmetrics-58.0.3.pom.md5 +1 -0
  25. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.3/expo.modules.appmetrics-58.0.3.pom.sha1 +1 -0
  26. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.3/expo.modules.appmetrics-58.0.3.pom.sha256 +1 -0
  27. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.3/expo.modules.appmetrics-58.0.3.pom.sha512 +1 -0
  28. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/maven-metadata.xml +4 -4
  29. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/maven-metadata.xml.md5 +1 -1
  30. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/maven-metadata.xml.sha1 +1 -1
  31. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/maven-metadata.xml.sha256 +1 -1
  32. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/maven-metadata.xml.sha512 +1 -1
  33. package/package.json +3 -3
  34. package/.npmignore +0 -27
  35. package/.tsbuildinfo +0 -1
  36. package/android/.gitignore +0 -261
  37. package/jest.config.js +0 -2
  38. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.2/expo.modules.appmetrics-58.0.2-sources.jar.md5 +0 -1
  39. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.2/expo.modules.appmetrics-58.0.2-sources.jar.sha1 +0 -1
  40. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.2/expo.modules.appmetrics-58.0.2-sources.jar.sha256 +0 -1
  41. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.2/expo.modules.appmetrics-58.0.2-sources.jar.sha512 +0 -1
  42. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.2/expo.modules.appmetrics-58.0.2.aar +0 -0
  43. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.2/expo.modules.appmetrics-58.0.2.aar.md5 +0 -1
  44. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.2/expo.modules.appmetrics-58.0.2.aar.sha1 +0 -1
  45. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.2/expo.modules.appmetrics-58.0.2.aar.sha256 +0 -1
  46. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.2/expo.modules.appmetrics-58.0.2.aar.sha512 +0 -1
  47. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.2/expo.modules.appmetrics-58.0.2.module.md5 +0 -1
  48. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.2/expo.modules.appmetrics-58.0.2.module.sha1 +0 -1
  49. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.2/expo.modules.appmetrics-58.0.2.module.sha256 +0 -1
  50. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.2/expo.modules.appmetrics-58.0.2.module.sha512 +0 -1
  51. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.2/expo.modules.appmetrics-58.0.2.pom.md5 +0 -1
  52. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.2/expo.modules.appmetrics-58.0.2.pom.sha1 +0 -1
  53. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.2/expo.modules.appmetrics-58.0.2.pom.sha256 +0 -1
  54. package/local-maven-repo/expo/modules/appmetrics/expo.modules.appmetrics/58.0.2/expo.modules.appmetrics-58.0.2.pom.sha512 +0 -1
  55. package/tsconfig.json +0 -9
package/CHANGELOG.md CHANGED
@@ -10,6 +10,12 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 58.0.3 — 2026-09-16
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [iOS] [Android] Stop attributing native crash stack frames to unrelated functions, and keep enough detail in the report to symbolicate them off-device. ([#49921](https://github.com/expo/expo/pull/49921) by [@tsapeta](https://github.com/tsapeta))
18
+
13
19
  ## 58.0.2 — 2026-09-15
14
20
 
15
21
  ### 💡 Others
@@ -142,7 +142,9 @@ data class CrashReport(
142
142
  }
143
143
 
144
144
  private const val MAX_CAUSE_DEPTH = 5
145
- private const val MAX_LOG_STACK_FRAMES = 25
145
+
146
+ /** Kept in step with the iOS limit so the same crash isn't cut at a different depth. */
147
+ private const val MAX_LOG_STACK_FRAMES = 50
146
148
  private const val MAX_LOG_STACKTRACE_LENGTH = 65_536
147
149
 
148
150
  private fun signalName(signal: Int): String =
@@ -9,7 +9,7 @@
9
9
  "publication": {
10
10
  "groupId": "expo.modules.appmetrics",
11
11
  "artifactId": "expo.modules.appmetrics",
12
- "version": "58.0.2",
12
+ "version": "58.0.3",
13
13
  "repository": "local-maven-repo"
14
14
  }
15
15
  }
@@ -6,7 +6,11 @@
6
6
  /// payload time window. Unhandled JavaScript errors are recorded separately as `js.exception` log
7
7
  /// events (see `ErrorReport`).
8
8
  public struct CrashReport: Codable, Sendable {
9
- private static let maxLogStackFrames = 25
9
+ /// Frames rendered into `exception.stacktrace`.
10
+ ///
11
+ /// A crash that started in JS spends most of its stack inside the Hermes interpreter before it
12
+ /// reaches our code, so a smaller limit cuts off the part you want.
13
+ private static let maxLogStackFrames = 50
10
14
  private static let maxLogStacktraceLength = 65_536
11
15
 
12
16
  /// Mach exception type (e.g. EXC_BAD_ACCESS, EXC_CRASH).
@@ -91,13 +95,14 @@ public struct CrashReport: Codable, Sendable {
91
95
  }
92
96
 
93
97
  func toLogRecord() -> LogRecord {
98
+ let selected = selectedFrames
94
99
  var attributes: [String: Any] = [
95
100
  "exception.type": exceptionReason?.exceptionName
96
101
  ?? exceptionType.map { exceptionName(for: $0) }
97
102
  ?? signal.map { signalName(for: $0) }
98
103
  ?? "NativeCrash",
99
104
  "exception.message": exceptionMessage,
100
- "exception.stacktrace": renderedStacktrace as Any,
105
+ "exception.stacktrace": selected.flatMap(renderStacktrace) as Any,
101
106
  "expo.error.source": "nativeCrash",
102
107
  "expo.error.is_fatal": true,
103
108
  ]
@@ -115,10 +120,26 @@ public struct CrashReport: Codable, Sendable {
115
120
  if let terminationReason {
116
121
  attributes["expo.crash.termination_reason"] = terminationReason
117
122
  }
123
+ // A stack overflow and a bad pointer both come through as EXC_BAD_ACCESS / SIGSEGV. Only the
124
+ // overflow lands on a stack guard page, and this is where that shows up.
125
+ if let virtualMemoryRegionInfo {
126
+ attributes["expo.crash.virtual_memory_region"] = virtualMemoryRegionInfo
127
+ }
118
128
  if let exceptionReason {
119
129
  attributes["expo.crash.objc_exception_type"] = exceptionReason.exceptionType
120
130
  attributes["expo.crash.objc_exception_message"] = exceptionReason.composedMessage
121
131
  }
132
+ if let callStacks = callStackTree?.callStacks {
133
+ attributes["expo.crash.thread_count"] = callStacks.count
134
+ attributes["expo.crash.thread_attributed"] = callStacks.contains { $0.threadAttributed == true }
135
+ }
136
+ if let selected, !selected.frames.isEmpty {
137
+ attributes["expo.crash.rendered_frame_count"] = selected.frames.count
138
+ attributes["expo.crash.total_frame_count"] = selected.total
139
+ if let binaryUUIDs = renderedBinaryUUIDs(in: selected.frames) {
140
+ attributes["expo.crash.binary_uuids"] = binaryUUIDs
141
+ }
142
+ }
122
143
  return LogRecord(
123
144
  name: "native.exception",
124
145
  attributes: attributes,
@@ -147,7 +168,11 @@ public struct CrashReport: Codable, Sendable {
147
168
  return "Native crash"
148
169
  }
149
170
 
150
- private var renderedStacktrace: String? {
171
+ /// Frames picked for the stack trace, plus how many the payload actually had.
172
+ ///
173
+ /// We stop collecting at `maxLogStackFrames`, so anything past that shows up as the gap between
174
+ /// `frames.count` and `total`.
175
+ private var selectedFrames: (frames: [CallStackTree.Frame], total: Int)? {
151
176
  guard let callStacks = callStackTree?.callStacks else {
152
177
  return nil
153
178
  }
@@ -155,13 +180,17 @@ public struct CrashReport: Codable, Sendable {
155
180
  let selectedStacks = attributedStacks.isEmpty ? callStacks : attributedStacks
156
181
  var frames: [CallStackTree.Frame] = []
157
182
  var totalFrames = 0
158
- var lines: [String] = []
159
183
  for callStack in selectedStacks {
160
184
  collectFrames(callStack.callStackRootFrames ?? [], into: &frames, total: &totalFrames)
161
185
  }
162
- lines.append(contentsOf: frames.map(renderFrame))
163
- if totalFrames > frames.count {
164
- lines.append("… +\(totalFrames - frames.count) more frames")
186
+ return (frames, totalFrames)
187
+ }
188
+
189
+ /// Renders the selected frames, appending a count of the ones that didn't fit.
190
+ private func renderStacktrace(_ selected: (frames: [CallStackTree.Frame], total: Int)) -> String? {
191
+ var lines = selected.frames.map(renderFrame)
192
+ if selected.total > selected.frames.count {
193
+ lines.append("… +\(selected.total - selected.frames.count) more frames")
165
194
  }
166
195
  guard !lines.isEmpty else {
167
196
  return nil
@@ -187,17 +216,49 @@ public struct CrashReport: Codable, Sendable {
187
216
  }
188
217
  }
189
218
 
190
- private func renderFrame(_ frame: CallStackTree.Frame) -> String {
191
- if let symbol = frame.symbol {
192
- return symbol
219
+ /// The binaries named in the trace and their UUIDs, as `<binary>:<uuid>` entries.
220
+ ///
221
+ /// An offset only means something against the build it came from, so these say which dSYM to
222
+ /// reach for.
223
+ private func renderedBinaryUUIDs(in frames: [CallStackTree.Frame]) -> [String]? {
224
+ var uuidsByBinary: [String: String] = [:]
225
+ for frame in frames {
226
+ guard let binaryName = frame.binaryName, let binaryUUID = frame.binaryUUID else {
227
+ continue
228
+ }
229
+ uuidsByBinary[binaryName] = binaryUUID
193
230
  }
194
- if let binaryName = frame.binaryName, let offset = frame.offsetIntoBinaryTextSegment {
195
- return "\(binaryName) + \(offset)"
231
+ guard !uuidsByBinary.isEmpty else {
232
+ return nil
196
233
  }
197
- if let address = frame.address {
198
- return "0x\(String(address, radix: 16))"
234
+ return uuidsByBinary.sorted { $0.key < $1.key }.map { "\($0.key):\($0.value)" }
235
+ }
236
+
237
+ /// Renders one frame as `<symbol> (<binary> + <offset>)`.
238
+ ///
239
+ /// We keep the address even when a symbol resolved, because on a stripped binary it can be the
240
+ /// wrong one, and it's what lets you re-symbolicate from the archive. Without an offset or an
241
+ /// address there's nothing to look up, so the binary name alone is left off.
242
+ private func renderFrame(_ frame: CallStackTree.Frame) -> String {
243
+ let location: String? = {
244
+ if let binaryName = frame.binaryName, let offset = frame.offsetIntoBinaryTextSegment {
245
+ return "\(binaryName) + \(offset)"
246
+ }
247
+ if let address = frame.address {
248
+ return "0x\(String(address, radix: 16))"
249
+ }
250
+ return nil
251
+ }()
252
+ switch (frame.symbol, location) {
253
+ case (let symbol?, let location?):
254
+ return "\(symbol) (\(location))"
255
+ case (let symbol?, nil):
256
+ return symbol
257
+ case (nil, let location?):
258
+ return location
259
+ case (nil, nil):
260
+ return frame.binaryName ?? "<unknown>"
199
261
  }
200
- return frame.binaryName ?? "<unknown>"
201
262
  }
202
263
 
203
264
  /// Mirrors the shape of `MXCallStackTree.JSONRepresentation()`. Every field is optional so that
@@ -25,6 +25,30 @@ import MachO
25
25
  /// Where `currentLoadAddress` is the slide-adjusted address of the binary's first segment
26
26
  /// (typically `__TEXT`) in this process. We grab it from the dyld image table.
27
27
  enum CrashReportSymbolicator {
28
+ /// How far past a symbol's start a frame can land and still be attributed to it.
29
+ ///
30
+ /// `dladdr` tells us the nearest symbol at or below an address, but not how far away it was, so
31
+ /// this is all we have to go on. A stripped binary can leave gaps megabytes wide between the
32
+ /// symbols it keeps, and every frame in one gets named after whichever symbol precedes it.
33
+ ///
34
+ /// 8 KB is a compromise. It clears the largest real function bodies we see, so a correct name
35
+ /// survives, while rejecting the gaps that produced wrong ones (the smallest we measured was
36
+ /// around 19 KB). A function bigger than this loses a name it should have kept, which is the
37
+ /// deliberate trade: a frame that says `binaryName + offset` can still be symbolicated from the
38
+ /// archive, but a frame named after unrelated code sends you somewhere that never ran.
39
+ static let maxSymbolMatchDistance: UInt64 = 8 * 1024
40
+
41
+ /// Whether `dladdr`'s answer is close enough to trust.
42
+ ///
43
+ /// `symbolStart` is `Dl_info.dli_saddr`, the start of the symbol it matched. A match below the
44
+ /// address we asked about means the two aren't related at all.
45
+ static func isSymbolMatchPlausible(address: UInt64, symbolStart: UInt64) -> Bool {
46
+ guard address >= symbolStart else {
47
+ return false
48
+ }
49
+ return address - symbolStart <= maxSymbolMatchDistance
50
+ }
51
+
28
52
  /// Annotates each frame in the tree with its resolved symbol, when one is available.
29
53
  static func symbolicate(_ tree: CrashReport.CallStackTree) -> CrashReport.CallStackTree {
30
54
  // Threads in a crash tree share many leaf frames (RunLoop guts, pthread entry points,
@@ -45,7 +69,9 @@ enum CrashReportSymbolicator {
45
69
  _ frame: CrashReport.CallStackTree.Frame,
46
70
  cache: inout [String: String]
47
71
  ) -> CrashReport.CallStackTree.Frame {
48
- let symbol = resolveSymbol(for: frame, cache: &cache)
72
+ // We resolve very little of a stripped binary, so keep whatever MetricKit gave us instead.
73
+ // Even its `<redacted>` marker says more than an empty field.
74
+ let symbol = resolveSymbol(for: frame, cache: &cache) ?? frame.symbol
49
75
  let subFrames = frame.subFrames?.map { symbolicateFrame($0, cache: &cache) }
50
76
  return CrashReport.CallStackTree.Frame(
51
77
  binaryName: frame.binaryName,
@@ -75,6 +101,14 @@ enum CrashReportSymbolicator {
75
101
  else {
76
102
  return nil
77
103
  }
104
+ guard let symbolStart = info.dli_saddr,
105
+ isSymbolMatchPlausible(
106
+ address: currentAddress,
107
+ symbolStart: UInt64(UInt(bitPattern: symbolStart))
108
+ )
109
+ else {
110
+ return nil
111
+ }
78
112
  let mangled = String(cString: symbolPtr)
79
113
  if let cached = cache[mangled] {
80
114
  return cached
@@ -67,6 +67,43 @@ struct CrashReportSymbolicatorTests {
67
67
  }
68
68
  }
69
69
 
70
+ @Suite("isSymbolMatchPlausible")
71
+ struct SymbolMatchDistanceTests {
72
+ @Test
73
+ func `accepts an exact hit on the symbol's start`() {
74
+ #expect(CrashReportSymbolicator.isSymbolMatchPlausible(address: 0x1000, symbolStart: 0x1000))
75
+ }
76
+
77
+ @Test
78
+ func `accepts an address inside a plausible function body`() {
79
+ #expect(CrashReportSymbolicator.isSymbolMatchPlausible(address: 0x1000 + 512, symbolStart: 0x1000))
80
+ }
81
+
82
+ @Test
83
+ func `accepts an address exactly at the limit`() {
84
+ let limit = CrashReportSymbolicator.maxSymbolMatchDistance
85
+ #expect(CrashReportSymbolicator.isSymbolMatchPlausible(address: 0x1000 + limit, symbolStart: 0x1000))
86
+ }
87
+
88
+ @Test
89
+ func `rejects an address one byte past the limit`() {
90
+ let limit = CrashReportSymbolicator.maxSymbolMatchDistance
91
+ #expect(!CrashReportSymbolicator.isSymbolMatchPlausible(address: 0x1000 + limit + 1, symbolStart: 0x1000))
92
+ }
93
+
94
+ @Test
95
+ func `rejects a match from across a symbol void`() {
96
+ // The shape that produced wrong names: a stripped binary leaves the nearest symbol hundreds
97
+ // of kilobytes below the frame we asked about.
98
+ #expect(!CrashReportSymbolicator.isSymbolMatchPlausible(address: 0x1000 + 770_160, symbolStart: 0x1000))
99
+ }
100
+
101
+ @Test
102
+ func `rejects a symbol that starts above the address`() {
103
+ #expect(!CrashReportSymbolicator.isSymbolMatchPlausible(address: 0x1000, symbolStart: 0x2000))
104
+ }
105
+ }
106
+
70
107
  @Suite("symbolicate")
71
108
  struct SymbolicateTests {
72
109
  @Test
@@ -227,8 +227,104 @@ struct CrashReportTests {
227
227
  }
228
228
 
229
229
  @Test
230
- func `renders at most twenty-five attributed stack frames and reports the omitted count`() throws {
231
- let attributedFrames = (0..<28).map { index in
230
+ func `describes how the trace was built`() throws {
231
+ let attributedFrames = (0..<53).map { index in
232
+ CrashReport.CallStackTree.Frame(
233
+ binaryName: "TestApp",
234
+ binaryUUID: "9F0C0F7E-1E55-4B0E-9C6E-1D6E6F0C0F7E",
235
+ address: nil,
236
+ offsetIntoBinaryTextSegment: UInt64(index),
237
+ sampleCount: nil,
238
+ subFrames: nil,
239
+ symbol: "frame\(index)"
240
+ )
241
+ }
242
+ let report = makeCrashReport(
243
+ timestampBegin: Date.now,
244
+ timestampEnd: Date.now,
245
+ callStackTree: CrashReport.CallStackTree(callStacks: [
246
+ CrashReport.CallStackTree.CallStack(
247
+ threadAttributed: true,
248
+ callStackRootFrames: attributedFrames
249
+ ),
250
+ CrashReport.CallStackTree.CallStack(
251
+ threadAttributed: false,
252
+ callStackRootFrames: [
253
+ CrashReport.CallStackTree.Frame(
254
+ binaryName: "Other",
255
+ binaryUUID: "11111111-1111-1111-1111-111111111111",
256
+ address: nil,
257
+ offsetIntoBinaryTextSegment: 0,
258
+ sampleCount: nil,
259
+ subFrames: nil,
260
+ symbol: "unattributed"
261
+ )
262
+ ]
263
+ ),
264
+ ])
265
+ )
266
+ let attributes = try #require(report.toLogRecord().attributes?.value as? [String: Any])
267
+ #expect(attributes["expo.crash.thread_count"] as? Int == 2)
268
+ #expect(attributes["expo.crash.thread_attributed"] as? Bool == true)
269
+ #expect(attributes["expo.crash.rendered_frame_count"] as? Int == 50)
270
+ #expect(attributes["expo.crash.total_frame_count"] as? Int == 53)
271
+ // Only the binaries actually rendered, so the unattributed thread's is absent.
272
+ #expect(
273
+ attributes["expo.crash.binary_uuids"] as? [String]
274
+ == ["TestApp:9F0C0F7E-1E55-4B0E-9C6E-1D6E6F0C0F7E"]
275
+ )
276
+ }
277
+
278
+ @Test
279
+ func `reports no thread as attributed when MetricKit flags none`() throws {
280
+ let report = makeCrashReport(
281
+ timestampBegin: Date.now,
282
+ timestampEnd: Date.now,
283
+ callStackTree: CrashReport.CallStackTree(callStacks: [
284
+ CrashReport.CallStackTree.CallStack(
285
+ threadAttributed: false,
286
+ callStackRootFrames: [
287
+ CrashReport.CallStackTree.Frame(
288
+ binaryName: "TestApp",
289
+ binaryUUID: nil,
290
+ address: nil,
291
+ offsetIntoBinaryTextSegment: 128,
292
+ sampleCount: nil,
293
+ subFrames: nil,
294
+ symbol: nil
295
+ )
296
+ ]
297
+ )
298
+ ])
299
+ )
300
+ let attributes = try #require(report.toLogRecord().attributes?.value as? [String: Any])
301
+ #expect(attributes["expo.crash.thread_attributed"] as? Bool == false)
302
+ #expect(attributes["expo.crash.thread_count"] as? Int == 1)
303
+ // No UUID on the frame, so the attribute is left off rather than emitted empty.
304
+ #expect(attributes["expo.crash.binary_uuids"] == nil)
305
+ // Nothing to name it with, so the trace keeps the address on its own.
306
+ let stacktrace = try #require(attributes["exception.stacktrace"] as? String)
307
+ #expect(stacktrace == "TestApp + 128")
308
+ }
309
+
310
+ @Test
311
+ func `reports the faulting memory region`() throws {
312
+ // A stack overflow looks like any other bad access until you check the region.
313
+ let report = makeCrashReport(
314
+ timestampBegin: Date.now,
315
+ timestampEnd: Date.now,
316
+ virtualMemoryRegionInfo: "0x16f603ff8 is in STACK GUARD region"
317
+ )
318
+ let attributes = try #require(report.toLogRecord().attributes?.value as? [String: Any])
319
+ #expect(
320
+ attributes["expo.crash.virtual_memory_region"] as? String
321
+ == "0x16f603ff8 is in STACK GUARD region"
322
+ )
323
+ }
324
+
325
+ @Test
326
+ func `renders at most fifty attributed stack frames and reports the omitted count`() throws {
327
+ let attributedFrames = (0..<53).map { index in
232
328
  CrashReport.CallStackTree.Frame(
233
329
  binaryName: "TestApp",
234
330
  binaryUUID: nil,
@@ -266,13 +362,15 @@ struct CrashReportTests {
266
362
  let attributes = try #require(report.toLogRecord().attributes?.value as? [String: Any])
267
363
  let stacktrace = try #require(attributes["exception.stacktrace"] as? String)
268
364
  let lines = stacktrace.split(separator: "\n")
269
- #expect(lines.count == 26)
365
+ #expect(lines.count == 51)
366
+ // These fixtures carry no offset or address, so there's nothing to append to the symbol.
270
367
  #expect(lines.first == "frame0")
271
- #expect(lines[24] == "frame24")
368
+ #expect(lines[49] == "frame49")
272
369
  #expect(lines.last == "… +3 more frames")
273
370
  #expect(!stacktrace.contains("unattributed"))
274
371
  }
275
372
  }
373
+
276
374
  }
277
375
 
278
376
  private func makeMainSessionRow(id: String, startDate: Date, endDate: Date?) -> SessionRow {
@@ -293,6 +391,7 @@ private func makeCrashReport(
293
391
  exceptionCode: Int? = 1,
294
392
  signal: Int? = 11,
295
393
  terminationReason: String? = nil,
394
+ virtualMemoryRegionInfo: String? = nil,
296
395
  exceptionReason: CrashReport.ExceptionReason? = nil,
297
396
  callStackTree: CrashReport.CallStackTree? = nil
298
397
  ) -> CrashReport {
@@ -301,7 +400,7 @@ private func makeCrashReport(
301
400
  exceptionCode: exceptionCode,
302
401
  signal: signal,
303
402
  terminationReason: terminationReason,
304
- virtualMemoryRegionInfo: nil,
403
+ virtualMemoryRegionInfo: virtualMemoryRegionInfo,
305
404
  exceptionReason: exceptionReason,
306
405
  callStackTree: callStackTree,
307
406
  appVersion: "1.0.0",
@@ -0,0 +1 @@
1
+ 0661059bd9da9fdc632692f5935b1affb5eec64236924f8d2a2eb39c7a661d17
@@ -0,0 +1 @@
1
+ 5d573a0770d6b680ee51d0b3358ba6e7f4a7635d1221da9d60f22da7603d060f9fa9730fb81ba10f3270a28deb353720615c666d67431b60b483cd99f84a9665
@@ -0,0 +1 @@
1
+ 8e24f97c737e5c43c49b53e7631f4ee8cfe449f1dc3de1d514ff4d9b9e6dac8c
@@ -0,0 +1 @@
1
+ 0cb5d9ad9ab62884991bea6333bc812ca0cff889d2ade2b72007fcf99664968c78ff4ce9b6b1da2ac3d9c945e3d038cb6f06ba90840cb3d61f073fe96493f278
@@ -3,7 +3,7 @@
3
3
  "component": {
4
4
  "group": "expo.modules.appmetrics",
5
5
  "module": "expo.modules.appmetrics",
6
- "version": "58.0.2",
6
+ "version": "58.0.3",
7
7
  "attributes": {
8
8
  "org.gradle.status": "release"
9
9
  }
@@ -33,13 +33,13 @@
33
33
  ],
34
34
  "files": [
35
35
  {
36
- "name": "expo.modules.appmetrics-58.0.2.aar",
37
- "url": "expo.modules.appmetrics-58.0.2.aar",
38
- "size": 562543,
39
- "sha512": "bd6895af329edd84e757ebd30c70ed2cf5c8dfb0dde8c6960f7ed6204f1e2c75e0555c351fa04d0ba0a960a6812dfba236eff5324f1b0fa329f49f85118473b5",
40
- "sha256": "b00a8c94814f5d7cd981372988769df332ccf238c98bc7933bc4da2b11c39f8e",
41
- "sha1": "1b8efd1d9de1d78402290109668e80a2ae7e146d",
42
- "md5": "d227fe53913841c18e7ce4a0a4c62810"
36
+ "name": "expo.modules.appmetrics-58.0.3.aar",
37
+ "url": "expo.modules.appmetrics-58.0.3.aar",
38
+ "size": 562544,
39
+ "sha512": "0cb5d9ad9ab62884991bea6333bc812ca0cff889d2ade2b72007fcf99664968c78ff4ce9b6b1da2ac3d9c945e3d038cb6f06ba90840cb3d61f073fe96493f278",
40
+ "sha256": "8e24f97c737e5c43c49b53e7631f4ee8cfe449f1dc3de1d514ff4d9b9e6dac8c",
41
+ "sha1": "64998825c428d4194a4fe0329d08b1cd514f47a5",
42
+ "md5": "2ab45664ecbe1e01bb2df0649ed86b2f"
43
43
  }
44
44
  ]
45
45
  },
@@ -122,13 +122,13 @@
122
122
  ],
123
123
  "files": [
124
124
  {
125
- "name": "expo.modules.appmetrics-58.0.2.aar",
126
- "url": "expo.modules.appmetrics-58.0.2.aar",
127
- "size": 562543,
128
- "sha512": "bd6895af329edd84e757ebd30c70ed2cf5c8dfb0dde8c6960f7ed6204f1e2c75e0555c351fa04d0ba0a960a6812dfba236eff5324f1b0fa329f49f85118473b5",
129
- "sha256": "b00a8c94814f5d7cd981372988769df332ccf238c98bc7933bc4da2b11c39f8e",
130
- "sha1": "1b8efd1d9de1d78402290109668e80a2ae7e146d",
131
- "md5": "d227fe53913841c18e7ce4a0a4c62810"
125
+ "name": "expo.modules.appmetrics-58.0.3.aar",
126
+ "url": "expo.modules.appmetrics-58.0.3.aar",
127
+ "size": 562544,
128
+ "sha512": "0cb5d9ad9ab62884991bea6333bc812ca0cff889d2ade2b72007fcf99664968c78ff4ce9b6b1da2ac3d9c945e3d038cb6f06ba90840cb3d61f073fe96493f278",
129
+ "sha256": "8e24f97c737e5c43c49b53e7631f4ee8cfe449f1dc3de1d514ff4d9b9e6dac8c",
130
+ "sha1": "64998825c428d4194a4fe0329d08b1cd514f47a5",
131
+ "md5": "2ab45664ecbe1e01bb2df0649ed86b2f"
132
132
  }
133
133
  ]
134
134
  },
@@ -142,13 +142,13 @@
142
142
  },
143
143
  "files": [
144
144
  {
145
- "name": "expo.modules.appmetrics-58.0.2-sources.jar",
146
- "url": "expo.modules.appmetrics-58.0.2-sources.jar",
147
- "size": 96393,
148
- "sha512": "7bbd90592dd5cc5fef6277c7aa63b9178a8a38042108ff1cb941133f8ea7aad37b372629b1974bf524efbf2d30a2ca4cb86ac3068ee3bbde25829645549ba5d2",
149
- "sha256": "b4bd82523d8da249dc014c63b68f55fe77cb6ff48401a4de9e6bab7ac3bb8cb2",
150
- "sha1": "589e2f69396bb989a2c552c6980e9df6f90d9c02",
151
- "md5": "cac970d002efcc28701f72325ec943c6"
145
+ "name": "expo.modules.appmetrics-58.0.3-sources.jar",
146
+ "url": "expo.modules.appmetrics-58.0.3-sources.jar",
147
+ "size": 96449,
148
+ "sha512": "5d573a0770d6b680ee51d0b3358ba6e7f4a7635d1221da9d60f22da7603d060f9fa9730fb81ba10f3270a28deb353720615c666d67431b60b483cd99f84a9665",
149
+ "sha256": "0661059bd9da9fdc632692f5935b1affb5eec64236924f8d2a2eb39c7a661d17",
150
+ "sha1": "dff5927e1af32bc372d0afe5658fe4fc6dfad0c0",
151
+ "md5": "29fe1d710a19838215db014557c0f062"
152
152
  }
153
153
  ]
154
154
  }
@@ -0,0 +1 @@
1
+ d4cff862a70bbfc6fbf28cf46fda033a9a684d659559b91235bf1e8ffb91b766
@@ -0,0 +1 @@
1
+ efa6befa8967dbf69d46eb483260fa28c86813c5d3747de6004240b65098baa0a0e9eeeda06c2e911d019bc201bc3447c4443cadb4d2561dbe61a4970ed244b3
@@ -9,7 +9,7 @@
9
9
  <modelVersion>4.0.0</modelVersion>
10
10
  <groupId>expo.modules.appmetrics</groupId>
11
11
  <artifactId>expo.modules.appmetrics</artifactId>
12
- <version>58.0.2</version>
12
+ <version>58.0.3</version>
13
13
  <packaging>aar</packaging>
14
14
  <name>expo.modules.appmetrics</name>
15
15
  <url>https://github.com/expo/expo</url>
@@ -0,0 +1 @@
1
+ c9d2f8d2b61fb09ac32a37f003afc24fcce9b367b7b6d4c9c30ddd6bf58a6045
@@ -0,0 +1 @@
1
+ 57a7382aee88fd5f6261ba3bef5bfccde95900755da2b6e7b16643b3de5c77433a71f8957d3e77a293a742637fae959abfe463fb43436818407ee92272d54e2f
@@ -3,11 +3,11 @@
3
3
  <groupId>expo.modules.appmetrics</groupId>
4
4
  <artifactId>expo.modules.appmetrics</artifactId>
5
5
  <versioning>
6
- <latest>58.0.2</latest>
7
- <release>58.0.2</release>
6
+ <latest>58.0.3</latest>
7
+ <release>58.0.3</release>
8
8
  <versions>
9
- <version>58.0.2</version>
9
+ <version>58.0.3</version>
10
10
  </versions>
11
- <lastUpdated>20260915202028</lastUpdated>
11
+ <lastUpdated>20260916230018</lastUpdated>
12
12
  </versioning>
13
13
  </metadata>
@@ -1 +1 @@
1
- 9f84cdc3a562bbebdf710d9ddd0ceace
1
+ 18431c73fa777a0297de73e0d05fb017
@@ -1 +1 @@
1
- 49841cfababd6e4aedabf1d90e58eb1fa683236b
1
+ 159a42eb9c22781893fa9e779b1ecd3503929b7b
@@ -1 +1 @@
1
- 57d1a503a6f71082182794d7420cc8309e7a6edaf41a551196a6a172f5dd54cf
1
+ 88ed2a1c959b39d31bedb1922f22bbb328da7710a2073a3c139dc327caaef692
@@ -1 +1 @@
1
- af1ba5d1def743079dad4161fbddc442877795b722e2e4c6d7ab0cdd9d83b1e2852d769ed6c5737fa2bc3e8c82e30b0cda3297c0d6ea5eaf0ff1198b7782a6bf
1
+ 8e7dc816fcb5e3ddd93c67a6003d851e2de31f55b0b344ff4c18d361c4755d5b0959bfda0979f2013ba30821d624152251b554fbf6c3d82792cc8ea6ce95c2a0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-app-metrics",
3
- "version": "58.0.2",
3
+ "version": "58.0.3",
4
4
  "description": "Expo module that collects app startup, frame rate, memory, and session metrics",
5
5
  "keywords": [
6
6
  "ExpoAppMetrics",
@@ -37,7 +37,7 @@
37
37
  "devDependencies": {
38
38
  "@testing-library/react-native": "^13.3.0",
39
39
  "@types/react": "~19.2.0",
40
- "expo": "58.0.0-preview.2",
40
+ "expo": "58.0.0-preview.3",
41
41
  "expo-module-scripts": "56.0.3"
42
42
  },
43
43
  "peerDependencies": {
@@ -46,7 +46,7 @@
46
46
  "react-native": "*"
47
47
  },
48
48
  "title": "Expo App Metrics",
49
- "gitHead": "313e67f5c9dfb16f9deca74a271cd1d4f33a6f82",
49
+ "gitHead": "917a61b581baeb852aa4fe331da2c1395d824351",
50
50
  "scripts": {
51
51
  "build": "expo-build src",
52
52
  "clean": "expo-module clean",
package/.npmignore DELETED
@@ -1,27 +0,0 @@
1
- # @generated by expo-module-scripts
2
-
3
- # Exclude all top-level hidden directories by convention
4
- /.*/
5
-
6
- # Exclude tarballs generated by `npm pack`
7
- /*.tgz
8
-
9
- __mocks__
10
- __tests__
11
- __rsc_tests__
12
-
13
- /babel.config.js
14
- /oxlint.config.mjs
15
- /android/src/androidTest/
16
- /android/src/test/
17
-
18
- !plugin/build
19
-
20
- # Ensure native dirs aren't excluded by root .gitignore
21
- !/ios
22
- !/android
23
-
24
- # Exclude native build output (pnpm pack ignores nested .gitignore files)
25
- .cxx/
26
- .gradle/
27
- /android/build/
package/.tsbuildinfo DELETED
@@ -1 +0,0 @@
1
- {"version":"6.0.3"}
@@ -1,261 +0,0 @@
1
-
2
- # Created by https://www.gitignore.io/api/java,maven,gradle,android,intellij,androidstudio
3
-
4
- ### Android ###
5
- # Built application files
6
- *.apk
7
- *.ap_
8
-
9
- # Files for the ART/Dalvik VM
10
- *.dex
11
-
12
- # Java class files
13
- *.class
14
-
15
- # Generated files
16
- bin/
17
- gen/
18
- out/
19
-
20
- # Gradle files
21
- .gradle/
22
- build/
23
-
24
- # Local configuration file (sdk path, etc)
25
- local.properties
26
-
27
- # Proguard folder generated by Eclipse
28
- proguard/
29
-
30
- # Log Files
31
- *.log
32
-
33
- # Android Studio Navigation editor temp files
34
- .navigation/
35
-
36
- # Android Studio captures folder
37
- captures/
38
-
39
- # Intellij
40
- *.iml
41
- .idea/workspace.xml
42
- .idea/tasks.xml
43
- .idea/gradle.xml
44
- .idea/dictionaries
45
- .idea/libraries
46
-
47
- # External native build folder generated in Android Studio 2.2 and later
48
- .externalNativeBuild
49
-
50
- # Freeline
51
- freeline.py
52
- freeline/
53
- freeline_project_description.json
54
-
55
- ### Android Patch ###
56
- gen-external-apklibs
57
-
58
- ### AndroidStudio ###
59
- # Covers files to be ignored for android development using Android Studio.
60
-
61
- # Built application files
62
-
63
- # Files for the ART/Dalvik VM
64
-
65
- # Java class files
66
-
67
- # Generated files
68
-
69
- # Gradle files
70
- .gradle
71
-
72
- # Signing files
73
- .signing/
74
-
75
- # Local configuration file (sdk path, etc)
76
-
77
- # Proguard folder generated by Eclipse
78
-
79
- # Log Files
80
-
81
- # Android Studio
82
- /*/build/
83
- /*/local.properties
84
- /*/out
85
- /*/*/build
86
- /*/*/production
87
- *.ipr
88
- *~
89
- *.swp
90
-
91
- # Android Patch
92
-
93
- # External native build folder generated in Android Studio 2.2 and later
94
-
95
- # NDK
96
- obj/
97
-
98
- # IntelliJ IDEA
99
- *.iws
100
- /out/
101
-
102
- # User-specific configurations
103
- .idea/libraries/
104
- .idea/.name
105
- .idea/compiler.xml
106
- .idea/copyright/profiles_settings.xml
107
- .idea/encodings.xml
108
- .idea/misc.xml
109
- .idea/modules.xml
110
- .idea/scopes/scope_settings.xml
111
- .idea/vcs.xml
112
- .idea/jsLibraryMappings.xml
113
- .idea/datasources.xml
114
- .idea/dataSources.ids
115
- .idea/sqlDataSources.xml
116
- .idea/dynamic.xml
117
- .idea/uiDesigner.xml
118
-
119
- # OS-specific files
120
- .DS_Store
121
- .DS_Store?
122
- ._*
123
- .Spotlight-V100
124
- .Trashes
125
- ehthumbs.db
126
- Thumbs.db
127
-
128
- # Legacy Eclipse project files
129
- .classpath
130
- .project
131
- .cproject
132
- .settings/
133
-
134
- # Mobile Tools for Java (J2ME)
135
- .mtj.tmp/
136
-
137
- # Package Files #
138
- *.war
139
- *.ear
140
-
141
- # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
142
- hs_err_pid*
143
-
144
- ## Plugin-specific files:
145
-
146
- # mpeltonen/sbt-idea plugin
147
- .idea_modules/
148
-
149
- # JIRA plugin
150
- atlassian-ide-plugin.xml
151
-
152
- # Mongo Explorer plugin
153
- .idea/mongoSettings.xml
154
-
155
- ### AndroidStudio Patch ###
156
-
157
- !/gradle/wrapper/gradle-wrapper.jar
158
-
159
- ### Intellij ###
160
- # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
161
- # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
162
-
163
- # User-specific stuff:
164
- .idea/**/workspace.xml
165
- .idea/**/tasks.xml
166
-
167
- # Sensitive or high-churn files:
168
- .idea/**/dataSources/
169
- .idea/**/dataSources.ids
170
- .idea/**/dataSources.xml
171
- .idea/**/dataSources.local.xml
172
- .idea/**/sqlDataSources.xml
173
- .idea/**/dynamic.xml
174
- .idea/**/uiDesigner.xml
175
-
176
- # Gradle:
177
- .idea/**/gradle.xml
178
- .idea/**/libraries
179
-
180
- # CMake
181
- cmake-build-debug/
182
-
183
- # Mongo Explorer plugin:
184
- .idea/**/mongoSettings.xml
185
-
186
- ## File-based project format:
187
-
188
- ## Plugin-specific files:
189
-
190
- # IntelliJ
191
-
192
- # mpeltonen/sbt-idea plugin
193
-
194
- # JIRA plugin
195
-
196
- # Cursive Clojure plugin
197
- .idea/replstate.xml
198
-
199
- # Ruby plugin and RubyMine
200
- /.rakeTasks
201
-
202
- ### Intellij Patch ###
203
- # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
204
-
205
- # *.iml
206
- # modules.xml
207
- # .idea/misc.xml
208
- # *.ipr
209
-
210
- # Sonarlint plugin
211
- .idea/sonarlint
212
-
213
- ### Java ###
214
- # Compiled class file
215
-
216
- # Log file
217
-
218
- # BlueJ files
219
- *.ctxt
220
-
221
- # Mobile Tools for Java (J2ME)
222
-
223
- # Package Files #
224
- *.jar
225
- *.zip
226
- *.tar.gz
227
- *.rar
228
-
229
- # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
230
-
231
- ### Maven ###
232
- target/
233
- pom.xml.tag
234
- pom.xml.releaseBackup
235
- pom.xml.versionsBackup
236
- pom.xml.next
237
- release.properties
238
- dependency-reduced-pom.xml
239
- buildNumber.properties
240
- .mvn/timing.properties
241
-
242
- # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored)
243
- !/.mvn/wrapper/maven-wrapper.jar
244
-
245
- ### Gradle ###
246
- **/build/
247
-
248
- # Ignore Gradle GUI config
249
- gradle-app.setting
250
-
251
- # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
252
- !gradle-wrapper.jar
253
-
254
- # Cache of project
255
- .gradletasknamecache
256
-
257
- # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
258
- # gradle/wrapper/gradle-wrapper.properties
259
-
260
-
261
- # End of https://www.gitignore.io/api/java,maven,gradle,android,intellij,androidstudio
package/jest.config.js DELETED
@@ -1,2 +0,0 @@
1
- /** @type {import('jest').Config} */
2
- module.exports = require('expo-module-scripts/jest-preset');
@@ -1 +0,0 @@
1
- b4bd82523d8da249dc014c63b68f55fe77cb6ff48401a4de9e6bab7ac3bb8cb2
@@ -1 +0,0 @@
1
- 7bbd90592dd5cc5fef6277c7aa63b9178a8a38042108ff1cb941133f8ea7aad37b372629b1974bf524efbf2d30a2ca4cb86ac3068ee3bbde25829645549ba5d2
@@ -1 +0,0 @@
1
- b00a8c94814f5d7cd981372988769df332ccf238c98bc7933bc4da2b11c39f8e
@@ -1 +0,0 @@
1
- bd6895af329edd84e757ebd30c70ed2cf5c8dfb0dde8c6960f7ed6204f1e2c75e0555c351fa04d0ba0a960a6812dfba236eff5324f1b0fa329f49f85118473b5
@@ -1 +0,0 @@
1
- a5455c9bdccd72d5bbf6bf0725a6626a1fd8b6a17a34fec8096e82475cac25d7
@@ -1 +0,0 @@
1
- 820e276b4b10dc38300e77aef9e88de1d9006974e9bdda2b48da4c3b3d91774690706fd34119d906ad5445f456bd38f9a7f811c3d97b862ffaa7777eb3d382f8
@@ -1 +0,0 @@
1
- 84a3a78f7e98a4fa856679b6d3125b90a02c434a3e8892edec59cbd68f9cd421
@@ -1 +0,0 @@
1
- 39123abff9c754e10e7407feb3d000a5562187efc5c872737f2a90b86035cc80fa309dc7941bd906e60d495da1aae54cc497dbff85edec349dba4e41ecc5f572
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "expo-module-scripts/tsconfig.base",
3
- "compilerOptions": {
4
- "rootDir": "./src",
5
- "noEmit": true,
6
- "emitDeclarationOnly": true
7
- },
8
- "include": ["./src"]
9
- }