@logbrew/react-native 0.1.17 → 0.1.19

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.
@@ -33,7 +33,7 @@ Pod::Spec.new do |spec|
33
33
  "ios/GeneratedAppleDiagnostics/**/*.swift"
34
34
  ]
35
35
  diagnostics.dependency "#{spec.name}/Core"
36
- diagnostics.dependency "KSCrash/Recording", "2.5.1"
36
+ diagnostics.dependency "KSCrash/Recording", "2.6.0"
37
37
  diagnostics.pod_target_xcconfig = {
38
38
  "DEFINES_MODULE" => "YES",
39
39
  "SWIFT_VERSION" => "5.0"
package/README.md CHANGED
@@ -293,10 +293,15 @@ module. For a bare React Native app, add the optional subspec inside the app
293
293
  target in `ios/Podfile`, then run `pod install`:
294
294
 
295
295
  ```ruby
296
- logbrew_react_native_path = File.dirname(`node --print "require.resolve('@logbrew/react-native')"`)
296
+ require 'pathname'
297
+ logbrew_react_native_path = Pathname(File.dirname(`bun --print "require.resolve('@logbrew/react-native')"`)).relative_path_from(Pathname(__dir__)).to_s
297
298
  pod 'LogBrewReactNative/AppleNativeDiagnostics', :path => logbrew_react_native_path
298
299
  ```
299
300
 
301
+ Keep the resolved package path relative to the Podfile directory. React Native
302
+ autolinking declares the core pod with that relative source; CocoaPods rejects
303
+ the same pod when the optional subspec uses an absolute source.
304
+
300
305
  The default `LogBrewReactNative/Core` subspec remains unchanged and supports
301
306
  iOS 13. `AppleNativeDiagnostics` requires iOS 15. Install it on the main thread
302
307
  before root registration, then start replay:
@@ -335,7 +340,8 @@ the configured project does not replay an earlier project's records with the
335
340
  new project's key. The raw local KSCrash report can still contain sensitive
336
341
  process details, so apply the app's consent and retention policy to it. Hosted
337
342
  replay uploads only the bounded crash or hang issue described by the Swift
338
- `LogBrewCrash` contract. Status and error results expose fixed codes and bounded
343
+ `LogBrewCrash` contract, including its capture-time service, deployment, OS,
344
+ device-class, and application-version context. Status and error results expose fixed codes and bounded
339
345
  counters, not keys, paths, raw reports, or native error text. A custom endpoint
340
346
  must be a plain HTTPS path without embedded authentication, a query, or a fragment.
341
347
 
package/expo.cjs CHANGED
@@ -1,20 +1,21 @@
1
1
  const PLUGIN_NAME = "@logbrew/react-native/expo";
2
2
  const START_MARKER = " # @generated by @logbrew/react-native/expo";
3
3
  const END_MARKER = " # @end @logbrew/react-native/expo";
4
+ const MANAGED_BLOCK = / # @generated by @logbrew\/react-native\/expo[\s\S]*? # @end @logbrew\/react-native\/expo\n?/gu;
4
5
  const POD_BLOCK = [
5
6
  START_MARKER,
6
- " logbrew_react_native_path = File.dirname(`node --print \"require.resolve('@logbrew/react-native')\"`)",
7
+ " require 'pathname'",
8
+ " logbrew_react_native_path = Pathname(File.dirname(`bun --print \"require.resolve('@logbrew/react-native')\"`)).relative_path_from(Pathname(__dir__)).to_s",
7
9
  " pod 'LogBrewReactNative/AppleNativeDiagnostics', :path => logbrew_react_native_path",
8
10
  END_MARKER
9
11
  ].join("\n");
10
12
 
11
13
  function withLogBrewReactNative(config, options = {}) {
12
14
  const { withPodfile } = require("expo/config-plugins");
13
- return withPodfile(config, (podfileConfig) => {
14
- podfileConfig.modResults.contents = modifyPodfile(
15
- podfileConfig.modResults.contents,
16
- options?.appleNativeDiagnostics !== false
17
- );
15
+ const enabled = options?.appleNativeDiagnostics !== false;
16
+ return withPodfile(config, podfileConfig => {
17
+ const podfile = podfileConfig.modResults;
18
+ podfile.contents = modifyPodfile(podfile.contents, enabled);
18
19
  return podfileConfig;
19
20
  });
20
21
  }
@@ -23,10 +24,7 @@ function modifyPodfile(contents, enabled = true) {
23
24
  if (typeof contents !== "string") {
24
25
  throw new TypeError(`${PLUGIN_NAME} expected Podfile text`);
25
26
  }
26
- const withoutManagedBlock = contents.replace(
27
- new RegExp(`${escapePattern(START_MARKER)}[\\s\\S]*?${escapePattern(END_MARKER)}\\n?`, "gu"),
28
- ""
29
- );
27
+ const withoutManagedBlock = contents.replace(MANAGED_BLOCK, "");
30
28
  if (!enabled) {
31
29
  return withoutManagedBlock;
32
30
  }
@@ -37,10 +35,6 @@ function modifyPodfile(contents, enabled = true) {
37
35
  return withoutManagedBlock.replace(targetPattern, `$1\n${POD_BLOCK}`);
38
36
  }
39
37
 
40
- function escapePattern(value) {
41
- return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
42
- }
43
-
44
38
  module.exports = withLogBrewReactNative;
45
39
  module.exports.modifyPodfile = modifyPodfile;
46
40
  module.exports.withLogBrewReactNative = withLogBrewReactNative;
package/index.cjs CHANGED
@@ -16,7 +16,7 @@ const {
16
16
  } = require("./metadata.cjs");
17
17
 
18
18
  const DEFAULT_SDK_NAME = "logbrew-react-native";
19
- const DEFAULT_SDK_VERSION = "0.1.17";
19
+ const DEFAULT_SDK_VERSION = "0.1.19";
20
20
  const DEFAULT_ENDPOINT = "https://api.logbrew.co/v1/events";
21
21
  const MAX_ACTION_NAME_LENGTH = 64;
22
22
  const MAX_PRODUCT_ANALYTICS_SURFACE_LENGTH = 256;
package/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  } from "./metadata.js";
17
17
 
18
18
  const DEFAULT_SDK_NAME = "logbrew-react-native";
19
- const DEFAULT_SDK_VERSION = "0.1.17";
19
+ const DEFAULT_SDK_VERSION = "0.1.19";
20
20
  const DEFAULT_ENDPOINT = "https://api.logbrew.co/v1/events";
21
21
  const MAX_ACTION_NAME_LENGTH = 64;
22
22
  const MAX_PRODUCT_ANALYTICS_SURFACE_LENGTH = 256;
@@ -3,7 +3,7 @@ import Foundation
3
3
  @objc(LBRNAppleNativeDiagnostics)
4
4
  public final class LBRNAppleNativeDiagnostics: NSObject, @unchecked Sendable {
5
5
  private static let shared = LBRNAppleNativeDiagnostics()
6
- private static let sdkVersion = "0.1.17"
6
+ private static let sdkVersion = "0.1.19"
7
7
 
8
8
  private let lock = NSLock()
9
9
  private let replayQueue = DispatchQueue(label: "co.logbrew.react-native.apple-diagnostics-replay")
@@ -28,6 +28,10 @@ struct CrashReportSanitizer {
28
28
  mechanism: mechanism(error?["type"] as? String),
29
29
  nativeStackFrames: nativeStackFrames,
30
30
  artifactIdentity: artifactIdentity,
31
+ context: nativeCrashContext(
32
+ system: rawReport["system"] as? [String: Any],
33
+ artifactIdentity: artifactIdentity,
34
+ ),
31
35
  hangState: nil,
32
36
  hangDurationMs: nil,
33
37
  source: .engine(reportID: reportID),
@@ -70,6 +74,55 @@ struct CrashReportSanitizer {
70
74
  }
71
75
  }
72
76
 
77
+ func nativeCrashContext(
78
+ system: [String: Any]?,
79
+ artifactIdentity: NativeArtifactIdentity?,
80
+ ) -> TelemetryContext? {
81
+ let value = { (key: String) in crashContextValue(system?[key]) }
82
+ let operatingSystem = value("system_name").map {
83
+ TelemetryOperatingSystem(name: $0, version: value("system_version"), build: value("os_version"))
84
+ }
85
+ let family = value("model")
86
+ let model = value("machine")
87
+ let architecture = value("cpu_arch")
88
+ let device = family == nil && model == nil && architecture == nil
89
+ ? nil
90
+ : TelemetryDevice(family: family, model: model, architecture: architecture)
91
+ let applicationName = value("CFBundleName")
92
+ let applicationVersion = value("CFBundleShortVersionString")
93
+ let applicationBuild = value("CFBundleVersion")
94
+ let application = applicationName == nil && applicationVersion == nil && applicationBuild == nil
95
+ ? nil
96
+ : TelemetryApplication(name: applicationName, version: applicationVersion, build: applicationBuild)
97
+ guard artifactIdentity != nil || operatingSystem != nil || device != nil || application != nil else {
98
+ return nil
99
+ }
100
+ return TelemetryContext(resource: TelemetryResource(
101
+ service: artifactIdentity.map { TelemetryNamedVersion(name: $0.service) },
102
+ deployment: artifactIdentity.map {
103
+ TelemetryDeployment(environment: $0.environment, release: $0.release)
104
+ },
105
+ operatingSystem: operatingSystem,
106
+ device: device,
107
+ application: application,
108
+ ))
109
+ }
110
+
111
+ private func crashContextValue(_ rawValue: Any?) -> String? {
112
+ guard let value = rawValue as? String else {
113
+ return nil
114
+ }
115
+ let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines)
116
+ guard (1 ... 256).contains(normalized.utf8.count),
117
+ !normalized.unicodeScalars.contains(where: CharacterSet.controlCharacters.contains),
118
+ !normalized.contains("/"),
119
+ !normalized.contains("\\")
120
+ else {
121
+ return nil
122
+ }
123
+ return normalized
124
+ }
125
+
73
126
  struct NativeCrashTimestamp: Comparable {
74
127
  let normalized: String
75
128
  private let wholeSecond: String
@@ -1,6 +1,5 @@
1
1
  // Generated by scripts/sync-apple-native-sources.mjs.
2
2
  // Edit the canonical Swift source, then regenerate this package boundary.
3
- import Foundation
4
3
 
5
4
  func nativeCrashExceptionChain(for exception: IssueException) -> IssueExceptionChain {
6
5
  IssueExceptionChain(entries: [
@@ -14,31 +13,3 @@ func nativeCrashExceptionChain(for exception: IssueException) -> IssueExceptionC
14
13
  ),
15
14
  ])
16
15
  }
17
-
18
- func nativeCrashExceptionChainMatches(
19
- _ value: Any?,
20
- expected exception: IssueException,
21
- ) -> Bool {
22
- guard let value else {
23
- // Preserve retry identity for an event queued by an older SDK before chains existed.
24
- return true
25
- }
26
- guard let chain = value as? [String: Any] else {
27
- return false
28
- }
29
- guard let mechanism = exception.mechanism else {
30
- return false
31
- }
32
- let expected = [
33
- "entries": [[
34
- "id": 0,
35
- "relationship": "reported",
36
- "type": exception.type,
37
- "messageState": "not_captured",
38
- "mechanism": ["type": mechanism.type, "handled": mechanism.handled],
39
- "stackFramesState": "not_captured",
40
- ]],
41
- "truncated": false,
42
- ] as NSDictionary
43
- return chain as NSDictionary == expected
44
- }
@@ -163,6 +163,7 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
163
163
 
164
164
  private let nativeStackFrames: [NativeStackFrame]?
165
165
  private let artifactIdentity: NativeArtifactIdentity?
166
+ private let context: TelemetryContext?
166
167
  private let hangState: NativeHangIncidentState?
167
168
  private let hangDurationMs: Double?
168
169
  let source: NativeCrashRecordSource
@@ -175,6 +176,7 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
175
176
  mechanism: NativeCrashMechanism,
176
177
  nativeStackFrames: [NativeStackFrame]?,
177
178
  artifactIdentity: NativeArtifactIdentity?,
179
+ context: TelemetryContext?,
178
180
  hangState: NativeHangIncidentState?,
179
181
  hangDurationMs: Double? = nil,
180
182
  source: NativeCrashRecordSource,
@@ -186,6 +188,7 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
186
188
  self.mechanism = mechanism
187
189
  self.nativeStackFrames = nativeStackFrames
188
190
  self.artifactIdentity = artifactIdentity
191
+ self.context = context
189
192
  self.hangState = hangState
190
193
  self.hangDurationMs = hangDurationMs
191
194
  self.source = source
@@ -237,6 +240,7 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
237
240
  exception: issueException,
238
241
  exceptionChain: nativeCrashExceptionChain(for: issueException),
239
242
  metadata: metadata,
243
+ context: context,
240
244
  nativeStackFrames: nativeStackFrames,
241
245
  )
242
246
  }
@@ -254,57 +258,13 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
254
258
  guard event["type"] as? String == "issue",
255
259
  event["timestamp"] as? String == timestamp,
256
260
  let attributes = event["attributes"] as? [String: Any],
257
- attributes["title"] as? String == issueAttributes.title,
258
- attributes["level"] as? String == issueAttributes.level.canonicalValue,
259
- attributes["message"] == nil,
260
- exceptionMatches(attributes["exception"]),
261
- nativeCrashExceptionChainMatches(
262
- attributes["exceptionChain"],
263
- expected: issueException,
264
- ),
265
- attributes["stackFrames"] == nil,
266
- attributes["breadcrumbs"] == nil,
267
- attributes["breadcrumbsTruncated"] == nil,
268
- attributes["context"] == nil,
269
- let metadata = attributes["metadata"] as? [String: Any],
270
- metadata as NSDictionary == expectedMetadata,
271
- nativeStackFramesMatch(attributes["nativeStackFrames"])
261
+ attributesMatch(attributes)
272
262
  else {
273
263
  return .collision
274
264
  }
275
265
  return .matching
276
266
  }
