@logbrew/react-native 0.1.10 → 0.1.12

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 (72) hide show
  1. package/LogBrewReactNative.podspec +25 -3
  2. package/README.md +174 -4
  3. package/android/src/main/java/co/logbrew/reactnative/EventRecordStore.java +611 -0
  4. package/android/src/main/java/co/logbrew/reactnative/FatalStoreModuleImpl.java +129 -0
  5. package/android/src/newarch/java/co/logbrew/reactnative/FatalStoreModule.java +26 -0
  6. package/android/src/oldarch/java/co/logbrew/reactnative/FatalStoreModule.java +26 -0
  7. package/apple-native-diagnostics.d.ts +52 -0
  8. package/apple-native-diagnostics.js +286 -0
  9. package/expo.cjs +46 -0
  10. package/expo.d.ts +16 -0
  11. package/expo.js +5 -0
  12. package/fatal-replay.cjs +12 -1
  13. package/global-errors.cjs +3 -1
  14. package/index.cjs +86 -2
  15. package/index.d.cts +7 -0
  16. package/index.d.ts +7 -0
  17. package/index.js +86 -2
  18. package/index.native.d.ts +47 -1
  19. package/index.native.js +81 -1
  20. package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.h +4 -0
  21. package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.mm +56 -0
  22. package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +293 -0
  23. package/ios/GeneratedAppleDiagnostics/LogBrew/AutomaticTelemetryContext.swift +72 -0
  24. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngine.swift +270 -0
  25. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineAutomatic.swift +331 -0
  26. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineDurable.swift +148 -0
  27. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineQueue.swift +343 -0
  28. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryLifecycle.swift +70 -0
  29. package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStore.swift +258 -0
  30. package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStoreRecovery.swift +330 -0
  31. package/ios/GeneratedAppleDiagnostics/LogBrew/EventEncoding.swift +114 -0
  32. package/ios/GeneratedAppleDiagnostics/LogBrew/IssueDiagnostics.swift +385 -0
  33. package/ios/GeneratedAppleDiagnostics/LogBrew/LifecycleTrace.swift +138 -0
  34. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewClient.swift +358 -0
  35. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewLogger.swift +262 -0
  36. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewTrace.swift +357 -0
  37. package/ios/GeneratedAppleDiagnostics/LogBrew/Metadata.swift +86 -0
  38. package/ios/GeneratedAppleDiagnostics/LogBrew/NativeStackFrame.swift +27 -0
  39. package/ios/GeneratedAppleDiagnostics/LogBrew/ProductTimeline.swift +228 -0
  40. package/ios/GeneratedAppleDiagnostics/LogBrew/PublicTypes.swift +334 -0
  41. package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContext.swift +197 -0
  42. package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContextValidation.swift +337 -0
  43. package/ios/GeneratedAppleDiagnostics/LogBrew/TraceEvidence.swift +79 -0
  44. package/ios/GeneratedAppleDiagnostics/LogBrew/Transport.swift +204 -0
  45. package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTrace.swift +277 -0
  46. package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTracer.swift +151 -0
  47. package/ios/GeneratedAppleDiagnostics/LogBrew/Validation.swift +165 -0
  48. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashEngine.swift +125 -0
  49. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashReportSanitizer.swift +139 -0
  50. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashStorageDirectory.swift +124 -0
  51. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeArtifactIdentity.swift +137 -0
  52. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCapture.swift +229 -0
  53. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashPublic.swift +386 -0
  54. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashReplay.swift +317 -0
  55. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangIncidentStore.swift +321 -0
  56. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdog.swift +386 -0
  57. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogContracts.swift +84 -0
  58. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogSystem.swift +139 -0
  59. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeMainThreadStackCapture.swift +84 -0
  60. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeStackFrameSanitizer.swift +170 -0
  61. package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +43 -0
  62. package/ios/LBRNEventRecordStore.h +29 -0
  63. package/ios/LBRNEventRecordStore.m +746 -0
  64. package/ios/LBRNFatalStoreModule.mm +159 -14
  65. package/ios/LBRNPrivateStorage.h +7 -0
  66. package/ios/LBRNPrivateStorage.m +38 -0
  67. package/package.json +47 -4
  68. package/persistent-delivery.native.js +282 -0
  69. package/promise-rejections.cjs +3 -1
  70. package/scripts/sync-apple-native-sources.mjs +93 -0
  71. package/src/NativeLogBrewAppleDiagnostics.ts +12 -0
  72. package/src/NativeLogBrewFatalStore.ts +9 -0
