@pmate/log-sdk 0.1.0 → 0.2.1
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 +23 -0
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +12 -1
- 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/README.md
CHANGED
|
@@ -53,3 +53,26 @@ Use authenticatedProfile from server authentication, never a client body/header.
|
|
|
53
53
|
|
|
54
54
|
## Counting
|
|
55
55
|
The envelope `id` is the event deduplication key. PV counts pv events; sessions deduplicate `(appId, accessId)`; Profile UV deduplicates `(appId, profileId)` in a defined time window/topic. Missing identifiers are reported separately. Never count HTTP requests or vitals as page views, or infer first-ever visits from a limited retention window.
|
|
56
|
+
|
|
57
|
+
## Error and crash diagnostics
|
|
58
|
+
|
|
59
|
+
Caught exceptions need explicit reporting; a global rejection listener cannot see
|
|
60
|
+
an exception already handled by application code. Use `log.captureError(error,
|
|
61
|
+
{ operation: 'chat.send' })` on browser, React and Node loggers. Error records use
|
|
62
|
+
the `error` topic and include bounded message/stack diagnostics with URL queries
|
|
63
|
+
removed. Applications must redact domain-specific sensitive strings before
|
|
64
|
+
calling this method.
|
|
65
|
+
|
|
66
|
+
Inside `LogProvider`, wrap the application in `LogErrorBoundary` with a `fallback`
|
|
67
|
+
React node. It reports descendant render/lifecycle failures with component stacks.
|
|
68
|
+
Event handlers, asynchronous operations and errors above the boundary require
|
|
69
|
+
separate reporting. Explicitly allowlisted `log.fetch` requests report non-2xx and
|
|
70
|
+
network failures without bodies or request URLs.
|
|
71
|
+
|
|
72
|
+
Node exports `installCrashReporter({ directory, send })`. Install it before
|
|
73
|
+
starting the service. It monitors fatal exceptions without suppressing Node's
|
|
74
|
+
fatal exit, persists up to 20 records, and exposes `replay()` for startup recovery.
|
|
75
|
+
The `send(entry)` callback must resolve only after server acknowledgement, retain
|
|
76
|
+
entry ID/time, and route records to the service's `crash` topic. Failed uploads
|
|
77
|
+
remain on disk. Use a service-specific private persistent directory. It cannot
|
|
78
|
+
capture SIGKILL, OOM kills, native faults, or failures before installation.
|
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)
|
|
@@ -93,7 +97,14 @@ export function createLogger(options) {
|
|
|
93
97
|
if (!validId(headers.get(REQUEST_HEADER)))
|
|
94
98
|
headers.set(REQUEST_HEADER, crypto.randomUUID());
|
|
95
99
|
// Refuse redirects so correlation headers cannot leak to another origin.
|
|
96
|
-
return fetch(input, { ...init, headers, redirect: "error" })
|
|
100
|
+
return fetch(input, { ...init, headers, redirect: "error" }).then(response => {
|
|
101
|
+
if (!response.ok)
|
|
102
|
+
transport.append("error", { data: { event: "http.client.failed", origin: url.origin, status: response.status, requestId: headers.get(REQUEST_HEADER), serverRequestId: response.headers.get(REQUEST_HEADER) } });
|
|
103
|
+
return response;
|
|
104
|
+
}, error => {
|
|
105
|
+
transport.append("error", errorInput(error, { operation: "http.fetch" }));
|
|
106
|
+
throw error;
|
|
107
|
+
});
|
|
97
108
|
}
|
|
98
109
|
return fetch(input, init);
|
|
99
110
|
},
|
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
|
+
}
|