277
267
 
278
- private var expectedMetadata: NSDictionary {
279
- var metadata: [String: Any] = [
280
- "crash.mechanism": mechanism.name,
281
- "crash.replayed": true,
282
- ]
283
- if let artifactIdentity {
284
- metadata["projectId"] = artifactIdentity.projectId
285
- metadata["release"] = artifactIdentity.release
286
- metadata["environment"] = artifactIdentity.environment
287
- metadata["service"] = artifactIdentity.service
288
- }
289
- if let hangState {
290
- metadata["crash.handled"] = hangState == .recovered
291
- }
292
- if let hangDurationMs {
293
- metadata["durationMs"] = hangDurationMs
294
- }
295
- return metadata as NSDictionary
296
- }
297
-
298
- private var expectedException: NSDictionary {
299
- [
300
- "type": hangState == nil ? "AppleNativeCrash" : "AppleNativeHang",
301
- "mechanism": [
302
- "type": mechanism.name,
303
- "handled": hangState == .recovered,
304
- ],
305
- ] as NSDictionary
306
- }
307
-
308
268
  private var issueException: IssueException {
309
269
  IssueException(
310
270
  type: hangState == nil ? "AppleNativeCrash" : "AppleNativeHang",
@@ -315,31 +275,16 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
315
275
  )
316
276
  }