@@ -1,6 +1,11 @@
1
1
  #import "LBRNFatalStoreModule.h"
2
2
 
3
+ #import "LBRNEventRecordStore.h"
3
4
  #import "LBRNFatalRecordStore.h"
5
+ #import "LBRNPrivateStorage.h"
6
+
7
+ #import <CommonCrypto/CommonDigest.h>
8
+ #import <TargetConditionals.h>
4
9
 
5
10
  #ifdef RCT_NEW_ARCH_ENABLED
6
11
  #import <LogBrewReactNativeSpec/LogBrewReactNativeSpec.h>
@@ -11,6 +16,51 @@ static NSDictionary *LBRNStorageError(void)
11
16
  return @{ @"status" : @"storage_error" };
12
17
  }
13
18
 
19
+ static BOOL LBRNPrepareProtectedDirectory(NSURL *directoryURL)
20
+ {
21
+ NSError *writeError = nil;
22
+ if (![directoryURL setResourceValue:@YES
23
+ forKey:NSURLIsExcludedFromBackupKey
24
+ error:&writeError]
25
+ || writeError != nil) {
26
+ return NO;
27
+ }
28
+ #if TARGET_OS_IPHONE
29
+ writeError = nil;
30
+ if (![directoryURL setResourceValue:NSFileProtectionCompleteUntilFirstUserAuthentication
31
+ forKey:NSURLFileProtectionKey
32
+ error:&writeError]
33
+ || writeError != nil) {
34
+ return NO;
35
+ }
36
+ #endif
37
+ NSNumber *excluded = nil;
38
+ NSError *readError = nil;
39
+ return [directoryURL getResourceValue:&excluded
40
+ forKey:NSURLIsExcludedFromBackupKey
41
+ error:&readError]
42
+ && readError == nil && excluded.boolValue;
43
+ }
44
+
45
+ static NSString *LBRNQueueHash(NSString *queueKey)
46
+ {
47
+ if (![queueKey isKindOfClass:[NSString class]]
48
+ || [queueKey stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].length == 0) {
49
+ return nil;
50
+ }
51
+ NSData *data = [queueKey dataUsingEncoding:NSUTF8StringEncoding];
52
+ if (data.length == 0 || data.length > 4096) {
53
+ return nil;
54
+ }
55
+ unsigned char digest[CC_SHA256_DIGEST_LENGTH];
56
+ CC_SHA256(data.bytes, (CC_LONG)data.length, digest);
57
+ NSMutableString *value = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
58
+ for (NSUInteger index = 0; index < CC_SHA256_DIGEST_LENGTH; index += 1) {
59
+ [value appendFormat:@"%02x", digest[index]];
60
+ }
61
+ return value;
62
+ }
63
+
14
64
  static NSDictionary *LBRNNormalizeRecord(NSDictionary *record)
