@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 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"]}
@@ -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 };
@@ -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,11 @@
1
+ import {
2
+ createCollector,
3
+ initJsErrorCollector,
4
+ reportJsError
5
+ } from "./chunk-GVQRIR62.js";
6
+ export {
7
+ createCollector,
8
+ initJsErrorCollector,
9
+ reportJsError
10
+ };
11
+ //# sourceMappingURL=index.js.map
@@ -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"]}
@@ -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 };
@@ -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
+ }