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.
Files changed (205) hide show
  1. package/LICENSE +20 -0
  2. package/NitroLogger.podspec +29 -0
  3. package/README.md +214 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +124 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +11 -0
  8. package/android/src/main/java/com/margelo/nitro/nitrologger/AndroidPlatformIo.kt +100 -0
  9. package/android/src/main/java/com/margelo/nitro/nitrologger/HybridFileSink.kt +292 -0
  10. package/android/src/main/java/com/margelo/nitro/nitrologger/HybridNativeConsoleSink.kt +23 -0
  11. package/android/src/main/java/com/margelo/nitro/nitrologger/LogFileWriter.kt +1328 -0
  12. package/android/src/main/java/com/margelo/nitro/nitrologger/LogRotationPolicy.kt +186 -0
  13. package/android/src/main/java/com/margelo/nitro/nitrologger/LogSecureFile.kt +80 -0
  14. package/android/src/main/java/com/margelo/nitro/nitrologger/LogWriterRegistry.kt +420 -0
  15. package/android/src/main/java/com/margelo/nitro/nitrologger/NativeConsoleWriter.kt +237 -0
  16. package/android/src/main/java/com/margelo/nitro/nitrologger/NitroLoggerPackage.kt +22 -0
  17. package/android/src/main/java/com/margelo/nitro/nitrologger/PlatformIo.kt +165 -0
  18. package/docs/PARITY.md +330 -0
  19. package/docs/PRIVACY.md +341 -0
  20. package/eslint-plugin/README.md +238 -0
  21. package/eslint-plugin/index.js +62 -0
  22. package/eslint-plugin/rules/literal-subsystem.js +102 -0
  23. package/eslint-plugin/rules/no-computed-metadata-key.js +174 -0
  24. package/eslint-plugin/rules/no-derived-correlation.js +171 -0
  25. package/eslint-plugin/rules/no-dynamic-message.js +60 -0
  26. package/eslint-plugin/shared.js +1948 -0
  27. package/ios/Gzip.swift +203 -0
  28. package/ios/HybridFileSink.swift +291 -0
  29. package/ios/HybridNativeConsoleSink.swift +20 -0
  30. package/ios/LogFileWriter.swift +1280 -0
  31. package/ios/LogSecureFile.swift +296 -0
  32. package/ios/LogWriterRegistry.swift +444 -0
  33. package/ios/NativeConsoleWriter.swift +180 -0
  34. package/lib/module/Logger.js +381 -0
  35. package/lib/module/Logger.js.map +1 -0
  36. package/lib/module/ScopedLogger.js +70 -0
  37. package/lib/module/ScopedLogger.js.map +1 -0
  38. package/lib/module/config.js +23 -0
  39. package/lib/module/config.js.map +1 -0
  40. package/lib/module/destinations/Batcher.js +647 -0
  41. package/lib/module/destinations/Batcher.js.map +1 -0
  42. package/lib/module/destinations/ConsoleDestination.js +51 -0
  43. package/lib/module/destinations/ConsoleDestination.js.map +1 -0
  44. package/lib/module/destinations/FileDestination.js +360 -0
  45. package/lib/module/destinations/FileDestination.js.map +1 -0
  46. package/lib/module/destinations/NativeConsoleDestination.js +170 -0
  47. package/lib/module/destinations/NativeConsoleDestination.js.map +1 -0
  48. package/lib/module/destinations/types.js +4 -0
  49. package/lib/module/destinations/types.js.map +1 -0
  50. package/lib/module/formatters/DefaultFormatter.js +136 -0
  51. package/lib/module/formatters/DefaultFormatter.js.map +1 -0
  52. package/lib/module/formatters/JsonLinesFormatter.js +162 -0
  53. package/lib/module/formatters/JsonLinesFormatter.js.map +1 -0
  54. package/lib/module/formatters/timestamp.js +15 -0
  55. package/lib/module/formatters/timestamp.js.map +1 -0
  56. package/lib/module/formatters/types.js +4 -0
  57. package/lib/module/formatters/types.js.map +1 -0
  58. package/lib/module/index.js +31 -0
  59. package/lib/module/index.js.map +1 -0
  60. package/lib/module/integrations/appState.js +81 -0
  61. package/lib/module/integrations/appState.js.map +1 -0
  62. package/lib/module/integrations/errorHandler.js +166 -0
  63. package/lib/module/integrations/errorHandler.js.map +1 -0
  64. package/lib/module/integrations/sanitizeError.js +238 -0
  65. package/lib/module/integrations/sanitizeError.js.map +1 -0
  66. package/lib/module/levels.js +35 -0
  67. package/lib/module/levels.js.map +1 -0
  68. package/lib/module/metadata.js +52 -0
  69. package/lib/module/metadata.js.map +1 -0
  70. package/lib/module/package.json +1 -0
  71. package/lib/module/privacy.js +349 -0
  72. package/lib/module/privacy.js.map +1 -0
  73. package/lib/module/specs/FileSink.nitro.js +4 -0
  74. package/lib/module/specs/FileSink.nitro.js.map +1 -0
  75. package/lib/module/specs/NativeConsoleSink.nitro.js +4 -0
  76. package/lib/module/specs/NativeConsoleSink.nitro.js.map +1 -0
  77. package/lib/module/types.js +4 -0
  78. package/lib/module/types.js.map +1 -0
  79. package/lib/module/utf8.js +42 -0
  80. package/lib/module/utf8.js.map +1 -0
  81. package/lib/typescript/package.json +1 -0
  82. package/lib/typescript/src/Logger.d.ts +122 -0
  83. package/lib/typescript/src/Logger.d.ts.map +1 -0
  84. package/lib/typescript/src/ScopedLogger.d.ts +36 -0
  85. package/lib/typescript/src/ScopedLogger.d.ts.map +1 -0
  86. package/lib/typescript/src/config.d.ts +12 -0
  87. package/lib/typescript/src/config.d.ts.map +1 -0
  88. package/lib/typescript/src/destinations/Batcher.d.ts +244 -0
  89. package/lib/typescript/src/destinations/Batcher.d.ts.map +1 -0
  90. package/lib/typescript/src/destinations/ConsoleDestination.d.ts +27 -0
  91. package/lib/typescript/src/destinations/ConsoleDestination.d.ts.map +1 -0
  92. package/lib/typescript/src/destinations/FileDestination.d.ts +200 -0
  93. package/lib/typescript/src/destinations/FileDestination.d.ts.map +1 -0
  94. package/lib/typescript/src/destinations/NativeConsoleDestination.d.ts +86 -0
  95. package/lib/typescript/src/destinations/NativeConsoleDestination.d.ts.map +1 -0
  96. package/lib/typescript/src/destinations/types.d.ts +19 -0
  97. package/lib/typescript/src/destinations/types.d.ts.map +1 -0
  98. package/lib/typescript/src/formatters/DefaultFormatter.d.ts +22 -0
  99. package/lib/typescript/src/formatters/DefaultFormatter.d.ts.map +1 -0
  100. package/lib/typescript/src/formatters/JsonLinesFormatter.d.ts +73 -0
  101. package/lib/typescript/src/formatters/JsonLinesFormatter.d.ts.map +1 -0
  102. package/lib/typescript/src/formatters/timestamp.d.ts +6 -0
  103. package/lib/typescript/src/formatters/timestamp.d.ts.map +1 -0
  104. package/lib/typescript/src/formatters/types.d.ts +27 -0
  105. package/lib/typescript/src/formatters/types.d.ts.map +1 -0
  106. package/lib/typescript/src/index.d.ts +34 -0
  107. package/lib/typescript/src/index.d.ts.map +1 -0
  108. package/lib/typescript/src/integrations/appState.d.ts +33 -0
  109. package/lib/typescript/src/integrations/appState.d.ts.map +1 -0
  110. package/lib/typescript/src/integrations/errorHandler.d.ts +55 -0
  111. package/lib/typescript/src/integrations/errorHandler.d.ts.map +1 -0
  112. package/lib/typescript/src/integrations/sanitizeError.d.ts +89 -0
  113. package/lib/typescript/src/integrations/sanitizeError.d.ts.map +1 -0
  114. package/lib/typescript/src/levels.d.ts +9 -0
  115. package/lib/typescript/src/levels.d.ts.map +1 -0
  116. package/lib/typescript/src/metadata.d.ts +23 -0
  117. package/lib/typescript/src/metadata.d.ts.map +1 -0
  118. package/lib/typescript/src/privacy.d.ts +103 -0
  119. package/lib/typescript/src/privacy.d.ts.map +1 -0
  120. package/lib/typescript/src/specs/FileSink.nitro.d.ts +131 -0
  121. package/lib/typescript/src/specs/FileSink.nitro.d.ts.map +1 -0
  122. package/lib/typescript/src/specs/NativeConsoleSink.nitro.d.ts +21 -0
  123. package/lib/typescript/src/specs/NativeConsoleSink.nitro.d.ts.map +1 -0
  124. package/lib/typescript/src/types.d.ts +37 -0
  125. package/lib/typescript/src/types.d.ts.map +1 -0
  126. package/lib/typescript/src/utf8.d.ts +15 -0
  127. package/lib/typescript/src/utf8.d.ts.map +1 -0
  128. package/nitro.json +33 -0
  129. package/nitrogen/generated/android/c++/JAppendResult.hpp +79 -0
  130. package/nitrogen/generated/android/c++/JClearOutcome.hpp +88 -0
  131. package/nitrogen/generated/android/c++/JFlushOutcome.hpp +81 -0
  132. package/nitrogen/generated/android/c++/JHybridFileSinkSpec.cpp +120 -0
  133. package/nitrogen/generated/android/c++/JHybridFileSinkSpec.hpp +69 -0
  134. package/nitrogen/generated/android/c++/JHybridNativeConsoleSinkSpec.cpp +71 -0
  135. package/nitrogen/generated/android/c++/JHybridNativeConsoleSinkSpec.hpp +64 -0
  136. package/nitrogen/generated/android/c++/JRejectReason.hpp +64 -0
  137. package/nitrogen/generated/android/c++/JRotationConfig.hpp +77 -0
  138. package/nitrogen/generated/android/c++/JSinkStatus.hpp +69 -0
  139. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/AppendResult.kt +76 -0
  140. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/ClearOutcome.kt +66 -0
  141. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/FlushOutcome.kt +81 -0
  142. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/HybridFileSinkSpec.kt +80 -0
  143. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/HybridNativeConsoleSinkSpec.kt +58 -0
  144. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/RejectReason.kt +25 -0
  145. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/RotationConfig.kt +76 -0
  146. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/SinkStatus.kt +66 -0
  147. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/nitrologgerOnLoad.kt +35 -0
  148. package/nitrogen/generated/android/nitrologger+autolinking.cmake +83 -0
  149. package/nitrogen/generated/android/nitrologger+autolinking.gradle +27 -0
  150. package/nitrogen/generated/android/nitrologgerOnLoad.cpp +70 -0
  151. package/nitrogen/generated/android/nitrologgerOnLoad.hpp +34 -0
  152. package/nitrogen/generated/ios/NitroLogger+autolinking.rb +62 -0
  153. package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Bridge.cpp +50 -0
  154. package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Bridge.hpp +216 -0
  155. package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Umbrella.hpp +69 -0
  156. package/nitrogen/generated/ios/NitroLoggerAutolinking.mm +41 -0
  157. package/nitrogen/generated/ios/NitroLoggerAutolinking.swift +38 -0
  158. package/nitrogen/generated/ios/c++/HybridFileSinkSpecSwift.cpp +11 -0
  159. package/nitrogen/generated/ios/c++/HybridFileSinkSpecSwift.hpp +150 -0
  160. package/nitrogen/generated/ios/c++/HybridNativeConsoleSinkSpecSwift.cpp +11 -0
  161. package/nitrogen/generated/ios/c++/HybridNativeConsoleSinkSpecSwift.hpp +87 -0
  162. package/nitrogen/generated/ios/swift/AppendResult.swift +60 -0
  163. package/nitrogen/generated/ios/swift/ClearOutcome.swift +50 -0
  164. package/nitrogen/generated/ios/swift/FlushOutcome.swift +59 -0
  165. package/nitrogen/generated/ios/swift/HybridFileSinkSpec.swift +61 -0
  166. package/nitrogen/generated/ios/swift/HybridFileSinkSpec_cxx.swift +227 -0
  167. package/nitrogen/generated/ios/swift/HybridNativeConsoleSinkSpec.swift +56 -0
  168. package/nitrogen/generated/ios/swift/HybridNativeConsoleSinkSpec_cxx.swift +148 -0
  169. package/nitrogen/generated/ios/swift/RejectReason.swift +48 -0
  170. package/nitrogen/generated/ios/swift/RotationConfig.swift +93 -0
  171. package/nitrogen/generated/ios/swift/SinkStatus.swift +44 -0
  172. package/nitrogen/generated/shared/c++/AppendResult.hpp +105 -0
  173. package/nitrogen/generated/shared/c++/ClearOutcome.hpp +96 -0
  174. package/nitrogen/generated/shared/c++/FlushOutcome.hpp +107 -0
  175. package/nitrogen/generated/shared/c++/HybridFileSinkSpec.cpp +28 -0
  176. package/nitrogen/generated/shared/c++/HybridFileSinkSpec.hpp +84 -0
  177. package/nitrogen/generated/shared/c++/HybridNativeConsoleSinkSpec.cpp +22 -0
  178. package/nitrogen/generated/shared/c++/HybridNativeConsoleSinkSpec.hpp +64 -0
  179. package/nitrogen/generated/shared/c++/RejectReason.hpp +84 -0
  180. package/nitrogen/generated/shared/c++/RotationConfig.hpp +103 -0
  181. package/nitrogen/generated/shared/c++/SinkStatus.hpp +95 -0
  182. package/package.json +151 -0
  183. package/src/Logger.ts +417 -0
  184. package/src/ScopedLogger.ts +94 -0
  185. package/src/config.ts +25 -0
  186. package/src/destinations/Batcher.ts +805 -0
  187. package/src/destinations/ConsoleDestination.ts +60 -0
  188. package/src/destinations/FileDestination.ts +473 -0
  189. package/src/destinations/NativeConsoleDestination.ts +220 -0
  190. package/src/destinations/types.ts +19 -0
  191. package/src/formatters/DefaultFormatter.ts +150 -0
  192. package/src/formatters/JsonLinesFormatter.ts +195 -0
  193. package/src/formatters/timestamp.ts +12 -0
  194. package/src/formatters/types.ts +29 -0
  195. package/src/index.tsx +106 -0
  196. package/src/integrations/appState.ts +111 -0
  197. package/src/integrations/errorHandler.ts +223 -0
  198. package/src/integrations/sanitizeError.ts +287 -0
  199. package/src/levels.ts +35 -0
  200. package/src/metadata.ts +52 -0
  201. package/src/privacy.ts +389 -0
  202. package/src/specs/FileSink.nitro.ts +144 -0
  203. package/src/specs/NativeConsoleSink.nitro.ts +21 -0
  204. package/src/types.ts +45 -0
  205. 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
+ }