15
65
  {
16
66
  if (![record isKindOfClass:[NSDictionary class]]) {
@@ -44,6 +94,8 @@ static NSDictionary *LBRNNormalizeRecord(NSDictionary *record)
44
94
  <NativeLogBrewFatalStoreSpec>
45
95
  #endif
46
96
  @property (nonatomic, nullable) LBRNFatalRecordStore *store;
97
+ @property (nonatomic, nullable) NSURL *eventStoreParentURL;
98
+ @property (nonatomic) NSMutableDictionary<NSString *, LBRNEventRecordStore *> *eventStores;
47
99
  @end
48
100
 
49
101
  @implementation LBRNFatalStoreModule
@@ -62,23 +114,13 @@ RCT_EXPORT_MODULE(LogBrewFatalStore)
62
114
  NSURL *baseURL =
63
115
  [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory
64
116
  inDomains:NSUserDomainMask].firstObject;
65
- if (baseURL != nil) {
117
+ if (baseURL != nil && LBRNPreparePrivateRootDirectory(baseURL)) {
118
+ _eventStoreParentURL = baseURL;
119
+ _eventStores = [NSMutableDictionary dictionary];
66
120
  NSURL *directoryURL = [baseURL URLByAppendingPathComponent:@"LogBrewFatalJS"
67
121
  isDirectory:YES];
68
122
  LBRNFatalDirectoryPreparation directoryPreparation = ^BOOL(NSURL *preparedURL) {
69
- NSError *writeError = nil;
70
- if (![preparedURL setResourceValue:@YES
71
- forKey:NSURLIsExcludedFromBackupKey
72
- error:&writeError]
73
- || writeError != nil) {
74
- return NO;
75
- }
76
- NSNumber *excluded = nil;
77
- NSError *readError = nil;
78
- return [preparedURL getResourceValue:&excluded
79
- forKey:NSURLIsExcludedFromBackupKey
80
- error:&readError]
81
- && readError == nil && excluded.boolValue;
123
+ return LBRNPrepareProtectedDirectory(preparedURL);
82
124
  };
83
125
  _store = [[LBRNFatalRecordStore alloc]
84
126
  initWithDirectoryURL:directoryURL
@@ -88,6 +130,30 @@ RCT_EXPORT_MODULE(LogBrewFatalStore)
88
130
  return self;
89
131
  }
90
132
 
133
+ - (nullable LBRNEventRecordStore *)eventStoreForQueueKey:(NSString *)queueKey
134
+ {
135
+ NSString *queueHash = LBRNQueueHash(queueKey);
136
+ if (queueHash == nil || self.eventStoreParentURL == nil) {
137
+ return nil;
138
+ }
139
+ @synchronized(self) {
140
+ LBRNEventRecordStore *existing = self.eventStores[queueHash];
141
+ if (existing != nil) {
142
+ return existing;
143
+ }
144
+ NSURL *directoryURL = [self.eventStoreParentURL
145
+ URLByAppendingPathComponent:[@"LogBrewEventsV1-" stringByAppendingString:queueHash]
146
+ isDirectory:YES];
147
+ LBRNEventRecordStore *created = [[LBRNEventRecordStore alloc]
148
+ initWithDirectoryURL:directoryURL
149
+ directoryPreparation:^BOOL(NSURL *preparedURL) {
150
+ return LBRNPrepareProtectedDirectory(preparedURL);
151
+ }];
152
+ self.eventStores[queueHash] = created;
153
+ return created;
154
+ }
155
+ }
156
+
91
157
  RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(writeFatalRecord:(NSDictionary *)record)
92
158
  {
93
159
  if (self.store == nil) {
@@ -136,6 +202,85 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(discardFatalRecord)
136
202
  }
137
203
  }
138
204
 
205
+ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(loadEventRecords:(NSString *)queueKey)
206
+ {
207
+ LBRNEventRecordStore *eventStore = [self eventStoreForQueueKey:queueKey];
208
+ if (eventStore == nil) {
209
+ return LBRNStorageError();
210
+ }
211
+ @try {
212
+ return [eventStore loadRecords];
213
+ } @catch (__unused NSException *exception) {
214
+ return LBRNStorageError();
215
+ }
216
+ }
217
+
218
+ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(appendEventRecord:(NSString *)queueKey
219
+ serializedEvent:(NSString *)serializedEvent
220
+ eventBytes:(double)eventBytes)
221
+ {
222
+ LBRNEventRecordStore *eventStore = [self eventStoreForQueueKey:queueKey];
223
+ if (eventStore == nil) {
224
+ return LBRNStorageError();
225
+ }
226
+ @try {
227
+ return [eventStore appendSerializedEvent:serializedEvent eventBytes:@(eventBytes)];
228
+ } @catch (__unused NSException *exception) {
229
+ return LBRNStorageError();
230
+ }
231
+ }
232
+
233
+ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(acknowledgeEventRecords:(NSString *)queueKey
234
+ count:(double)count)
235
+ {
236
+ LBRNEventRecordStore *eventStore = [self eventStoreForQueueKey:queueKey];
237
+ if (eventStore == nil) {
238
+ return LBRNStorageError();
239
+ }
240
+ @try {
241
+ return [eventStore acknowledgeRecordCount:@(count)];
242
+ } @catch (__unused NSException *exception) {
243
+ return LBRNStorageError();
244
+ }
245
+ }
246
+
247
+ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(purgeEventRecords:(NSString *)queueKey)
248
+ {
249
+ LBRNEventRecordStore *eventStore = [self eventStoreForQueueKey:queueKey];
250
+ if (eventStore == nil) {
251
+ return LBRNStorageError();
252
+ }
253
+ @try {
254
+ return [eventStore purgeRecords];
255
+ } @catch (__unused NSException *exception) {
256
+ return LBRNStorageError();
257
+ }
258
+ }
259
+
260
+ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(closeEventStore:(NSString *)queueKey)
261
+ {
262
+ NSString *queueHash = LBRNQueueHash(queueKey);
263
+ if (queueHash == nil) {
264
+ return LBRNStorageError();
265
+ }
266
+ LBRNEventRecordStore *eventStore = nil;
267
+ @synchronized(self) {
268
+ eventStore = self.eventStores[queueHash];
269
+ }
270
+ if (eventStore == nil) {
271
+ return @{ @"status" : @"closed" };
272
+ }
273
+ @try {
274
+ return [eventStore closeStore];
275
+ } @catch (__unused NSException *exception) {
276
+ return LBRNStorageError();
277
+ } @finally {
278
+ @synchronized(self) {
279
+ [self.eventStores removeObjectForKey:queueHash];
280
+ }
281
+ }
282
+ }
283
+
139
284
  #ifdef RCT_NEW_ARCH_ENABLED
140
285
  - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
141
286
  (const facebook::react::ObjCTurboModule::InitParams &)params
@@ -0,0 +1,7 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ FOUNDATION_EXPORT BOOL LBRNPreparePrivateRootDirectory(NSURL *directoryURL);
6
+
7
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,38 @@
1
+ #import "LBRNPrivateStorage.h"
2
+
3
+ #import <errno.h>
4
+ #import <fcntl.h>
5
+ #import <sys/stat.h>
6
+ #import <unistd.h>
7
+
8
+ BOOL LBRNPreparePrivateRootDirectory(NSURL *directoryURL)
9
+ {
10
+ if (!directoryURL.isFileURL) {
11
+ return NO;
12
+ }
13
+ const char *path = directoryURL.fileSystemRepresentation;
14
+ struct stat pathInfo;
15
+ if (lstat(path, &pathInfo) != 0) {
16
+ if (errno != ENOENT || mkdir(path, 0700) != 0) {
17
+ return NO;
18
+ }
19
+ } else if (!S_ISDIR(pathInfo.st_mode) || S_ISLNK(pathInfo.st_mode)) {
20
+ return NO;
21
+ }
22
+
23
+ int directoryFD = open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
24
+ if (directoryFD < 0) {
25
+ return NO;
26
+ }
27
+ struct stat openedInfo;
28
+ BOOL prepared = fstat(directoryFD, &openedInfo) == 0
29
+ && S_ISDIR(openedInfo.st_mode)
30
+ && fchmod(directoryFD, 0700) == 0
31
+ && lstat(path, &pathInfo) == 0
32
+ && S_ISDIR(pathInfo.st_mode)
33
+ && !S_ISLNK(pathInfo.st_mode)
34
+ && openedInfo.st_dev == pathInfo.st_dev
35
+ && openedInfo.st_ino == pathInfo.st_ino;
36
+ close(directoryFD);
37
+ return prepared;
38
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@logbrew/react-native",
3
- "version": "0.1.10",
4
- "description": "React Native screen, error, trace, action, and network timeline helpers for LogBrew.",
3
+ "version": "0.1.12",
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",
7
7
  "module": "./index.js",
@@ -114,6 +114,28 @@
114
114
  "default": "./metro.cjs"
115
115
  },
116
116
  "default": "./metro.js"
117
+ },
118
+ "./apple-native-diagnostics": {
119
+ "react-native": {
120
+ "types": "./apple-native-diagnostics.d.ts",
121
+ "default": "./apple-native-diagnostics.js"
122
+ },
123
+ "import": {
124
+ "types": "./apple-native-diagnostics.d.ts",
125
+ "default": "./apple-native-diagnostics.js"
126
+ },
127
+ "default": "./apple-native-diagnostics.js"
128
+ },
129
+ "./expo": {
130
+ "import": {
131
+ "types": "./expo.d.ts",
132
+ "default": "./expo.js"
133
+ },
134
+ "require": {
135
+ "types": "./expo.d.ts",
136
+ "default": "./expo.cjs"
137
+ },
138
+ "default": "./expo.cjs"
117
139
  }
118
140
  },
119
141
  "files": [
@@ -138,6 +160,7 @@
138
160
  "global-errors.cjs",
139
161
  "promise-rejections.cjs",
140
162
  "fatal-replay.cjs",
163
+ "persistent-delivery.native.js",
141
164
  "global-errors.js",
142
165
  "global-errors.native.js",
143
166
  "global-errors.d.ts",
@@ -160,6 +183,11 @@
160
183
  "metro.js",
161
184
  "metro.d.ts",
162
185
  "metro.d.cts",
186
+ "apple-native-diagnostics.js",
187
+ "apple-native-diagnostics.d.ts",
188
+ "expo.cjs",
189
+ "expo.js",
190
+ "expo.d.ts",
163
191
  "LogBrewReactNative.podspec",
164
192
  "android/build.gradle",
165
193
  "android/src/main",
@@ -169,8 +197,15 @@
169
197
  "ios/LBRNFatalRecordStore.m",
170
198
  "ios/LBRNFatalStoreModule.h",
171
199
  "ios/LBRNFatalStoreModule.mm",
200
+ "ios/LBRNPrivateStorage.h",
201
+ "ios/LBRNPrivateStorage.m",
202
+ "ios/LBRNEventRecordStore.h",
203
+ "ios/LBRNEventRecordStore.m",
204
+ "ios/AppleDiagnostics",
205
+ "ios/GeneratedAppleDiagnostics",
172
206
  "react-native.config.js",
173
207
  "src",
208
+ "scripts/sync-apple-native-sources.mjs",
174
209
  "README.md",
175
210
  "examples"
176
211
  ],
@@ -183,7 +218,9 @@
183
218
  "logs",
184
219
  "traces",
185
220
  "events",
186
- "source-maps"
221
+ "source-maps",
222
+ "crash-reporting",
223
+ "app-hangs"
187
224
  ],
188
225
  "sideEffects": false,
189
226
  "codegenConfig": {
@@ -204,10 +241,16 @@
204
241
  },
205
242
  "peerDependencies": {
206
243
  "@logbrew/sdk": "^0.1.5",
244
+ "expo": ">=49",
207
245
  "react": ">=18",
208
246
  "react-native": ">=0.72"
209
247
  },
248
+ "peerDependenciesMeta": {
249
+ "expo": {
250
+ "optional": true
251
+ }
252
+ },
210
253
  "scripts": {
211
- "test": "node --check index.js && node --check index.cjs && node --check index.native.js && node --check apollo.js && node --check apollo.cjs && node --check instrumentation.js && node --check instrumentation.cjs && node --check lifecycle.js && node --check lifecycle.cjs && node --check global-errors.js && node --check global-errors.native.js && node --check global-errors.cjs && node --check promise-rejections.cjs && node --check fatal-replay.cjs && node --check metadata.js && node --check metadata.cjs && node --check native-bridge.js && node --check native-bridge.cjs && node --check resource-fetch.js && node --check resource-fetch.cjs && node --check release-artifacts.js && node --check release-artifacts.cjs && node --check metro.js && node --check metro.cjs && node --check examples/index.mjs && node --check examples/apollo-link-spans.mjs && node --check examples/instrumentation-kit.mjs && node --check examples/lifecycle-spans.mjs && node --check examples/native-bridge-scope.mjs && node --check examples/navigation-resource-spans.mjs && node --check examples/readme-example.mjs && node --check examples/real-user-smoke.mjs && node --check examples/resource-fetch-spans.mjs && node --check examples/trace-correlation.mjs && node --test"
254
+ "test": "node scripts/sync-apple-native-sources.mjs --check && node --check index.js && node --check index.cjs && node --check index.native.js && node --check apple-native-diagnostics.js && node --check expo.js && node --check expo.cjs && node --check apollo.js && node --check apollo.cjs && node --check instrumentation.js && node --check instrumentation.cjs && node --check lifecycle.js && node --check lifecycle.cjs && node --check global-errors.js && node --check global-errors.native.js && node --check global-errors.cjs && node --check promise-rejections.cjs && node --check fatal-replay.cjs && node --check metadata.js && node --check metadata.cjs && node --check native-bridge.js && node --check native-bridge.cjs && node --check resource-fetch.js && node --check resource-fetch.cjs && node --check release-artifacts.js && node --check release-artifacts.cjs && node --check metro.js && node --check metro.cjs && node --check examples/index.mjs && node --check examples/apollo-link-spans.mjs && node --check examples/instrumentation-kit.mjs && node --check examples/lifecycle-spans.mjs && node --check examples/native-bridge-scope.mjs && node --check examples/navigation-resource-spans.mjs && node --check examples/readme-example.mjs && node --check examples/real-user-smoke.mjs && node --check examples/resource-fetch-spans.mjs && node --check examples/trace-correlation.mjs && node --test"
212
255
  }
213
256
  }
