react-native-nitro-logger 0.1.0
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/LICENSE +20 -0
- package/NitroLogger.podspec +29 -0
- package/README.md +214 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +124 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +11 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/AndroidPlatformIo.kt +100 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/HybridFileSink.kt +292 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/HybridNativeConsoleSink.kt +23 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogFileWriter.kt +1328 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogRotationPolicy.kt +186 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogSecureFile.kt +80 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogWriterRegistry.kt +420 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/NativeConsoleWriter.kt +237 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/NitroLoggerPackage.kt +22 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/PlatformIo.kt +165 -0
- package/docs/PARITY.md +330 -0
- package/docs/PRIVACY.md +341 -0
- package/eslint-plugin/README.md +238 -0
- package/eslint-plugin/index.js +62 -0
- package/eslint-plugin/rules/literal-subsystem.js +102 -0
- package/eslint-plugin/rules/no-computed-metadata-key.js +174 -0
- package/eslint-plugin/rules/no-derived-correlation.js +171 -0
- package/eslint-plugin/rules/no-dynamic-message.js +60 -0
- package/eslint-plugin/shared.js +1948 -0
- package/ios/Gzip.swift +203 -0
- package/ios/HybridFileSink.swift +291 -0
- package/ios/HybridNativeConsoleSink.swift +20 -0
- package/ios/LogFileWriter.swift +1280 -0
- package/ios/LogSecureFile.swift +296 -0
- package/ios/LogWriterRegistry.swift +444 -0
- package/ios/NativeConsoleWriter.swift +180 -0
- package/lib/module/Logger.js +381 -0
- package/lib/module/Logger.js.map +1 -0
- package/lib/module/ScopedLogger.js +70 -0
- package/lib/module/ScopedLogger.js.map +1 -0
- package/lib/module/config.js +23 -0
- package/lib/module/config.js.map +1 -0
- package/lib/module/destinations/Batcher.js +647 -0
- package/lib/module/destinations/Batcher.js.map +1 -0
- package/lib/module/destinations/ConsoleDestination.js +51 -0
- package/lib/module/destinations/ConsoleDestination.js.map +1 -0
- package/lib/module/destinations/FileDestination.js +360 -0
- package/lib/module/destinations/FileDestination.js.map +1 -0
- package/lib/module/destinations/NativeConsoleDestination.js +170 -0
- package/lib/module/destinations/NativeConsoleDestination.js.map +1 -0
- package/lib/module/destinations/types.js +4 -0
- package/lib/module/destinations/types.js.map +1 -0
- package/lib/module/formatters/DefaultFormatter.js +136 -0
- package/lib/module/formatters/DefaultFormatter.js.map +1 -0
- package/lib/module/formatters/JsonLinesFormatter.js +162 -0
- package/lib/module/formatters/JsonLinesFormatter.js.map +1 -0
- package/lib/module/formatters/timestamp.js +15 -0
- package/lib/module/formatters/timestamp.js.map +1 -0
- package/lib/module/formatters/types.js +4 -0
- package/lib/module/formatters/types.js.map +1 -0
- package/lib/module/index.js +31 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/integrations/appState.js +81 -0
- package/lib/module/integrations/appState.js.map +1 -0
- package/lib/module/integrations/errorHandler.js +166 -0
- package/lib/module/integrations/errorHandler.js.map +1 -0
- package/lib/module/integrations/sanitizeError.js +238 -0
- package/lib/module/integrations/sanitizeError.js.map +1 -0
- package/lib/module/levels.js +35 -0
- package/lib/module/levels.js.map +1 -0
- package/lib/module/metadata.js +52 -0
- package/lib/module/metadata.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/privacy.js +349 -0
- package/lib/module/privacy.js.map +1 -0
- package/lib/module/specs/FileSink.nitro.js +4 -0
- package/lib/module/specs/FileSink.nitro.js.map +1 -0
- package/lib/module/specs/NativeConsoleSink.nitro.js +4 -0
- package/lib/module/specs/NativeConsoleSink.nitro.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utf8.js +42 -0
- package/lib/module/utf8.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/Logger.d.ts +122 -0
- package/lib/typescript/src/Logger.d.ts.map +1 -0
- package/lib/typescript/src/ScopedLogger.d.ts +36 -0
- package/lib/typescript/src/ScopedLogger.d.ts.map +1 -0
- package/lib/typescript/src/config.d.ts +12 -0
- package/lib/typescript/src/config.d.ts.map +1 -0
- package/lib/typescript/src/destinations/Batcher.d.ts +244 -0
- package/lib/typescript/src/destinations/Batcher.d.ts.map +1 -0
- package/lib/typescript/src/destinations/ConsoleDestination.d.ts +27 -0
- package/lib/typescript/src/destinations/ConsoleDestination.d.ts.map +1 -0
- package/lib/typescript/src/destinations/FileDestination.d.ts +200 -0
- package/lib/typescript/src/destinations/FileDestination.d.ts.map +1 -0
- package/lib/typescript/src/destinations/NativeConsoleDestination.d.ts +86 -0
- package/lib/typescript/src/destinations/NativeConsoleDestination.d.ts.map +1 -0
- package/lib/typescript/src/destinations/types.d.ts +19 -0
- package/lib/typescript/src/destinations/types.d.ts.map +1 -0
- package/lib/typescript/src/formatters/DefaultFormatter.d.ts +22 -0
- package/lib/typescript/src/formatters/DefaultFormatter.d.ts.map +1 -0
- package/lib/typescript/src/formatters/JsonLinesFormatter.d.ts +73 -0
- package/lib/typescript/src/formatters/JsonLinesFormatter.d.ts.map +1 -0
- package/lib/typescript/src/formatters/timestamp.d.ts +6 -0
- package/lib/typescript/src/formatters/timestamp.d.ts.map +1 -0
- package/lib/typescript/src/formatters/types.d.ts +27 -0
- package/lib/typescript/src/formatters/types.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +34 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/integrations/appState.d.ts +33 -0
- package/lib/typescript/src/integrations/appState.d.ts.map +1 -0
- package/lib/typescript/src/integrations/errorHandler.d.ts +55 -0
- package/lib/typescript/src/integrations/errorHandler.d.ts.map +1 -0
- package/lib/typescript/src/integrations/sanitizeError.d.ts +89 -0
- package/lib/typescript/src/integrations/sanitizeError.d.ts.map +1 -0
- package/lib/typescript/src/levels.d.ts +9 -0
- package/lib/typescript/src/levels.d.ts.map +1 -0
- package/lib/typescript/src/metadata.d.ts +23 -0
- package/lib/typescript/src/metadata.d.ts.map +1 -0
- package/lib/typescript/src/privacy.d.ts +103 -0
- package/lib/typescript/src/privacy.d.ts.map +1 -0
- package/lib/typescript/src/specs/FileSink.nitro.d.ts +131 -0
- package/lib/typescript/src/specs/FileSink.nitro.d.ts.map +1 -0
- package/lib/typescript/src/specs/NativeConsoleSink.nitro.d.ts +21 -0
- package/lib/typescript/src/specs/NativeConsoleSink.nitro.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +37 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utf8.d.ts +15 -0
- package/lib/typescript/src/utf8.d.ts.map +1 -0
- package/nitro.json +33 -0
- package/nitrogen/generated/android/c++/JAppendResult.hpp +79 -0
- package/nitrogen/generated/android/c++/JClearOutcome.hpp +88 -0
- package/nitrogen/generated/android/c++/JFlushOutcome.hpp +81 -0
- package/nitrogen/generated/android/c++/JHybridFileSinkSpec.cpp +120 -0
- package/nitrogen/generated/android/c++/JHybridFileSinkSpec.hpp +69 -0
- package/nitrogen/generated/android/c++/JHybridNativeConsoleSinkSpec.cpp +71 -0
- package/nitrogen/generated/android/c++/JHybridNativeConsoleSinkSpec.hpp +64 -0
- package/nitrogen/generated/android/c++/JRejectReason.hpp +64 -0
- package/nitrogen/generated/android/c++/JRotationConfig.hpp +77 -0
- package/nitrogen/generated/android/c++/JSinkStatus.hpp +69 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/AppendResult.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/ClearOutcome.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/FlushOutcome.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/HybridFileSinkSpec.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/HybridNativeConsoleSinkSpec.kt +58 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/RejectReason.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/RotationConfig.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/SinkStatus.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/nitrologgerOnLoad.kt +35 -0
- package/nitrogen/generated/android/nitrologger+autolinking.cmake +83 -0
- package/nitrogen/generated/android/nitrologger+autolinking.gradle +27 -0
- package/nitrogen/generated/android/nitrologgerOnLoad.cpp +70 -0
- package/nitrogen/generated/android/nitrologgerOnLoad.hpp +34 -0
- package/nitrogen/generated/ios/NitroLogger+autolinking.rb +62 -0
- package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Bridge.cpp +50 -0
- package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Bridge.hpp +216 -0
- package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Umbrella.hpp +69 -0
- package/nitrogen/generated/ios/NitroLoggerAutolinking.mm +41 -0
- package/nitrogen/generated/ios/NitroLoggerAutolinking.swift +38 -0
- package/nitrogen/generated/ios/c++/HybridFileSinkSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridFileSinkSpecSwift.hpp +150 -0
- package/nitrogen/generated/ios/c++/HybridNativeConsoleSinkSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNativeConsoleSinkSpecSwift.hpp +87 -0
- package/nitrogen/generated/ios/swift/AppendResult.swift +60 -0
- package/nitrogen/generated/ios/swift/ClearOutcome.swift +50 -0
- package/nitrogen/generated/ios/swift/FlushOutcome.swift +59 -0
- package/nitrogen/generated/ios/swift/HybridFileSinkSpec.swift +61 -0
- package/nitrogen/generated/ios/swift/HybridFileSinkSpec_cxx.swift +227 -0
- package/nitrogen/generated/ios/swift/HybridNativeConsoleSinkSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridNativeConsoleSinkSpec_cxx.swift +148 -0
- package/nitrogen/generated/ios/swift/RejectReason.swift +48 -0
- package/nitrogen/generated/ios/swift/RotationConfig.swift +93 -0
- package/nitrogen/generated/ios/swift/SinkStatus.swift +44 -0
- package/nitrogen/generated/shared/c++/AppendResult.hpp +105 -0
- package/nitrogen/generated/shared/c++/ClearOutcome.hpp +96 -0
- package/nitrogen/generated/shared/c++/FlushOutcome.hpp +107 -0
- package/nitrogen/generated/shared/c++/HybridFileSinkSpec.cpp +28 -0
- package/nitrogen/generated/shared/c++/HybridFileSinkSpec.hpp +84 -0
- package/nitrogen/generated/shared/c++/HybridNativeConsoleSinkSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridNativeConsoleSinkSpec.hpp +64 -0
- package/nitrogen/generated/shared/c++/RejectReason.hpp +84 -0
- package/nitrogen/generated/shared/c++/RotationConfig.hpp +103 -0
- package/nitrogen/generated/shared/c++/SinkStatus.hpp +95 -0
- package/package.json +151 -0
- package/src/Logger.ts +417 -0
- package/src/ScopedLogger.ts +94 -0
- package/src/config.ts +25 -0
- package/src/destinations/Batcher.ts +805 -0
- package/src/destinations/ConsoleDestination.ts +60 -0
- package/src/destinations/FileDestination.ts +473 -0
- package/src/destinations/NativeConsoleDestination.ts +220 -0
- package/src/destinations/types.ts +19 -0
- package/src/formatters/DefaultFormatter.ts +150 -0
- package/src/formatters/JsonLinesFormatter.ts +195 -0
- package/src/formatters/timestamp.ts +12 -0
- package/src/formatters/types.ts +29 -0
- package/src/index.tsx +106 -0
- package/src/integrations/appState.ts +111 -0
- package/src/integrations/errorHandler.ts +223 -0
- package/src/integrations/sanitizeError.ts +287 -0
- package/src/levels.ts +35 -0
- package/src/metadata.ts +52 -0
- package/src/privacy.ts +389 -0
- package/src/specs/FileSink.nitro.ts +144 -0
- package/src/specs/NativeConsoleSink.nitro.ts +21 -0
- package/src/types.ts +45 -0
- package/src/utf8.ts +39 -0
package/src/index.tsx
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { NitroModules } from 'react-native-nitro-modules';
|
|
2
|
+
import type { FileSink } from './specs/FileSink.nitro';
|
|
3
|
+
import type { NativeConsoleSink } from './specs/NativeConsoleSink.nitro';
|
|
4
|
+
|
|
5
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
6
|
+
export { Log, Logger } from './Logger';
|
|
7
|
+
export type { LogOptions } from './Logger';
|
|
8
|
+
export { ScopedLogger } from './ScopedLogger';
|
|
9
|
+
export type {
|
|
10
|
+
LogLevel,
|
|
11
|
+
LazyMessage,
|
|
12
|
+
LogPrimitive,
|
|
13
|
+
LogValue,
|
|
14
|
+
LogMetadata,
|
|
15
|
+
RedactedMetadata,
|
|
16
|
+
LogEntry,
|
|
17
|
+
} from './types';
|
|
18
|
+
export { pub, priv } from './privacy';
|
|
19
|
+
export {
|
|
20
|
+
METADATA_KEY_PATTERN_SOURCE,
|
|
21
|
+
DROPPED_COUNT_KEY,
|
|
22
|
+
MAX_CATALOG_SIZE,
|
|
23
|
+
UNREADABLE_VALUE,
|
|
24
|
+
} from './privacy';
|
|
25
|
+
export type { PublicValue, PrivateValue, PrivacyDefault } from './privacy';
|
|
26
|
+
export type { LogDestination } from './destinations/types';
|
|
27
|
+
export { ConsoleDestination } from './destinations/ConsoleDestination';
|
|
28
|
+
export { FileDestination } from './destinations/FileDestination';
|
|
29
|
+
export type {
|
|
30
|
+
FileDestinationOptions,
|
|
31
|
+
FileSinkLike,
|
|
32
|
+
PurgeOutcome,
|
|
33
|
+
} from './destinations/FileDestination';
|
|
34
|
+
export { NativeConsoleDestination } from './destinations/NativeConsoleDestination';
|
|
35
|
+
export type {
|
|
36
|
+
NativeConsoleDestinationOptions,
|
|
37
|
+
NativeConsoleSinkLike,
|
|
38
|
+
} from './destinations/NativeConsoleDestination';
|
|
39
|
+
export { installErrorHandler } from './integrations/errorHandler';
|
|
40
|
+
export {
|
|
41
|
+
ERROR_METADATA_KEYS,
|
|
42
|
+
UNCAUGHT_ERROR_MESSAGE,
|
|
43
|
+
} from './integrations/errorHandler';
|
|
44
|
+
export type {
|
|
45
|
+
ErrorHandlerOptions,
|
|
46
|
+
ErrorUtilsLike,
|
|
47
|
+
Uninstall,
|
|
48
|
+
} from './integrations/errorHandler';
|
|
49
|
+
export { flushOnBackground } from './integrations/appState';
|
|
50
|
+
export type {
|
|
51
|
+
FlushOnBackgroundOptions,
|
|
52
|
+
AppStateLike,
|
|
53
|
+
} from './integrations/appState';
|
|
54
|
+
export {
|
|
55
|
+
sanitizeError,
|
|
56
|
+
DEFAULT_BUNDLE_NAMES,
|
|
57
|
+
DEFAULT_MAX_FRAMES,
|
|
58
|
+
REDACTED_FRAME,
|
|
59
|
+
REDACTED_MESSAGE,
|
|
60
|
+
UNKNOWN_ERROR_NAME,
|
|
61
|
+
NON_ERROR_THROWN,
|
|
62
|
+
} from './integrations/sanitizeError';
|
|
63
|
+
export type {
|
|
64
|
+
SanitizedError,
|
|
65
|
+
SanitizeErrorOptions,
|
|
66
|
+
} from './integrations/sanitizeError';
|
|
67
|
+
export { Batcher } from './destinations/Batcher';
|
|
68
|
+
export type {
|
|
69
|
+
BatchTarget,
|
|
70
|
+
BatchFlushOutcome,
|
|
71
|
+
BatcherOptions,
|
|
72
|
+
FenceReason,
|
|
73
|
+
LossCounts,
|
|
74
|
+
} from './destinations/Batcher';
|
|
75
|
+
export { utf8Length } from './utf8';
|
|
76
|
+
export type { LogFormatter } from './formatters/types';
|
|
77
|
+
export { DefaultFormatter } from './formatters/DefaultFormatter';
|
|
78
|
+
export { JsonLinesFormatter } from './formatters/JsonLinesFormatter';
|
|
79
|
+
export type {
|
|
80
|
+
JsonTimestampStyle,
|
|
81
|
+
JsonLinesFormatterOptions,
|
|
82
|
+
} from './formatters/JsonLinesFormatter';
|
|
83
|
+
|
|
84
|
+
// ── Spike-era raw sink access ───────────────────────────────────────────────
|
|
85
|
+
// Used by the example app's M0 harness; becomes internal once the
|
|
86
|
+
// FileDestination (M4/M5) and NativeConsoleDestination (M6) wrap these.
|
|
87
|
+
export type {
|
|
88
|
+
FileSink,
|
|
89
|
+
RotationConfig,
|
|
90
|
+
RejectReason,
|
|
91
|
+
SinkStatus,
|
|
92
|
+
AppendResult,
|
|
93
|
+
FlushOutcome,
|
|
94
|
+
ClearOutcome,
|
|
95
|
+
} from './specs/FileSink.nitro';
|
|
96
|
+
export type { NativeConsoleSink } from './specs/NativeConsoleSink.nitro';
|
|
97
|
+
|
|
98
|
+
export function createFileSink(): FileSink {
|
|
99
|
+
return NitroModules.createHybridObject<FileSink>('FileSink');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function createNativeConsoleSink(): NativeConsoleSink {
|
|
103
|
+
return NitroModules.createHybridObject<NativeConsoleSink>(
|
|
104
|
+
'NativeConsoleSink'
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { Logger } from '../Logger';
|
|
2
|
+
import { Log } from '../Logger';
|
|
3
|
+
import type { Uninstall } from './errorHandler';
|
|
4
|
+
|
|
5
|
+
/** The part of RN's `AppState` this uses. Injected in tests. */
|
|
6
|
+
export interface AppStateLike {
|
|
7
|
+
addEventListener(
|
|
8
|
+
type: 'change',
|
|
9
|
+
listener: (state: string) => void
|
|
10
|
+
): { remove(): void } | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FlushOnBackgroundOptions {
|
|
14
|
+
/** Defaults to the shared `Log`. */
|
|
15
|
+
readonly logger?: Logger;
|
|
16
|
+
/** Injected for tests; defaults to `require('react-native').AppState`. */
|
|
17
|
+
readonly appState?: AppStateLike;
|
|
18
|
+
/**
|
|
19
|
+
* Budget for the backgrounding flush. Default 2000 ms.
|
|
20
|
+
*
|
|
21
|
+
* Backgrounding is not a crash, but it is the last moment the JS thread is
|
|
22
|
+
* reliably scheduled: iOS may freeze timers immediately afterwards, and an
|
|
23
|
+
* entry still sitting in a 100 ms batch window can wait there until the app
|
|
24
|
+
* is next opened — or be lost if it is killed while suspended.
|
|
25
|
+
*/
|
|
26
|
+
readonly deadlineMs?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DEFAULT_DEADLINE_MS = 2000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* States that mean "the JS thread is about to stop being scheduled".
|
|
33
|
+
*
|
|
34
|
+
* Both, not just `background`: iOS passes through `inactive` on the way there,
|
|
35
|
+
* and on some transitions — a phone call, the app switcher — that is as far as
|
|
36
|
+
* it gets. Flushing twice costs nothing, because a flush with nothing pending
|
|
37
|
+
* is a length check on an empty buffer in every destination this library ships.
|
|
38
|
+
*/
|
|
39
|
+
const LEAVING_FOREGROUND: ReadonlySet<string> = new Set([
|
|
40
|
+
'inactive',
|
|
41
|
+
'background',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Flushes every destination when the app leaves the foreground.
|
|
46
|
+
*
|
|
47
|
+
* Returns an idempotent uninstall handle. Removing the listener matters more
|
|
48
|
+
* than it looks: `AppState` outlives any component, so a subscription left
|
|
49
|
+
* behind keeps a disposed logger reachable and flushes it on every transition
|
|
50
|
+
* for the rest of the process.
|
|
51
|
+
*/
|
|
52
|
+
export function flushOnBackground(
|
|
53
|
+
options?: FlushOnBackgroundOptions
|
|
54
|
+
): Uninstall {
|
|
55
|
+
const appState = options?.appState ?? resolveAppState();
|
|
56
|
+
if (!appState) return () => {};
|
|
57
|
+
|
|
58
|
+
const logger = options?.logger ?? Log;
|
|
59
|
+
const deadlineMs = budget(options?.deadlineMs);
|
|
60
|
+
|
|
61
|
+
let subscription: { remove(): void } | undefined;
|
|
62
|
+
try {
|
|
63
|
+
subscription = appState.addEventListener('change', (state) => {
|
|
64
|
+
if (!LEAVING_FOREGROUND.has(state)) return;
|
|
65
|
+
try {
|
|
66
|
+
logger.flush(deadlineMs);
|
|
67
|
+
} catch {
|
|
68
|
+
// A destination that cannot flush must not throw out of an AppState
|
|
69
|
+
// listener, where React Native has nowhere to put the exception.
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
} catch {
|
|
73
|
+
return () => {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let removed = false;
|
|
77
|
+
return () => {
|
|
78
|
+
if (removed) return;
|
|
79
|
+
removed = true;
|
|
80
|
+
try {
|
|
81
|
+
subscription?.remove();
|
|
82
|
+
} catch {
|
|
83
|
+
// Already gone.
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* `AppState` without importing `react-native` at module load.
|
|
90
|
+
*
|
|
91
|
+
* A static import would make this module — and therefore the package entry
|
|
92
|
+
* point — unloadable anywhere React Native is not present, which includes the
|
|
93
|
+
* Node process the unit tests run in.
|
|
94
|
+
*/
|
|
95
|
+
function resolveAppState(): AppStateLike | undefined {
|
|
96
|
+
try {
|
|
97
|
+
const rn = require('react-native') as { AppState?: unknown };
|
|
98
|
+
const candidate = rn.AppState;
|
|
99
|
+
if (typeof candidate !== 'object' || candidate === null) return undefined;
|
|
100
|
+
const add = (candidate as { addEventListener?: unknown }).addEventListener;
|
|
101
|
+
return typeof add === 'function' ? (candidate as AppStateLike) : undefined;
|
|
102
|
+
} catch {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function budget(value: number | undefined): number {
|
|
108
|
+
if (value === undefined) return DEFAULT_DEADLINE_MS;
|
|
109
|
+
if (!Number.isFinite(value) || value < 0) return DEFAULT_DEADLINE_MS;
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import type { Logger } from '../Logger';
|
|
2
|
+
import { Log } from '../Logger';
|
|
3
|
+
import { pub } from '../privacy';
|
|
4
|
+
import type { SanitizeErrorOptions } from './sanitizeError';
|
|
5
|
+
import { sanitizeError } from './sanitizeError';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* React Native's global error hook.
|
|
9
|
+
*
|
|
10
|
+
* `ErrorUtils` is undocumented and has been de-facto stable for a decade, which
|
|
11
|
+
* is the whole risk: it is typed here rather than imported so the library never
|
|
12
|
+
* depends on a declaration file that may not exist, and every access is
|
|
13
|
+
* guarded so a runtime without it degrades to doing nothing.
|
|
14
|
+
*/
|
|
15
|
+
export interface ErrorUtilsLike {
|
|
16
|
+
setGlobalHandler(handler: (error: unknown, isFatal?: boolean) => void): void;
|
|
17
|
+
getGlobalHandler?():
|
|
18
|
+
((error: unknown, isFatal?: boolean) => void) | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Metadata keys this handler writes. Add them to a strict key catalog. */
|
|
22
|
+
export const ERROR_METADATA_KEYS = [
|
|
23
|
+
'errorName',
|
|
24
|
+
'errorMessage',
|
|
25
|
+
'errorFrames',
|
|
26
|
+
'errorFrameCount',
|
|
27
|
+
'errorFramesTruncated',
|
|
28
|
+
'fatal',
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
/** The message this handler logs. A literal, as the lint rule requires. */
|
|
32
|
+
export const UNCAUGHT_ERROR_MESSAGE = 'uncaught error';
|
|
33
|
+
|
|
34
|
+
export interface ErrorHandlerOptions extends SanitizeErrorOptions {
|
|
35
|
+
/** Defaults to the shared `Log`. */
|
|
36
|
+
readonly logger?: Logger;
|
|
37
|
+
/** Injected for tests; defaults to the global `ErrorUtils`. */
|
|
38
|
+
readonly errorUtils?: ErrorUtilsLike;
|
|
39
|
+
/**
|
|
40
|
+
* How long a fatal error's flush may take. Default 2000 ms.
|
|
41
|
+
*
|
|
42
|
+
* This is the crash path and the only chance the buffered entries get, so it
|
|
43
|
+
* is worth more than a normal flush — but it runs on the JS thread while the
|
|
44
|
+
* app is already dying, and a watchdog kill loses the log entirely.
|
|
45
|
+
*/
|
|
46
|
+
readonly fatalFlushMs?: number;
|
|
47
|
+
/** Subsystem to tag the entry with. */
|
|
48
|
+
readonly subsystem?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Pass the error on to whatever handler was installed before. Default true:
|
|
51
|
+
* suppressing it removes the dev RedBox and, in production, converts a crash
|
|
52
|
+
* into a silently broken app.
|
|
53
|
+
*/
|
|
54
|
+
readonly chain?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const DEFAULT_FATAL_FLUSH_MS = 2000;
|
|
58
|
+
|
|
59
|
+
/** Undoes an install. Idempotent; safe to call after another handler took over. */
|
|
60
|
+
export type Uninstall = () => void;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Logs uncaught errors, flushes on fatal ones, and hands the error onward.
|
|
64
|
+
*
|
|
65
|
+
* The flush is the reason this exists. A crash is precisely when the buffered
|
|
66
|
+
* entries explaining it are still in memory, and precisely when nothing will
|
|
67
|
+
* come along later to write them.
|
|
68
|
+
*
|
|
69
|
+
* Nothing about the error is trusted — see {@link sanitizeError}. What reaches
|
|
70
|
+
* the log is a built-in class name, a redacted message outside dev, and stack
|
|
71
|
+
* positions in files whose names were already known.
|
|
72
|
+
*/
|
|
73
|
+
export function installErrorHandler(options?: ErrorHandlerOptions): Uninstall {
|
|
74
|
+
const errorUtils = options?.errorUtils ?? globalErrorUtils();
|
|
75
|
+
if (!errorUtils) {
|
|
76
|
+
// No hook on this runtime — a bare Node test, a web target. Returning a
|
|
77
|
+
// no-op uninstall keeps the caller's teardown symmetrical either way.
|
|
78
|
+
return () => {};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const logger = options?.logger ?? Log;
|
|
82
|
+
const chain = options?.chain ?? true;
|
|
83
|
+
const fatalFlushMs = flushBudget(options?.fatalFlushMs);
|
|
84
|
+
const previous = readPrevious(errorUtils);
|
|
85
|
+
|
|
86
|
+
const handler = (error: unknown, isFatal?: boolean): void => {
|
|
87
|
+
// Uninstalled, but still the installed handler: something installed over
|
|
88
|
+
// us and then restored us on its way out. Pass straight through rather
|
|
89
|
+
// than logging to a logger the caller has finished with.
|
|
90
|
+
if (INSTALLED.get(handler)?.active !== true) {
|
|
91
|
+
liveHandler(previous)?.(error, isFatal);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Every step is isolated. A handler that throws while reporting a crash
|
|
96
|
+
// replaces a diagnosable failure with an undiagnosable one, and takes the
|
|
97
|
+
// previous handler — the RedBox, the crash reporter — down with it.
|
|
98
|
+
try {
|
|
99
|
+
const sanitized = sanitizeError(error, options);
|
|
100
|
+
logger.error(
|
|
101
|
+
UNCAUGHT_ERROR_MESSAGE,
|
|
102
|
+
{
|
|
103
|
+
errorName: pub(sanitized.name),
|
|
104
|
+
errorMessage: pub(sanitized.message),
|
|
105
|
+
errorFrames: pub(sanitized.frames.join(' | ')),
|
|
106
|
+
// Wrapped for the same reason as the three above, and it was an
|
|
107
|
+
// oversight that they were not: every one of these is generated
|
|
108
|
+
// here, from a count, a flag this function was handed, and a
|
|
109
|
+
// decision this module made. None can carry caller data. Left bare
|
|
110
|
+
// they follow the privacy default, so under `privacyDefault('private')`
|
|
111
|
+
// a crash report renders `fatal: <private>` — which withholds
|
|
112
|
+
// nothing, because there was nothing to withhold, and costs the
|
|
113
|
+
// reader the one field that says whether the app is still running.
|
|
114
|
+
errorFrameCount: pub(sanitized.frameCount),
|
|
115
|
+
errorFramesTruncated: pub(sanitized.framesTruncated),
|
|
116
|
+
fatal: pub(isFatal === true),
|
|
117
|
+
},
|
|
118
|
+
options?.subsystem
|
|
119
|
+
);
|
|
120
|
+
} catch {
|
|
121
|
+
// Reporting failed; the flush and the chain below still matter.
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (isFatal) {
|
|
125
|
+
try {
|
|
126
|
+
logger.flush(fatalFlushMs);
|
|
127
|
+
} catch {
|
|
128
|
+
// A destination that cannot flush must not stop the crash from
|
|
129
|
+
// reaching the handler that shows it.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (chain) {
|
|
134
|
+
liveHandler(previous)?.(error, isFatal);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
INSTALLED.set(handler, { active: true, previous });
|
|
139
|
+
errorUtils.setGlobalHandler(handler);
|
|
140
|
+
|
|
141
|
+
return () => {
|
|
142
|
+
const state = INSTALLED.get(handler);
|
|
143
|
+
if (!state || !state.active) return;
|
|
144
|
+
|
|
145
|
+
// Marked dead first, and unconditionally. Uninstalling out of order is
|
|
146
|
+
// ordinary — two libraries, a hot reload — and if this only took effect
|
|
147
|
+
// when ours happened to be the current handler, a later uninstall that
|
|
148
|
+
// restored us would bring a logger back to life that its owner had
|
|
149
|
+
// already let go of.
|
|
150
|
+
state.active = false;
|
|
151
|
+
|
|
152
|
+
// Something else is installed over us. Leave it alone: putting `previous`
|
|
153
|
+
// back would silently uninstall theirs. The flag above makes our wrapper
|
|
154
|
+
// inert if they ever restore it.
|
|
155
|
+
if (readPrevious(errorUtils) !== handler) return;
|
|
156
|
+
errorUtils.setGlobalHandler(liveHandler(state.previous) ?? (() => {}));
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
type GlobalHandler = (error: unknown, isFatal?: boolean) => void;
|
|
161
|
+
|
|
162
|
+
interface InstallState {
|
|
163
|
+
active: boolean;
|
|
164
|
+
previous: GlobalHandler | undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The handlers this module installed, and whether each is still live.
|
|
169
|
+
*
|
|
170
|
+
* A WeakMap rather than a property on the function: the entry disappears with
|
|
171
|
+
* the handler, and nothing about our bookkeeping is visible to code that
|
|
172
|
+
* inspects the global handler.
|
|
173
|
+
*/
|
|
174
|
+
const INSTALLED = new WeakMap<GlobalHandler, InstallState>();
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The first handler in the chain that is still installed.
|
|
178
|
+
*
|
|
179
|
+
* Walks past our own uninstalled wrappers. Any handler we did not install is
|
|
180
|
+
* live by definition — we have no way to know otherwise, and assuming it is
|
|
181
|
+
* gone would drop the RedBox or a crash reporter.
|
|
182
|
+
*/
|
|
183
|
+
function liveHandler(
|
|
184
|
+
handler: GlobalHandler | undefined
|
|
185
|
+
): GlobalHandler | undefined {
|
|
186
|
+
const seen = new Set<GlobalHandler>();
|
|
187
|
+
let candidate = handler;
|
|
188
|
+
while (candidate) {
|
|
189
|
+
const state = INSTALLED.get(candidate);
|
|
190
|
+
if (!state || state.active) return candidate;
|
|
191
|
+
// A cycle needs two of our own handlers pointing at each other, which
|
|
192
|
+
// takes a restore we did not do — but the crash path is the wrong place
|
|
193
|
+
// to find out the hard way.
|
|
194
|
+
if (seen.has(candidate)) return undefined;
|
|
195
|
+
seen.add(candidate);
|
|
196
|
+
candidate = state.previous;
|
|
197
|
+
}
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function readPrevious(errorUtils: ErrorUtilsLike): GlobalHandler | undefined {
|
|
202
|
+
try {
|
|
203
|
+
return errorUtils.getGlobalHandler?.();
|
|
204
|
+
} catch {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The global `ErrorUtils`, if this runtime has one. */
|
|
210
|
+
function globalErrorUtils(): ErrorUtilsLike | undefined {
|
|
211
|
+
const candidate = (globalThis as { ErrorUtils?: unknown }).ErrorUtils;
|
|
212
|
+
if (typeof candidate !== 'object' || candidate === null) return undefined;
|
|
213
|
+
const setter = (candidate as { setGlobalHandler?: unknown }).setGlobalHandler;
|
|
214
|
+
return typeof setter === 'function'
|
|
215
|
+
? (candidate as ErrorUtilsLike)
|
|
216
|
+
: undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function flushBudget(value: number | undefined): number {
|
|
220
|
+
if (value === undefined) return DEFAULT_FATAL_FLUSH_MS;
|
|
221
|
+
if (!Number.isFinite(value) || value < 0) return DEFAULT_FATAL_FLUSH_MS;
|
|
222
|
+
return value;
|
|
223
|
+
}
|