@pmate/log-sdk 0.1.0 → 0.2.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/dist/browser.d.ts +2 -0
- package/dist/browser.js +4 -0
- package/dist/crash.d.ts +9 -0
- package/dist/crash.js +34 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +22 -0
- package/dist/node.d.ts +3 -0
- package/dist/node.js +5 -0
- package/dist/react.d.ts +20 -2
- package/dist/react.js +17 -1
- package/package.json +1 -1
package/dist/browser.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { errorInput } from "./errors.js";
|
|
1
2
|
import { type BrowserLogOptions } from "@pmate/log";
|
|
2
3
|
import { type LogContext, type LogInput } from "./shared.js";
|
|
3
4
|
export type BrowserLoggerOptions = BrowserLogOptions & {
|
|
@@ -8,6 +9,7 @@ export type BrowserLoggerOptions = BrowserLogOptions & {
|
|
|
8
9
|
export type BrowserLogger = ReturnType<typeof createLogger>;
|
|
9
10
|
/** Own one instance per application document. Server imports never touch window. */
|
|
10
11
|
export declare function createLogger(options: BrowserLoggerOptions): {
|
|
12
|
+
captureError(error: unknown, context?: Parameters<typeof errorInput>[1]): void;
|
|
11
13
|
getContext: () => LogContext;
|
|
12
14
|
setProfileId(value?: string | null): void;
|
|
13
15
|
append(topic: string, input: LogInput | string): void;
|
package/dist/browser.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { errorInput } from "./errors.js";
|
|
1
2
|
import { createLogger as createTransport } from "@pmate/log";
|
|
2
3
|
import { createAccessSession } from "./access.js";
|
|
3
4
|
import { ACCESS_HEADER, PROFILE_HEADER, REQUEST_HEADER, validId, validateAppId, withContext } from "./shared.js";
|
|
@@ -61,6 +62,9 @@ export function createLogger(options) {
|
|
|
61
62
|
throw error;
|
|
62
63
|
}
|
|
63
64
|
return {
|
|
65
|
+
captureError(error, context) {
|
|
66
|
+
this.append("error", errorInput(error, context));
|
|
67
|
+
},
|
|
64
68
|
getContext: context,
|
|
65
69
|
setProfileId(value) {
|
|
66
70
|
if (closed)
|
package/dist/crash.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LogEntry } from "./shared.js";
|
|
2
|
+
/** Monitor (never swallow) fatal exceptions. Replay requires an acknowledged sink. */
|
|
3
|
+
export declare function installCrashReporter(options: {
|
|
4
|
+
directory: string;
|
|
5
|
+
send: (entry: LogEntry) => Promise<void>;
|
|
6
|
+
}): {
|
|
7
|
+
replay(): Promise<void>;
|
|
8
|
+
close(): void;
|
|
9
|
+
};
|
package/dist/crash.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { errorInput } from "./errors.js";
|
|
5
|
+
/** Monitor (never swallow) fatal exceptions. Replay requires an acknowledged sink. */
|
|
6
|
+
export function installCrashReporter(options) {
|
|
7
|
+
mkdirSync(options.directory, { recursive: true, mode: 0o700 });
|
|
8
|
+
const files = () => readdirSync(options.directory).filter(name => /^crash-[a-f0-9-]+\.json$/.test(name)).sort();
|
|
9
|
+
const monitor = (error, origin) => {
|
|
10
|
+
try {
|
|
11
|
+
const id = randomUUID();
|
|
12
|
+
const entry = { id, time: Date.now(), ...errorInput(error, { operation: origin, fatal: true }) };
|
|
13
|
+
// Bound on-disk admission while preserving the newest fatal diagnostic.
|
|
14
|
+
for (const name of files().slice(0, Math.max(0, files().length - 19)))
|
|
15
|
+
unlinkSync(join(options.directory, name));
|
|
16
|
+
writeFileSync(join(options.directory, `crash-${id}.json`), JSON.stringify(entry), { mode: 0o600, flag: "wx" });
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
process.stderr.write("PMate crash persistence failed\n");
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
process.on("uncaughtExceptionMonitor", monitor);
|
|
23
|
+
return {
|
|
24
|
+
async replay() {
|
|
25
|
+
for (const name of files()) {
|
|
26
|
+
const file = join(options.directory, name);
|
|
27
|
+
const entry = JSON.parse(readFileSync(file, "utf8"));
|
|
28
|
+
await options.send(entry);
|
|
29
|
+
unlinkSync(file); // Only after explicit acknowledgement; keep ID/time on retry.
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
close() { process.off("uncaughtExceptionMonitor", monitor); },
|
|
33
|
+
};
|
|
34
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type LogInput } from "./shared.js";
|
|
2
|
+
/** Bound diagnostic strings and strip URL parameters before transport. */
|
|
3
|
+
export declare function errorInput(error: unknown, context?: {
|
|
4
|
+
operation?: string;
|
|
5
|
+
componentStack?: string;
|
|
6
|
+
fatal?: boolean;
|
|
7
|
+
}): LogInput;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { redact } from "./shared.js";
|
|
2
|
+
/** Bound diagnostic strings and strip URL parameters before transport. */
|
|
3
|
+
export function errorInput(error, context = {}) {
|
|
4
|
+
const clean = (value) => String(redact(value)).replace(/https?:\/\/[^\s)]+/g, value => {
|
|
5
|
+
try {
|
|
6
|
+
const url = new URL(value);
|
|
7
|
+
return `${url.origin}${url.pathname}`;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return "[url]";
|
|
11
|
+
}
|
|
12
|
+
}).slice(0, 4000);
|
|
13
|
+
const value = error instanceof Error ? error : new Error("Non-Error exception");
|
|
14
|
+
return { data: {
|
|
15
|
+
event: context.fatal ? "runtime.crash" : "runtime.error",
|
|
16
|
+
errorName: clean(value.name), message: clean(value.message),
|
|
17
|
+
...(value.stack ? { stack: clean(value.stack) } : {}),
|
|
18
|
+
...(context.operation ? { operation: context.operation } : {}),
|
|
19
|
+
...(context.componentStack ? { componentStack: clean(context.componentStack) } : {}),
|
|
20
|
+
fatal: context.fatal ?? false,
|
|
21
|
+
} };
|
|
22
|
+
}
|
package/dist/node.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { errorInput } from "./errors.js";
|
|
1
2
|
import { type LogContext, type LogEntry, type LogInput } from "./shared.js";
|
|
2
3
|
export type LogBatch = {
|
|
3
4
|
serviceId: string;
|
|
@@ -21,6 +22,7 @@ export type NodeLoggerOptions = {
|
|
|
21
22
|
}) => void;
|
|
22
23
|
};
|
|
23
24
|
export declare function createLogger(options: NodeLoggerOptions): {
|
|
25
|
+
captureError(error: unknown, context?: Parameters<typeof errorInput>[1]): void;
|
|
24
26
|
getContext: () => LogContext;
|
|
25
27
|
/** verifiedProfile comes from server authentication, never request headers/body. */
|
|
26
28
|
withRequest<T>(request: Request, verifiedProfile: {
|
|
@@ -30,3 +32,4 @@ export declare function createLogger(options: NodeLoggerOptions): {
|
|
|
30
32
|
flush: () => Promise<void>;
|
|
31
33
|
close(): Promise<void>;
|
|
32
34
|
};
|
|
35
|
+
export { installCrashReporter } from "./crash.js";
|
package/dist/node.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { errorInput } from "./errors.js";
|
|
1
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { ACCESS_HEADER, REQUEST_HEADER, redact, validId, validateAppId, withContext } from "./shared.js";
|
|
@@ -83,6 +84,9 @@ export function createLogger(options) {
|
|
|
83
84
|
const timer = setInterval(() => { void flush(); }, Math.max(1000, options.flushIntervalMs ?? 5000));
|
|
84
85
|
timer.unref();
|
|
85
86
|
return {
|
|
87
|
+
captureError(error, context) {
|
|
88
|
+
this.append("error", errorInput(error, context));
|
|
89
|
+
},
|
|
86
90
|
getContext: () => context.getStore() ?? anonymous,
|
|
87
91
|
/** verifiedProfile comes from server authentication, never request headers/body. */
|
|
88
92
|
withRequest(request, verifiedProfile, run) {
|
|
@@ -126,3 +130,4 @@ export function createLogger(options) {
|
|
|
126
130
|
} },
|
|
127
131
|
};
|
|
128
132
|
}
|
|
133
|
+
export { installCrashReporter } from "./crash.js";
|
package/dist/react.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type ReactNode } from "react";
|
|
1
|
+
import { Component, type ErrorInfo, type ReactNode } from "react";
|
|
2
2
|
import { type BrowserLogger, type BrowserLoggerOptions } from "./browser.js";
|
|
3
|
-
export type ReactLogger = Pick<BrowserLogger, "append" | "fetch" | "flush">;
|
|
3
|
+
export type ReactLogger = Pick<BrowserLogger, "append" | "fetch" | "flush" | "captureError">;
|
|
4
4
|
/** Keep options stable for the Provider's lifetime. Update identity through profileId. */
|
|
5
5
|
export declare function LogProvider({ options, profileId, children }: {
|
|
6
6
|
options: Omit<BrowserLoggerOptions, "profileId">;
|
|
@@ -8,3 +8,21 @@ export declare function LogProvider({ options, profileId, children }: {
|
|
|
8
8
|
children: ReactNode;
|
|
9
9
|
}): import("react").FunctionComponentElement<import("react").ProviderProps<ReactLogger | null>>;
|
|
10
10
|
export declare function useLog(): ReactLogger;
|
|
11
|
+
/** Place inside LogProvider; catches render/lifecycle failures of descendants. */
|
|
12
|
+
export declare class LogErrorBoundary extends Component<{
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
fallback: ReactNode;
|
|
15
|
+
}, {
|
|
16
|
+
failed: boolean;
|
|
17
|
+
}> {
|
|
18
|
+
static contextType: import("react").Context<ReactLogger | null>;
|
|
19
|
+
context: ReactLogger | null;
|
|
20
|
+
state: {
|
|
21
|
+
failed: boolean;
|
|
22
|
+
};
|
|
23
|
+
static getDerivedStateFromError(): {
|
|
24
|
+
failed: boolean;
|
|
25
|
+
};
|
|
26
|
+
componentDidCatch(error: Error, info: ErrorInfo): void;
|
|
27
|
+
render(): ReactNode;
|
|
28
|
+
}
|
package/dist/react.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { createContext, createElement, useContext, useEffect, useMemo, useRef } from "react";
|
|
1
|
+
import { Component, createContext, createElement, useContext, useEffect, useMemo, useRef } from "react";
|
|
2
2
|
import { createLogger } from "./browser.js";
|
|
3
|
+
import { errorInput } from "./errors.js";
|
|
3
4
|
const Context = createContext(null);
|
|
4
5
|
/** Keep options stable for the Provider's lifetime. Update identity through profileId. */
|
|
5
6
|
export function LogProvider({ options, profileId, children }) {
|
|
@@ -15,6 +16,7 @@ export function LogProvider({ options, profileId, children }) {
|
|
|
15
16
|
const buffered = useRef([]);
|
|
16
17
|
identity.current = profileId;
|
|
17
18
|
const value = useMemo(() => ({
|
|
19
|
+
captureError(error, context) { value.append("error", errorInput(error, context)); },
|
|
18
20
|
append(topic, input) {
|
|
19
21
|
if (logger.current) {
|
|
20
22
|
logger.current.setProfileId(identity.current);
|
|
@@ -78,3 +80,17 @@ export function useLog() {
|
|
|
78
80
|
throw Error("useLog requires LogProvider");
|
|
79
81
|
return logger;
|
|
80
82
|
}
|
|
83
|
+
/** Place inside LogProvider; catches render/lifecycle failures of descendants. */
|
|
84
|
+
export class LogErrorBoundary extends Component {
|
|
85
|
+
static contextType = Context;
|
|
86
|
+
state = { failed: false };
|
|
87
|
+
static getDerivedStateFromError() { return { failed: true }; }
|
|
88
|
+
componentDidCatch(error, info) {
|
|
89
|
+
try {
|
|
90
|
+
this.context?.append("error", errorInput(error, { operation: "react.render", componentStack: info.componentStack ?? undefined, fatal: true }));
|
|
91
|
+
void this.context?.flush().catch(() => { });
|
|
92
|
+
}
|
|
93
|
+
catch { /* Reporting must not crash the fallback. */ }
|
|
94
|
+
}
|
|
95
|
+
render() { return this.state.failed ? this.props.fallback : this.props.children; }
|
|
96
|
+
}
|