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
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a thrown value into something a log file is allowed to contain.
|
|
3
|
+
*
|
|
4
|
+
* An uncaught error is the one place the logger handles a string it did not
|
|
5
|
+
* construct, cannot see the provenance of, and did not put through the privacy
|
|
6
|
+
* layer. `new Error(\`no chart for ${patient.mrn}\`)` is an entirely ordinary
|
|
7
|
+
* line of application code, and its message reaches here verbatim. So nothing
|
|
8
|
+
* from the error is trusted: the class name is allowlisted down to a fixed
|
|
9
|
+
* token, the message is dropped outside dev, and stack frames are reduced to a
|
|
10
|
+
* position in a file whose name we already knew.
|
|
11
|
+
*
|
|
12
|
+
* The result is safe enough to mark `pub()`, which is the point — sanitising is
|
|
13
|
+
* what earns an error the right to be rendered.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Error constructors the language itself defines. A name from this set says
|
|
18
|
+
* only which built-in was thrown, which is a fact about the runtime rather than
|
|
19
|
+
* about the data.
|
|
20
|
+
*
|
|
21
|
+
* Anything else collapses to {@link UNKNOWN_ERROR_NAME}. A custom subclass is
|
|
22
|
+
* named by application code, and `class PatientNotFoundError` is a perfectly
|
|
23
|
+
* natural thing to write and a perfectly bad thing to log.
|
|
24
|
+
*/
|
|
25
|
+
const BUILT_IN_ERROR_NAMES: ReadonlySet<string> = new Set([
|
|
26
|
+
'Error',
|
|
27
|
+
'AggregateError',
|
|
28
|
+
'EvalError',
|
|
29
|
+
'RangeError',
|
|
30
|
+
'ReferenceError',
|
|
31
|
+
'SyntaxError',
|
|
32
|
+
'TypeError',
|
|
33
|
+
'URIError',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
/** What an unrecognised error class is reported as. */
|
|
37
|
+
export const UNKNOWN_ERROR_NAME = 'Error';
|
|
38
|
+
|
|
39
|
+
/** What a stack frame in a file we cannot vouch for is reported as. */
|
|
40
|
+
export const REDACTED_FRAME = '<frame>';
|
|
41
|
+
|
|
42
|
+
/** What an error message is reported as outside a dev build. */
|
|
43
|
+
export const REDACTED_MESSAGE = '<redacted>';
|
|
44
|
+
|
|
45
|
+
/** What a thrown non-Error is reported as. */
|
|
46
|
+
export const NON_ERROR_THROWN = '<non-error>';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Bundle basenames a frame may be named after.
|
|
50
|
+
*
|
|
51
|
+
* A frame is only useful if you can find the file, and the files an app ships
|
|
52
|
+
* are known in advance. Everything else — a path from `node_modules`, a
|
|
53
|
+
* `blob:` URL, a filename built at runtime — is reported positionally instead.
|
|
54
|
+
* R3's point stands on its own: a basename can be perfectly regex-valid and
|
|
55
|
+
* still be `patient-12345.js`.
|
|
56
|
+
*/
|
|
57
|
+
export const DEFAULT_BUNDLE_NAMES: readonly string[] = [
|
|
58
|
+
'index.bundle',
|
|
59
|
+
'main.jsbundle',
|
|
60
|
+
'index.android.bundle',
|
|
61
|
+
'index.ios.bundle',
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
/** Frames beyond this are dropped; the count of what was dropped is kept. */
|
|
65
|
+
export const DEFAULT_MAX_FRAMES = 12;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* How much of a stack is read at all, regardless of how many frames are kept.
|
|
69
|
+
*
|
|
70
|
+
* `stack` is a property of an object someone threw, which means its size is
|
|
71
|
+
* chosen by whatever threw it and not by any engine limit. Splitting it whole
|
|
72
|
+
* to count frames we then discard hands an arbitrary allocation to the one
|
|
73
|
+
* code path that runs while the app is already crashing. A real Hermes or V8
|
|
74
|
+
* stack is a few kilobytes and a few dozen lines; anything past these bounds
|
|
75
|
+
* is reported as truncated rather than read.
|
|
76
|
+
*/
|
|
77
|
+
export const MAX_STACK_CHARS = 64 * 1024;
|
|
78
|
+
export const MAX_STACK_LINES = 256;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* How much of a single line the frame pattern is run against.
|
|
82
|
+
*
|
|
83
|
+
* Capping the whole stack is not enough on its own. {@link FRAME_TAIL} ends in
|
|
84
|
+
* `$`, and its leading `[^\s()]+` backtracks: given one very long line with no
|
|
85
|
+
* separator in it, the engine retries from every offset and the match becomes
|
|
86
|
+
* quadratic. A 64 KB line took four seconds to reject — inside the crash
|
|
87
|
+
* handler. The pattern is anchored at the end, so the last kilobyte decides it
|
|
88
|
+
* for any line that could really be a frame.
|
|
89
|
+
*/
|
|
90
|
+
export const MAX_FRAME_LINE_CHARS = 1024;
|
|
91
|
+
|
|
92
|
+
export interface SanitizedError {
|
|
93
|
+
/** An allowlisted built-in name, or `'Error'`. */
|
|
94
|
+
readonly name: string;
|
|
95
|
+
/** The real message in a dev build; {@link REDACTED_MESSAGE} otherwise. */
|
|
96
|
+
readonly message: string;
|
|
97
|
+
/** Sanitised frames, newest first, at most `maxFrames` of them. */
|
|
98
|
+
readonly frames: readonly string[];
|
|
99
|
+
/** How many frames were found in the part of the stack that was read. */
|
|
100
|
+
readonly frameCount: number;
|
|
101
|
+
/**
|
|
102
|
+
* The stack was longer than {@link MAX_STACK_CHARS} or
|
|
103
|
+
* {@link MAX_STACK_LINES} and was not read to the end, so `frameCount` is a
|
|
104
|
+
* floor rather than a total.
|
|
105
|
+
*/
|
|
106
|
+
readonly framesTruncated: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface SanitizeErrorOptions {
|
|
110
|
+
readonly bundleNames?: readonly string[];
|
|
111
|
+
readonly maxFrames?: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The reveal branch exists only in dev builds. Metro substitutes a literal for
|
|
115
|
+
// `__DEV__`, so in a release bundle this whole block — sentinel included — is
|
|
116
|
+
// dead code and is stripped. CI greps the release bundle for the sentinel.
|
|
117
|
+
let revealBranchSentinel = '';
|
|
118
|
+
|
|
119
|
+
function messageRevealAllowed(): boolean {
|
|
120
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
121
|
+
revealBranchSentinel = '__NITRO_LOGGER_ERROR_REVEAL__';
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** @internal Test/CI hook — non-empty only once the dev reveal branch ran. */
|
|
128
|
+
export function __errorRevealBranchSentinel(): string {
|
|
129
|
+
return revealBranchSentinel;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Reduces a thrown value to a name, a message, and a list of positions.
|
|
134
|
+
*
|
|
135
|
+
* Total by construction: every branch that could throw — a getter on `message`,
|
|
136
|
+
* a `stack` that is not a string, a `name` that is a Symbol — ends at a fixed
|
|
137
|
+
* token rather than propagating. An error handler that throws while reporting
|
|
138
|
+
* an error takes the crash report with it.
|
|
139
|
+
*/
|
|
140
|
+
export function sanitizeError(
|
|
141
|
+
thrown: unknown,
|
|
142
|
+
options?: SanitizeErrorOptions
|
|
143
|
+
): SanitizedError {
|
|
144
|
+
const bundleNames = new Set(options?.bundleNames ?? DEFAULT_BUNDLE_NAMES);
|
|
145
|
+
const maxFrames = boundedFrames(options?.maxFrames);
|
|
146
|
+
|
|
147
|
+
// Read each property exactly once, and touch the thrown value in no other
|
|
148
|
+
// way.
|
|
149
|
+
//
|
|
150
|
+
// Not `instanceof Error`: that consults the prototype chain, and a Proxy can
|
|
151
|
+
// throw from its `getPrototypeOf` trap — so the one operation meant to
|
|
152
|
+
// classify a hostile value safely is itself an operation it can hijack.
|
|
153
|
+
// Reading once also matters on its own: a getter is free to return something
|
|
154
|
+
// innocuous when classified and something else when rendered, so classifying
|
|
155
|
+
// and rendering must be the same read.
|
|
156
|
+
const source =
|
|
157
|
+
typeof thrown === 'object' && thrown !== null ? thrown : undefined;
|
|
158
|
+
const stack = source ? readString(source, 'stack') : undefined;
|
|
159
|
+
const message = source ? readString(source, 'message') : undefined;
|
|
160
|
+
const name = source ? readString(source, 'name') : undefined;
|
|
161
|
+
|
|
162
|
+
if (stack === undefined && message === undefined) {
|
|
163
|
+
// A thrown string is the case that matters: `throw \`no chart for ${mrn}\``
|
|
164
|
+
// would otherwise arrive as the message. Its content never appears.
|
|
165
|
+
return {
|
|
166
|
+
name: NON_ERROR_THROWN,
|
|
167
|
+
message: REDACTED_MESSAGE,
|
|
168
|
+
frames: [],
|
|
169
|
+
frameCount: 0,
|
|
170
|
+
framesTruncated: false,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const parsed = parseFrames(stack, bundleNames);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
name: sanitizeName(name),
|
|
178
|
+
message: messageRevealAllowed() ? (message ?? '') : REDACTED_MESSAGE,
|
|
179
|
+
frames: parsed.frames.slice(0, maxFrames),
|
|
180
|
+
frameCount: parsed.frames.length,
|
|
181
|
+
framesTruncated: parsed.truncated,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Property reads on a thrown value are not safe.
|
|
187
|
+
*
|
|
188
|
+
* `error.message` can be a getter, and a getter on an object someone threw can
|
|
189
|
+
* do anything — including throw again, from inside the handler that exists to
|
|
190
|
+
* report the first failure.
|
|
191
|
+
*/
|
|
192
|
+
function readString(source: object, key: string): string | undefined {
|
|
193
|
+
try {
|
|
194
|
+
const value = (source as Record<string, unknown>)[key];
|
|
195
|
+
return typeof value === 'string' ? value : undefined;
|
|
196
|
+
} catch {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function sanitizeName(name: string | undefined): string {
|
|
202
|
+
if (name === undefined) return UNKNOWN_ERROR_NAME;
|
|
203
|
+
return BUILT_IN_ERROR_NAMES.has(name) ? name : UNKNOWN_ERROR_NAME;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function boundedFrames(value: number | undefined): number {
|
|
207
|
+
if (value === undefined) return DEFAULT_MAX_FRAMES;
|
|
208
|
+
if (!Number.isSafeInteger(value) || value < 0) return DEFAULT_MAX_FRAMES;
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The trailing `file:line:column` of a stack line, in any of the shapes
|
|
214
|
+
* JavaScriptCore, Hermes and V8 produce:
|
|
215
|
+
*
|
|
216
|
+
* at fn (/path/index.bundle:1:2)
|
|
217
|
+
* at /path/index.bundle:1:2
|
|
218
|
+
* fn@http://localhost:8081/index.bundle?platform=ios:1:2
|
|
219
|
+
* at fn (address at /path/index.android.bundle:1:2)
|
|
220
|
+
*
|
|
221
|
+
* Only the position is taken. The function name is discarded with everything
|
|
222
|
+
* else: it adds nothing a file and line does not already give, and it is one
|
|
223
|
+
* more string from application code that nobody has vetted.
|
|
224
|
+
*/
|
|
225
|
+
const FRAME_TAIL = /([^\s()]+):(\d+):(\d+)\)?$/;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Walks the stack a line at a time within a fixed budget.
|
|
229
|
+
*
|
|
230
|
+
* Deliberately not `stack.split('\n')`: that materialises every line of a
|
|
231
|
+
* string whose length the thrown object chose, to produce frames almost all of
|
|
232
|
+
* which are then dropped. Scanning in place means an oversized stack costs a
|
|
233
|
+
* bounded amount of work and is reported as truncated.
|
|
234
|
+
*/
|
|
235
|
+
function parseFrames(
|
|
236
|
+
stack: string | undefined,
|
|
237
|
+
bundleNames: ReadonlySet<string>
|
|
238
|
+
): { frames: string[]; truncated: boolean } {
|
|
239
|
+
if (stack === undefined) return { frames: [], truncated: false };
|
|
240
|
+
|
|
241
|
+
// Cut to the budget up front, then never look at the original again.
|
|
242
|
+
//
|
|
243
|
+
// Bounding the loop was not enough on its own: `indexOf` searches to the end
|
|
244
|
+
// of whatever string it is given, so a multi-megabyte stack with no newline
|
|
245
|
+
// in it — or just a very long last line — was still scanned in full by the
|
|
246
|
+
// search for a line ending that was never going to come.
|
|
247
|
+
const input =
|
|
248
|
+
stack.length > MAX_STACK_CHARS ? stack.slice(0, MAX_STACK_CHARS) : stack;
|
|
249
|
+
const truncatedInput = input.length < stack.length;
|
|
250
|
+
|
|
251
|
+
const frames: string[] = [];
|
|
252
|
+
let start = 0;
|
|
253
|
+
let lines = 0;
|
|
254
|
+
|
|
255
|
+
while (start < input.length && lines < MAX_STACK_LINES) {
|
|
256
|
+
let end = input.indexOf('\n', start);
|
|
257
|
+
if (end < 0) end = input.length;
|
|
258
|
+
// The tail, not the head: the pattern is anchored at the end of the line.
|
|
259
|
+
const from = Math.max(start, end - MAX_FRAME_LINE_CHARS);
|
|
260
|
+
const line = input.slice(from, end).trim();
|
|
261
|
+
start = end + 1;
|
|
262
|
+
lines += 1;
|
|
263
|
+
|
|
264
|
+
const match = FRAME_TAIL.exec(line);
|
|
265
|
+
// A line that does not parse is discarded rather than guessed at. The
|
|
266
|
+
// first line of a V8 stack is `Name: message`, which is exactly the string
|
|
267
|
+
// this module exists to keep out.
|
|
268
|
+
if (!match) continue;
|
|
269
|
+
|
|
270
|
+
const [, location, lineNumber, column] = match;
|
|
271
|
+
const file = basename(location!);
|
|
272
|
+
frames.push(
|
|
273
|
+
bundleNames.has(file) ? `${file}:${lineNumber}:${column}` : REDACTED_FRAME
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { frames, truncated: truncatedInput || start < input.length };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** The filename part of a path or URL, without any query or fragment. */
|
|
281
|
+
function basename(location: string): string {
|
|
282
|
+
const cut = Math.max(location.lastIndexOf('/'), location.lastIndexOf('\\'));
|
|
283
|
+
let name = cut >= 0 ? location.slice(cut + 1) : location;
|
|
284
|
+
const query = name.search(/[?#]/);
|
|
285
|
+
if (query >= 0) name = name.slice(0, query);
|
|
286
|
+
return name;
|
|
287
|
+
}
|
package/src/levels.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { LogLevel } from './types';
|
|
2
|
+
|
|
3
|
+
/** Numeric severity order; also the codes that cross the bridge. */
|
|
4
|
+
export const LEVEL_ORDER: Record<LogLevel, number> = {
|
|
5
|
+
verbose: 0,
|
|
6
|
+
debug: 1,
|
|
7
|
+
info: 2,
|
|
8
|
+
warning: 3,
|
|
9
|
+
error: 4,
|
|
10
|
+
todo: 5,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function levelAtLeast(level: LogLevel, minimum: LogLevel): boolean {
|
|
14
|
+
return LEVEL_ORDER[level] >= LEVEL_ORDER[minimum];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Fixed-width 5-character tags, byte-identical to SwiftLogger's. */
|
|
18
|
+
export const LEVEL_TAG: Record<LogLevel, string> = {
|
|
19
|
+
verbose: 'TRACE',
|
|
20
|
+
debug: 'DEBUG',
|
|
21
|
+
info: ' INFO',
|
|
22
|
+
warning: ' WARN',
|
|
23
|
+
error: 'ERROR',
|
|
24
|
+
todo: ' TODO',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Uppercase names used by the JSON formatter (SwiftLogger rawValue parity). */
|
|
28
|
+
export const LEVEL_NAME: Record<LogLevel, string> = {
|
|
29
|
+
verbose: 'VERBOSE',
|
|
30
|
+
debug: 'DEBUG',
|
|
31
|
+
info: 'INFO',
|
|
32
|
+
warning: 'WARNING',
|
|
33
|
+
error: 'ERROR',
|
|
34
|
+
todo: 'TODO',
|
|
35
|
+
};
|
package/src/metadata.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { LogMetadata } from './types';
|
|
2
|
+
import { UNREADABLE_VALUE } from './privacy';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Copies caller metadata with per-property isolation, for the one place a
|
|
6
|
+
* snapshot is genuinely required: a scope's defaults, captured once at
|
|
7
|
+
* construction so later mutation of the caller's object cannot change what
|
|
8
|
+
* future messages report.
|
|
9
|
+
*
|
|
10
|
+
* Caller objects are untrusted. A Proxy with a throwing `ownKeys` trap
|
|
11
|
+
* yields `undefined` — the whole object is dropped, payload-free. A throwing
|
|
12
|
+
* property getter keeps the key but substitutes {@link UNREADABLE_VALUE}, so
|
|
13
|
+
* redaction still sees the key, rejects it, and counts it; dropping the key
|
|
14
|
+
* here would make it disappear from `droppedMetadataCount` entirely.
|
|
15
|
+
*
|
|
16
|
+
* The copy has a null prototype. Plain-object assignment would route a
|
|
17
|
+
* `__proto__` key through `Object.prototype`'s legacy setter, silently
|
|
18
|
+
* losing that value and letting a JS caller reshape the snapshot.
|
|
19
|
+
*
|
|
20
|
+
* Call-site metadata is deliberately NOT snapshotted — it goes to
|
|
21
|
+
* `redactMetadata` as a source so its keys are validated before any value
|
|
22
|
+
* is read.
|
|
23
|
+
*/
|
|
24
|
+
export function safeSnapshotMetadata(
|
|
25
|
+
metadata: LogMetadata | undefined
|
|
26
|
+
): LogMetadata | undefined {
|
|
27
|
+
if (!metadata) return undefined;
|
|
28
|
+
let keys: string[];
|
|
29
|
+
try {
|
|
30
|
+
keys = Object.keys(metadata);
|
|
31
|
+
} catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const result: LogMetadata = Object.create(null);
|
|
35
|
+
let count = 0;
|
|
36
|
+
for (const key of keys) {
|
|
37
|
+
let value: LogMetadata[string];
|
|
38
|
+
try {
|
|
39
|
+
const read = metadata[key] as LogMetadata[string] | undefined;
|
|
40
|
+
// An `undefined` value is a rejected value, not an absent key. Skipping
|
|
41
|
+
// it here would make the key vanish before redaction could count it,
|
|
42
|
+
// so scope defaults would under-report drops that direct metadata
|
|
43
|
+
// reports correctly.
|
|
44
|
+
value = read === undefined ? UNREADABLE_VALUE : read;
|
|
45
|
+
} catch {
|
|
46
|
+
value = UNREADABLE_VALUE;
|
|
47
|
+
}
|
|
48
|
+
result[key] = value;
|
|
49
|
+
count += 1;
|
|
50
|
+
}
|
|
51
|
+
return count > 0 ? result : undefined;
|
|
52
|
+
}
|