@logbrew/react-native 0.1.11 → 0.1.13
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.
- package/LogBrewReactNative.podspec +25 -3
- package/README.md +106 -3
- package/apple-native-diagnostics.d.ts +52 -0
- package/apple-native-diagnostics.js +286 -0
- package/expo.cjs +46 -0
- package/expo.d.ts +16 -0
- package/expo.js +5 -0
- package/fatal-replay.cjs +12 -1
- package/global-errors.cjs +3 -1
- package/index.cjs +84 -2
- package/index.d.cts +4 -0
- package/index.d.ts +4 -0
- package/index.js +84 -2
- package/index.native.d.ts +4 -0
- package/index.native.js +14 -1
- package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.h +4 -0
- package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.mm +56 -0
- package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +293 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/AutomaticTelemetryContext.swift +72 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngine.swift +270 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineAutomatic.swift +331 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineDurable.swift +148 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineQueue.swift +343 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryLifecycle.swift +70 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStore.swift +258 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStoreRecovery.swift +330 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/EventEncoding.swift +114 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/IssueDiagnostics.swift +385 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LifecycleTrace.swift +138 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewClient.swift +358 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewLogger.swift +262 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewTrace.swift +357 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/Metadata.swift +86 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/NativeStackFrame.swift +27 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/ProductTimeline.swift +228 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/PublicTypes.swift +334 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContext.swift +197 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContextValidation.swift +337 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/TraceEvidence.swift +79 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/Transport.swift +204 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTrace.swift +277 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTracer.swift +151 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/Validation.swift +165 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashEngine.swift +125 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashReportSanitizer.swift +139 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashStorageDirectory.swift +124 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeArtifactIdentity.swift +137 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCapture.swift +229 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashPublic.swift +386 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashReplay.swift +317 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangIncidentStore.swift +321 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdog.swift +386 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogContracts.swift +84 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogSystem.swift +139 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeMainThreadStackCapture.swift +84 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeStackFrameSanitizer.swift +170 -0
- package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +43 -0
- package/package.json +43 -5
- package/promise-rejections.cjs +3 -1
- package/scripts/sync-apple-native-sources.mjs +93 -0
- package/src/NativeLogBrewAppleDiagnostics.ts +12 -0
|
@@ -12,11 +12,33 @@ Pod::Spec.new do |spec|
|
|
|
12
12
|
spec.platforms = { :ios => "13.0" }
|
|
13
13
|
spec.source = {
|
|
14
14
|
:git => "https://github.com/LogBrewCo/sdk.git",
|
|
15
|
-
:tag => "react-native
|
|
15
|
+
:tag => "js/logbrew-react-native/v#{spec.version}"
|
|
16
16
|
}
|
|
17
|
-
spec.
|
|
18
|
-
spec.
|
|
17
|
+
spec.default_subspecs = "Core"
|
|
18
|
+
spec.swift_versions = ["5.0"]
|
|
19
19
|
spec.dependency "React-Core"
|
|
20
20
|
|
|
21
|
+
spec.subspec "Core" do |core|
|
|
22
|
+
core.source_files = "ios/**/*.{h,m,mm}"
|
|
23
|
+
core.exclude_files = [
|
|
24
|
+
"ios/AppleDiagnostics/**/*",
|
|
25
|
+
"ios/Tests/**/*"
|
|
26
|
+
]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
spec.subspec "AppleNativeDiagnostics" do |diagnostics|
|
|
30
|
+
diagnostics.ios.deployment_target = "15.0"
|
|
31
|
+
diagnostics.source_files = [
|
|
32
|
+
"ios/AppleDiagnostics/**/*.{h,m,mm,swift}",
|
|
33
|
+
"ios/GeneratedAppleDiagnostics/**/*.swift"
|
|
34
|
+
]
|
|
35
|
+
diagnostics.dependency "#{spec.name}/Core"
|
|
36
|
+
diagnostics.dependency "KSCrash/Recording", "2.5.1"
|
|
37
|
+
diagnostics.pod_target_xcconfig = {
|
|
38
|
+
"DEFINES_MODULE" => "YES",
|
|
39
|
+
"SWIFT_VERSION" => "5.0"
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
21
43
|
install_modules_dependencies(spec) if respond_to?(:install_modules_dependencies, true)
|
|
22
44
|
end
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
React Native helpers for the public LogBrew JavaScript SDK.
|
|
8
8
|
|
|
9
|
-
This package
|
|
9
|
+
This package keeps event validation, retry, flush, and shutdown behavior in `@logbrew/sdk`. It adds mobile helpers for screen views, app-state changes, product actions, API milestones, bounded issue breadcrumbs, typed handled and unhandled JavaScript errors, Promise rejection tracking, provider and hook usage, W3C trace correlation and propagation, lifecycle spans, resource spans, reversible instrumentation, and opt-in Apple native crash and app-hang diagnostics.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
@@ -227,7 +227,7 @@ withLogBrewTrace(trace, () => {
|
|
|
227
227
|
|
|
228
228
|
## Error Capture
|
|
229
229
|
|
|
230
|
-
Use `captureReactNativeError()` in app-owned error boundaries, route handlers, async catch blocks, or global handlers. It records
|
|
230
|
+
Use `captureReactNativeError()` in app-owned error boundaries, route handlers, async catch blocks, or global handlers. It records a typed exception with `react-native.error`, `handled: true`, prior screen/app-state/action/network breadcrumbs, and React Native context. Global errors and Promise rejections use their own mechanism with `handled: false`. Raw stack text stays omitted by default:
|
|
231
231
|
|
|
232
232
|
```js
|
|
233
233
|
import { captureReactNativeError } from "@logbrew/react-native";
|
|
@@ -271,6 +271,100 @@ The React Native conditional export obtains LogBrew's synchronous native fatal s
|
|
|
271
271
|
|
|
272
272
|
Automatic events exclude the original error message, raw stack, arbitrary metadata, full URLs, hosts, query strings, local absolute paths, payloads, and native error text. `onDiagnostic` receives only a fixed code. This error-handler integration does not claim mathematically exactly-once delivery, native crash capture, ANR or hang detection, general offline queueing by the fatal-record slot, or symbolication. The client-level persistent queue above owns normal event restart delivery.
|
|
273
273
|
|
|
274
|
+
### Apple native crash and app-hang diagnostics
|
|
275
|
+
|
|
276
|
+
Apple native diagnostics are an iOS-only, opt-in build feature. They capture a
|
|
277
|
+
fatal native process crash and replay one privacy-bounded issue after the next
|
|
278
|
+
launch. Set `hangThresholdSeconds` to also capture recovered or ongoing UIKit
|
|
279
|
+
main-thread hangs. Android native crash and ANR capture are not included.
|
|
280
|
+
|
|
281
|
+
For Expo prebuild or development builds, add the config plugin:
|
|
282
|
+
|
|
283
|
+
```json
|
|
284
|
+
{
|
|
285
|
+
"expo": {
|
|
286
|
+
"plugins": ["@logbrew/react-native/expo"]
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Then run your normal Expo prebuild or EAS build. Expo Go cannot load this native
|
|
292
|
+
module. For a bare React Native app, add the optional subspec inside the app
|
|
293
|
+
target in `ios/Podfile`, then run `pod install`:
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
logbrew_react_native_path = File.dirname(`node --print "require.resolve('@logbrew/react-native')"`)
|
|
297
|
+
pod 'LogBrewReactNative/AppleNativeDiagnostics', :path => logbrew_react_native_path
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
The default `LogBrewReactNative/Core` subspec remains unchanged and supports
|
|
301
|
+
iOS 13. `AppleNativeDiagnostics` requires iOS 15. Install it on the main thread
|
|
302
|
+
before root registration, then start replay:
|
|
303
|
+
|
|
304
|
+
```js
|
|
305
|
+
import {
|
|
306
|
+
installLogBrewAppleNativeDiagnostics,
|
|
307
|
+
replayLogBrewAppleNativeDiagnostics
|
|
308
|
+
} from "@logbrew/react-native/apple-native-diagnostics";
|
|
309
|
+
|
|
310
|
+
const nativeStatus = installLogBrewAppleNativeDiagnostics({
|
|
311
|
+
clientKey: process.env.EXPO_PUBLIC_LOGBREW_CLIENT_KEY,
|
|
312
|
+
projectId: "550e8400-e29b-41d4-a716-446655440000",
|
|
313
|
+
release: "com.example.app@1.2.3+45",
|
|
314
|
+
environment: "production",
|
|
315
|
+
service: "ios-app",
|
|
316
|
+
fatalHandlerOwnership: "logbrew",
|
|
317
|
+
hangThresholdSeconds: 2
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
void replayLogBrewAppleNativeDiagnostics().catch((error) => {
|
|
321
|
+
console.warn(error?.code ?? "native_diagnostics_failed");
|
|
322
|
+
});
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
`fatalHandlerOwnership: "logbrew"` is an explicit process-lifetime ownership
|
|
326
|
+
claim. Do not install these diagnostics while Sentry or another integration
|
|
327
|
+
owns the native fatal signal or Mach exception handlers. Linking both SDKs is
|
|
328
|
+
allowed, but only one integration may install native fatal capture in a given
|
|
329
|
+
process. LogBrew cannot transfer or remove that ownership before process
|
|
330
|
+
restart.
|
|
331
|
+
|
|
332
|
+
Installation creates app-private, data-protected storage that iOS excludes from
|
|
333
|
+
device data archives. Pending reports are partitioned by project, so changing
|
|
334
|
+
the configured project does not replay an earlier project's records with the
|
|
335
|
+
new project's key. The raw local KSCrash report can still contain sensitive
|
|
336
|
+
process details, so apply the app's consent and retention policy to it. Hosted
|
|
337
|
+
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
|
|
339
|
+
counters, not keys, paths, raw reports, or native error text. A custom endpoint
|
|
340
|
+
must be a plain HTTPS path without embedded authentication, a query, or a fragment.
|
|
341
|
+
|
|
342
|
+
Use the same project, release, environment, and service values for the runtime
|
|
343
|
+
and its Apple dSYM. After signing in with `logbrew login`, upload and inspect
|
|
344
|
+
the exact debug object with the released CLI:
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
logbrew debug-artifacts upload ios/build/MyApp.app.dSYM \
|
|
348
|
+
--project 550e8400-e29b-41d4-a716-446655440000 \
|
|
349
|
+
--release com.example.app@1.2.3+45 \
|
|
350
|
+
--environment production \
|
|
351
|
+
--service ios-app \
|
|
352
|
+
--json
|
|
353
|
+
|
|
354
|
+
logbrew debug-artifacts lookup \
|
|
355
|
+
--project 550e8400-e29b-41d4-a716-446655440000 \
|
|
356
|
+
--release com.example.app@1.2.3+45 \
|
|
357
|
+
--environment production \
|
|
358
|
+
--service ios-app \
|
|
359
|
+
--image-uuid <mach_o_uuid> \
|
|
360
|
+
--architecture arm64 \
|
|
361
|
+
--json
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
The SDK does not upload debug objects during a build or at runtime. Hosted
|
|
365
|
+
symbolication begins only after the exact Mach-O UUID and architecture lookup
|
|
366
|
+
succeeds.
|
|
367
|
+
|
|
274
368
|
### Opt-in Hermes Promise rejection tracking
|
|
275
369
|
|
|
276
370
|
React Native exposes one Promise rejection tracker slot. Claim it explicitly
|
|
@@ -454,6 +548,8 @@ export function App({ client }) {
|
|
|
454
548
|
}
|
|
455
549
|
```
|
|
456
550
|
|
|
551
|
+
Screen views carry the versioned `screen_view` analytics classification, and explicit product actions carry `interaction`. The SDK uses the app-owned screen name as a bounded surface and does not inspect view hierarchies, selectors, or input values. Caller metadata cannot replace the reserved classification. See the repository [product analytics capture contract](../../docs/product-analytics-contract.md).
|
|
552
|
+
|
|
457
553
|
The package ships a `react-native` entry that imports `AppState` and `Platform` for Metro, while the default Node entry accepts those dependencies explicitly. That keeps mobile setup explicit instead of pretending a Node process is a native runtime.
|
|
458
554
|
|
|
459
555
|
## Trace Propagation
|
|
@@ -781,7 +877,14 @@ uploadLogBrewReactNativeReleaseArtifacts({
|
|
|
781
877
|
|
|
782
878
|
The helper requires explicit `release`, `environment`, `service`, and `platform` metadata. Hosted uploads also require a UUID `projectId`; local preparation and loopback upload remain valid without it. It defaults minified bundle URLs to `app:///react-native/<platform>/...`, removes query strings and hashes from manifest URLs, and strips source paths under `root` or `stripSourcePrefix`. Hosted endpoints must use HTTPS and must not include embedded auth values, query strings, or fragments. The helper never uses normal SDK ingest keys or account/session API auth values. When `sourcemap` points at a final Hermes-composed map, the helper makes the bundle's `sourceMappingURL` point at that explicit map, so stale packager-map comments do not block manifest generation. The explicit Metro wrapper changes only app-owned serialization and one bounded runtime Debug-ID registry; neither helper patches Gradle, Xcode, global fetch/XHR, request payloads, or transport behavior.
|
|
783
879
|
|
|
784
|
-
React Native native symbols are
|
|
880
|
+
React Native native symbols are release artifacts, not runtime telemetry. Use
|
|
881
|
+
the repository release-artifact tooling for local dry-run checks of app-owned
|
|
882
|
+
outputs such as `ios/build/.../*.dSYM`, Android mapping files, and native shared
|
|
883
|
+
objects. Apple dSYM upload, storage, exact UUID and architecture lookup, and
|
|
884
|
+
hosted symbolication are available through `logbrew debug-artifacts`. The SDK
|
|
885
|
+
does not upload those artifacts automatically. Normal JavaScript error capture
|
|
886
|
+
does not produce native crash symbolication; use the opt-in Apple diagnostics
|
|
887
|
+
flow above with an exact uploaded dSYM.
|
|
785
888
|
|
|
786
889
|
## Example Source
|
|
787
890
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
type LogBrewAppleNativeDiagnosticsAuth =
|
|
2
|
+
| { apiKey: string; clientKey?: never }
|
|
3
|
+
| { clientKey: string; apiKey?: never };
|
|
4
|
+
|
|
5
|
+
export type LogBrewAppleNativeDiagnosticsConfiguration =
|
|
6
|
+
LogBrewAppleNativeDiagnosticsAuth & {
|
|
7
|
+
projectId: string;
|
|
8
|
+
release: string;
|
|
9
|
+
environment: string;
|
|
10
|
+
service: string;
|
|
11
|
+
/** Explicitly confirms LogBrew is the process's only native fatal-handler owner. */
|
|
12
|
+
fatalHandlerOwnership: "logbrew";
|
|
13
|
+
/** Enable app-hang capture with a threshold from 1 through 30 seconds. Omit to disable. */
|
|
14
|
+
hangThresholdSeconds?: number;
|
|
15
|
+
/** Event ingestion endpoint. Defaults to the hosted LogBrew endpoint. */
|
|
16
|
+
endpoint?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type LogBrewAppleNativeDiagnosticsStatus = Readonly<{
|
|
20
|
+
status: "already_installed" | "installed" | "not_installed" | "ready";
|
|
21
|
+
lifecycle: "failed" | "idle" | "installed" | "replaying" | "stopped" | "unknown";
|
|
22
|
+
pending: number;
|
|
23
|
+
acknowledged: number;
|
|
24
|
+
discarded: number;
|
|
25
|
+
}>;
|
|
26
|
+
|
|
27
|
+
export type LogBrewAppleNativeDiagnosticsReplayResult = Readonly<{
|
|
28
|
+
status: "replayed";
|
|
29
|
+
attempted: number;
|
|
30
|
+
acknowledged: number;
|
|
31
|
+
discarded: number;
|
|
32
|
+
pending: number;
|
|
33
|
+
}>;
|
|
34
|
+
|
|
35
|
+
export declare function installLogBrewAppleNativeDiagnostics(
|
|
36
|
+
configuration: LogBrewAppleNativeDiagnosticsConfiguration
|
|
37
|
+
): LogBrewAppleNativeDiagnosticsStatus;
|
|
38
|
+
|
|
39
|
+
export declare function replayLogBrewAppleNativeDiagnostics(): Promise<
|
|
40
|
+
LogBrewAppleNativeDiagnosticsReplayResult
|
|
41
|
+
>;
|
|
42
|
+
|
|
43
|
+
export declare function getLogBrewAppleNativeDiagnosticsStatus():
|
|
44
|
+
LogBrewAppleNativeDiagnosticsStatus;
|
|
45
|
+
|
|
46
|
+
declare const defaultExport: {
|
|
47
|
+
getLogBrewAppleNativeDiagnosticsStatus: typeof getLogBrewAppleNativeDiagnosticsStatus;
|
|
48
|
+
installLogBrewAppleNativeDiagnostics: typeof installLogBrewAppleNativeDiagnostics;
|
|
49
|
+
replayLogBrewAppleNativeDiagnostics: typeof replayLogBrewAppleNativeDiagnostics;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default defaultExport;
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { SdkError } from "@logbrew/sdk";
|
|
2
|
+
import { NativeModules, Platform, TurboModuleRegistry } from "react-native";
|
|
3
|
+
|
|
4
|
+
const INSTALL_STATUSES = new Set(["already_installed", "installed"]);
|
|
5
|
+
const LIFECYCLE_STATUSES = new Set([
|
|
6
|
+
"failed",
|
|
7
|
+
"idle",
|
|
8
|
+
"installed",
|
|
9
|
+
"replaying",
|
|
10
|
+
"stopped",
|
|
11
|
+
"unknown"
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export function installLogBrewAppleNativeDiagnostics(configuration = {}) {
|
|
15
|
+
requireApplePlatform();
|
|
16
|
+
const nativeModule = requireNativeModule();
|
|
17
|
+
const payload = normalizeConfiguration(configuration);
|
|
18
|
+
const result = callNative(nativeModule, "installNativeDiagnostics", payload);
|
|
19
|
+
return requireStatusResult("install", result, INSTALL_STATUSES);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function replayLogBrewAppleNativeDiagnostics() {
|
|
23
|
+
requireApplePlatform();
|
|
24
|
+
const nativeModule = requireNativeModule();
|
|
25
|
+
let result;
|
|
26
|
+
try {
|
|
27
|
+
result = await callNative(nativeModule, "replayNativeDiagnostics");
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error instanceof SdkError) {
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
throw new SdkError(
|
|
33
|
+
"native_diagnostics_failed",
|
|
34
|
+
"LogBrew Apple native diagnostics replay failed"
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return requireReplayResult(result);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getLogBrewAppleNativeDiagnosticsStatus() {
|
|
41
|
+
requireApplePlatform();
|
|
42
|
+
const nativeModule = requireNativeModule();
|
|
43
|
+
const result = callNative(nativeModule, "nativeDiagnosticsStatus");
|
|
44
|
+
return requireStatusResult("status", result, new Set(["not_installed", "ready"]));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeConfiguration(configuration) {
|
|
48
|
+
if (!configuration
|
|
49
|
+
|| Array.isArray(configuration)
|
|
50
|
+
|| typeof configuration !== "object") {
|
|
51
|
+
throw configurationError("configuration must be an object");
|
|
52
|
+
}
|
|
53
|
+
const allowedKeys = new Set([
|
|
54
|
+
"apiKey",
|
|
55
|
+
"clientKey",
|
|
56
|
+
"endpoint",
|
|
57
|
+
"environment",
|
|
58
|
+
"fatalHandlerOwnership",
|
|
59
|
+
"hangThresholdSeconds",
|
|
60
|
+
"projectId",
|
|
61
|
+
"release",
|
|
62
|
+
"service"
|
|
63
|
+
]);
|
|
64
|
+
for (const key of Object.keys(configuration)) {
|
|
65
|
+
if (!allowedKeys.has(key)) {
|
|
66
|
+
throw configurationError(`configuration contains unsupported key ${JSON.stringify(key)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (Object.prototype.hasOwnProperty.call(configuration, "apiKey")
|
|
70
|
+
&& Object.prototype.hasOwnProperty.call(configuration, "clientKey")) {
|
|
71
|
+
throw configurationError("apiKey and clientKey are mutually exclusive");
|
|
72
|
+
}
|
|
73
|
+
const authKey = exactString(
|
|
74
|
+
"apiKey or clientKey",
|
|
75
|
+
configuration.clientKey ?? configuration.apiKey,
|
|
76
|
+
4096
|
|
77
|
+
);
|
|
78
|
+
const projectId = exactString("projectId", configuration.projectId, 64);
|
|
79
|
+
if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/u.test(projectId)) {
|
|
80
|
+
throw configurationError("projectId must be a lowercase UUID");
|
|
81
|
+
}
|
|
82
|
+
if (configuration.fatalHandlerOwnership !== "logbrew") {
|
|
83
|
+
throw configurationError(
|
|
84
|
+
"fatalHandlerOwnership must be the exact string logbrew after confirming no other native fatal handler is installed"
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const payload = {
|
|
89
|
+
apiKey: authKey,
|
|
90
|
+
environment: exactString("environment", configuration.environment),
|
|
91
|
+
fatalHandlerOwnership: "logbrew",
|
|
92
|
+
projectId,
|
|
93
|
+
release: exactString("release", configuration.release),
|
|
94
|
+
service: exactString("service", configuration.service)
|
|
95
|
+
};
|
|
96
|
+
if (configuration.endpoint !== undefined) {
|
|
97
|
+
payload.endpoint = exactDeliveryEndpoint(configuration.endpoint);
|
|
98
|
+
}
|
|
99
|
+
if (configuration.hangThresholdSeconds !== undefined) {
|
|
100
|
+
const threshold = configuration.hangThresholdSeconds;
|
|
101
|
+
if (typeof threshold !== "number"
|
|
102
|
+
|| !Number.isFinite(threshold)
|
|
103
|
+
|| threshold < 1
|
|
104
|
+
|| threshold > 30) {
|
|
105
|
+
throw configurationError("hangThresholdSeconds must be a finite number from 1 through 30");
|
|
106
|
+
}
|
|
107
|
+
payload.hangThresholdSeconds = threshold;
|
|
108
|
+
}
|
|
109
|
+
return payload;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function requireApplePlatform() {
|
|
113
|
+
if (Platform?.OS !== "ios") {
|
|
114
|
+
throw new SdkError(
|
|
115
|
+
"unsupported_platform",
|
|
116
|
+
"LogBrew Apple native diagnostics are available only in an iOS native build"
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function requireNativeModule() {
|
|
122
|
+
let nativeModule;
|
|
123
|
+
try {
|
|
124
|
+
nativeModule = TurboModuleRegistry?.get?.("LogBrewAppleDiagnostics")
|
|
125
|
+
?? NativeModules?.LogBrewAppleDiagnostics;
|
|
126
|
+
} catch {
|
|
127
|
+
nativeModule = undefined;
|
|
128
|
+
}
|
|
129
|
+
if (!nativeModule) {
|
|
130
|
+
throw new SdkError(
|
|
131
|
+
"native_diagnostics_unavailable",
|
|
132
|
+
"LogBrew Apple native diagnostics require the AppleDiagnostics CocoaPods subspec; Expo projects can add @logbrew/react-native/expo"
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return nativeModule;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function callNative(nativeModule, method, ...args) {
|
|
139
|
+
if (typeof nativeModule?.[method] !== "function") {
|
|
140
|
+
throw new SdkError(
|
|
141
|
+
"native_diagnostics_unavailable",
|
|
142
|
+
`linked LogBrew Apple diagnostics do not implement ${method}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
return nativeModule[method](...args);
|
|
147
|
+
} catch {
|
|
148
|
+
throw new SdkError(
|
|
149
|
+
"native_diagnostics_failed",
|
|
150
|
+
`LogBrew Apple native diagnostics ${method} failed`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function requireStatusResult(operation, result, allowedStatuses) {
|
|
156
|
+
if (isErrorResult(result)) {
|
|
157
|
+
throw new SdkError(
|
|
158
|
+
result.code,
|
|
159
|
+
`LogBrew Apple native diagnostics ${operation} failed with ${result.code}`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (!isPlainObject(result)
|
|
163
|
+
|| !allowedStatuses.has(result.status)
|
|
164
|
+
|| !LIFECYCLE_STATUSES.has(result.lifecycle)
|
|
165
|
+
|| !isCount(result.pending)
|
|
166
|
+
|| !isCount(result.acknowledged)
|
|
167
|
+
|| !isCount(result.discarded)) {
|
|
168
|
+
throw new SdkError(
|
|
169
|
+
"native_diagnostics_invalid_response",
|
|
170
|
+
`LogBrew Apple native diagnostics ${operation} returned an invalid response`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return Object.freeze({
|
|
174
|
+
acknowledged: result.acknowledged,
|
|
175
|
+
discarded: result.discarded,
|
|
176
|
+
lifecycle: result.lifecycle,
|
|
177
|
+
pending: result.pending,
|
|
178
|
+
status: result.status
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function requireReplayResult(result) {
|
|
183
|
+
if (isErrorResult(result)) {
|
|
184
|
+
throw new SdkError(
|
|
185
|
+
result.code,
|
|
186
|
+
`LogBrew Apple native diagnostics replay failed with ${result.code}`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (!isPlainObject(result)
|
|
190
|
+
|| result.status !== "replayed"
|
|
191
|
+
|| !isCount(result.attempted)
|
|
192
|
+
|| !isCount(result.acknowledged)
|
|
193
|
+
|| !isCount(result.discarded)
|
|
194
|
+
|| !isCount(result.pending)
|
|
195
|
+
|| result.acknowledged + result.discarded > result.attempted) {
|
|
196
|
+
throw new SdkError(
|
|
197
|
+
"native_diagnostics_invalid_response",
|
|
198
|
+
"LogBrew Apple native diagnostics replay returned an invalid response"
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return Object.freeze({
|
|
202
|
+
acknowledged: result.acknowledged,
|
|
203
|
+
attempted: result.attempted,
|
|
204
|
+
discarded: result.discarded,
|
|
205
|
+
pending: result.pending,
|
|
206
|
+
status: result.status
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function isErrorResult(result) {
|
|
211
|
+
return isPlainObject(result)
|
|
212
|
+
&& result.status === "error"
|
|
213
|
+
&& typeof result.code === "string"
|
|
214
|
+
&& /^[a-z0-9_]{1,128}$/u.test(result.code);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isPlainObject(value) {
|
|
218
|
+
return value !== null && !Array.isArray(value) && typeof value === "object";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function isCount(value) {
|
|
222
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function exactString(name, value, maxBytes = 256) {
|
|
226
|
+
if (typeof value !== "string"
|
|
227
|
+
|| value.length === 0
|
|
228
|
+
|| value.trim() !== value
|
|
229
|
+
|| hasControlCharacter(value)
|
|
230
|
+
|| utf8Length(value) > maxBytes) {
|
|
231
|
+
throw configurationError(`${name} must be a non-empty bounded string without surrounding whitespace or control characters`);
|
|
232
|
+
}
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function exactDeliveryEndpoint(value) {
|
|
237
|
+
const endpoint = exactString("endpoint", value, 2048);
|
|
238
|
+
let parsed;
|
|
239
|
+
try {
|
|
240
|
+
parsed = new URL(endpoint);
|
|
241
|
+
} catch {
|
|
242
|
+
throw configurationError("endpoint must be an absolute HTTPS URL");
|
|
243
|
+
}
|
|
244
|
+
if (parsed.protocol !== "https:") {
|
|
245
|
+
throw configurationError("endpoint must use HTTPS");
|
|
246
|
+
}
|
|
247
|
+
if (parsed.username
|
|
248
|
+
|| parsed.password
|
|
249
|
+
|| parsed.search
|
|
250
|
+
|| parsed.hash
|
|
251
|
+
|| endpoint !== `${parsed.origin}${parsed.pathname}`) {
|
|
252
|
+
throw configurationError("endpoint must be a plain HTTPS path without embedded auth, query, or fragment values");
|
|
253
|
+
}
|
|
254
|
+
return endpoint;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function hasControlCharacter(value) {
|
|
258
|
+
for (const scalar of value) {
|
|
259
|
+
const codePoint = scalar.codePointAt(0);
|
|
260
|
+
if (codePoint <= 0x1f || codePoint === 0x7f) {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function utf8Length(value) {
|
|
268
|
+
let bytes = 0;
|
|
269
|
+
for (const scalar of value) {
|
|
270
|
+
const codePoint = scalar.codePointAt(0);
|
|
271
|
+
bytes += codePoint <= 0x7f ? 1 : codePoint <= 0x7ff ? 2 : codePoint <= 0xffff ? 3 : 4;
|
|
272
|
+
}
|
|
273
|
+
return bytes;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function configurationError(message) {
|
|
277
|
+
return new SdkError("configuration_error", `LogBrew Apple native diagnostics ${message}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const logBrewAppleNativeDiagnostics = {
|
|
281
|
+
getLogBrewAppleNativeDiagnosticsStatus,
|
|
282
|
+
installLogBrewAppleNativeDiagnostics,
|
|
283
|
+
replayLogBrewAppleNativeDiagnostics
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export default logBrewAppleNativeDiagnostics;
|
package/expo.cjs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const PLUGIN_NAME = "@logbrew/react-native/expo";
|
|
2
|
+
const START_MARKER = " # @generated by @logbrew/react-native/expo";
|
|
3
|
+
const END_MARKER = " # @end @logbrew/react-native/expo";
|
|
4
|
+
const POD_BLOCK = [
|
|
5
|
+
START_MARKER,
|
|
6
|
+
" logbrew_react_native_path = File.dirname(`node --print \"require.resolve('@logbrew/react-native')\"`)",
|
|
7
|
+
" pod 'LogBrewReactNative/AppleNativeDiagnostics', :path => logbrew_react_native_path",
|
|
8
|
+
END_MARKER
|
|
9
|
+
].join("\n");
|
|
10
|
+
|
|
11
|
+
function withLogBrewReactNative(config, options = {}) {
|
|
12
|
+
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
|
+
);
|
|
18
|
+
return podfileConfig;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function modifyPodfile(contents, enabled = true) {
|
|
23
|
+
if (typeof contents !== "string") {
|
|
24
|
+
throw new TypeError(`${PLUGIN_NAME} expected Podfile text`);
|
|
25
|
+
}
|
|
26
|
+
const withoutManagedBlock = contents.replace(
|
|
27
|
+
new RegExp(`${escapePattern(START_MARKER)}[\\s\\S]*?${escapePattern(END_MARKER)}\\n?`, "gu"),
|
|
28
|
+
""
|
|
29
|
+
);
|
|
30
|
+
if (!enabled) {
|
|
31
|
+
return withoutManagedBlock;
|
|
32
|
+
}
|
|
33
|
+
const targetPattern = /^(target\s+['"][^'"]+['"]\s+do\s*)$/mu;
|
|
34
|
+
if (!targetPattern.test(withoutManagedBlock)) {
|
|
35
|
+
throw new Error(`${PLUGIN_NAME} could not locate an iOS application target in the Podfile`);
|
|
36
|
+
}
|
|
37
|
+
return withoutManagedBlock.replace(targetPattern, `$1\n${POD_BLOCK}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function escapePattern(value) {
|
|
41
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = withLogBrewReactNative;
|
|
45
|
+
module.exports.modifyPodfile = modifyPodfile;
|
|
46
|
+
module.exports.withLogBrewReactNative = withLogBrewReactNative;
|
package/expo.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type LogBrewExpoPluginOptions = {
|
|
2
|
+
/** Add the opt-in Apple native crash and app-hang CocoaPods subspec. Defaults to true. */
|
|
3
|
+
appleNativeDiagnostics?: boolean;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type ExpoConfigLike = Record<string, unknown>;
|
|
7
|
+
export type LogBrewExpoConfigPlugin = (
|
|
8
|
+
config: ExpoConfigLike,
|
|
9
|
+
options?: LogBrewExpoPluginOptions
|
|
10
|
+
) => ExpoConfigLike;
|
|
11
|
+
|
|
12
|
+
export declare const withLogBrewReactNative: LogBrewExpoConfigPlugin;
|
|
13
|
+
export declare function modifyPodfile(contents: string, enabled?: boolean): string;
|
|
14
|
+
|
|
15
|
+
declare const defaultExport: LogBrewExpoConfigPlugin;
|
|
16
|
+
export default defaultExport;
|
package/expo.js
ADDED
package/fatal-replay.cjs
CHANGED
|
@@ -293,7 +293,11 @@ function fatalStackFrames(error, stackFrame) {
|
|
|
293
293
|
const frame = stackFrame(line);
|
|
294
294
|
const filename = frame ? normalizedStoredFatalFilename(frame.filename) : undefined;
|
|
295
295
|
if (filename && validFatalFilename(filename)) {
|
|
296
|
-
frames.push(Object.freeze({
|
|
296
|
+
frames.push(Object.freeze({
|
|
297
|
+
column: frame.column,
|
|
298
|
+
filename,
|
|
299
|
+
line: frame.line
|
|
300
|
+
}));
|
|
297
301
|
if (frames.length === MAX_FATAL_STACK_FRAMES) {
|
|
298
302
|
break;
|
|
299
303
|
}
|
|
@@ -304,6 +308,13 @@ function fatalStackFrames(error, stackFrame) {
|
|
|
304
308
|
|
|
305
309
|
function fatalEventAttributes(record, eventMessage) {
|
|
306
310
|
return Object.freeze({
|
|
311
|
+
exception: Object.freeze({
|
|
312
|
+
type: record.errorName,
|
|
313
|
+
mechanism: Object.freeze({
|
|
314
|
+
type: "react_native_error_utils",
|
|
315
|
+
handled: false
|
|
316
|
+
})
|
|
317
|
+
}),
|
|
307
318
|
level: "fatal",
|
|
308
319
|
message: eventMessage,
|
|
309
320
|
metadata: Object.freeze({
|
package/global-errors.cjs
CHANGED
|
@@ -196,7 +196,9 @@ function createAutomaticErrorEvent(error) {
|
|
|
196
196
|
}
|
|
197
197
|
const event = createReactNativeErrorEvent(sanitizedError, {
|
|
198
198
|
id: nextEventId(),
|
|
199
|
-
|
|
199
|
+
handled: false,
|
|
200
|
+
includeStack: false,
|
|
201
|
+
mechanism: "react_native_error_utils"
|
|
200
202
|
});
|
|
201
203
|
return {
|
|
202
204
|
...event,
|