317
277
 
318
- private func exceptionMatches(_ value: Any?) -> Bool {
319
- guard let value else {
320
- // Preserve exact retry identity for an event queued by an older SDK before typed exceptions existed.
321
- return true
322
- }
323
- guard let exception = value as? [String: Any] else {
324
- return false
325
- }
326
- return exception as NSDictionary == expectedException
327
- }
328
-
329
- private func nativeStackFramesMatch(_ value: Any?) -> Bool {
330
- guard let nativeStackFrames else {
331
- return value == nil
332
- }
333
- guard let frames = value as? [[String: Any]], frames.count == nativeStackFrames.count else {
278
+ private func attributesMatch(_ actual: [String: Any]) -> Bool {
279
+ guard let encoded = try? JSONEncoder().encode(issueAttributes),
280
+ var expected = try? JSONSerialization.jsonObject(with: encoded) as? [String: Any]
281
+ else {
334
282
  return false
335
283
  }
336
- return zip(frames, nativeStackFrames).allSatisfy { frame, expected in
337
- frame as NSDictionary == [
338
- "imageUuid": expected.imageUuid,
339
- "architecture": expected.architecture.rawValue,
340
- "instructionOffset": expected.instructionOffset,
341
- ] as NSDictionary
284
+ for legacyField in ["exception", "exceptionChain", "context"] where actual[legacyField] == nil {
285
+ expected.removeValue(forKey: legacyField)
342
286
  }
287
+ return actual as NSDictionary == expected as NSDictionary
343
288
  }
344
289
  }
345
290
 
@@ -93,6 +93,7 @@ struct NativeHangIncident: Codable, Equatable {
93
93
  mechanism: .deadlock,
94
94
  nativeStackFrames: nativeStackFrames,
95
95
  artifactIdentity: artifactIdentity.validatedIdentity(),
96
+ context: nil,
96
97
  hangState: state,
97
98
  hangDurationMs: durationMs,
98
99
  source: .hang(eventID: eventID),
@@ -29,14 +29,14 @@
29
29
  "swift/logbrew-swift/Sources/LogBrew/URLSessionTracer.swift": "0cdf08f92d6111c0dc2883b87837b81f69b5c2f9a8e5501780a114c23a741af7",
30
30
  "swift/logbrew-swift/Sources/LogBrew/Validation.swift": "07ff5c2daeb829b1df4f50edb0b79727f9a81b1c9b5cde5f995f5ad6ce660aa7",
31
31
  "swift/logbrew-swift/Sources/LogBrewCrash/CrashEngine.swift": "7a46035fcaa72dd7504b4e274a92070d73dd1c45fe20e008474340d6d7c44ede",
32
- "swift/logbrew-swift/Sources/LogBrewCrash/CrashReportSanitizer.swift": "a872ad63532999e74af1e9685a9382d0e02b93c6f9b942b371d47f56f8b961c0",
32
+ "swift/logbrew-swift/Sources/LogBrewCrash/CrashReportSanitizer.swift": "8798c8900ea28a85d6909183dc8435bc08da96573b89310d26ed178217fbc04b",
33
33
  "swift/logbrew-swift/Sources/LogBrewCrash/CrashStorageDirectory.swift": "7cd566703cbf704dc99155451ee93dcace12c35c805abc09a6dcf23975cf43f9",
34
34
  "swift/logbrew-swift/Sources/LogBrewCrash/NativeArtifactIdentity.swift": "3d785ad717dcf7302451531c18b5e1183983270a6cd78dd543a407e3facc6ae0",
35
35
  "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashCapture.swift": "e9ab5f1850a096f40592a025d16c5ffa2fdff0222004a37056898cf1cb4b0dd9",
36
- "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashExceptionEvidence.swift": "08819652ce0796bee9f502de1202d236887afbcb7794d4db9adc5b6c2928ca2c",
37
- "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashPublic.swift": "fe0db58c4ad348a2601d984cdf097147f0b2093baad64861aef48cc052697d9c",
36
+ "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashExceptionEvidence.swift": "ce191e46298d91d87b379528d610caa88b924b15a8dd912b4a164b51eb172a7a",
37
+ "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashPublic.swift": "7c1d028bea143c6ab34ebddaaaab64aeb69d4d950ec7a42264c567e672991748",
38
38
  "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashReplay.swift": "3eb3d93b32cf14de416f56fa30d6d65e6f268b5ec9d1a67081d7525c4efd4f80",
39
- "swift/logbrew-swift/Sources/LogBrewCrash/NativeHangIncidentStore.swift": "02413ca7e003917faadcc9aa612b5d98897c75b39f67c3c845948dd9349a9d8f",
39
+ "swift/logbrew-swift/Sources/LogBrewCrash/NativeHangIncidentStore.swift": "5a8150083f16d6b495c9c63434809dc5f11f13ebb23211f57151bad62040781b",
40
40
  "swift/logbrew-swift/Sources/LogBrewCrash/NativeHangWatchdog.swift": "37b25904cd6576e20c40500be2c153156403b57ad7b4186a1b6b35b414145ecb",
41
41
  "swift/logbrew-swift/Sources/LogBrewCrash/NativeHangWatchdogContracts.swift": "59f51554d86c326122bb2bdce47c5a7e01996bd61f2b371eba23855032487b9f",
42
42
  "swift/logbrew-swift/Sources/LogBrewCrash/NativeHangWatchdogSystem.swift": "b87e3db246691d5b0719898cdc02ba068836647b1ac9339a73cf866eabe235ae",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logbrew/react-native",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "React Native offline delivery, Apple native diagnostics, screen, error, trace, action, and network timeline helpers for LogBrew.",
5
5
  "type": "module",
6
6
  "main": "./index.cjs",