@cresenity/cresjs-error-collector 0.1.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/LICENSE +21 -0
- package/README.md +89 -0
- package/dist/chunk-GVQRIR62.js +114 -0
- package/dist/chunk-GVQRIR62.js.map +1 -0
- package/dist/index.cjs +142 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +55 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +204 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +30 -0
- package/dist/react.d.ts +30 -0
- package/dist/react.js +70 -0
- package/dist/react.js.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cresenity
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# cresjs-error-collector
|
|
2
|
+
|
|
3
|
+
Browser/React error collector for CF (Cresenity Framework) apps. Reports uncaught JS errors,
|
|
4
|
+
unhandled promise rejections, and caught React render errors **directly to devcloud** — cross-
|
|
5
|
+
origin, no backend relay — landing in the same Exception Collector PHP exceptions already use.
|
|
6
|
+
|
|
7
|
+
## How it talks to devcloud
|
|
8
|
+
|
|
9
|
+
The browser POSTs straight to devcloud's `v1/jsExceptions` endpoint (CORS-open), authenticated
|
|
10
|
+
with a query-string `key` — not a header, since `navigator.sendBeacon()` (the primary transport
|
|
11
|
+
here, so a report still goes out even if the page is closing) can't set custom headers at all.
|
|
12
|
+
|
|
13
|
+
That `key` is `app.js_ingest_key` from devcloud (Manager → Project → App → "JS Ingest Key") — a
|
|
14
|
+
low-privilege, per-app token deliberately safe to expose in page source, the same way a Sentry
|
|
15
|
+
DSN's public key is. It can only submit JS error reports through this one endpoint; it is **not**
|
|
16
|
+
`app.api_key`/`secret_key` (those stay server-side, used by devcloud's APM trace ingestion and
|
|
17
|
+
other server-to-server calls).
|
|
18
|
+
|
|
19
|
+
**Never hardcode the devcloud endpoint URL or the key in your app's source.** Both come from
|
|
20
|
+
server-side config injected into the page at render time — see "Wiring it into a page" below.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
Not published to a private registry — public on npm under the `@cresenity` scope:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @cresenity/cresjs-error-collector
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
### Vanilla (any page, framework-agnostic)
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { initJsErrorCollector } from '@cresenity/cresjs-error-collector';
|
|
36
|
+
|
|
37
|
+
initJsErrorCollector();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Attaches `window.onerror`/`unhandledrejection` listeners. Reads `window.__CF_JS_COLLECTOR_CONFIG__`
|
|
41
|
+
(`{ endpoint, key } | null`) by default — no-ops entirely (not even attaching the listeners) when
|
|
42
|
+
that's missing. Pass `{ endpoint, key }` directly to `createCollector()` instead if your host page
|
|
43
|
+
doesn't go through that injection point.
|
|
44
|
+
|
|
45
|
+
### React
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { CresjsErrorBoundary } from '@cresenity/cresjs-error-collector/react';
|
|
49
|
+
|
|
50
|
+
<CresjsErrorBoundary>
|
|
51
|
+
<App />
|
|
52
|
+
</CresjsErrorBoundary>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
React never lets a render/lifecycle/hook error bubble to `window.onerror` once a boundary exists
|
|
56
|
+
above it — `CresjsErrorBoundary` is that boundary, reporting via the same mechanism. Errors outside
|
|
57
|
+
React's render cycle (event handlers, timers, unhandled promise rejections) still need the vanilla
|
|
58
|
+
`initJsErrorCollector()` call too — the two cover different failure modes, use both.
|
|
59
|
+
|
|
60
|
+
Pass `fallback` (node, or `(error, reset) => node`) for a custom crash UI instead of the built-in
|
|
61
|
+
one, and `onError` for an extra side effect (toast, local logging) alongside the report.
|
|
62
|
+
|
|
63
|
+
## Wiring it into a page (CF-specific)
|
|
64
|
+
|
|
65
|
+
Something server-side has to set `window.__CF_JS_COLLECTOR_CONFIG__` before this package's code
|
|
66
|
+
runs, reading `devcloud.jsCollector.url` / `devcloud.jsCollector.key` (`system/config/devcloud.php`
|
|
67
|
+
in the CF monorepo — both env-driven, no hardcoded defaults):
|
|
68
|
+
|
|
69
|
+
```html
|
|
70
|
+
<script>
|
|
71
|
+
window.__CF_JS_COLLECTOR_CONFIG__ = { endpoint: "...", key: "..." };
|
|
72
|
+
</script>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`CApp`'s `RendererTrait` already does this for every classic CF page automatically. A standalone
|
|
76
|
+
SPA shell (one that bypasses `CApp::renderScripts()` entirely, e.g. a bare `<!DOCTYPE html>` blade)
|
|
77
|
+
needs the same snippet added by hand — see `application/devcloud/default/views/page/
|
|
78
|
+
home-console.blade.php` for the reference.
|
|
79
|
+
|
|
80
|
+
## Build
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm install
|
|
84
|
+
npm run build # tsup -> dist/ (esm+cjs+d.ts, two entries: index, react)
|
|
85
|
+
npm run typecheck
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`npm install` from a git URL runs the `prepare` script automatically, so consumers get a built
|
|
89
|
+
`dist/` without needing to run `npm run build` themselves.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// src/reporter.ts
|
|
2
|
+
var DEFAULT_MAX_REPORTS_PER_LOAD = 20;
|
|
3
|
+
function defaultGetConfig() {
|
|
4
|
+
if (typeof window === "undefined") {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
const config = window.__CF_JS_COLLECTOR_CONFIG__;
|
|
8
|
+
if (!config || !config.endpoint || !config.key) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
return { endpoint: config.endpoint, key: config.key };
|
|
12
|
+
}
|
|
13
|
+
function createCollector(options = {}) {
|
|
14
|
+
var _a;
|
|
15
|
+
const maxReportsPerLoad = (_a = options.maxReportsPerLoad) != null ? _a : DEFAULT_MAX_REPORTS_PER_LOAD;
|
|
16
|
+
const getConfig = () => {
|
|
17
|
+
var _a2;
|
|
18
|
+
if (options.endpoint && options.key) {
|
|
19
|
+
return { endpoint: options.endpoint, key: options.key };
|
|
20
|
+
}
|
|
21
|
+
return ((_a2 = options.getConfig) != null ? _a2 : defaultGetConfig)();
|
|
22
|
+
};
|
|
23
|
+
let sentCount = 0;
|
|
24
|
+
let attached = false;
|
|
25
|
+
function send(payload) {
|
|
26
|
+
const config = getConfig();
|
|
27
|
+
if (!config || sentCount >= maxReportsPerLoad) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
sentCount += 1;
|
|
31
|
+
try {
|
|
32
|
+
const url = `${config.endpoint}?key=${encodeURIComponent(config.key)}`;
|
|
33
|
+
const body = JSON.stringify(payload);
|
|
34
|
+
if (typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
35
|
+
navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (typeof fetch === "function") {
|
|
39
|
+
fetch(url, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: { "Content-Type": "application/json" },
|
|
42
|
+
body,
|
|
43
|
+
keepalive: true,
|
|
44
|
+
mode: "cors"
|
|
45
|
+
}).catch(() => {
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function onError(event) {
|
|
52
|
+
if (!event || !event.error && !event.message) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const jsError = event.error;
|
|
56
|
+
send({
|
|
57
|
+
message: String(jsError && jsError.message || event.message || "Unknown error"),
|
|
58
|
+
stack: jsError && jsError.stack ? String(jsError.stack) : null,
|
|
59
|
+
name: jsError && jsError.name ? jsError.name : null,
|
|
60
|
+
filename: event.filename || null,
|
|
61
|
+
lineno: event.lineno || null,
|
|
62
|
+
colno: event.colno || null,
|
|
63
|
+
url: window.location.href,
|
|
64
|
+
type: "error"
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function onUnhandledRejection(event) {
|
|
68
|
+
const reason = event && event.reason;
|
|
69
|
+
const message = reason && reason.message ? reason.message : String(reason);
|
|
70
|
+
send({
|
|
71
|
+
message: String(message),
|
|
72
|
+
stack: reason && reason.stack ? String(reason.stack) : null,
|
|
73
|
+
name: reason && reason.name ? reason.name : "UnhandledRejection",
|
|
74
|
+
filename: null,
|
|
75
|
+
lineno: null,
|
|
76
|
+
colno: null,
|
|
77
|
+
url: window.location.href,
|
|
78
|
+
type: "unhandledrejection"
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
init() {
|
|
83
|
+
if (attached || typeof window === "undefined") {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
attached = true;
|
|
87
|
+
window.addEventListener("error", onError);
|
|
88
|
+
window.addEventListener("unhandledrejection", onUnhandledRejection);
|
|
89
|
+
},
|
|
90
|
+
destroy() {
|
|
91
|
+
if (!attached) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
attached = false;
|
|
95
|
+
window.removeEventListener("error", onError);
|
|
96
|
+
window.removeEventListener("unhandledrejection", onUnhandledRejection);
|
|
97
|
+
},
|
|
98
|
+
report: send
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
var defaultCollector = createCollector();
|
|
102
|
+
function initJsErrorCollector() {
|
|
103
|
+
defaultCollector.init();
|
|
104
|
+
}
|
|
105
|
+
function reportJsError(payload) {
|
|
106
|
+
defaultCollector.report(payload);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export {
|
|
110
|
+
createCollector,
|
|
111
|
+
initJsErrorCollector,
|
|
112
|
+
reportJsError
|
|
113
|
+
};
|
|
114
|
+
//# sourceMappingURL=chunk-GVQRIR62.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/reporter.ts"],"sourcesContent":["/**\n * Framework-agnostic reporter - window.onerror/unhandledrejection + a manual report() call.\n * POSTs DIRECTLY to devcloud's v1/jsExceptions endpoint, cross-origin (CORS-open on that side) -\n * not through the reporting app's own backend at all. Auth is a query-string `key`\n * (app.js_ingest_key, a low-privilege token deliberately safe to expose in page source, like a\n * Sentry DSN's public key) rather than a header, since navigator.sendBeacon() - the primary\n * transport here, used so a report still gets sent even if the page is closing - cannot set\n * custom headers at all.\n *\n * This is the single source of truth cres.js itself depends on for its own window.onerror/\n * unhandledrejection coverage - a fix/change here reaches every classic CF page AND every React\n * SPA built on this package without touching either separately.\n */\n\nexport interface JsErrorReport {\n message: string;\n stack?: string | null;\n name?: string | null;\n filename?: string | null;\n lineno?: number | null;\n colno?: number | null;\n url?: string;\n /** 'error' | 'unhandledrejection' | 'react' | a caller-defined label for a manual report() */\n type?: string;\n}\n\nexport interface CollectorConfig {\n /** devcloud's ingest endpoint, e.g. \"https://devcloud.example.com/v1/jsExceptions\". Never hardcode this - always pass it in from server-side config (see the README). */\n endpoint: string;\n /** app.js_ingest_key from devcloud (Manager > Project > App > \"JS Ingest Key\"). Safe to expose client-side - see the docblock above. */\n key: string;\n}\n\nexport interface CollectorOptions extends Partial<CollectorConfig> {\n /** Hard cap per page load - an error storm must not turn into a request storm. */\n maxReportsPerLoad?: number;\n /**\n * Defaults to reading window.__CF_JS_COLLECTOR_CONFIG__ ({endpoint, key} or null), injected\n * server-side by CApp's RendererTrait (classic CF pages) or an SPA's own shell, from\n * `devcloud.jsCollector.*` config - so an app that never configured this gets zero reporting\n * overhead, not just a dropped request. Pass endpoint/key directly instead (or override this)\n * only for a host that doesn't go through that injection point.\n */\n getConfig?: () => CollectorConfig | null;\n}\n\nconst DEFAULT_MAX_REPORTS_PER_LOAD = 20;\n\nfunction defaultGetConfig(): CollectorConfig | null {\n if (typeof window === 'undefined') {\n return null;\n }\n const config = (window as any).__CF_JS_COLLECTOR_CONFIG__;\n if (!config || !config.endpoint || !config.key) {\n return null;\n }\n\n return { endpoint: config.endpoint, key: config.key };\n}\n\nexport interface Collector {\n /** Attach window.onerror/unhandledrejection listeners. Safe to call multiple times. */\n init: () => void;\n /** Detach the listeners attached by init(). */\n destroy: () => void;\n /** Send one report directly - what ErrorBoundary/componentDidCatch calls. */\n report: (payload: JsErrorReport) => void;\n}\n\nexport function createCollector(options: CollectorOptions = {}): Collector {\n const maxReportsPerLoad = options.maxReportsPerLoad ?? DEFAULT_MAX_REPORTS_PER_LOAD;\n const getConfig = (): CollectorConfig | null => {\n if (options.endpoint && options.key) {\n return { endpoint: options.endpoint, key: options.key };\n }\n return (options.getConfig ?? defaultGetConfig)();\n };\n\n let sentCount = 0;\n let attached = false;\n\n function send(payload: JsErrorReport): void {\n const config = getConfig();\n if (!config || sentCount >= maxReportsPerLoad) {\n return;\n }\n sentCount += 1;\n\n try {\n const url = `${config.endpoint}?key=${encodeURIComponent(config.key)}`;\n const body = JSON.stringify(payload);\n if (typeof navigator !== 'undefined' && navigator.sendBeacon) {\n navigator.sendBeacon(url, new Blob([body], { type: 'application/json' }));\n return;\n }\n if (typeof fetch === 'function') {\n fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n keepalive: true,\n mode: 'cors',\n }).catch(() => {\n // a failed report must never itself surface as another error\n });\n }\n } catch {\n // same - stay silent\n }\n }\n\n function onError(event: ErrorEvent): void {\n if (!event || (!event.error && !event.message)) {\n // plain resource load failure (img/script/link) - not a JS exception\n return;\n }\n const jsError = event.error;\n send({\n message: String((jsError && jsError.message) || event.message || 'Unknown error'),\n stack: jsError && jsError.stack ? String(jsError.stack) : null,\n name: jsError && jsError.name ? jsError.name : null,\n filename: event.filename || null,\n lineno: event.lineno || null,\n colno: event.colno || null,\n url: window.location.href,\n type: 'error',\n });\n }\n\n function onUnhandledRejection(event: PromiseRejectionEvent): void {\n const reason: any = event && event.reason;\n const message = reason && reason.message ? reason.message : String(reason);\n send({\n message: String(message),\n stack: reason && reason.stack ? String(reason.stack) : null,\n name: reason && reason.name ? reason.name : 'UnhandledRejection',\n filename: null,\n lineno: null,\n colno: null,\n url: window.location.href,\n type: 'unhandledrejection',\n });\n }\n\n return {\n init() {\n if (attached || typeof window === 'undefined') {\n return;\n }\n attached = true;\n window.addEventListener('error', onError);\n window.addEventListener('unhandledrejection', onUnhandledRejection);\n },\n destroy() {\n if (!attached) {\n return;\n }\n attached = false;\n window.removeEventListener('error', onError);\n window.removeEventListener('unhandledrejection', onUnhandledRejection);\n },\n report: send,\n };\n}\n\n// Default singleton - covers the common case (one collector per page) without every consumer\n// needing to manage its own instance.\nconst defaultCollector = createCollector();\n\nexport function initJsErrorCollector(): void {\n defaultCollector.init();\n}\n\nexport function reportJsError(payload: JsErrorReport): void {\n defaultCollector.report(payload);\n}\n"],"mappings":"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","names":["_a"]}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
createCollector: () => createCollector,
|
|
24
|
+
initJsErrorCollector: () => initJsErrorCollector,
|
|
25
|
+
reportJsError: () => reportJsError
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(src_exports);
|
|
28
|
+
|
|
29
|
+
// src/reporter.ts
|
|
30
|
+
var DEFAULT_MAX_REPORTS_PER_LOAD = 20;
|
|
31
|
+
function defaultGetConfig() {
|
|
32
|
+
if (typeof window === "undefined") {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const config = window.__CF_JS_COLLECTOR_CONFIG__;
|
|
36
|
+
if (!config || !config.endpoint || !config.key) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return { endpoint: config.endpoint, key: config.key };
|
|
40
|
+
}
|
|
41
|
+
function createCollector(options = {}) {
|
|
42
|
+
var _a;
|
|
43
|
+
const maxReportsPerLoad = (_a = options.maxReportsPerLoad) != null ? _a : DEFAULT_MAX_REPORTS_PER_LOAD;
|
|
44
|
+
const getConfig = () => {
|
|
45
|
+
var _a2;
|
|
46
|
+
if (options.endpoint && options.key) {
|
|
47
|
+
return { endpoint: options.endpoint, key: options.key };
|
|
48
|
+
}
|
|
49
|
+
return ((_a2 = options.getConfig) != null ? _a2 : defaultGetConfig)();
|
|
50
|
+
};
|
|
51
|
+
let sentCount = 0;
|
|
52
|
+
let attached = false;
|
|
53
|
+
function send(payload) {
|
|
54
|
+
const config = getConfig();
|
|
55
|
+
if (!config || sentCount >= maxReportsPerLoad) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
sentCount += 1;
|
|
59
|
+
try {
|
|
60
|
+
const url = `${config.endpoint}?key=${encodeURIComponent(config.key)}`;
|
|
61
|
+
const body = JSON.stringify(payload);
|
|
62
|
+
if (typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
63
|
+
navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (typeof fetch === "function") {
|
|
67
|
+
fetch(url, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: { "Content-Type": "application/json" },
|
|
70
|
+
body,
|
|
71
|
+
keepalive: true,
|
|
72
|
+
mode: "cors"
|
|
73
|
+
}).catch(() => {
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function onError(event) {
|
|
80
|
+
if (!event || !event.error && !event.message) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const jsError = event.error;
|
|
84
|
+
send({
|
|
85
|
+
message: String(jsError && jsError.message || event.message || "Unknown error"),
|
|
86
|
+
stack: jsError && jsError.stack ? String(jsError.stack) : null,
|
|
87
|
+
name: jsError && jsError.name ? jsError.name : null,
|
|
88
|
+
filename: event.filename || null,
|
|
89
|
+
lineno: event.lineno || null,
|
|
90
|
+
colno: event.colno || null,
|
|
91
|
+
url: window.location.href,
|
|
92
|
+
type: "error"
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function onUnhandledRejection(event) {
|
|
96
|
+
const reason = event && event.reason;
|
|
97
|
+
const message = reason && reason.message ? reason.message : String(reason);
|
|
98
|
+
send({
|
|
99
|
+
message: String(message),
|
|
100
|
+
stack: reason && reason.stack ? String(reason.stack) : null,
|
|
101
|
+
name: reason && reason.name ? reason.name : "UnhandledRejection",
|
|
102
|
+
filename: null,
|
|
103
|
+
lineno: null,
|
|
104
|
+
colno: null,
|
|
105
|
+
url: window.location.href,
|
|
106
|
+
type: "unhandledrejection"
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
init() {
|
|
111
|
+
if (attached || typeof window === "undefined") {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
attached = true;
|
|
115
|
+
window.addEventListener("error", onError);
|
|
116
|
+
window.addEventListener("unhandledrejection", onUnhandledRejection);
|
|
117
|
+
},
|
|
118
|
+
destroy() {
|
|
119
|
+
if (!attached) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
attached = false;
|
|
123
|
+
window.removeEventListener("error", onError);
|
|
124
|
+
window.removeEventListener("unhandledrejection", onUnhandledRejection);
|
|
125
|
+
},
|
|
126
|
+
report: send
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
var defaultCollector = createCollector();
|
|
130
|
+
function initJsErrorCollector() {
|
|
131
|
+
defaultCollector.init();
|
|
132
|
+
}
|
|
133
|
+
function reportJsError(payload) {
|
|
134
|
+
defaultCollector.report(payload);
|
|
135
|
+
}
|
|
136
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
137
|
+
0 && (module.exports = {
|
|
138
|
+
createCollector,
|
|
139
|
+
initJsErrorCollector,
|
|
140
|
+
reportJsError
|
|
141
|
+
});
|
|
142
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/reporter.ts"],"sourcesContent":["export {\n createCollector,\n initJsErrorCollector,\n reportJsError,\n} from './reporter';\nexport type { Collector, CollectorConfig, CollectorOptions, JsErrorReport } from './reporter';\n","/**\n * Framework-agnostic reporter - window.onerror/unhandledrejection + a manual report() call.\n * POSTs DIRECTLY to devcloud's v1/jsExceptions endpoint, cross-origin (CORS-open on that side) -\n * not through the reporting app's own backend at all. Auth is a query-string `key`\n * (app.js_ingest_key, a low-privilege token deliberately safe to expose in page source, like a\n * Sentry DSN's public key) rather than a header, since navigator.sendBeacon() - the primary\n * transport here, used so a report still gets sent even if the page is closing - cannot set\n * custom headers at all.\n *\n * This is the single source of truth cres.js itself depends on for its own window.onerror/\n * unhandledrejection coverage - a fix/change here reaches every classic CF page AND every React\n * SPA built on this package without touching either separately.\n */\n\nexport interface JsErrorReport {\n message: string;\n stack?: string | null;\n name?: string | null;\n filename?: string | null;\n lineno?: number | null;\n colno?: number | null;\n url?: string;\n /** 'error' | 'unhandledrejection' | 'react' | a caller-defined label for a manual report() */\n type?: string;\n}\n\nexport interface CollectorConfig {\n /** devcloud's ingest endpoint, e.g. \"https://devcloud.example.com/v1/jsExceptions\". Never hardcode this - always pass it in from server-side config (see the README). */\n endpoint: string;\n /** app.js_ingest_key from devcloud (Manager > Project > App > \"JS Ingest Key\"). Safe to expose client-side - see the docblock above. */\n key: string;\n}\n\nexport interface CollectorOptions extends Partial<CollectorConfig> {\n /** Hard cap per page load - an error storm must not turn into a request storm. */\n maxReportsPerLoad?: number;\n /**\n * Defaults to reading window.__CF_JS_COLLECTOR_CONFIG__ ({endpoint, key} or null), injected\n * server-side by CApp's RendererTrait (classic CF pages) or an SPA's own shell, from\n * `devcloud.jsCollector.*` config - so an app that never configured this gets zero reporting\n * overhead, not just a dropped request. Pass endpoint/key directly instead (or override this)\n * only for a host that doesn't go through that injection point.\n */\n getConfig?: () => CollectorConfig | null;\n}\n\nconst DEFAULT_MAX_REPORTS_PER_LOAD = 20;\n\nfunction defaultGetConfig(): CollectorConfig | null {\n if (typeof window === 'undefined') {\n return null;\n }\n const config = (window as any).__CF_JS_COLLECTOR_CONFIG__;\n if (!config || !config.endpoint || !config.key) {\n return null;\n }\n\n return { endpoint: config.endpoint, key: config.key };\n}\n\nexport interface Collector {\n /** Attach window.onerror/unhandledrejection listeners. Safe to call multiple times. */\n init: () => void;\n /** Detach the listeners attached by init(). */\n destroy: () => void;\n /** Send one report directly - what ErrorBoundary/componentDidCatch calls. */\n report: (payload: JsErrorReport) => void;\n}\n\nexport function createCollector(options: CollectorOptions = {}): Collector {\n const maxReportsPerLoad = options.maxReportsPerLoad ?? DEFAULT_MAX_REPORTS_PER_LOAD;\n const getConfig = (): CollectorConfig | null => {\n if (options.endpoint && options.key) {\n return { endpoint: options.endpoint, key: options.key };\n }\n return (options.getConfig ?? defaultGetConfig)();\n };\n\n let sentCount = 0;\n let attached = false;\n\n function send(payload: JsErrorReport): void {\n const config = getConfig();\n if (!config || sentCount >= maxReportsPerLoad) {\n return;\n }\n sentCount += 1;\n\n try {\n const url = `${config.endpoint}?key=${encodeURIComponent(config.key)}`;\n const body = JSON.stringify(payload);\n if (typeof navigator !== 'undefined' && navigator.sendBeacon) {\n navigator.sendBeacon(url, new Blob([body], { type: 'application/json' }));\n return;\n }\n if (typeof fetch === 'function') {\n fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n keepalive: true,\n mode: 'cors',\n }).catch(() => {\n // a failed report must never itself surface as another error\n });\n }\n } catch {\n // same - stay silent\n }\n }\n\n function onError(event: ErrorEvent): void {\n if (!event || (!event.error && !event.message)) {\n // plain resource load failure (img/script/link) - not a JS exception\n return;\n }\n const jsError = event.error;\n send({\n message: String((jsError && jsError.message) || event.message || 'Unknown error'),\n stack: jsError && jsError.stack ? String(jsError.stack) : null,\n name: jsError && jsError.name ? jsError.name : null,\n filename: event.filename || null,\n lineno: event.lineno || null,\n colno: event.colno || null,\n url: window.location.href,\n type: 'error',\n });\n }\n\n function onUnhandledRejection(event: PromiseRejectionEvent): void {\n const reason: any = event && event.reason;\n const message = reason && reason.message ? reason.message : String(reason);\n send({\n message: String(message),\n stack: reason && reason.stack ? String(reason.stack) : null,\n name: reason && reason.name ? reason.name : 'UnhandledRejection',\n filename: null,\n lineno: null,\n colno: null,\n url: window.location.href,\n type: 'unhandledrejection',\n });\n }\n\n return {\n init() {\n if (attached || typeof window === 'undefined') {\n return;\n }\n attached = true;\n window.addEventListener('error', onError);\n window.addEventListener('unhandledrejection', onUnhandledRejection);\n },\n destroy() {\n if (!attached) {\n return;\n }\n attached = false;\n window.removeEventListener('error', onError);\n window.removeEventListener('unhandledrejection', onUnhandledRejection);\n },\n report: send,\n };\n}\n\n// Default singleton - covers the common case (one collector per page) without every consumer\n// needing to manage its own instance.\nconst defaultCollector = createCollector();\n\nexport function initJsErrorCollector(): void {\n defaultCollector.init();\n}\n\nexport function reportJsError(payload: JsErrorReport): void {\n defaultCollector.report(payload);\n}\n"],"mappings":"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","names":["_a"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-agnostic reporter - window.onerror/unhandledrejection + a manual report() call.
|
|
3
|
+
* POSTs DIRECTLY to devcloud's v1/jsExceptions endpoint, cross-origin (CORS-open on that side) -
|
|
4
|
+
* not through the reporting app's own backend at all. Auth is a query-string `key`
|
|
5
|
+
* (app.js_ingest_key, a low-privilege token deliberately safe to expose in page source, like a
|
|
6
|
+
* Sentry DSN's public key) rather than a header, since navigator.sendBeacon() - the primary
|
|
7
|
+
* transport here, used so a report still gets sent even if the page is closing - cannot set
|
|
8
|
+
* custom headers at all.
|
|
9
|
+
*
|
|
10
|
+
* This is the single source of truth cres.js itself depends on for its own window.onerror/
|
|
11
|
+
* unhandledrejection coverage - a fix/change here reaches every classic CF page AND every React
|
|
12
|
+
* SPA built on this package without touching either separately.
|
|
13
|
+
*/
|
|
14
|
+
interface JsErrorReport {
|
|
15
|
+
message: string;
|
|
16
|
+
stack?: string | null;
|
|
17
|
+
name?: string | null;
|
|
18
|
+
filename?: string | null;
|
|
19
|
+
lineno?: number | null;
|
|
20
|
+
colno?: number | null;
|
|
21
|
+
url?: string;
|
|
22
|
+
/** 'error' | 'unhandledrejection' | 'react' | a caller-defined label for a manual report() */
|
|
23
|
+
type?: string;
|
|
24
|
+
}
|
|
25
|
+
interface CollectorConfig {
|
|
26
|
+
/** devcloud's ingest endpoint, e.g. "https://devcloud.example.com/v1/jsExceptions". Never hardcode this - always pass it in from server-side config (see the README). */
|
|
27
|
+
endpoint: string;
|
|
28
|
+
/** app.js_ingest_key from devcloud (Manager > Project > App > "JS Ingest Key"). Safe to expose client-side - see the docblock above. */
|
|
29
|
+
key: string;
|
|
30
|
+
}
|
|
31
|
+
interface CollectorOptions extends Partial<CollectorConfig> {
|
|
32
|
+
/** Hard cap per page load - an error storm must not turn into a request storm. */
|
|
33
|
+
maxReportsPerLoad?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Defaults to reading window.__CF_JS_COLLECTOR_CONFIG__ ({endpoint, key} or null), injected
|
|
36
|
+
* server-side by CApp's RendererTrait (classic CF pages) or an SPA's own shell, from
|
|
37
|
+
* `devcloud.jsCollector.*` config - so an app that never configured this gets zero reporting
|
|
38
|
+
* overhead, not just a dropped request. Pass endpoint/key directly instead (or override this)
|
|
39
|
+
* only for a host that doesn't go through that injection point.
|
|
40
|
+
*/
|
|
41
|
+
getConfig?: () => CollectorConfig | null;
|
|
42
|
+
}
|
|
43
|
+
interface Collector {
|
|
44
|
+
/** Attach window.onerror/unhandledrejection listeners. Safe to call multiple times. */
|
|
45
|
+
init: () => void;
|
|
46
|
+
/** Detach the listeners attached by init(). */
|
|
47
|
+
destroy: () => void;
|
|
48
|
+
/** Send one report directly - what ErrorBoundary/componentDidCatch calls. */
|
|
49
|
+
report: (payload: JsErrorReport) => void;
|
|
50
|
+
}
|
|
51
|
+
declare function createCollector(options?: CollectorOptions): Collector;
|
|
52
|
+
declare function initJsErrorCollector(): void;
|
|
53
|
+
declare function reportJsError(payload: JsErrorReport): void;
|
|
54
|
+
|
|
55
|
+
export { type Collector, type CollectorConfig, type CollectorOptions, type JsErrorReport, createCollector, initJsErrorCollector, reportJsError };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-agnostic reporter - window.onerror/unhandledrejection + a manual report() call.
|
|
3
|
+
* POSTs DIRECTLY to devcloud's v1/jsExceptions endpoint, cross-origin (CORS-open on that side) -
|
|
4
|
+
* not through the reporting app's own backend at all. Auth is a query-string `key`
|
|
5
|
+
* (app.js_ingest_key, a low-privilege token deliberately safe to expose in page source, like a
|
|
6
|
+
* Sentry DSN's public key) rather than a header, since navigator.sendBeacon() - the primary
|
|
7
|
+
* transport here, used so a report still gets sent even if the page is closing - cannot set
|
|
8
|
+
* custom headers at all.
|
|
9
|
+
*
|
|
10
|
+
* This is the single source of truth cres.js itself depends on for its own window.onerror/
|
|
11
|
+
* unhandledrejection coverage - a fix/change here reaches every classic CF page AND every React
|
|
12
|
+
* SPA built on this package without touching either separately.
|
|
13
|
+
*/
|
|
14
|
+
interface JsErrorReport {
|
|
15
|
+
message: string;
|
|
16
|
+
stack?: string | null;
|
|
17
|
+
name?: string | null;
|
|
18
|
+
filename?: string | null;
|
|
19
|
+
lineno?: number | null;
|
|
20
|
+
colno?: number | null;
|
|
21
|
+
url?: string;
|
|
22
|
+
/** 'error' | 'unhandledrejection' | 'react' | a caller-defined label for a manual report() */
|
|
23
|
+
type?: string;
|
|
24
|
+
}
|
|
25
|
+
interface CollectorConfig {
|
|
26
|
+
/** devcloud's ingest endpoint, e.g. "https://devcloud.example.com/v1/jsExceptions". Never hardcode this - always pass it in from server-side config (see the README). */
|
|
27
|
+
endpoint: string;
|
|
28
|
+
/** app.js_ingest_key from devcloud (Manager > Project > App > "JS Ingest Key"). Safe to expose client-side - see the docblock above. */
|
|
29
|
+
key: string;
|
|
30
|
+
}
|
|
31
|
+
interface CollectorOptions extends Partial<CollectorConfig> {
|
|
32
|
+
/** Hard cap per page load - an error storm must not turn into a request storm. */
|
|
33
|
+
maxReportsPerLoad?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Defaults to reading window.__CF_JS_COLLECTOR_CONFIG__ ({endpoint, key} or null), injected
|
|
36
|
+
* server-side by CApp's RendererTrait (classic CF pages) or an SPA's own shell, from
|
|
37
|
+
* `devcloud.jsCollector.*` config - so an app that never configured this gets zero reporting
|
|
38
|
+
* overhead, not just a dropped request. Pass endpoint/key directly instead (or override this)
|
|
39
|
+
* only for a host that doesn't go through that injection point.
|
|
40
|
+
*/
|
|
41
|
+
getConfig?: () => CollectorConfig | null;
|
|
42
|
+
}
|
|
43
|
+
interface Collector {
|
|
44
|
+
/** Attach window.onerror/unhandledrejection listeners. Safe to call multiple times. */
|
|
45
|
+
init: () => void;
|
|
46
|
+
/** Detach the listeners attached by init(). */
|
|
47
|
+
destroy: () => void;
|
|
48
|
+
/** Send one report directly - what ErrorBoundary/componentDidCatch calls. */
|
|
49
|
+
report: (payload: JsErrorReport) => void;
|
|
50
|
+
}
|
|
51
|
+
declare function createCollector(options?: CollectorOptions): Collector;
|
|
52
|
+
declare function initJsErrorCollector(): void;
|
|
53
|
+
declare function reportJsError(payload: JsErrorReport): void;
|
|
54
|
+
|
|
55
|
+
export { type Collector, type CollectorConfig, type CollectorOptions, type JsErrorReport, createCollector, initJsErrorCollector, reportJsError };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/react.cjs
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/react.ts
|
|
21
|
+
var react_exports = {};
|
|
22
|
+
__export(react_exports, {
|
|
23
|
+
CresjsErrorBoundary: () => CresjsErrorBoundary,
|
|
24
|
+
initJsErrorCollector: () => initJsErrorCollector,
|
|
25
|
+
reportJsError: () => reportJsError
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(react_exports);
|
|
28
|
+
|
|
29
|
+
// src/ErrorBoundary.tsx
|
|
30
|
+
var import_react = require("react");
|
|
31
|
+
|
|
32
|
+
// src/reporter.ts
|
|
33
|
+
var DEFAULT_MAX_REPORTS_PER_LOAD = 20;
|
|
34
|
+
function defaultGetConfig() {
|
|
35
|
+
if (typeof window === "undefined") {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const config = window.__CF_JS_COLLECTOR_CONFIG__;
|
|
39
|
+
if (!config || !config.endpoint || !config.key) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return { endpoint: config.endpoint, key: config.key };
|
|
43
|
+
}
|
|
44
|
+
function createCollector(options = {}) {
|
|
45
|
+
var _a;
|
|
46
|
+
const maxReportsPerLoad = (_a = options.maxReportsPerLoad) != null ? _a : DEFAULT_MAX_REPORTS_PER_LOAD;
|
|
47
|
+
const getConfig = () => {
|
|
48
|
+
var _a2;
|
|
49
|
+
if (options.endpoint && options.key) {
|
|
50
|
+
return { endpoint: options.endpoint, key: options.key };
|
|
51
|
+
}
|
|
52
|
+
return ((_a2 = options.getConfig) != null ? _a2 : defaultGetConfig)();
|
|
53
|
+
};
|
|
54
|
+
let sentCount = 0;
|
|
55
|
+
let attached = false;
|
|
56
|
+
function send(payload) {
|
|
57
|
+
const config = getConfig();
|
|
58
|
+
if (!config || sentCount >= maxReportsPerLoad) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
sentCount += 1;
|
|
62
|
+
try {
|
|
63
|
+
const url = `${config.endpoint}?key=${encodeURIComponent(config.key)}`;
|
|
64
|
+
const body = JSON.stringify(payload);
|
|
65
|
+
if (typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
66
|
+
navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (typeof fetch === "function") {
|
|
70
|
+
fetch(url, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { "Content-Type": "application/json" },
|
|
73
|
+
body,
|
|
74
|
+
keepalive: true,
|
|
75
|
+
mode: "cors"
|
|
76
|
+
}).catch(() => {
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function onError(event) {
|
|
83
|
+
if (!event || !event.error && !event.message) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const jsError = event.error;
|
|
87
|
+
send({
|
|
88
|
+
message: String(jsError && jsError.message || event.message || "Unknown error"),
|
|
89
|
+
stack: jsError && jsError.stack ? String(jsError.stack) : null,
|
|
90
|
+
name: jsError && jsError.name ? jsError.name : null,
|
|
91
|
+
filename: event.filename || null,
|
|
92
|
+
lineno: event.lineno || null,
|
|
93
|
+
colno: event.colno || null,
|
|
94
|
+
url: window.location.href,
|
|
95
|
+
type: "error"
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function onUnhandledRejection(event) {
|
|
99
|
+
const reason = event && event.reason;
|
|
100
|
+
const message = reason && reason.message ? reason.message : String(reason);
|
|
101
|
+
send({
|
|
102
|
+
message: String(message),
|
|
103
|
+
stack: reason && reason.stack ? String(reason.stack) : null,
|
|
104
|
+
name: reason && reason.name ? reason.name : "UnhandledRejection",
|
|
105
|
+
filename: null,
|
|
106
|
+
lineno: null,
|
|
107
|
+
colno: null,
|
|
108
|
+
url: window.location.href,
|
|
109
|
+
type: "unhandledrejection"
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
init() {
|
|
114
|
+
if (attached || typeof window === "undefined") {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
attached = true;
|
|
118
|
+
window.addEventListener("error", onError);
|
|
119
|
+
window.addEventListener("unhandledrejection", onUnhandledRejection);
|
|
120
|
+
},
|
|
121
|
+
destroy() {
|
|
122
|
+
if (!attached) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
attached = false;
|
|
126
|
+
window.removeEventListener("error", onError);
|
|
127
|
+
window.removeEventListener("unhandledrejection", onUnhandledRejection);
|
|
128
|
+
},
|
|
129
|
+
report: send
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
var defaultCollector = createCollector();
|
|
133
|
+
function initJsErrorCollector() {
|
|
134
|
+
defaultCollector.init();
|
|
135
|
+
}
|
|
136
|
+
function reportJsError(payload) {
|
|
137
|
+
defaultCollector.report(payload);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/ErrorBoundary.tsx
|
|
141
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
142
|
+
var CresjsErrorBoundary = class extends import_react.Component {
|
|
143
|
+
constructor() {
|
|
144
|
+
super(...arguments);
|
|
145
|
+
this.state = { error: null };
|
|
146
|
+
this.reset = () => {
|
|
147
|
+
this.setState({ error: null });
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
static getDerivedStateFromError(error) {
|
|
151
|
+
return { error };
|
|
152
|
+
}
|
|
153
|
+
componentDidCatch(error, errorInfo) {
|
|
154
|
+
var _a, _b;
|
|
155
|
+
reportJsError({
|
|
156
|
+
message: error.message || String(error),
|
|
157
|
+
stack: error.stack || null,
|
|
158
|
+
name: error.name || null,
|
|
159
|
+
url: typeof window !== "undefined" ? window.location.href : void 0,
|
|
160
|
+
type: "react"
|
|
161
|
+
});
|
|
162
|
+
(_b = (_a = this.props).onError) == null ? void 0 : _b.call(_a, error, errorInfo);
|
|
163
|
+
}
|
|
164
|
+
render() {
|
|
165
|
+
const { error } = this.state;
|
|
166
|
+
if (!error) {
|
|
167
|
+
return this.props.children;
|
|
168
|
+
}
|
|
169
|
+
const { fallback } = this.props;
|
|
170
|
+
if (typeof fallback === "function") {
|
|
171
|
+
return fallback(error, this.reset);
|
|
172
|
+
}
|
|
173
|
+
if (fallback) {
|
|
174
|
+
return fallback;
|
|
175
|
+
}
|
|
176
|
+
return DEFAULT_FALLBACK;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
var DEFAULT_FALLBACK = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
180
|
+
"div",
|
|
181
|
+
{
|
|
182
|
+
style: {
|
|
183
|
+
display: "flex",
|
|
184
|
+
flexDirection: "column",
|
|
185
|
+
alignItems: "center",
|
|
186
|
+
justifyContent: "center",
|
|
187
|
+
gap: "0.75rem",
|
|
188
|
+
padding: "3rem 1.5rem",
|
|
189
|
+
textAlign: "center",
|
|
190
|
+
fontFamily: "system-ui, sans-serif"
|
|
191
|
+
},
|
|
192
|
+
children: [
|
|
193
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: 0, fontSize: "1rem", fontWeight: 600 }, children: "Terjadi kesalahan." }),
|
|
194
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: 0, fontSize: "0.875rem", opacity: 0.7 }, children: "Halaman ini sudah dilaporkan otomatis. Silakan muat ulang." })
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
199
|
+
0 && (module.exports = {
|
|
200
|
+
CresjsErrorBoundary,
|
|
201
|
+
initJsErrorCollector,
|
|
202
|
+
reportJsError
|
|
203
|
+
});
|
|
204
|
+
//# sourceMappingURL=react.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/react.ts","../src/ErrorBoundary.tsx","../src/reporter.ts"],"sourcesContent":["export { CresjsErrorBoundary } from './ErrorBoundary';\nexport type { CresjsErrorBoundaryProps } from './ErrorBoundary';\n// Re-exported so a React-only consumer doesn't also need to import from the base entry.\nexport { initJsErrorCollector, reportJsError } from './reporter';\nexport type { JsErrorReport } from './reporter';\n","import { Component } from 'react';\nimport type { ErrorInfo, ReactNode } from 'react';\nimport { reportJsError } from './reporter';\n\nexport interface CresjsErrorBoundaryProps {\n children: ReactNode;\n /** Custom fallback UI, or a render function given the error and a reset() to retry. */\n fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);\n /** Extra side effect (toast, local logging, etc.) - runs in addition to the report, not instead of it. */\n onError?: (error: Error, errorInfo: ErrorInfo) => void;\n}\n\ninterface CresjsErrorBoundaryState {\n error: Error | null;\n}\n\n/**\n * React only calls componentDidCatch()/getDerivedStateFromError() on the NEAREST boundary above\n * a thrown render/lifecycle/hook error - it never lets that kind of error bubble to\n * window.onerror (see reporter.ts's docblock for the other half: errors outside React's render\n * cycle - plain event handlers, timers, unhandled promise rejections - still reach window and are\n * covered by createCollector()/initJsErrorCollector() instead). Wrap your app root (or any\n * subtree worth isolating a crash to) in this to get both halves.\n */\nexport class CresjsErrorBoundary extends Component<CresjsErrorBoundaryProps, CresjsErrorBoundaryState> {\n state: CresjsErrorBoundaryState = { error: null };\n\n static getDerivedStateFromError(error: Error): CresjsErrorBoundaryState {\n return { error };\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo): void {\n reportJsError({\n message: error.message || String(error),\n stack: error.stack || null,\n name: error.name || null,\n url: typeof window !== 'undefined' ? window.location.href : undefined,\n type: 'react',\n });\n this.props.onError?.(error, errorInfo);\n }\n\n reset = (): void => {\n this.setState({ error: null });\n };\n\n render(): ReactNode {\n const { error } = this.state;\n if (!error) {\n return this.props.children;\n }\n\n const { fallback } = this.props;\n if (typeof fallback === 'function') {\n return fallback(error, this.reset);\n }\n if (fallback) {\n return fallback;\n }\n\n return DEFAULT_FALLBACK;\n }\n}\n\nconst DEFAULT_FALLBACK = (\n <div\n style={{\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n justifyContent: 'center',\n gap: '0.75rem',\n padding: '3rem 1.5rem',\n textAlign: 'center',\n fontFamily: 'system-ui, sans-serif',\n }}\n >\n <p style={{ margin: 0, fontSize: '1rem', fontWeight: 600 }}>Terjadi kesalahan.</p>\n <p style={{ margin: 0, fontSize: '0.875rem', opacity: 0.7 }}>\n Halaman ini sudah dilaporkan otomatis. Silakan muat ulang.\n </p>\n </div>\n);\n","/**\n * Framework-agnostic reporter - window.onerror/unhandledrejection + a manual report() call.\n * POSTs DIRECTLY to devcloud's v1/jsExceptions endpoint, cross-origin (CORS-open on that side) -\n * not through the reporting app's own backend at all. Auth is a query-string `key`\n * (app.js_ingest_key, a low-privilege token deliberately safe to expose in page source, like a\n * Sentry DSN's public key) rather than a header, since navigator.sendBeacon() - the primary\n * transport here, used so a report still gets sent even if the page is closing - cannot set\n * custom headers at all.\n *\n * This is the single source of truth cres.js itself depends on for its own window.onerror/\n * unhandledrejection coverage - a fix/change here reaches every classic CF page AND every React\n * SPA built on this package without touching either separately.\n */\n\nexport interface JsErrorReport {\n message: string;\n stack?: string | null;\n name?: string | null;\n filename?: string | null;\n lineno?: number | null;\n colno?: number | null;\n url?: string;\n /** 'error' | 'unhandledrejection' | 'react' | a caller-defined label for a manual report() */\n type?: string;\n}\n\nexport interface CollectorConfig {\n /** devcloud's ingest endpoint, e.g. \"https://devcloud.example.com/v1/jsExceptions\". Never hardcode this - always pass it in from server-side config (see the README). */\n endpoint: string;\n /** app.js_ingest_key from devcloud (Manager > Project > App > \"JS Ingest Key\"). Safe to expose client-side - see the docblock above. */\n key: string;\n}\n\nexport interface CollectorOptions extends Partial<CollectorConfig> {\n /** Hard cap per page load - an error storm must not turn into a request storm. */\n maxReportsPerLoad?: number;\n /**\n * Defaults to reading window.__CF_JS_COLLECTOR_CONFIG__ ({endpoint, key} or null), injected\n * server-side by CApp's RendererTrait (classic CF pages) or an SPA's own shell, from\n * `devcloud.jsCollector.*` config - so an app that never configured this gets zero reporting\n * overhead, not just a dropped request. Pass endpoint/key directly instead (or override this)\n * only for a host that doesn't go through that injection point.\n */\n getConfig?: () => CollectorConfig | null;\n}\n\nconst DEFAULT_MAX_REPORTS_PER_LOAD = 20;\n\nfunction defaultGetConfig(): CollectorConfig | null {\n if (typeof window === 'undefined') {\n return null;\n }\n const config = (window as any).__CF_JS_COLLECTOR_CONFIG__;\n if (!config || !config.endpoint || !config.key) {\n return null;\n }\n\n return { endpoint: config.endpoint, key: config.key };\n}\n\nexport interface Collector {\n /** Attach window.onerror/unhandledrejection listeners. Safe to call multiple times. */\n init: () => void;\n /** Detach the listeners attached by init(). */\n destroy: () => void;\n /** Send one report directly - what ErrorBoundary/componentDidCatch calls. */\n report: (payload: JsErrorReport) => void;\n}\n\nexport function createCollector(options: CollectorOptions = {}): Collector {\n const maxReportsPerLoad = options.maxReportsPerLoad ?? DEFAULT_MAX_REPORTS_PER_LOAD;\n const getConfig = (): CollectorConfig | null => {\n if (options.endpoint && options.key) {\n return { endpoint: options.endpoint, key: options.key };\n }\n return (options.getConfig ?? defaultGetConfig)();\n };\n\n let sentCount = 0;\n let attached = false;\n\n function send(payload: JsErrorReport): void {\n const config = getConfig();\n if (!config || sentCount >= maxReportsPerLoad) {\n return;\n }\n sentCount += 1;\n\n try {\n const url = `${config.endpoint}?key=${encodeURIComponent(config.key)}`;\n const body = JSON.stringify(payload);\n if (typeof navigator !== 'undefined' && navigator.sendBeacon) {\n navigator.sendBeacon(url, new Blob([body], { type: 'application/json' }));\n return;\n }\n if (typeof fetch === 'function') {\n fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n keepalive: true,\n mode: 'cors',\n }).catch(() => {\n // a failed report must never itself surface as another error\n });\n }\n } catch {\n // same - stay silent\n }\n }\n\n function onError(event: ErrorEvent): void {\n if (!event || (!event.error && !event.message)) {\n // plain resource load failure (img/script/link) - not a JS exception\n return;\n }\n const jsError = event.error;\n send({\n message: String((jsError && jsError.message) || event.message || 'Unknown error'),\n stack: jsError && jsError.stack ? String(jsError.stack) : null,\n name: jsError && jsError.name ? jsError.name : null,\n filename: event.filename || null,\n lineno: event.lineno || null,\n colno: event.colno || null,\n url: window.location.href,\n type: 'error',\n });\n }\n\n function onUnhandledRejection(event: PromiseRejectionEvent): void {\n const reason: any = event && event.reason;\n const message = reason && reason.message ? reason.message : String(reason);\n send({\n message: String(message),\n stack: reason && reason.stack ? String(reason.stack) : null,\n name: reason && reason.name ? reason.name : 'UnhandledRejection',\n filename: null,\n lineno: null,\n colno: null,\n url: window.location.href,\n type: 'unhandledrejection',\n });\n }\n\n return {\n init() {\n if (attached || typeof window === 'undefined') {\n return;\n }\n attached = true;\n window.addEventListener('error', onError);\n window.addEventListener('unhandledrejection', onUnhandledRejection);\n },\n destroy() {\n if (!attached) {\n return;\n }\n attached = false;\n window.removeEventListener('error', onError);\n window.removeEventListener('unhandledrejection', onUnhandledRejection);\n },\n report: send,\n };\n}\n\n// Default singleton - covers the common case (one collector per page) without every consumer\n// needing to manage its own instance.\nconst defaultCollector = createCollector();\n\nexport function initJsErrorCollector(): void {\n defaultCollector.init();\n}\n\nexport function reportJsError(payload: JsErrorReport): void {\n defaultCollector.report(payload);\n}\n"],"mappings":"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","names":["_a"]}
|
package/dist/react.d.cts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Component, ReactNode, ErrorInfo } from 'react';
|
|
2
|
+
export { JsErrorReport, initJsErrorCollector, reportJsError } from './index.cjs';
|
|
3
|
+
|
|
4
|
+
interface CresjsErrorBoundaryProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
/** Custom fallback UI, or a render function given the error and a reset() to retry. */
|
|
7
|
+
fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
|
|
8
|
+
/** Extra side effect (toast, local logging, etc.) - runs in addition to the report, not instead of it. */
|
|
9
|
+
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
10
|
+
}
|
|
11
|
+
interface CresjsErrorBoundaryState {
|
|
12
|
+
error: Error | null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* React only calls componentDidCatch()/getDerivedStateFromError() on the NEAREST boundary above
|
|
16
|
+
* a thrown render/lifecycle/hook error - it never lets that kind of error bubble to
|
|
17
|
+
* window.onerror (see reporter.ts's docblock for the other half: errors outside React's render
|
|
18
|
+
* cycle - plain event handlers, timers, unhandled promise rejections - still reach window and are
|
|
19
|
+
* covered by createCollector()/initJsErrorCollector() instead). Wrap your app root (or any
|
|
20
|
+
* subtree worth isolating a crash to) in this to get both halves.
|
|
21
|
+
*/
|
|
22
|
+
declare class CresjsErrorBoundary extends Component<CresjsErrorBoundaryProps, CresjsErrorBoundaryState> {
|
|
23
|
+
state: CresjsErrorBoundaryState;
|
|
24
|
+
static getDerivedStateFromError(error: Error): CresjsErrorBoundaryState;
|
|
25
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo): void;
|
|
26
|
+
reset: () => void;
|
|
27
|
+
render(): ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { CresjsErrorBoundary, type CresjsErrorBoundaryProps };
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Component, ReactNode, ErrorInfo } from 'react';
|
|
2
|
+
export { JsErrorReport, initJsErrorCollector, reportJsError } from './index.js';
|
|
3
|
+
|
|
4
|
+
interface CresjsErrorBoundaryProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
/** Custom fallback UI, or a render function given the error and a reset() to retry. */
|
|
7
|
+
fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
|
|
8
|
+
/** Extra side effect (toast, local logging, etc.) - runs in addition to the report, not instead of it. */
|
|
9
|
+
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
10
|
+
}
|
|
11
|
+
interface CresjsErrorBoundaryState {
|
|
12
|
+
error: Error | null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* React only calls componentDidCatch()/getDerivedStateFromError() on the NEAREST boundary above
|
|
16
|
+
* a thrown render/lifecycle/hook error - it never lets that kind of error bubble to
|
|
17
|
+
* window.onerror (see reporter.ts's docblock for the other half: errors outside React's render
|
|
18
|
+
* cycle - plain event handlers, timers, unhandled promise rejections - still reach window and are
|
|
19
|
+
* covered by createCollector()/initJsErrorCollector() instead). Wrap your app root (or any
|
|
20
|
+
* subtree worth isolating a crash to) in this to get both halves.
|
|
21
|
+
*/
|
|
22
|
+
declare class CresjsErrorBoundary extends Component<CresjsErrorBoundaryProps, CresjsErrorBoundaryState> {
|
|
23
|
+
state: CresjsErrorBoundaryState;
|
|
24
|
+
static getDerivedStateFromError(error: Error): CresjsErrorBoundaryState;
|
|
25
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo): void;
|
|
26
|
+
reset: () => void;
|
|
27
|
+
render(): ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { CresjsErrorBoundary, type CresjsErrorBoundaryProps };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
initJsErrorCollector,
|
|
3
|
+
reportJsError
|
|
4
|
+
} from "./chunk-GVQRIR62.js";
|
|
5
|
+
|
|
6
|
+
// src/ErrorBoundary.tsx
|
|
7
|
+
import { Component } from "react";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
var CresjsErrorBoundary = class extends Component {
|
|
10
|
+
constructor() {
|
|
11
|
+
super(...arguments);
|
|
12
|
+
this.state = { error: null };
|
|
13
|
+
this.reset = () => {
|
|
14
|
+
this.setState({ error: null });
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
static getDerivedStateFromError(error) {
|
|
18
|
+
return { error };
|
|
19
|
+
}
|
|
20
|
+
componentDidCatch(error, errorInfo) {
|
|
21
|
+
var _a, _b;
|
|
22
|
+
reportJsError({
|
|
23
|
+
message: error.message || String(error),
|
|
24
|
+
stack: error.stack || null,
|
|
25
|
+
name: error.name || null,
|
|
26
|
+
url: typeof window !== "undefined" ? window.location.href : void 0,
|
|
27
|
+
type: "react"
|
|
28
|
+
});
|
|
29
|
+
(_b = (_a = this.props).onError) == null ? void 0 : _b.call(_a, error, errorInfo);
|
|
30
|
+
}
|
|
31
|
+
render() {
|
|
32
|
+
const { error } = this.state;
|
|
33
|
+
if (!error) {
|
|
34
|
+
return this.props.children;
|
|
35
|
+
}
|
|
36
|
+
const { fallback } = this.props;
|
|
37
|
+
if (typeof fallback === "function") {
|
|
38
|
+
return fallback(error, this.reset);
|
|
39
|
+
}
|
|
40
|
+
if (fallback) {
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
return DEFAULT_FALLBACK;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var DEFAULT_FALLBACK = /* @__PURE__ */ jsxs(
|
|
47
|
+
"div",
|
|
48
|
+
{
|
|
49
|
+
style: {
|
|
50
|
+
display: "flex",
|
|
51
|
+
flexDirection: "column",
|
|
52
|
+
alignItems: "center",
|
|
53
|
+
justifyContent: "center",
|
|
54
|
+
gap: "0.75rem",
|
|
55
|
+
padding: "3rem 1.5rem",
|
|
56
|
+
textAlign: "center",
|
|
57
|
+
fontFamily: "system-ui, sans-serif"
|
|
58
|
+
},
|
|
59
|
+
children: [
|
|
60
|
+
/* @__PURE__ */ jsx("p", { style: { margin: 0, fontSize: "1rem", fontWeight: 600 }, children: "Terjadi kesalahan." }),
|
|
61
|
+
/* @__PURE__ */ jsx("p", { style: { margin: 0, fontSize: "0.875rem", opacity: 0.7 }, children: "Halaman ini sudah dilaporkan otomatis. Silakan muat ulang." })
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
export {
|
|
66
|
+
CresjsErrorBoundary,
|
|
67
|
+
initJsErrorCollector,
|
|
68
|
+
reportJsError
|
|
69
|
+
};
|
|
70
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ErrorBoundary.tsx"],"sourcesContent":["import { Component } from 'react';\nimport type { ErrorInfo, ReactNode } from 'react';\nimport { reportJsError } from './reporter';\n\nexport interface CresjsErrorBoundaryProps {\n children: ReactNode;\n /** Custom fallback UI, or a render function given the error and a reset() to retry. */\n fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);\n /** Extra side effect (toast, local logging, etc.) - runs in addition to the report, not instead of it. */\n onError?: (error: Error, errorInfo: ErrorInfo) => void;\n}\n\ninterface CresjsErrorBoundaryState {\n error: Error | null;\n}\n\n/**\n * React only calls componentDidCatch()/getDerivedStateFromError() on the NEAREST boundary above\n * a thrown render/lifecycle/hook error - it never lets that kind of error bubble to\n * window.onerror (see reporter.ts's docblock for the other half: errors outside React's render\n * cycle - plain event handlers, timers, unhandled promise rejections - still reach window and are\n * covered by createCollector()/initJsErrorCollector() instead). Wrap your app root (or any\n * subtree worth isolating a crash to) in this to get both halves.\n */\nexport class CresjsErrorBoundary extends Component<CresjsErrorBoundaryProps, CresjsErrorBoundaryState> {\n state: CresjsErrorBoundaryState = { error: null };\n\n static getDerivedStateFromError(error: Error): CresjsErrorBoundaryState {\n return { error };\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo): void {\n reportJsError({\n message: error.message || String(error),\n stack: error.stack || null,\n name: error.name || null,\n url: typeof window !== 'undefined' ? window.location.href : undefined,\n type: 'react',\n });\n this.props.onError?.(error, errorInfo);\n }\n\n reset = (): void => {\n this.setState({ error: null });\n };\n\n render(): ReactNode {\n const { error } = this.state;\n if (!error) {\n return this.props.children;\n }\n\n const { fallback } = this.props;\n if (typeof fallback === 'function') {\n return fallback(error, this.reset);\n }\n if (fallback) {\n return fallback;\n }\n\n return DEFAULT_FALLBACK;\n }\n}\n\nconst DEFAULT_FALLBACK = (\n <div\n style={{\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n justifyContent: 'center',\n gap: '0.75rem',\n padding: '3rem 1.5rem',\n textAlign: 'center',\n fontFamily: 'system-ui, sans-serif',\n }}\n >\n <p style={{ margin: 0, fontSize: '1rem', fontWeight: 600 }}>Terjadi kesalahan.</p>\n <p style={{ margin: 0, fontSize: '0.875rem', opacity: 0.7 }}>\n Halaman ini sudah dilaporkan otomatis. Silakan muat ulang.\n </p>\n </div>\n);\n"],"mappings":";;;;;;AAAA,SAAS,iBAAiB;AAiEtB,SAYI,KAZJ;AAzCG,IAAM,sBAAN,cAAkC,UAA8D;AAAA,EAAhG;AAAA;AACH,iBAAkC,EAAE,OAAO,KAAK;AAiBhD,iBAAQ,MAAY;AAChB,WAAK,SAAS,EAAE,OAAO,KAAK,CAAC;AAAA,IACjC;AAAA;AAAA,EAjBA,OAAO,yBAAyB,OAAwC;AACpE,WAAO,EAAE,MAAM;AAAA,EACnB;AAAA,EAEA,kBAAkB,OAAc,WAA4B;AA/BhE;AAgCQ,kBAAc;AAAA,MACV,SAAS,MAAM,WAAW,OAAO,KAAK;AAAA,MACtC,OAAO,MAAM,SAAS;AAAA,MACtB,MAAM,MAAM,QAAQ;AAAA,MACpB,KAAK,OAAO,WAAW,cAAc,OAAO,SAAS,OAAO;AAAA,MAC5D,MAAM;AAAA,IACV,CAAC;AACD,qBAAK,OAAM,YAAX,4BAAqB,OAAO;AAAA,EAChC;AAAA,EAMA,SAAoB;AAChB,UAAM,EAAE,MAAM,IAAI,KAAK;AACvB,QAAI,CAAC,OAAO;AACR,aAAO,KAAK,MAAM;AAAA,IACtB;AAEA,UAAM,EAAE,SAAS,IAAI,KAAK;AAC1B,QAAI,OAAO,aAAa,YAAY;AAChC,aAAO,SAAS,OAAO,KAAK,KAAK;AAAA,IACrC;AACA,QAAI,UAAU;AACV,aAAO;AAAA,IACX;AAEA,WAAO;AAAA,EACX;AACJ;AAEA,IAAM,mBACF;AAAA,EAAC;AAAA;AAAA,IACG,OAAO;AAAA,MACH,SAAS;AAAA,MACT,eAAe;AAAA,MACf,YAAY;AAAA,MACZ,gBAAgB;AAAA,MAChB,KAAK;AAAA,MACL,SAAS;AAAA,MACT,WAAW;AAAA,MACX,YAAY;AAAA,IAChB;AAAA,IAEA;AAAA,0BAAC,OAAE,OAAO,EAAE,QAAQ,GAAG,UAAU,QAAQ,YAAY,IAAI,GAAG,gCAAkB;AAAA,MAC9E,oBAAC,OAAE,OAAO,EAAE,QAAQ,GAAG,UAAU,YAAY,SAAS,IAAI,GAAG,wEAE7D;AAAA;AAAA;AACJ;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cresenity/cresjs-error-collector",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Browser/React error collector for CF (Cresenity Framework) apps - reports to devcloud's Exception Collector via POST /cresenity/jsError. Used by cres.js itself and by every app's React SPA.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/cresenity/cresjs-error-collector.git"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/index.cjs",
|
|
15
|
+
"module": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"require": "./dist/index.cjs"
|
|
22
|
+
},
|
|
23
|
+
"./react": {
|
|
24
|
+
"types": "./dist/react.d.ts",
|
|
25
|
+
"import": "./dist/react.js",
|
|
26
|
+
"require": "./dist/react.cjs"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup",
|
|
34
|
+
"prepare": "tsup",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"dev": "tsup --watch"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"react": ">=16.8.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"react": {
|
|
43
|
+
"optional": true
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/react": "^18.3.0",
|
|
48
|
+
"react": "^18.3.0",
|
|
49
|
+
"tsup": "^8.3.0",
|
|
50
|
+
"typescript": "^5.6.0"
|
|
51
|
+
}
|
|
52
|
+
}
|