@mongrov/core 0.5.1 → 0.7.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/README.md +10 -3
- package/dist/context/logging-provider.d.ts.map +1 -1
- package/dist/context/logging-provider.js +42 -6
- package/dist/context/logging-provider.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/internal-log.d.ts +31 -0
- package/dist/internal-log.d.ts.map +1 -0
- package/dist/internal-log.js +50 -0
- package/dist/internal-log.js.map +1 -0
- package/dist/logger.d.ts +15 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +256 -30
- package/dist/logger.js.map +1 -1
- package/dist/network-state.d.ts +6 -0
- package/dist/network-state.d.ts.map +1 -1
- package/dist/network-state.js +43 -5
- package/dist/network-state.js.map +1 -1
- package/dist/offline-queue.d.ts +6 -0
- package/dist/offline-queue.d.ts.map +1 -1
- package/dist/offline-queue.js +50 -9
- package/dist/offline-queue.js.map +1 -1
- package/dist/redact.d.ts +13 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +43 -0
- package/dist/redact.js.map +1 -0
- package/dist/transports/file.d.ts +49 -2
- package/dist/transports/file.d.ts.map +1 -1
- package/dist/transports/file.js +228 -55
- package/dist/transports/file.js.map +1 -1
- package/dist/transports/webhook.d.ts +11 -1
- package/dist/transports/webhook.d.ts.map +1 -1
- package/dist/transports/webhook.js +44 -8
- package/dist/transports/webhook.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,11 +96,18 @@ In-memory circular buffer. Default size: 1000 entries. No persistence.
|
|
|
96
96
|
|
|
97
97
|
### File Transport
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
JSONL files, one per UTC hour (`logs-YYYY-MM-DDTHH-NNN.txt`), with a new `NNN`
|
|
100
|
+
when a file passes `maxFileSizeKB`. On expo-file-system 19+ every batch is a true
|
|
101
|
+
append (the file is never read to be written), so a kill mid-write can cut at most
|
|
102
|
+
the last line. Older expo-file-system falls back to read + rewrite. Cleanup deletes
|
|
103
|
+
whole files only: past `retentionDays`, then oldest first until under `maxSizeMB`;
|
|
104
|
+
the file being written is never touched. `LoggingProvider` flushes when the app
|
|
105
|
+
goes to the background. `exportFileLogs()` returns every retained line, oldest first,
|
|
106
|
+
including pre-0.7 `logs-YYYY-MM-DD.txt` day files.
|
|
100
107
|
|
|
101
108
|
```typescript
|
|
102
|
-
{ file: true } // defaults
|
|
103
|
-
{ file: { maxSizeMB: 10, retentionDays: 14 } }
|
|
109
|
+
{ file: true } // defaults: 5 MB, 7 days, 1024 KB per file
|
|
110
|
+
{ file: { maxSizeMB: 10, retentionDays: 14, maxFileSizeKB: 512 } }
|
|
104
111
|
```
|
|
105
112
|
|
|
106
113
|
### Webhook Transport
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logging-provider.d.ts","sourceRoot":"","sources":["../../src/context/logging-provider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAC5C,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"logging-provider.d.ts","sourceRoot":"","sources":["../../src/context/logging-provider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAC5C,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAQ9B,UAAU,oBAAoB;IAC5B,MAAM,EAAE,YAAY,CAAA;IACpB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B;AAED,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,oBAAoB,2CA0EzE;AAED,wBAAgB,SAAS,IAAI,MAAM,CAMlC"}
|
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useContext, useEffect,
|
|
2
|
+
import { createContext, useContext, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { AppState } from 'react-native';
|
|
3
4
|
import { createLogger } from '../logger';
|
|
5
|
+
import { setNetworkStateLogger } from '../network-state';
|
|
4
6
|
const LoggerContext = createContext(null);
|
|
5
7
|
export function LoggingProvider({ config, children }) {
|
|
6
|
-
//
|
|
8
|
+
// The config is read when a logger is created; later changes to it are not
|
|
9
|
+
// applied to a live logger.
|
|
7
10
|
const configRef = useRef(config);
|
|
8
11
|
configRef.current = config;
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
const [logger, setLogger] = useState(() => createLogger(config));
|
|
13
|
+
// Set by the effect cleanup. A cleanup is not always a real unmount: Strict
|
|
14
|
+
// Mode and Fast Refresh run cleanup then the effect again on the SAME
|
|
15
|
+
// component, which used to keep handing out a destroyed logger (webhook
|
|
16
|
+
// timer stopped, offline queue gone). A re-run that finds the logger
|
|
17
|
+
// destroyed replaces it.
|
|
18
|
+
const destroyedRef = useRef(false);
|
|
11
19
|
useEffect(() => {
|
|
20
|
+
if (destroyedRef.current) {
|
|
21
|
+
destroyedRef.current = false;
|
|
22
|
+
// eslint-disable-next-line react-hooks-extra/no-direct-set-state-in-use-effect -- replacing a destroyed logger is the point of this branch
|
|
23
|
+
setLogger(createLogger(configRef.current));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Connectivity changes and polling failures land in this logger.
|
|
27
|
+
setNetworkStateLogger(logger);
|
|
12
28
|
// Try to track screen via expo-router
|
|
13
29
|
let unsubscribe;
|
|
14
30
|
try {
|
|
@@ -23,13 +39,33 @@ export function LoggingProvider({ config, children }) {
|
|
|
23
39
|
}
|
|
24
40
|
}
|
|
25
41
|
catch {
|
|
26
|
-
// expo-router not available — screen tracking disabled
|
|
42
|
+
// expected: expo-router not available — screen tracking disabled
|
|
27
43
|
}
|
|
28
44
|
return () => {
|
|
29
45
|
unsubscribe?.();
|
|
30
|
-
|
|
46
|
+
destroyedRef.current = true;
|
|
47
|
+
setNetworkStateLogger(null);
|
|
48
|
+
logger.destroy().catch((error) => {
|
|
49
|
+
// The logger is going away; its last batch may not reach the webhook.
|
|
50
|
+
if (__DEV__)
|
|
51
|
+
console.warn('[Logger] destroy failed', String(error));
|
|
52
|
+
});
|
|
31
53
|
};
|
|
32
54
|
}, [logger]);
|
|
55
|
+
// iOS may suspend or kill a backgrounded app at any moment (a BLE sync
|
|
56
|
+
// wakes it, then it is gone), so whatever is still batched goes to disk on
|
|
57
|
+
// the way out rather than with the next line.
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const subscription = AppState.addEventListener('change', (state) => {
|
|
60
|
+
if (state !== 'background' && state !== 'inactive')
|
|
61
|
+
return;
|
|
62
|
+
logger.flush().catch((error) => {
|
|
63
|
+
if (__DEV__)
|
|
64
|
+
console.warn('[Logger] flush on background failed', String(error));
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
return () => subscription.remove();
|
|
68
|
+
}, [logger]);
|
|
33
69
|
return (_jsx(LoggerContext.Provider, { value: logger, children: children }));
|
|
34
70
|
}
|
|
35
71
|
export function useLogger() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logging-provider.js","sourceRoot":"","sources":["../../src/context/logging-provider.tsx"],"names":[],"mappings":";AAGA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,
|
|
1
|
+
{"version":3,"file":"logging-provider.js","sourceRoot":"","sources":["../../src/context/logging-provider.tsx"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export { LoggingProvider, useLogger } from './context/logging-provider';
|
|
2
|
+
export type { InternalLogger } from './internal-log';
|
|
2
3
|
export { createLogger } from './logger';
|
|
3
4
|
export type { Logger } from './logger';
|
|
4
|
-
export { addNetworkStateListener, getNetworkState, useNetworkState } from './network-state';
|
|
5
|
+
export { addNetworkStateListener, getNetworkState, setNetworkStateLogger, useNetworkState } from './network-state';
|
|
5
6
|
export { OfflineQueue } from './offline-queue';
|
|
7
|
+
export { defaultRedact, REDACTED } from './redact';
|
|
6
8
|
export { FileTransport } from './transports/file';
|
|
7
9
|
export { RingBufferTransport } from './transports/ring-buffer';
|
|
8
10
|
export { WebhookTransport } from './transports/webhook';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEvE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAEvC,YAAY,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEvE,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAEpD,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAEvC,YAAY,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAEtC,OAAO,EAAE,uBAAuB,EAAE,eAAe,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAGlH,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE9C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAGjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAE9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAGvD,YAAY,EACV,UAAU,EACV,UAAU,EACV,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,YAAY,EACZ,gBAAgB,EAChB,aAAa,GACd,MAAM,SAAS,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -3,9 +3,11 @@ export { LoggingProvider, useLogger } from './context/logging-provider';
|
|
|
3
3
|
// Logger
|
|
4
4
|
export { createLogger } from './logger';
|
|
5
5
|
// Network (used internally, exported for convenience)
|
|
6
|
-
export { addNetworkStateListener, getNetworkState, useNetworkState } from './network-state';
|
|
6
|
+
export { addNetworkStateListener, getNetworkState, setNetworkStateLogger, useNetworkState } from './network-state';
|
|
7
7
|
// Offline queue (exported for advanced usage)
|
|
8
8
|
export { OfflineQueue } from './offline-queue';
|
|
9
|
+
// Redaction (the default `LoggerConfig.redact`, for composing app rules)
|
|
10
|
+
export { defaultRedact, REDACTED } from './redact';
|
|
9
11
|
export { FileTransport } from './transports/file';
|
|
10
12
|
// Transports (for custom composition)
|
|
11
13
|
export { RingBufferTransport } from './transports/ring-buffer';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,UAAU;AACV,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,UAAU;AACV,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAGvE,SAAS;AACT,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAGvC,sDAAsD;AACtD,OAAO,EAAE,uBAAuB,EAAE,eAAe,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAElH,8CAA8C;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,yEAAyE;AACzE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEjD,sCAAsC;AACtC,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAE9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How core's own parts (transports, offline queue, network state) report
|
|
3
|
+
* their failures. `createLogger` hands each part a logger that writes to every
|
|
4
|
+
* transport EXCEPT the one reporting — so a failing file write lands in the
|
|
5
|
+
* ring buffer instead of failing again and reporting again.
|
|
6
|
+
*/
|
|
7
|
+
export interface InternalLogger {
|
|
8
|
+
debug: (message: string, data?: Record<string, unknown>) => void;
|
|
9
|
+
info: (message: string, data?: Record<string, unknown>) => void;
|
|
10
|
+
warn: (message: string, data?: Record<string, unknown>) => void;
|
|
11
|
+
error: (message: string, data?: Record<string, unknown>) => void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Fallback for a part used on its own, outside `createLogger`: failures to
|
|
15
|
+
* the dev console only (release has no reader for console output).
|
|
16
|
+
*/
|
|
17
|
+
export declare function devConsoleLogger(): InternalLogger;
|
|
18
|
+
/**
|
|
19
|
+
* Collapses a run of identical failures into one report at its start and one
|
|
20
|
+
* at its end. A dead disk or a webhook answering 401 fails on every batch;
|
|
21
|
+
* reporting each would flood the ring buffer with the same line.
|
|
22
|
+
*/
|
|
23
|
+
export declare class FailureGate {
|
|
24
|
+
private failures;
|
|
25
|
+
/** True on the first failure of a run — the one worth reporting. */
|
|
26
|
+
fail(): boolean;
|
|
27
|
+
/** Failures in the run that just ended (0 if none). */
|
|
28
|
+
recover(): number;
|
|
29
|
+
get failing(): boolean;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=internal-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal-log.d.ts","sourceRoot":"","sources":["../src/internal-log.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;IAChE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;IAC/D,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;IAC/D,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;CACjE;AAWD;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,cAAc,CAajD;AAED;;;;GAIG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAI;IAEpB,oEAAoE;IACpE,IAAI,IAAI,OAAO;IAKf,uDAAuD;IACvD,OAAO,IAAI,MAAM;IAMjB,IAAI,OAAO,IAAI,OAAO,CAErB;CACF"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
function isDev() {
|
|
2
|
+
try {
|
|
3
|
+
return typeof __DEV__ !== 'undefined' ? __DEV__ : false;
|
|
4
|
+
}
|
|
5
|
+
catch {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Fallback for a part used on its own, outside `createLogger`: failures to
|
|
11
|
+
* the dev console only (release has no reader for console output).
|
|
12
|
+
*/
|
|
13
|
+
export function devConsoleLogger() {
|
|
14
|
+
const out = (fn) => (message, data) => {
|
|
15
|
+
if (isDev())
|
|
16
|
+
fn(message, data ?? '');
|
|
17
|
+
};
|
|
18
|
+
const none = () => { };
|
|
19
|
+
return {
|
|
20
|
+
debug: none,
|
|
21
|
+
info: none,
|
|
22
|
+
warn: out(console.warn),
|
|
23
|
+
error: out(console.error),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Collapses a run of identical failures into one report at its start and one
|
|
28
|
+
* at its end. A dead disk or a webhook answering 401 fails on every batch;
|
|
29
|
+
* reporting each would flood the ring buffer with the same line.
|
|
30
|
+
*/
|
|
31
|
+
export class FailureGate {
|
|
32
|
+
constructor() {
|
|
33
|
+
this.failures = 0;
|
|
34
|
+
}
|
|
35
|
+
/** True on the first failure of a run — the one worth reporting. */
|
|
36
|
+
fail() {
|
|
37
|
+
this.failures++;
|
|
38
|
+
return this.failures === 1;
|
|
39
|
+
}
|
|
40
|
+
/** Failures in the run that just ended (0 if none). */
|
|
41
|
+
recover() {
|
|
42
|
+
const n = this.failures;
|
|
43
|
+
this.failures = 0;
|
|
44
|
+
return n;
|
|
45
|
+
}
|
|
46
|
+
get failing() {
|
|
47
|
+
return this.failures > 0;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=internal-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal-log.js","sourceRoot":"","sources":["../src/internal-log.ts"],"names":[],"mappings":"AAaA,SAAS,KAAK;IACZ,IAAI,CAAC;QACH,OAAO,OAAO,OAAO,KAAK,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAA;IACzD,CAAC;IACD,MAAM,CAAC;QACL,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB;IAC9B,MAAM,GAAG,GAAG,CAAC,EAAgC,EAAE,EAAE,CAC/C,CAAC,OAAe,EAAE,IAA8B,EAAE,EAAE;QAClD,IAAI,KAAK,EAAE;YACT,EAAE,CAAC,OAAO,EAAE,IAAI,IAAI,EAAE,CAAC,CAAA;IAC3B,CAAC,CAAA;IACH,MAAM,IAAI,GAAG,GAAG,EAAE,GAAE,CAAC,CAAA;IACrB,OAAO;QACL,KAAK,EAAE,IAAI;QACX,IAAI,EAAE,IAAI;QACV,IAAI,EAAE,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC;QACvB,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC;KAC1B,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,OAAO,WAAW;IAAxB;QACU,aAAQ,GAAG,CAAC,CAAA;IAkBtB,CAAC;IAhBC,oEAAoE;IACpE,IAAI;QACF,IAAI,CAAC,QAAQ,EAAE,CAAA;QACf,OAAO,IAAI,CAAC,QAAQ,KAAK,CAAC,CAAA;IAC5B,CAAC;IAED,uDAAuD;IACvD,OAAO;QACL,MAAM,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAA;QACvB,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAA;QACjB,OAAO,CAAC,CAAA;IACV,CAAC;IAED,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAA;IAC1B,CAAC;CACF"}
|
package/dist/logger.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { LogEntry, LogFilter, LoggerConfig } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* A JSON-safe snapshot of `data`, taken when the line is logged.
|
|
4
|
+
*
|
|
5
|
+
* Entries used to hold the caller's object by reference, so the export showed
|
|
6
|
+
* whatever it had been mutated into later; an `Error` exported as `{}` (its
|
|
7
|
+
* fields are non-enumerable); and one circular reference or BigInt made
|
|
8
|
+
* `exportLogs` throw, which emptied the whole export.
|
|
9
|
+
*/
|
|
10
|
+
export declare function sanitizeLogData(data?: Record<string, unknown>): Record<string, unknown> | undefined;
|
|
2
11
|
export interface Logger {
|
|
3
12
|
debug: (message: string, data?: Record<string, unknown>) => void;
|
|
4
13
|
info: (message: string, data?: Record<string, unknown>) => void;
|
|
@@ -10,6 +19,12 @@ export interface Logger {
|
|
|
10
19
|
setContext: (key: string, value: string) => void;
|
|
11
20
|
getLogs: (filter?: LogFilter) => LogEntry[];
|
|
12
21
|
exportLogs: (filter?: LogFilter) => string;
|
|
22
|
+
/**
|
|
23
|
+
* Every line the file transport has kept (all retained days, oldest first,
|
|
24
|
+
* one JSON entry per line). Survives restarts and crashes, unlike the ring
|
|
25
|
+
* buffer. Rejects when the file transport is not configured.
|
|
26
|
+
*/
|
|
27
|
+
exportFileLogs: () => Promise<string>;
|
|
13
28
|
flush: () => Promise<void>;
|
|
14
29
|
destroy: () => Promise<void>;
|
|
15
30
|
}
|
package/dist/logger.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAGV,QAAQ,EACR,SAAS,EACT,YAAY,EAIb,MAAM,SAAS,CAAA;AAgHhB;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAYnG;AAsDD,MAAM,WAAW,MAAM;IACrB,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;IAChE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;IAC/D,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;IAC/D,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;IAChE,gBAAgB,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;IAE3E,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,SAAS,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAEhD,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,SAAS,KAAK,QAAQ,EAAE,CAAA;IAC3C,UAAU,EAAE,CAAC,MAAM,CAAC,EAAE,SAAS,KAAK,MAAM,CAAA;IAC1C;;;;OAIG;IACH,cAAc,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;IAErC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7B;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAgPzD"}
|
package/dist/logger.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { FailureGate } from './internal-log';
|
|
2
|
+
import { defaultRedact } from './redact';
|
|
1
3
|
import { FileTransport } from './transports/file';
|
|
2
4
|
import { RingBufferTransport } from './transports/ring-buffer';
|
|
3
5
|
import { WebhookTransport } from './transports/webhook';
|
|
@@ -7,6 +9,142 @@ const LEVEL_ORDER = {
|
|
|
7
9
|
warn: 2,
|
|
8
10
|
error: 3,
|
|
9
11
|
};
|
|
12
|
+
function isLogLevel(value) {
|
|
13
|
+
return typeof value === 'string' && value in LEVEL_ORDER;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* `minLevel` arrives from env in practice (`EXPO_PUBLIC_LOG_LEVEL`), where
|
|
17
|
+
* `'INFO'` or a typo is easy. An unknown level used to index LEVEL_ORDER to
|
|
18
|
+
* `undefined`, every comparison came out false, and nothing was filtered.
|
|
19
|
+
*/
|
|
20
|
+
function resolveMinLevel(configured, fallback) {
|
|
21
|
+
if (configured == null || configured === '')
|
|
22
|
+
return fallback;
|
|
23
|
+
const normalized = typeof configured === 'string' ? configured.trim().toLowerCase() : configured;
|
|
24
|
+
if (isLogLevel(normalized))
|
|
25
|
+
return normalized;
|
|
26
|
+
if (isDev())
|
|
27
|
+
console.warn(`[Logger] unknown minLevel ${JSON.stringify(configured)}; using '${fallback}'`);
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
const MAX_DEPTH = 6;
|
|
31
|
+
const MAX_CAUSE_DEPTH = 3;
|
|
32
|
+
// One logged readings array or document used to land in full in the file and
|
|
33
|
+
// the webhook. Entries are capped instead; the marker says how much was cut.
|
|
34
|
+
const MAX_STRING_LENGTH = 2000;
|
|
35
|
+
const MAX_STACK_LENGTH = 8000;
|
|
36
|
+
const MAX_ARRAY_ITEMS = 50;
|
|
37
|
+
function capString(text, max = MAX_STRING_LENGTH) {
|
|
38
|
+
return text.length > max ? `${text.slice(0, max)}…[+${text.length - max} chars]` : text;
|
|
39
|
+
}
|
|
40
|
+
function serializeError(error, seen, depth, causeDepth) {
|
|
41
|
+
const out = { name: error.name, message: capString(error.message) };
|
|
42
|
+
if (error.stack)
|
|
43
|
+
out.stack = capString(error.stack, MAX_STACK_LENGTH);
|
|
44
|
+
// Own enumerable fields (`code`, `status`, ...) carry the useful detail on
|
|
45
|
+
// native and DB errors.
|
|
46
|
+
for (const [key, value] of Object.entries(error)) {
|
|
47
|
+
if (!(key in out))
|
|
48
|
+
out[key] = sanitizeValue(value, seen, depth + 1);
|
|
49
|
+
}
|
|
50
|
+
const cause = error.cause;
|
|
51
|
+
if (cause !== undefined) {
|
|
52
|
+
out.cause = causeDepth >= MAX_CAUSE_DEPTH
|
|
53
|
+
? '[Cause depth exceeded]'
|
|
54
|
+
: cause instanceof Error
|
|
55
|
+
? serializeError(cause, seen, depth + 1, causeDepth + 1)
|
|
56
|
+
: sanitizeValue(cause, seen, depth + 1);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
function sanitizeValue(value, seen, depth) {
|
|
61
|
+
if (value === null || value === undefined)
|
|
62
|
+
return value;
|
|
63
|
+
switch (typeof value) {
|
|
64
|
+
case 'string':
|
|
65
|
+
return capString(value);
|
|
66
|
+
case 'number':
|
|
67
|
+
case 'boolean':
|
|
68
|
+
return value;
|
|
69
|
+
case 'bigint':
|
|
70
|
+
return value.toString();
|
|
71
|
+
case 'function':
|
|
72
|
+
return `[Function ${value.name || 'anonymous'}]`;
|
|
73
|
+
case 'symbol':
|
|
74
|
+
return value.toString();
|
|
75
|
+
}
|
|
76
|
+
const obj = value;
|
|
77
|
+
if (seen.has(obj))
|
|
78
|
+
return '[Circular]';
|
|
79
|
+
if (depth >= MAX_DEPTH)
|
|
80
|
+
return '[Depth exceeded]';
|
|
81
|
+
seen.add(obj);
|
|
82
|
+
try {
|
|
83
|
+
if (obj instanceof Error)
|
|
84
|
+
return serializeError(obj, seen, depth, 0);
|
|
85
|
+
if (obj instanceof Date)
|
|
86
|
+
return Number.isNaN(obj.getTime()) ? 'Invalid Date' : obj.toISOString();
|
|
87
|
+
if (Array.isArray(obj)) {
|
|
88
|
+
const items = obj.slice(0, MAX_ARRAY_ITEMS).map(item => sanitizeValue(item, seen, depth + 1));
|
|
89
|
+
if (obj.length > MAX_ARRAY_ITEMS)
|
|
90
|
+
items.push(`[+${obj.length - MAX_ARRAY_ITEMS} more]`);
|
|
91
|
+
return items;
|
|
92
|
+
}
|
|
93
|
+
const out = {};
|
|
94
|
+
for (const [key, item] of Object.entries(obj))
|
|
95
|
+
out[key] = sanitizeValue(item, seen, depth + 1);
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
// Only ancestors count as circular; the same object twice side by side
|
|
100
|
+
// is a repeat, not a cycle.
|
|
101
|
+
seen.delete(obj);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* A JSON-safe snapshot of `data`, taken when the line is logged.
|
|
106
|
+
*
|
|
107
|
+
* Entries used to hold the caller's object by reference, so the export showed
|
|
108
|
+
* whatever it had been mutated into later; an `Error` exported as `{}` (its
|
|
109
|
+
* fields are non-enumerable); and one circular reference or BigInt made
|
|
110
|
+
* `exportLogs` throw, which emptied the whole export.
|
|
111
|
+
*/
|
|
112
|
+
export function sanitizeLogData(data) {
|
|
113
|
+
if (data === undefined || data === null)
|
|
114
|
+
return undefined;
|
|
115
|
+
try {
|
|
116
|
+
const out = sanitizeValue(data, new WeakSet(), 0);
|
|
117
|
+
return out !== null && typeof out === 'object' && !Array.isArray(out)
|
|
118
|
+
? out
|
|
119
|
+
: { value: out };
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
return { unserializable: String(error) };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function stringifyEntries(entries) {
|
|
126
|
+
try {
|
|
127
|
+
return JSON.stringify(entries, null, 2);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Per entry, so one bad line cannot empty the export.
|
|
131
|
+
const parts = entries.map((entry) => {
|
|
132
|
+
try {
|
|
133
|
+
return JSON.stringify(entry, null, 2);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
return JSON.stringify({
|
|
137
|
+
id: entry.id,
|
|
138
|
+
timestamp: entry.timestamp,
|
|
139
|
+
level: entry.level,
|
|
140
|
+
message: entry.message,
|
|
141
|
+
data: { unserializable: String(error) },
|
|
142
|
+
}, null, 2);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
return `[\n${parts.join(',\n')}\n]`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
10
148
|
let counter = 0;
|
|
11
149
|
function generateId() {
|
|
12
150
|
return `${Date.now()}-${++counter}`;
|
|
@@ -33,10 +171,11 @@ function isDev() {
|
|
|
33
171
|
}
|
|
34
172
|
export function createLogger(config) {
|
|
35
173
|
const dev = isDev();
|
|
36
|
-
const minLevel = config.minLevel
|
|
174
|
+
const minLevel = resolveMinLevel(config.minLevel, dev ? 'debug' : 'info');
|
|
37
175
|
const minLevelNum = LEVEL_ORDER[minLevel];
|
|
38
176
|
const sessionId = generateSessionId();
|
|
39
177
|
const platform = getPlatform();
|
|
178
|
+
const redact = config.redact === false ? null : config.redact ?? defaultRedact;
|
|
40
179
|
// Mutable context
|
|
41
180
|
const ctx = {
|
|
42
181
|
sessionId,
|
|
@@ -45,10 +184,19 @@ export function createLogger(config) {
|
|
|
45
184
|
platform,
|
|
46
185
|
};
|
|
47
186
|
const extraContext = {};
|
|
187
|
+
// Per-transport failure runs, so a transport that rejects every entry
|
|
188
|
+
// reports once rather than once per line.
|
|
189
|
+
const sendFailures = new Map();
|
|
190
|
+
// Set while a core part's report is being routed. A report raised from
|
|
191
|
+
// inside another report is dropped: core must never log about logging in
|
|
192
|
+
// a loop.
|
|
193
|
+
let reporting = false;
|
|
48
194
|
// Initialize transports
|
|
49
195
|
const transports = [];
|
|
50
196
|
let ringBuffer = null;
|
|
197
|
+
let fileTransport = null;
|
|
51
198
|
let webhookTransport = null;
|
|
199
|
+
let fileUnavailable = null;
|
|
52
200
|
// Ring buffer
|
|
53
201
|
if (config.ringBuffer) {
|
|
54
202
|
const rbConfig = typeof config.ringBuffer === 'boolean' ? {} : config.ringBuffer;
|
|
@@ -58,62 +206,134 @@ export function createLogger(config) {
|
|
|
58
206
|
// File transport
|
|
59
207
|
if (config.file) {
|
|
60
208
|
const fileConfig = typeof config.file === 'boolean' ? {} : config.file;
|
|
61
|
-
|
|
209
|
+
try {
|
|
210
|
+
fileTransport = new FileTransport(fileConfig, internalLogger('file'));
|
|
211
|
+
transports.push(fileTransport);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
// expo-file-system missing: logging still works without the file.
|
|
215
|
+
// Reported once the other transports exist (below).
|
|
216
|
+
fileUnavailable = String(error);
|
|
217
|
+
}
|
|
62
218
|
}
|
|
63
219
|
// Webhook transport
|
|
64
220
|
if (config.webhook) {
|
|
65
|
-
webhookTransport = new WebhookTransport(config.webhook);
|
|
221
|
+
webhookTransport = new WebhookTransport(config.webhook, internalLogger('webhook'));
|
|
66
222
|
transports.push(webhookTransport);
|
|
67
223
|
}
|
|
68
224
|
// Custom transports
|
|
69
225
|
if (config.transports) {
|
|
70
226
|
transports.push(...config.transports);
|
|
71
227
|
}
|
|
228
|
+
if (fileUnavailable !== null)
|
|
229
|
+
log('warn', '[Logger] file transport unavailable; file log disabled', { error: fileUnavailable });
|
|
72
230
|
function buildEntry(level, message, data) {
|
|
73
|
-
|
|
231
|
+
const entry = {
|
|
74
232
|
id: generateId(),
|
|
75
233
|
timestamp: new Date().toISOString(),
|
|
76
234
|
level,
|
|
77
|
-
message,
|
|
78
|
-
data,
|
|
235
|
+
message: capString(String(message)),
|
|
236
|
+
data: sanitizeLogData(data),
|
|
79
237
|
context: { ...ctx, ...extraContext },
|
|
80
238
|
};
|
|
239
|
+
if (!redact)
|
|
240
|
+
return entry;
|
|
241
|
+
try {
|
|
242
|
+
return redact(entry);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
// A throwing app hook must not leak the line it failed on, nor throw
|
|
246
|
+
// into the caller: fall back to the default rules and say so.
|
|
247
|
+
const safe = defaultRedact(entry);
|
|
248
|
+
return { ...safe, data: { ...safe.data, redactFailed: String(error) } };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function route(entry, skip) {
|
|
252
|
+
for (const transport of transports) {
|
|
253
|
+
if (transport.name === skip)
|
|
254
|
+
continue;
|
|
255
|
+
transport.send([entry]).then(() => {
|
|
256
|
+
const failed = sendFailures.get(transport.name)?.recover() ?? 0;
|
|
257
|
+
if (failed > 0)
|
|
258
|
+
report(transport.name, 'info', '[Logger] transport recovered', { transport: transport.name, failedSends: failed });
|
|
259
|
+
}, (error) => {
|
|
260
|
+
let gate = sendFailures.get(transport.name);
|
|
261
|
+
if (!gate) {
|
|
262
|
+
gate = new FailureGate();
|
|
263
|
+
sendFailures.set(transport.name, gate);
|
|
264
|
+
}
|
|
265
|
+
if (gate.fail())
|
|
266
|
+
report(transport.name, 'error', '[Logger] transport send failed', { transport: transport.name, error: String(error) });
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function toConsole(level, message, data) {
|
|
271
|
+
const consoleFn = level === 'error'
|
|
272
|
+
? console.error
|
|
273
|
+
: level === 'warn'
|
|
274
|
+
? console.warn
|
|
275
|
+
: level === 'debug'
|
|
276
|
+
? console.debug
|
|
277
|
+
: console.log;
|
|
278
|
+
consoleFn(`[${level.toUpperCase()}] ${message}`, data ?? '');
|
|
81
279
|
}
|
|
82
|
-
|
|
83
|
-
|
|
280
|
+
/**
|
|
281
|
+
* `force` skips the `minLevel` filter. `error` is never filtered either:
|
|
282
|
+
* it is the top level, and stays recorded whatever the config says.
|
|
283
|
+
*/
|
|
284
|
+
function log(level, message, data, force = false) {
|
|
285
|
+
if (!force && level !== 'error' && LEVEL_ORDER[level] < minLevelNum)
|
|
84
286
|
return;
|
|
85
287
|
const entry = buildEntry(level, message, data);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
288
|
+
if (dev)
|
|
289
|
+
toConsole(level, entry.message, entry.data);
|
|
290
|
+
route(entry);
|
|
291
|
+
}
|
|
292
|
+
/** A report from core's own `source` transport: routed everywhere else. */
|
|
293
|
+
function report(source, level, message, data) {
|
|
294
|
+
if (reporting || LEVEL_ORDER[level] < minLevelNum)
|
|
295
|
+
return;
|
|
296
|
+
reporting = true;
|
|
297
|
+
try {
|
|
298
|
+
const entry = buildEntry(level, message, data);
|
|
299
|
+
if (dev)
|
|
300
|
+
toConsole(level, entry.message, entry.data);
|
|
301
|
+
route(entry, source);
|
|
96
302
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
transport.send([entry]).catch(() => {
|
|
100
|
-
// Transport errors are non-fatal
|
|
101
|
-
});
|
|
303
|
+
finally {
|
|
304
|
+
reporting = false;
|
|
102
305
|
}
|
|
103
306
|
}
|
|
307
|
+
function internalLogger(source) {
|
|
308
|
+
return {
|
|
309
|
+
debug: (message, data) => report(source, 'debug', message, data),
|
|
310
|
+
info: (message, data) => report(source, 'info', message, data),
|
|
311
|
+
warn: (message, data) => report(source, 'warn', message, data),
|
|
312
|
+
error: (message, data) => report(source, 'error', message, data),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
104
315
|
const logger = {
|
|
105
316
|
debug: (message, data) => log('debug', message, data),
|
|
106
317
|
info: (message, data) => log('info', message, data),
|
|
107
318
|
warn: (message, data) => log('warn', message, data),
|
|
108
319
|
error: (message, data) => log('error', message, data),
|
|
109
320
|
captureException(error, context) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
321
|
+
// JS callers hand over whatever was thrown; a non-Error must not turn
|
|
322
|
+
// into an `undefined` message.
|
|
323
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
324
|
+
// Recorded whatever `minLevel` says.
|
|
325
|
+
log('error', err.message, {
|
|
326
|
+
stack: err.stack,
|
|
327
|
+
name: err.name,
|
|
113
328
|
...context,
|
|
114
|
-
});
|
|
329
|
+
}, true);
|
|
115
330
|
if (config.onException) {
|
|
116
|
-
|
|
331
|
+
try {
|
|
332
|
+
config.onException(err, context);
|
|
333
|
+
}
|
|
334
|
+
catch (hookError) {
|
|
335
|
+
log('error', '[Logger] onException hook threw', { error: String(hookError) }, true);
|
|
336
|
+
}
|
|
117
337
|
}
|
|
118
338
|
},
|
|
119
339
|
setUser(userId) {
|
|
@@ -136,10 +356,16 @@ export function createLogger(config) {
|
|
|
136
356
|
if (!ringBuffer) {
|
|
137
357
|
throw new Error('Ring buffer not configured. Enable ringBuffer in LoggerConfig.');
|
|
138
358
|
}
|
|
139
|
-
|
|
140
|
-
|
|
359
|
+
return stringifyEntries(ringBuffer.getEntries(filter));
|
|
360
|
+
},
|
|
361
|
+
async exportFileLogs() {
|
|
362
|
+
if (!fileTransport) {
|
|
363
|
+
throw new Error('File transport not configured. Enable file in LoggerConfig.');
|
|
364
|
+
}
|
|
365
|
+
return fileTransport.readAll();
|
|
141
366
|
},
|
|
142
367
|
async flush() {
|
|
368
|
+
await fileTransport?.flush();
|
|
143
369
|
if (webhookTransport) {
|
|
144
370
|
await webhookTransport.flush();
|
|
145
371
|
}
|