@@ -0,0 +1,282 @@
1
+ import { SdkError } from "@logbrew/sdk";
2
+ import { NativeModules, TurboModuleRegistry } from "react-native";
3
+
4
+ const ACTIVE_QUEUE_KEYS = new Set();
5
+ const MAX_NATIVE_QUEUE_BYTES = 4 * 1024 * 1024;
6
+ const MAX_NATIVE_QUEUE_EVENTS = 1000;
7
+ const NATIVE_STORE_METHODS = [
8
+ "acknowledgeEventRecords",
9
+ "appendEventRecord",
10
+ "closeEventStore",
11
+ "loadEventRecords",
12
+ "purgeEventRecords"
13
+ ];
14
+ const PERSISTENT_QUEUE_MODES = new Set(["auto", "disabled", "required"]);
15
+
16
+ export function resolveReactNativePersistentEventStore({
17
+ authKey,
18
+ eventStore,
19
+ hasExplicitPersistentQueue = false,
20
+ maxQueueBytes,
21
+ maxQueueSize,
22
+ persistentQueue = "auto"
23
+ }) {
24
+ validatePersistentQueueMode(persistentQueue);
25
+ if (eventStore !== undefined) {
26
+ if (hasExplicitPersistentQueue) {
27
+ throw new SdkError(
28
+ "configuration_error",
29
+ "eventStore and persistentQueue are mutually exclusive"
30
+ );
31
+ }
32
+ return { eventStore, abort() {}, release() {} };
33
+ }
34
+ if (persistentQueue === "disabled") {
35
+ return { eventStore: undefined, abort() {}, release() {} };
36
+ }
37
+
38
+ const nativeStore = defaultNativeStore();
39
+ if (!nativeStore) {
40
+ if (persistentQueue === "required") {
41
+ throw new SdkError(
42
+ "configuration_error",
43
+ "React Native persistent queue requires the linked LogBrew native module"
44
+ );
45
+ }
46
+ return { eventStore: undefined, abort() {}, release() {} };
47
+ }
48
+ validateNativeQueueLimits({ maxQueueBytes, maxQueueSize });
49
+ if (ACTIVE_QUEUE_KEYS.has(authKey)) {
50
+ throw new SdkError(
51
+ "configuration_error",
52
+ "React Native persistent queue already has an active client for this key"
53
+ );
54
+ }
55
+
56
+ ACTIVE_QUEUE_KEYS.add(authKey);
57
+ let released = false;
58
+ const release = () => {
59
+ if (!released) {
60
+ released = true;
61
+ ACTIVE_QUEUE_KEYS.delete(authKey);
62
+ }
63
+ };
64
+ const nativeEventStore = createNativeEventStore(nativeStore, authKey, release);
65
+ return {
66
+ eventStore: nativeEventStore,
67
+ abort() {
68
+ try {
69
+ nativeEventStore.close();
70
+ } catch {
71
+ release();
72
+ }
73
+ },
74
+ release
75
+ };
76
+ }
77
+
78
+ export function purgeReactNativePersistentQueue({ apiKey, clientKey } = {}) {
79
+ const authKey = clientKey ?? apiKey;
80
+ if (typeof authKey !== "string" || authKey.trim() === "") {
81
+ throw new SdkError(
82
+ "configuration_error",
83
+ "purgeLogBrewReactNativePersistentQueue requires clientKey or apiKey"
84
+ );
85
+ }
86
+ if (ACTIVE_QUEUE_KEYS.has(authKey)) {
87
+ throw new SdkError(
88
+ "persistence_error",
89
+ "cannot purge a React Native persistent queue while its client is active"
90
+ );
91
+ }
92
+ const nativeStore = defaultNativeStore();
93
+ if (!nativeStore) {
94
+ throw new SdkError(
95
+ "configuration_error",
96
+ "React Native persistent queue requires the linked LogBrew native module"
97
+ );
98
+ }
99
+ let failure;
100
+ try {
101
+ requireStatus(
102
+ "purge",
103
+ callNative(nativeStore, "purgeEventRecords", authKey),
104
+ "purged"
105
+ );
106
+ } catch (error) {
107
+ failure = error;
108
+ }
109
+ try {
110
+ requireStatus(
111
+ "close",
112
+ callNative(nativeStore, "closeEventStore", authKey),
113
+ "closed"
114
+ );
115
+ } catch (error) {
116
+ failure ??= error;
117
+ }
118
+ if (failure) {
119
+ throw failure;
120
+ }
121
+ }
122
+
123
+ function createNativeEventStore(nativeStore, authKey, release) {
124
+ return {
125
+ load() {
126
+ const result = requireStatus(
127
+ "load",
128
+ callNative(nativeStore, "loadEventRecords", authKey),
129
+ "loaded"
130
+ );
131
+ if (!Array.isArray(result.records)) {
132
+ throw persistenceFailure("load");
133
+ }
134
+ return result.records.map((record) => {
135
+ if (!record
136
+ || Array.isArray(record)
137
+ || typeof record !== "object"
138
+ || typeof record.serializedEvent !== "string"
139
+ || !Number.isSafeInteger(record.eventBytes)
140
+ || record.eventBytes <= 0) {
141
+ throw persistenceFailure("load");
142
+ }
143
+ let event;
144
+ try {
145
+ event = JSON.parse(record.serializedEvent);
146
+ } catch {
147
+ throw persistenceFailure("load");
148
+ }
149
+ return {
150
+ event,
151
+ eventBytes: record.eventBytes,
152
+ serializedEvent: record.serializedEvent
153
+ };
154
+ });
155
+ },
156
+ append(record) {
157
+ requireStatus(
158
+ "append",
159
+ callNative(
160
+ nativeStore,
161
+ "appendEventRecord",
162
+ authKey,
163
+ record.serializedEvent,
164
+ record.eventBytes
165
+ ),
166
+ "appended"
167
+ );
168
+ },
169
+ acknowledge(count) {
170
+ requireStatus(
171
+ "acknowledge",
172
+ callNative(nativeStore, "acknowledgeEventRecords", authKey, count),
173
+ "acknowledged"
174
+ );
175
+ },
176
+ purge() {
177
+ requireStatus(
178
+ "purge",
179
+ callNative(nativeStore, "purgeEventRecords", authKey),
180
+ "purged"
181
+ );
182
+ },
183
+ close() {
184
+ try {
185
+ requireStatus(
186
+ "close",
187
+ callNative(nativeStore, "closeEventStore", authKey),
188
+ "closed"
189
+ );
190
+ } finally {
191
+ release();
192
+ }
193
+ }
194
+ };
195
+ }
196
+
197
+ function defaultNativeStore() {
198
+ try {
199
+ const nativeStore = TurboModuleRegistry?.get?.("LogBrewFatalStore")
200
+ ?? NativeModules?.LogBrewFatalStore;
201
+ return NATIVE_STORE_METHODS.every((method) => typeof nativeStore?.[method] === "function")
202
+ ? nativeStore
203
+ : undefined;
204
+ } catch {
205
+ return undefined;
206
+ }
207
+ }
208
+
209
+ function validateNativeQueueLimits({ maxQueueBytes, maxQueueSize }) {
210
+ if (maxQueueBytes !== undefined
211
+ && (!Number.isSafeInteger(maxQueueBytes)
212
+ || maxQueueBytes <= 0
213
+ || maxQueueBytes > MAX_NATIVE_QUEUE_BYTES)) {
214
+ throw new SdkError(
215
+ "configuration_error",
216
+ `persistent React Native maxQueueBytes must be at most ${MAX_NATIVE_QUEUE_BYTES}`
217
+ );
218
+ }
219
+ if (maxQueueSize !== undefined
220
+ && (!Number.isSafeInteger(maxQueueSize)
221
+ || maxQueueSize <= 0
222
+ || maxQueueSize > MAX_NATIVE_QUEUE_EVENTS)) {
223
+ throw new SdkError(
224
+ "configuration_error",
225
+ `persistent React Native maxQueueSize must be at most ${MAX_NATIVE_QUEUE_EVENTS}`
226
+ );
227
+ }
228
+ }
229
+
230
+ function validatePersistentQueueMode(mode) {
231
+ if (!PERSISTENT_QUEUE_MODES.has(mode)) {
232
+ throw new SdkError(
233
+ "configuration_error",
234
+ "persistentQueue must be auto, required, or disabled"
235
+ );
236
+ }
237
+ }
238
+
239
+ function callNative(nativeStore, method, ...args) {
240
+ if (typeof nativeStore?.[method] !== "function") {
241
+ throw persistenceFailure(methodName(method));
242
+ }
243
+ try {
244
+ return nativeStore[method](...args);
245
+ } catch {
246
+ throw persistenceFailure(methodName(method));
247
+ }
248
+ }
249
+
250
+ function requireStatus(operation, result, expected) {
251
+ if (!result
252
+ || Array.isArray(result)
253
+ || typeof result !== "object"
254
+ || result.status !== expected) {
255
+ throw persistenceFailure(operation);
256
+ }
257
+ return result;
258
+ }
259
+
260
+ function methodName(method) {
261
+ switch (method) {
262
+ case "loadEventRecords":
263
+ return "load";
264
+ case "appendEventRecord":
265
+ return "append";
266
+ case "acknowledgeEventRecords":
267
+ return "acknowledge";
268
+ case "purgeEventRecords":
269
+ return "purge";
270
+ case "closeEventStore":
271
+ return "close";
272
+ default:
273
+ return "operation";
274
+ }
275
+ }
276
+
277
+ function persistenceFailure(operation) {
278
+ return new SdkError(
279
+ "persistence_error",
280
+ `React Native persistent queue ${operation} failed`
281
+ );
282
+ }
@@ -223,7 +223,9 @@ function createEvent(mechanism) {
223
223
  delete error.stack;
224
224
  const event = createReactNativeErrorEvent(error, {
225
225
  id: nextEventId(),
226
- includeStack: false
226
+ handled: false,
227
+ includeStack: false,
228
+ mechanism
227
229
  });
228
230
  return {
229
231
  ...event,