@plasius/error 1.0.5 → 1.0.6
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/CHANGELOG.md +22 -1
- package/README.md +53 -0
- package/dist/errorboundary.d.ts +18 -0
- package/dist/errorboundary.d.ts.map +1 -1
- package/dist/errorboundary.js +15 -1
- package/dist/globalcrash.d.ts +17 -0
- package/dist/globalcrash.d.ts.map +1 -0
- package/dist/globalcrash.js +134 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist-cjs/errorboundary.d.ts +18 -0
- package/dist-cjs/errorboundary.d.ts.map +1 -1
- package/dist-cjs/errorboundary.js +15 -1
- package/dist-cjs/globalcrash.d.ts +17 -0
- package/dist-cjs/globalcrash.d.ts.map +1 -0
- package/dist-cjs/globalcrash.js +137 -0
- package/dist-cjs/index.d.ts +1 -0
- package/dist-cjs/index.d.ts.map +1 -1
- package/dist-cjs/index.js +1 -0
- package/dist-cjs/package.json +3 -0
- package/docs/adrs/adr-0003-dual-esm-cjs-runtime-compatibility.md +34 -0
- package/docs/adrs/index.md +1 -0
- package/package.json +3 -3
- package/src/errorboundary.tsx +38 -2
- package/src/globalcrash.ts +214 -0
- package/src/index.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -20,6 +20,26 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
|
|
|
20
20
|
- **Security**
|
|
21
21
|
- (placeholder)
|
|
22
22
|
|
|
23
|
+
## [1.0.6] - 2026-03-01
|
|
24
|
+
|
|
25
|
+
- **Added**
|
|
26
|
+
- Error-boundary analytics integration hooks (`analyticsClient`, `errorContext`, `onErrorCaptured`).
|
|
27
|
+
- Exported `ErrorBoundaryReport` contract for forwarding boundary crashes to analytics clients.
|
|
28
|
+
- `installGlobalCrashReporter` for browser and process-level crash capture outside React boundaries.
|
|
29
|
+
|
|
30
|
+
- **Changed**
|
|
31
|
+
- `ErrorBoundary` now forwards captured errors to an injected analytics-compatible client.
|
|
32
|
+
- Corrected render behavior so `fallback` is displayed only after an error is captured.
|
|
33
|
+
|
|
34
|
+
- **Fixed**
|
|
35
|
+
- Enforced CommonJS runtime compatibility for dual-build output by generating and validating `dist-cjs/package.json` (`type: commonjs`) during build and package verification.
|
|
36
|
+
- Added tests covering analytics forwarding and no-error render behavior with fallback props.
|
|
37
|
+
- Added tests validating global crash capture (`window.error`, `window.unhandledrejection`, process exception/rejection handlers).
|
|
38
|
+
- Fixed CommonJS runtime compatibility by marking `dist-cjs/` output as `type: commonjs`, preventing `exports is not defined in ES module scope` when required by Node consumers.
|
|
39
|
+
|
|
40
|
+
- **Security**
|
|
41
|
+
- Boundary reporting payload is constrained to essential debugging fields and delegated to analytics sanitization policies.
|
|
42
|
+
|
|
23
43
|
## [1.0.5] - 2026-02-28
|
|
24
44
|
|
|
25
45
|
- **Added**
|
|
@@ -76,7 +96,7 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
|
|
|
76
96
|
|
|
77
97
|
---
|
|
78
98
|
|
|
79
|
-
[Unreleased]: https://github.com/Plasius-LTD/error/compare/v1.0.
|
|
99
|
+
[Unreleased]: https://github.com/Plasius-LTD/error/compare/v1.0.6...HEAD
|
|
80
100
|
|
|
81
101
|
## [1.0.0] - 2026-02-11
|
|
82
102
|
|
|
@@ -94,3 +114,4 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
|
|
|
94
114
|
[1.0.3]: https://github.com/Plasius-LTD/error/releases/tag/v1.0.3
|
|
95
115
|
[1.0.4]: https://github.com/Plasius-LTD/error/releases/tag/v1.0.4
|
|
96
116
|
[1.0.5]: https://github.com/Plasius-LTD/error/releases/tag/v1.0.5
|
|
117
|
+
[1.0.6]: https://github.com/Plasius-LTD/error/releases/tag/v1.0.6
|
package/README.md
CHANGED
|
@@ -17,12 +17,65 @@ Public package containing shared error boundary and error-handling utilities for
|
|
|
17
17
|
npm install @plasius/error
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
## Module formats
|
|
21
|
+
|
|
22
|
+
`@plasius/error` ships dual module outputs:
|
|
23
|
+
|
|
24
|
+
- ESM via `exports.import` (`dist/index.js`)
|
|
25
|
+
- CJS via `exports.require` (`dist-cjs/index.js`, explicitly marked CommonJS)
|
|
26
|
+
|
|
20
27
|
## Usage
|
|
21
28
|
|
|
22
29
|
```ts
|
|
23
30
|
import { ErrorBoundary } from "@plasius/error";
|
|
24
31
|
```
|
|
25
32
|
|
|
33
|
+
### Error reporting with `@plasius/analytics`
|
|
34
|
+
|
|
35
|
+
`ErrorBoundary` can forward captured errors into an analytics-compatible client:
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { ErrorBoundary } from "@plasius/error";
|
|
39
|
+
import { createFrontendAnalyticsClient } from "@plasius/analytics";
|
|
40
|
+
|
|
41
|
+
const analytics = createFrontendAnalyticsClient({
|
|
42
|
+
source: "sharedcomponents",
|
|
43
|
+
endpoint: "https://analytics.example.com/collect",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
<ErrorBoundary
|
|
47
|
+
name="CheckoutBoundary"
|
|
48
|
+
analyticsClient={analytics}
|
|
49
|
+
errorContext={{ feature: "checkout" }}
|
|
50
|
+
>
|
|
51
|
+
<CheckoutPage />
|
|
52
|
+
</ErrorBoundary>;
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The boundary forwards a minimal report (`boundary`, `error`, component stack, severity, context), and `@plasius/analytics` handles sanitization and secure transport rules.
|
|
56
|
+
|
|
57
|
+
### Whole application crash capture
|
|
58
|
+
|
|
59
|
+
`installGlobalCrashReporter` captures crashes outside React boundaries where available:
|
|
60
|
+
- browser: `window.error`, `window.unhandledrejection`
|
|
61
|
+
- server/runtime: `process.uncaughtException`, `process.unhandledRejection`
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import {
|
|
65
|
+
ErrorBoundary,
|
|
66
|
+
installGlobalCrashReporter,
|
|
67
|
+
} from "@plasius/error";
|
|
68
|
+
|
|
69
|
+
const crashReporter = installGlobalCrashReporter({
|
|
70
|
+
boundaryName: "GlobalApplication",
|
|
71
|
+
analyticsClient: analytics,
|
|
72
|
+
errorContext: { app: "frontend" },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// later during teardown
|
|
76
|
+
crashReporter.dispose();
|
|
77
|
+
```
|
|
78
|
+
|
|
26
79
|
## Development
|
|
27
80
|
|
|
28
81
|
```bash
|
package/dist/errorboundary.d.ts
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
export type ErrorBoundarySeverity = "error" | "fatal";
|
|
3
|
+
export interface ErrorBoundaryReport {
|
|
4
|
+
boundary: string;
|
|
5
|
+
error: unknown;
|
|
6
|
+
componentStack?: string;
|
|
7
|
+
handled?: boolean;
|
|
8
|
+
severity?: ErrorBoundarySeverity;
|
|
9
|
+
context?: Record<string, unknown>;
|
|
10
|
+
timestamp?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface ErrorBoundaryAnalyticsClient {
|
|
13
|
+
reportError: (report: ErrorBoundaryReport) => void;
|
|
14
|
+
}
|
|
2
15
|
interface ErrorBoundaryProps {
|
|
3
16
|
name: string;
|
|
4
17
|
children: React.ReactNode;
|
|
5
18
|
fallback?: React.ReactNode;
|
|
19
|
+
analyticsClient?: ErrorBoundaryAnalyticsClient;
|
|
20
|
+
errorContext?: Record<string, unknown>;
|
|
21
|
+
severity?: ErrorBoundarySeverity;
|
|
22
|
+
onErrorCaptured?: (report: ErrorBoundaryReport, info: React.ErrorInfo) => void;
|
|
6
23
|
}
|
|
7
24
|
interface ErrorBoundaryState {
|
|
8
25
|
hasError: boolean;
|
|
@@ -12,6 +29,7 @@ export declare class ErrorBoundary extends React.Component<ErrorBoundaryProps, E
|
|
|
12
29
|
static getDerivedStateFromError(): {
|
|
13
30
|
hasError: boolean;
|
|
14
31
|
};
|
|
32
|
+
private buildErrorReport;
|
|
15
33
|
componentDidCatch(error: Error, info: React.ErrorInfo): void;
|
|
16
34
|
render(): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | import("react/jsx-runtime").JSX.Element | null | undefined;
|
|
17
35
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errorboundary.d.ts","sourceRoot":"","sources":["../src/errorboundary.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,UAAU,kBAAkB;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"errorboundary.d.ts","sourceRoot":"","sources":["../src/errorboundary.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,OAAO,CAAC;AAEtD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,qBAAqB,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,4BAA4B;IAC3C,WAAW,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;CACpD;AAED,UAAU,kBAAkB;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,eAAe,CAAC,EAAE,4BAA4B,CAAC;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,QAAQ,CAAC,EAAE,qBAAqB,CAAC;IACjC,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,EAAE,IAAI,EAAE,KAAK,CAAC,SAAS,KAAK,IAAI,CAAC;CAChF;AAED,UAAU,kBAAkB;IAC1B,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,qBAAa,aAAc,SAAQ,KAAK,CAAC,SAAS,CAChD,kBAAkB,EAClB,kBAAkB,CACnB;gBACa,KAAK,EAAE,kBAAkB;IAKrC,MAAM,CAAC,wBAAwB;;;IAI/B,OAAO,CAAC,gBAAgB;IAYf,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,SAAS;IAOrD,MAAM;CAShB;AAED,eAAe,aAAa,CAAC"}
|
package/dist/errorboundary.js
CHANGED
|
@@ -8,14 +8,28 @@ export class ErrorBoundary extends React.Component {
|
|
|
8
8
|
static getDerivedStateFromError() {
|
|
9
9
|
return { hasError: true };
|
|
10
10
|
}
|
|
11
|
+
buildErrorReport(error, info) {
|
|
12
|
+
return {
|
|
13
|
+
boundary: this.props.name,
|
|
14
|
+
error,
|
|
15
|
+
componentStack: info.componentStack ?? undefined,
|
|
16
|
+
handled: true,
|
|
17
|
+
severity: this.props.severity ?? "error",
|
|
18
|
+
context: this.props.errorContext,
|
|
19
|
+
timestamp: Date.now(),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
11
22
|
componentDidCatch(error, info) {
|
|
23
|
+
const report = this.buildErrorReport(error, info);
|
|
24
|
+
this.props.analyticsClient?.reportError(report);
|
|
25
|
+
this.props.onErrorCaptured?.(report, info);
|
|
12
26
|
console.error(`Error caught by ${this.props.name}:`, error, info);
|
|
13
27
|
}
|
|
14
28
|
render() {
|
|
15
29
|
if (this.state.hasError) {
|
|
16
30
|
return (this.props.fallback ?? _jsxs("h2", { children: [this.props.name, " encountered an error."] }));
|
|
17
31
|
}
|
|
18
|
-
return this.props.
|
|
32
|
+
return this.props.children;
|
|
19
33
|
}
|
|
20
34
|
}
|
|
21
35
|
export default ErrorBoundary;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ErrorBoundaryAnalyticsClient, ErrorBoundaryReport, ErrorBoundarySeverity } from "./errorboundary.js";
|
|
2
|
+
export type GlobalCrashOrigin = "window.error" | "window.unhandledrejection" | "process.uncaughtException" | "process.unhandledRejection";
|
|
3
|
+
export interface GlobalCrashReporterOptions {
|
|
4
|
+
boundaryName?: string;
|
|
5
|
+
analyticsClient?: ErrorBoundaryAnalyticsClient;
|
|
6
|
+
errorContext?: Record<string, unknown>;
|
|
7
|
+
severity?: ErrorBoundarySeverity;
|
|
8
|
+
onErrorCaptured?: (report: ErrorBoundaryReport, origin: GlobalCrashOrigin) => void;
|
|
9
|
+
captureBrowserErrors?: boolean;
|
|
10
|
+
captureBrowserRejections?: boolean;
|
|
11
|
+
captureProcessErrors?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface GlobalCrashReporterController {
|
|
14
|
+
dispose: () => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function installGlobalCrashReporter(options?: GlobalCrashReporterOptions): GlobalCrashReporterController;
|
|
17
|
+
//# sourceMappingURL=globalcrash.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"globalcrash.d.ts","sourceRoot":"","sources":["../src/globalcrash.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,4BAA4B,EAC5B,mBAAmB,EACnB,qBAAqB,EACtB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,MAAM,iBAAiB,GACzB,cAAc,GACd,2BAA2B,GAC3B,2BAA2B,GAC3B,4BAA4B,CAAC;AAEjC,MAAM,WAAW,0BAA0B;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,4BAA4B,CAAC;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,QAAQ,CAAC,EAAE,qBAAqB,CAAC;IACjC,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,EAAE,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACnF,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AA0FD,wBAAgB,0BAA0B,CACxC,OAAO,GAAE,0BAA+B,GACvC,6BAA6B,CAgG/B"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
function isBrowserEventTarget(value) {
|
|
2
|
+
if (!value || typeof value !== "object") {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
const candidate = value;
|
|
6
|
+
return (typeof candidate.addEventListener === "function" &&
|
|
7
|
+
typeof candidate.removeEventListener === "function");
|
|
8
|
+
}
|
|
9
|
+
function isNodeProcessTarget(value) {
|
|
10
|
+
if (!value || typeof value !== "object") {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const candidate = value;
|
|
14
|
+
return typeof candidate.on === "function" && typeof candidate.off === "function";
|
|
15
|
+
}
|
|
16
|
+
function normalizeError(error) {
|
|
17
|
+
if (error instanceof Error) {
|
|
18
|
+
return error;
|
|
19
|
+
}
|
|
20
|
+
if (typeof error === "string") {
|
|
21
|
+
return new Error(error);
|
|
22
|
+
}
|
|
23
|
+
if (error === null || error === undefined) {
|
|
24
|
+
return new Error("Unknown crash reason");
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return new Error(JSON.stringify(error));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return new Error(String(error));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function extractErrorEventContext(event) {
|
|
34
|
+
const details = {};
|
|
35
|
+
if (!event || typeof event !== "object") {
|
|
36
|
+
return {
|
|
37
|
+
error: event,
|
|
38
|
+
details,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const record = event;
|
|
42
|
+
if (typeof record.message === "string" && record.message.trim()) {
|
|
43
|
+
details.message = record.message;
|
|
44
|
+
}
|
|
45
|
+
if (typeof record.filename === "string" && record.filename.trim()) {
|
|
46
|
+
details.filename = record.filename;
|
|
47
|
+
}
|
|
48
|
+
if (typeof record.lineno === "number") {
|
|
49
|
+
details.lineno = record.lineno;
|
|
50
|
+
}
|
|
51
|
+
if (typeof record.colno === "number") {
|
|
52
|
+
details.colno = record.colno;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
error: record.error ?? record.reason ?? record,
|
|
56
|
+
details,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function installGlobalCrashReporter(options = {}) {
|
|
60
|
+
const boundaryName = options.boundaryName?.trim() || "GlobalApplication";
|
|
61
|
+
const severity = options.severity ?? "fatal";
|
|
62
|
+
const baseContext = {
|
|
63
|
+
...(options.errorContext ?? {}),
|
|
64
|
+
};
|
|
65
|
+
const disposers = [];
|
|
66
|
+
const emitReport = (origin, rawError, contextDetails = {}) => {
|
|
67
|
+
const error = normalizeError(rawError);
|
|
68
|
+
const report = {
|
|
69
|
+
boundary: boundaryName,
|
|
70
|
+
error,
|
|
71
|
+
handled: false,
|
|
72
|
+
severity,
|
|
73
|
+
context: {
|
|
74
|
+
...baseContext,
|
|
75
|
+
crashOrigin: origin,
|
|
76
|
+
...contextDetails,
|
|
77
|
+
},
|
|
78
|
+
timestamp: Date.now(),
|
|
79
|
+
};
|
|
80
|
+
options.analyticsClient?.reportError(report);
|
|
81
|
+
options.onErrorCaptured?.(report, origin);
|
|
82
|
+
};
|
|
83
|
+
const browserGlobal = typeof window !== "undefined" ? window : globalThis.window;
|
|
84
|
+
if (isBrowserEventTarget(browserGlobal)) {
|
|
85
|
+
if (options.captureBrowserErrors !== false) {
|
|
86
|
+
const handleBrowserError = (event) => {
|
|
87
|
+
const { error, details } = extractErrorEventContext(event);
|
|
88
|
+
emitReport("window.error", error, details);
|
|
89
|
+
};
|
|
90
|
+
browserGlobal.addEventListener("error", handleBrowserError);
|
|
91
|
+
disposers.push(() => {
|
|
92
|
+
browserGlobal.removeEventListener("error", handleBrowserError);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (options.captureBrowserRejections !== false) {
|
|
96
|
+
const handleUnhandledRejection = (event) => {
|
|
97
|
+
const { error, details } = extractErrorEventContext(event);
|
|
98
|
+
emitReport("window.unhandledrejection", error, details);
|
|
99
|
+
};
|
|
100
|
+
browserGlobal.addEventListener("unhandledrejection", handleUnhandledRejection);
|
|
101
|
+
disposers.push(() => {
|
|
102
|
+
browserGlobal.removeEventListener("unhandledrejection", handleUnhandledRejection);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const processGlobal = globalThis.process;
|
|
107
|
+
if (options.captureProcessErrors !== false && isNodeProcessTarget(processGlobal)) {
|
|
108
|
+
const handleUncaughtException = (error) => {
|
|
109
|
+
emitReport("process.uncaughtException", error);
|
|
110
|
+
};
|
|
111
|
+
const handleUnhandledRejection = (reason) => {
|
|
112
|
+
emitReport("process.unhandledRejection", reason);
|
|
113
|
+
};
|
|
114
|
+
processGlobal.on("uncaughtException", handleUncaughtException);
|
|
115
|
+
processGlobal.on("unhandledRejection", handleUnhandledRejection);
|
|
116
|
+
disposers.push(() => {
|
|
117
|
+
processGlobal.off("uncaughtException", handleUncaughtException);
|
|
118
|
+
processGlobal.off("unhandledRejection", handleUnhandledRejection);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
let disposed = false;
|
|
122
|
+
return {
|
|
123
|
+
dispose() {
|
|
124
|
+
if (disposed) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
disposed = true;
|
|
128
|
+
for (const dispose of disposers) {
|
|
129
|
+
dispose();
|
|
130
|
+
}
|
|
131
|
+
disposers.length = 0;
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
export type ErrorBoundarySeverity = "error" | "fatal";
|
|
3
|
+
export interface ErrorBoundaryReport {
|
|
4
|
+
boundary: string;
|
|
5
|
+
error: unknown;
|
|
6
|
+
componentStack?: string;
|
|
7
|
+
handled?: boolean;
|
|
8
|
+
severity?: ErrorBoundarySeverity;
|
|
9
|
+
context?: Record<string, unknown>;
|
|
10
|
+
timestamp?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface ErrorBoundaryAnalyticsClient {
|
|
13
|
+
reportError: (report: ErrorBoundaryReport) => void;
|
|
14
|
+
}
|
|
2
15
|
interface ErrorBoundaryProps {
|
|
3
16
|
name: string;
|
|
4
17
|
children: React.ReactNode;
|
|
5
18
|
fallback?: React.ReactNode;
|
|
19
|
+
analyticsClient?: ErrorBoundaryAnalyticsClient;
|
|
20
|
+
errorContext?: Record<string, unknown>;
|
|
21
|
+
severity?: ErrorBoundarySeverity;
|
|
22
|
+
onErrorCaptured?: (report: ErrorBoundaryReport, info: React.ErrorInfo) => void;
|
|
6
23
|
}
|
|
7
24
|
interface ErrorBoundaryState {
|
|
8
25
|
hasError: boolean;
|
|
@@ -12,6 +29,7 @@ export declare class ErrorBoundary extends React.Component<ErrorBoundaryProps, E
|
|
|
12
29
|
static getDerivedStateFromError(): {
|
|
13
30
|
hasError: boolean;
|
|
14
31
|
};
|
|
32
|
+
private buildErrorReport;
|
|
15
33
|
componentDidCatch(error: Error, info: React.ErrorInfo): void;
|
|
16
34
|
render(): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | import("react/jsx-runtime").JSX.Element | null | undefined;
|
|
17
35
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errorboundary.d.ts","sourceRoot":"","sources":["../src/errorboundary.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,UAAU,kBAAkB;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"errorboundary.d.ts","sourceRoot":"","sources":["../src/errorboundary.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,OAAO,CAAC;AAEtD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,qBAAqB,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,4BAA4B;IAC3C,WAAW,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;CACpD;AAED,UAAU,kBAAkB;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,eAAe,CAAC,EAAE,4BAA4B,CAAC;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,QAAQ,CAAC,EAAE,qBAAqB,CAAC;IACjC,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,EAAE,IAAI,EAAE,KAAK,CAAC,SAAS,KAAK,IAAI,CAAC;CAChF;AAED,UAAU,kBAAkB;IAC1B,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,qBAAa,aAAc,SAAQ,KAAK,CAAC,SAAS,CAChD,kBAAkB,EAClB,kBAAkB,CACnB;gBACa,KAAK,EAAE,kBAAkB;IAKrC,MAAM,CAAC,wBAAwB;;;IAI/B,OAAO,CAAC,gBAAgB;IAYf,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,SAAS;IAOrD,MAAM;CAShB;AAED,eAAe,aAAa,CAAC"}
|
|
@@ -14,14 +14,28 @@ class ErrorBoundary extends react_1.default.Component {
|
|
|
14
14
|
static getDerivedStateFromError() {
|
|
15
15
|
return { hasError: true };
|
|
16
16
|
}
|
|
17
|
+
buildErrorReport(error, info) {
|
|
18
|
+
return {
|
|
19
|
+
boundary: this.props.name,
|
|
20
|
+
error,
|
|
21
|
+
componentStack: info.componentStack ?? undefined,
|
|
22
|
+
handled: true,
|
|
23
|
+
severity: this.props.severity ?? "error",
|
|
24
|
+
context: this.props.errorContext,
|
|
25
|
+
timestamp: Date.now(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
17
28
|
componentDidCatch(error, info) {
|
|
29
|
+
const report = this.buildErrorReport(error, info);
|
|
30
|
+
this.props.analyticsClient?.reportError(report);
|
|
31
|
+
this.props.onErrorCaptured?.(report, info);
|
|
18
32
|
console.error(`Error caught by ${this.props.name}:`, error, info);
|
|
19
33
|
}
|
|
20
34
|
render() {
|
|
21
35
|
if (this.state.hasError) {
|
|
22
36
|
return (this.props.fallback ?? (0, jsx_runtime_1.jsxs)("h2", { children: [this.props.name, " encountered an error."] }));
|
|
23
37
|
}
|
|
24
|
-
return this.props.
|
|
38
|
+
return this.props.children;
|
|
25
39
|
}
|
|
26
40
|
}
|
|
27
41
|
exports.ErrorBoundary = ErrorBoundary;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ErrorBoundaryAnalyticsClient, ErrorBoundaryReport, ErrorBoundarySeverity } from "./errorboundary.js";
|
|
2
|
+
export type GlobalCrashOrigin = "window.error" | "window.unhandledrejection" | "process.uncaughtException" | "process.unhandledRejection";
|
|
3
|
+
export interface GlobalCrashReporterOptions {
|
|
4
|
+
boundaryName?: string;
|
|
5
|
+
analyticsClient?: ErrorBoundaryAnalyticsClient;
|
|
6
|
+
errorContext?: Record<string, unknown>;
|
|
7
|
+
severity?: ErrorBoundarySeverity;
|
|
8
|
+
onErrorCaptured?: (report: ErrorBoundaryReport, origin: GlobalCrashOrigin) => void;
|
|
9
|
+
captureBrowserErrors?: boolean;
|
|
10
|
+
captureBrowserRejections?: boolean;
|
|
11
|
+
captureProcessErrors?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface GlobalCrashReporterController {
|
|
14
|
+
dispose: () => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function installGlobalCrashReporter(options?: GlobalCrashReporterOptions): GlobalCrashReporterController;
|
|
17
|
+
//# sourceMappingURL=globalcrash.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"globalcrash.d.ts","sourceRoot":"","sources":["../src/globalcrash.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,4BAA4B,EAC5B,mBAAmB,EACnB,qBAAqB,EACtB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,MAAM,iBAAiB,GACzB,cAAc,GACd,2BAA2B,GAC3B,2BAA2B,GAC3B,4BAA4B,CAAC;AAEjC,MAAM,WAAW,0BAA0B;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,4BAA4B,CAAC;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,QAAQ,CAAC,EAAE,qBAAqB,CAAC;IACjC,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,EAAE,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACnF,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AA0FD,wBAAgB,0BAA0B,CACxC,OAAO,GAAE,0BAA+B,GACvC,6BAA6B,CAgG/B"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.installGlobalCrashReporter = installGlobalCrashReporter;
|
|
4
|
+
function isBrowserEventTarget(value) {
|
|
5
|
+
if (!value || typeof value !== "object") {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
const candidate = value;
|
|
9
|
+
return (typeof candidate.addEventListener === "function" &&
|
|
10
|
+
typeof candidate.removeEventListener === "function");
|
|
11
|
+
}
|
|
12
|
+
function isNodeProcessTarget(value) {
|
|
13
|
+
if (!value || typeof value !== "object") {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const candidate = value;
|
|
17
|
+
return typeof candidate.on === "function" && typeof candidate.off === "function";
|
|
18
|
+
}
|
|
19
|
+
function normalizeError(error) {
|
|
20
|
+
if (error instanceof Error) {
|
|
21
|
+
return error;
|
|
22
|
+
}
|
|
23
|
+
if (typeof error === "string") {
|
|
24
|
+
return new Error(error);
|
|
25
|
+
}
|
|
26
|
+
if (error === null || error === undefined) {
|
|
27
|
+
return new Error("Unknown crash reason");
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return new Error(JSON.stringify(error));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return new Error(String(error));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function extractErrorEventContext(event) {
|
|
37
|
+
const details = {};
|
|
38
|
+
if (!event || typeof event !== "object") {
|
|
39
|
+
return {
|
|
40
|
+
error: event,
|
|
41
|
+
details,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const record = event;
|
|
45
|
+
if (typeof record.message === "string" && record.message.trim()) {
|
|
46
|
+
details.message = record.message;
|
|
47
|
+
}
|
|
48
|
+
if (typeof record.filename === "string" && record.filename.trim()) {
|
|
49
|
+
details.filename = record.filename;
|
|
50
|
+
}
|
|
51
|
+
if (typeof record.lineno === "number") {
|
|
52
|
+
details.lineno = record.lineno;
|
|
53
|
+
}
|
|
54
|
+
if (typeof record.colno === "number") {
|
|
55
|
+
details.colno = record.colno;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
error: record.error ?? record.reason ?? record,
|
|
59
|
+
details,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function installGlobalCrashReporter(options = {}) {
|
|
63
|
+
const boundaryName = options.boundaryName?.trim() || "GlobalApplication";
|
|
64
|
+
const severity = options.severity ?? "fatal";
|
|
65
|
+
const baseContext = {
|
|
66
|
+
...(options.errorContext ?? {}),
|
|
67
|
+
};
|
|
68
|
+
const disposers = [];
|
|
69
|
+
const emitReport = (origin, rawError, contextDetails = {}) => {
|
|
70
|
+
const error = normalizeError(rawError);
|
|
71
|
+
const report = {
|
|
72
|
+
boundary: boundaryName,
|
|
73
|
+
error,
|
|
74
|
+
handled: false,
|
|
75
|
+
severity,
|
|
76
|
+
context: {
|
|
77
|
+
...baseContext,
|
|
78
|
+
crashOrigin: origin,
|
|
79
|
+
...contextDetails,
|
|
80
|
+
},
|
|
81
|
+
timestamp: Date.now(),
|
|
82
|
+
};
|
|
83
|
+
options.analyticsClient?.reportError(report);
|
|
84
|
+
options.onErrorCaptured?.(report, origin);
|
|
85
|
+
};
|
|
86
|
+
const browserGlobal = typeof window !== "undefined" ? window : globalThis.window;
|
|
87
|
+
if (isBrowserEventTarget(browserGlobal)) {
|
|
88
|
+
if (options.captureBrowserErrors !== false) {
|
|
89
|
+
const handleBrowserError = (event) => {
|
|
90
|
+
const { error, details } = extractErrorEventContext(event);
|
|
91
|
+
emitReport("window.error", error, details);
|
|
92
|
+
};
|
|
93
|
+
browserGlobal.addEventListener("error", handleBrowserError);
|
|
94
|
+
disposers.push(() => {
|
|
95
|
+
browserGlobal.removeEventListener("error", handleBrowserError);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (options.captureBrowserRejections !== false) {
|
|
99
|
+
const handleUnhandledRejection = (event) => {
|
|
100
|
+
const { error, details } = extractErrorEventContext(event);
|
|
101
|
+
emitReport("window.unhandledrejection", error, details);
|
|
102
|
+
};
|
|
103
|
+
browserGlobal.addEventListener("unhandledrejection", handleUnhandledRejection);
|
|
104
|
+
disposers.push(() => {
|
|
105
|
+
browserGlobal.removeEventListener("unhandledrejection", handleUnhandledRejection);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const processGlobal = globalThis.process;
|
|
110
|
+
if (options.captureProcessErrors !== false && isNodeProcessTarget(processGlobal)) {
|
|
111
|
+
const handleUncaughtException = (error) => {
|
|
112
|
+
emitReport("process.uncaughtException", error);
|
|
113
|
+
};
|
|
114
|
+
const handleUnhandledRejection = (reason) => {
|
|
115
|
+
emitReport("process.unhandledRejection", reason);
|
|
116
|
+
};
|
|
117
|
+
processGlobal.on("uncaughtException", handleUncaughtException);
|
|
118
|
+
processGlobal.on("unhandledRejection", handleUnhandledRejection);
|
|
119
|
+
disposers.push(() => {
|
|
120
|
+
processGlobal.off("uncaughtException", handleUncaughtException);
|
|
121
|
+
processGlobal.off("unhandledRejection", handleUnhandledRejection);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
let disposed = false;
|
|
125
|
+
return {
|
|
126
|
+
dispose() {
|
|
127
|
+
if (disposed) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
disposed = true;
|
|
131
|
+
for (const dispose of disposers) {
|
|
132
|
+
dispose();
|
|
133
|
+
}
|
|
134
|
+
disposers.length = 0;
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
package/dist-cjs/index.d.ts
CHANGED
package/dist-cjs/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC"}
|
package/dist-cjs/index.js
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# ADR-0003: Dual ESM and CJS Runtime Compatibility
|
|
2
|
+
|
|
3
|
+
- Date: 2026-03-01
|
|
4
|
+
- Status: Accepted
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
`@plasius/error` is published with `type: module` and dual exports for ESM and CJS
|
|
9
|
+
consumers. The CJS build artifacts are emitted under `dist-cjs/*.js`, and Node
|
|
10
|
+
can interpret those files as ESM unless that directory is explicitly marked as
|
|
11
|
+
CommonJS.
|
|
12
|
+
|
|
13
|
+
This caused Node `require("@plasius/error")` consumers to fail at runtime with:
|
|
14
|
+
`exports is not defined in ES module scope`.
|
|
15
|
+
|
|
16
|
+
## Decision
|
|
17
|
+
|
|
18
|
+
Keep dual output and explicitly mark the `dist-cjs/` build directory with a
|
|
19
|
+
generated `dist-cjs/package.json` containing:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"type": "commonjs"
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Add publish verification checks to ensure this metadata exists and is included
|
|
28
|
+
in the packed artifact.
|
|
29
|
+
|
|
30
|
+
## Consequences
|
|
31
|
+
|
|
32
|
+
- CJS consumers resolve and execute `@plasius/error` reliably.
|
|
33
|
+
- ESM consumers remain unchanged (`dist/index.js`).
|
|
34
|
+
- Packaging safety improves by preventing accidental regressions during release.
|
package/docs/adrs/index.md
CHANGED
|
@@ -2,3 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
- [ADR-0001: Standalone @plasius/error Package Scope](./adr-0001-error-package-scope.md)
|
|
4
4
|
- [ADR-0002: Public Repository Governance Baseline](./adr-0002-public-repo-governance.md)
|
|
5
|
+
- [ADR-0003: Dual ESM and CJS Runtime Compatibility](./adr-0003-dual-esm-cjs-runtime-compatibility.md)
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plasius/error",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"main": "./dist-cjs/index.js",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"private": false,
|
|
7
7
|
"type": "module",
|
|
8
8
|
"description": "Error handling functions and types for Plasius projects",
|
|
9
9
|
"scripts": {
|
|
10
|
-
"build": "tsc --build --listEmittedFiles && rsync -av --include '*/' --include '*.module.css' --exclude '*' src/ dist/ || true && npm run build:cjs",
|
|
10
|
+
"build": "tsc --build --listEmittedFiles && (rsync -av --include '*/' --include '*.module.css' --exclude '*' src/ dist/ || true) && npm run build:cjs",
|
|
11
11
|
"test": "vitest run",
|
|
12
12
|
"test:watch": "vitest",
|
|
13
13
|
"test:coverage": "vitest run --coverage",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"audit:npm": "npm audit --audit-level=moderate || true",
|
|
21
21
|
"audit:test": "vitest run --coverage",
|
|
22
22
|
"audit:all": "npm-run-all -l audit:ts audit:eslint audit:deps audit:npm audit:test",
|
|
23
|
-
"build:cjs": "tsc -p tsconfig.json --module commonjs --moduleResolution node --outDir dist-cjs --tsBuildInfoFile dist-cjs/tsconfig.tsbuildinfo --listEmittedFiles && rsync -av --include '*/' --include '*.module.css' --exclude '*' src/ dist-cjs/ || true",
|
|
23
|
+
"build:cjs": "tsc -p tsconfig.json --module commonjs --moduleResolution node --outDir dist-cjs --tsBuildInfoFile dist-cjs/tsconfig.tsbuildinfo --listEmittedFiles && (rsync -av --include '*/' --include '*.module.css' --exclude '*' src/ dist-cjs/ || true) && node scripts/write-cjs-package-json.cjs",
|
|
24
24
|
"lint": "eslint .",
|
|
25
25
|
"prepare": "npm run build",
|
|
26
26
|
"pack:check": "node scripts/verify-public-package.cjs",
|
package/src/errorboundary.tsx
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
+
export type ErrorBoundarySeverity = "error" | "fatal";
|
|
4
|
+
|
|
5
|
+
export interface ErrorBoundaryReport {
|
|
6
|
+
boundary: string;
|
|
7
|
+
error: unknown;
|
|
8
|
+
componentStack?: string;
|
|
9
|
+
handled?: boolean;
|
|
10
|
+
severity?: ErrorBoundarySeverity;
|
|
11
|
+
context?: Record<string, unknown>;
|
|
12
|
+
timestamp?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ErrorBoundaryAnalyticsClient {
|
|
16
|
+
reportError: (report: ErrorBoundaryReport) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
3
19
|
interface ErrorBoundaryProps {
|
|
4
20
|
name: string;
|
|
5
21
|
children: React.ReactNode;
|
|
6
22
|
fallback?: React.ReactNode;
|
|
23
|
+
analyticsClient?: ErrorBoundaryAnalyticsClient;
|
|
24
|
+
errorContext?: Record<string, unknown>;
|
|
25
|
+
severity?: ErrorBoundarySeverity;
|
|
26
|
+
onErrorCaptured?: (report: ErrorBoundaryReport, info: React.ErrorInfo) => void;
|
|
7
27
|
}
|
|
8
28
|
|
|
9
29
|
interface ErrorBoundaryState {
|
|
@@ -23,7 +43,22 @@ export class ErrorBoundary extends React.Component<
|
|
|
23
43
|
return { hasError: true };
|
|
24
44
|
}
|
|
25
45
|
|
|
46
|
+
private buildErrorReport(error: Error, info: React.ErrorInfo): ErrorBoundaryReport {
|
|
47
|
+
return {
|
|
48
|
+
boundary: this.props.name,
|
|
49
|
+
error,
|
|
50
|
+
componentStack: info.componentStack ?? undefined,
|
|
51
|
+
handled: true,
|
|
52
|
+
severity: this.props.severity ?? "error",
|
|
53
|
+
context: this.props.errorContext,
|
|
54
|
+
timestamp: Date.now(),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
26
58
|
override componentDidCatch(error: Error, info: React.ErrorInfo) {
|
|
59
|
+
const report = this.buildErrorReport(error, info);
|
|
60
|
+
this.props.analyticsClient?.reportError(report);
|
|
61
|
+
this.props.onErrorCaptured?.(report, info);
|
|
27
62
|
console.error(`Error caught by ${this.props.name}:`, error, info);
|
|
28
63
|
}
|
|
29
64
|
|
|
@@ -33,8 +68,9 @@ export class ErrorBoundary extends React.Component<
|
|
|
33
68
|
this.props.fallback ?? <h2>{this.props.name} encountered an error.</h2>
|
|
34
69
|
);
|
|
35
70
|
}
|
|
36
|
-
|
|
71
|
+
|
|
72
|
+
return this.props.children;
|
|
37
73
|
}
|
|
38
74
|
}
|
|
39
75
|
|
|
40
|
-
export default ErrorBoundary;
|
|
76
|
+
export default ErrorBoundary;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ErrorBoundaryAnalyticsClient,
|
|
3
|
+
ErrorBoundaryReport,
|
|
4
|
+
ErrorBoundarySeverity,
|
|
5
|
+
} from "./errorboundary.js";
|
|
6
|
+
|
|
7
|
+
export type GlobalCrashOrigin =
|
|
8
|
+
| "window.error"
|
|
9
|
+
| "window.unhandledrejection"
|
|
10
|
+
| "process.uncaughtException"
|
|
11
|
+
| "process.unhandledRejection";
|
|
12
|
+
|
|
13
|
+
export interface GlobalCrashReporterOptions {
|
|
14
|
+
boundaryName?: string;
|
|
15
|
+
analyticsClient?: ErrorBoundaryAnalyticsClient;
|
|
16
|
+
errorContext?: Record<string, unknown>;
|
|
17
|
+
severity?: ErrorBoundarySeverity;
|
|
18
|
+
onErrorCaptured?: (report: ErrorBoundaryReport, origin: GlobalCrashOrigin) => void;
|
|
19
|
+
captureBrowserErrors?: boolean;
|
|
20
|
+
captureBrowserRejections?: boolean;
|
|
21
|
+
captureProcessErrors?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface GlobalCrashReporterController {
|
|
25
|
+
dispose: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type BrowserEventTarget = {
|
|
29
|
+
addEventListener: (eventName: string, handler: (event: unknown) => void) => void;
|
|
30
|
+
removeEventListener: (eventName: string, handler: (event: unknown) => void) => void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type NodeProcessTarget = {
|
|
34
|
+
on: (eventName: string, handler: (...args: unknown[]) => void) => void;
|
|
35
|
+
off: (eventName: string, handler: (...args: unknown[]) => void) => void;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function isBrowserEventTarget(value: unknown): value is BrowserEventTarget {
|
|
39
|
+
if (!value || typeof value !== "object") {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const candidate = value as Partial<BrowserEventTarget>;
|
|
44
|
+
return (
|
|
45
|
+
typeof candidate.addEventListener === "function" &&
|
|
46
|
+
typeof candidate.removeEventListener === "function"
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isNodeProcessTarget(value: unknown): value is NodeProcessTarget {
|
|
51
|
+
if (!value || typeof value !== "object") {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const candidate = value as Partial<NodeProcessTarget>;
|
|
56
|
+
return typeof candidate.on === "function" && typeof candidate.off === "function";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeError(error: unknown): Error {
|
|
60
|
+
if (error instanceof Error) {
|
|
61
|
+
return error;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (typeof error === "string") {
|
|
65
|
+
return new Error(error);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (error === null || error === undefined) {
|
|
69
|
+
return new Error("Unknown crash reason");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
return new Error(JSON.stringify(error));
|
|
74
|
+
} catch {
|
|
75
|
+
return new Error(String(error));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function extractErrorEventContext(event: unknown): {
|
|
80
|
+
error: unknown;
|
|
81
|
+
details: Record<string, unknown>;
|
|
82
|
+
} {
|
|
83
|
+
const details: Record<string, unknown> = {};
|
|
84
|
+
|
|
85
|
+
if (!event || typeof event !== "object") {
|
|
86
|
+
return {
|
|
87
|
+
error: event,
|
|
88
|
+
details,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const record = event as Record<string, unknown>;
|
|
93
|
+
|
|
94
|
+
if (typeof record.message === "string" && record.message.trim()) {
|
|
95
|
+
details.message = record.message;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (typeof record.filename === "string" && record.filename.trim()) {
|
|
99
|
+
details.filename = record.filename;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (typeof record.lineno === "number") {
|
|
103
|
+
details.lineno = record.lineno;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (typeof record.colno === "number") {
|
|
107
|
+
details.colno = record.colno;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
error: record.error ?? record.reason ?? record,
|
|
112
|
+
details,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function installGlobalCrashReporter(
|
|
117
|
+
options: GlobalCrashReporterOptions = {}
|
|
118
|
+
): GlobalCrashReporterController {
|
|
119
|
+
const boundaryName = options.boundaryName?.trim() || "GlobalApplication";
|
|
120
|
+
const severity = options.severity ?? "fatal";
|
|
121
|
+
const baseContext = {
|
|
122
|
+
...(options.errorContext ?? {}),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const disposers: Array<() => void> = [];
|
|
126
|
+
|
|
127
|
+
const emitReport = (
|
|
128
|
+
origin: GlobalCrashOrigin,
|
|
129
|
+
rawError: unknown,
|
|
130
|
+
contextDetails: Record<string, unknown> = {}
|
|
131
|
+
) => {
|
|
132
|
+
const error = normalizeError(rawError);
|
|
133
|
+
const report: ErrorBoundaryReport = {
|
|
134
|
+
boundary: boundaryName,
|
|
135
|
+
error,
|
|
136
|
+
handled: false,
|
|
137
|
+
severity,
|
|
138
|
+
context: {
|
|
139
|
+
...baseContext,
|
|
140
|
+
crashOrigin: origin,
|
|
141
|
+
...contextDetails,
|
|
142
|
+
},
|
|
143
|
+
timestamp: Date.now(),
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
options.analyticsClient?.reportError(report);
|
|
147
|
+
options.onErrorCaptured?.(report, origin);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const browserGlobal =
|
|
151
|
+
typeof window !== "undefined" ? window : (globalThis as { window?: unknown }).window;
|
|
152
|
+
|
|
153
|
+
if (isBrowserEventTarget(browserGlobal)) {
|
|
154
|
+
if (options.captureBrowserErrors !== false) {
|
|
155
|
+
const handleBrowserError = (event: unknown) => {
|
|
156
|
+
const { error, details } = extractErrorEventContext(event);
|
|
157
|
+
emitReport("window.error", error, details);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
browserGlobal.addEventListener("error", handleBrowserError);
|
|
161
|
+
disposers.push(() => {
|
|
162
|
+
browserGlobal.removeEventListener("error", handleBrowserError);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (options.captureBrowserRejections !== false) {
|
|
167
|
+
const handleUnhandledRejection = (event: unknown) => {
|
|
168
|
+
const { error, details } = extractErrorEventContext(event);
|
|
169
|
+
emitReport("window.unhandledrejection", error, details);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
browserGlobal.addEventListener("unhandledrejection", handleUnhandledRejection);
|
|
173
|
+
disposers.push(() => {
|
|
174
|
+
browserGlobal.removeEventListener("unhandledrejection", handleUnhandledRejection);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const processGlobal = (globalThis as { process?: unknown }).process;
|
|
180
|
+
|
|
181
|
+
if (options.captureProcessErrors !== false && isNodeProcessTarget(processGlobal)) {
|
|
182
|
+
const handleUncaughtException = (error: unknown) => {
|
|
183
|
+
emitReport("process.uncaughtException", error);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const handleUnhandledRejection = (reason: unknown) => {
|
|
187
|
+
emitReport("process.unhandledRejection", reason);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
processGlobal.on("uncaughtException", handleUncaughtException);
|
|
191
|
+
processGlobal.on("unhandledRejection", handleUnhandledRejection);
|
|
192
|
+
|
|
193
|
+
disposers.push(() => {
|
|
194
|
+
processGlobal.off("uncaughtException", handleUncaughtException);
|
|
195
|
+
processGlobal.off("unhandledRejection", handleUnhandledRejection);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let disposed = false;
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
dispose() {
|
|
203
|
+
if (disposed) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
disposed = true;
|
|
208
|
+
for (const dispose of disposers) {
|
|
209
|
+
dispose();
|
|
210
|
+
}
|
|
211
|
+
disposers.length = 0;
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
package/src/index.ts
CHANGED