@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 CHANGED
@@ -96,11 +96,18 @@ In-memory circular buffer. Default size: 1000 entries. No persistence.
96
96
 
97
97
  ### File Transport
98
98
 
99
- Rolling daily log files. JSONL format.
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;AAM9B,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,2CAqCzE;AAED,wBAAgB,SAAS,IAAI,MAAM,CAMlC"}
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, useMemo, useRef } from 'react';
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
- // Stable ref to config so the effect doesn't re-run on every render
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
- // Create logger once, re-create if destroyed by Strict Mode remount
10
- const logger = useMemo(() => createLogger(configRef.current), []);
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
- logger.destroy();
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,OAAO,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAC7E,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAExC,MAAM,aAAa,GAAG,aAAa,CAAgB,IAAI,CAAC,CAAA;AAOxD,MAAM,UAAU,eAAe,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAwB;IACxE,oEAAoE;IACpE,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAA;IAChC,SAAS,CAAC,OAAO,GAAG,MAAM,CAAA;IAE1B,oEAAoE;IACpE,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,CAAA;IAEjE,SAAS,CAAC,GAAG,EAAE;QACb,sCAAsC;QACtC,IAAI,WAAqC,CAAA;QACzC,IAAI,CAAC;YACH,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;YACzC,IAAI,MAAM,IAAI,OAAO,MAAM,CAAC,SAAS,KAAK,UAAU,EAAE,CAAC;gBACrD,WAAW,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,KAA2C,EAAE,EAAE;oBAC7E,IAAI,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;wBAC1B,MAAM,YAAY,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;wBAC1D,MAAM,CAAC,SAAS,CAAC,YAAY,CAAC,IAAI,CAAC,CAAA;oBACrC,CAAC;gBACH,CAAC,CAAC,CAAA;YACJ,CAAC;QACH,CAAC;QACD,MAAM,CAAC;YACL,uDAAuD;QACzD,CAAC;QAED,OAAO,GAAG,EAAE;YACV,WAAW,EAAE,EAAE,CAAA;YACf,MAAM,CAAC,OAAO,EAAE,CAAA;QAClB,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAA;IAEZ,OAAO,CACL,KAAC,aAAa,CAAC,QAAQ,IAAC,KAAK,EAAE,MAAM,YAClC,QAAQ,GACc,CAC1B,CAAA;AACH,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,MAAM,MAAM,GAAG,UAAU,CAAC,aAAa,CAAC,CAAA;IACxC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAA;IACpE,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC"}
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';
@@ -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;AAGtC,OAAO,EAAE,uBAAuB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAE3F,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,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"}
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;AACvE,SAAS;AACT,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAIvC,sDAAsD;AACtD,OAAO,EAAE,uBAAuB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAC3F,8CAA8C;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,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"}
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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,QAAQ,EACR,SAAS,EACT,YAAY,EAIb,MAAM,SAAS,CAAA;AAwChB,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;IAE1C,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,CAgJzD"}
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 ?? (dev ? 'debug' : 'info');
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
- transports.push(new FileTransport(fileConfig));
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
- return {
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
- function log(level, message, data) {
83
- if (LEVEL_ORDER[level] < minLevelNum)
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
- // Console output in dev
87
- if (dev) {
88
- const consoleFn = level === 'error'
89
- ? console.error
90
- : level === 'warn'
91
- ? console.warn
92
- : level === 'debug'
93
- ? console.debug
94
- : console.log;
95
- consoleFn(`[${level.toUpperCase()}] ${message}`, data ?? '');
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
- // Route to all transports (non-blocking)
98
- for (const transport of transports) {
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
- log('error', error.message, {
111
- stack: error.stack,
112
- name: error.name,
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
- config.onException(error, context);
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
- const entries = ringBuffer.getEntries(filter);
140
- return JSON.stringify(entries, null, 2);
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
  }