peakflow-api 0.0.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/AGENTS.md ADDED
@@ -0,0 +1,27 @@
1
+ # AGENTS
2
+
3
+ This repo hosts the Peakflow JavaScript client for error reporting across browser, Node, and Expo/React Native.
4
+
5
+ ## Project Overview
6
+ - Source code lives in `src/`.
7
+ - Build output is emitted to `build/` (not committed).
8
+ - Tests use Jasmine under `spec/`.
9
+
10
+ ## Development Workflow
11
+ - Install dependencies: `npm install`.
12
+ - Type check: `npm run typecheck`.
13
+ - Lint: `npm run lint`.
14
+ - Test: `npm test`.
15
+ - Build (emits minified bundle to `build/`): `npm run build`.
16
+
17
+ ## Error Listener Usage
18
+ - Browser: `connectOnError()` and `connectUnhandledRejection()`.
19
+ - Node: `connectNodeUncaughtException()` and `connectNodeUnhandledRejection()`.
20
+ - Expo/React Native: `connectExpoErrorHandlers()`.
21
+ - `connect()` auto-wires handlers based on runtime.
22
+
23
+ ## Notes for Agents
24
+ - Avoid adding global browser dependencies unless guarded by runtime checks.
25
+ - When changing API exports, update `README.md` examples.
26
+ - Keep JSDoc accurate; TypeScript checks JS via `checkJs`.
27
+ - Ensure new browser-only logic is opt-in and documented.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # peakflow
2
+
3
+ JavaScript client for Peakflow error reporting with browser, Node, and Expo/React Native support.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install peakflow
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```js
14
+ import {BugReporting, debuggerInstance} from "peakflow"
15
+
16
+ const bugReporting = new BugReporting({authToken: "your-token"})
17
+ bugReporting.connect()
18
+ ```
19
+
20
+ ## Browser error listeners
21
+
22
+ ```js
23
+ import {BugReporting} from "peakflow"
24
+
25
+ const bugReporting = new BugReporting({authToken: "your-token"})
26
+
27
+ // Optional: enable source map parsing for script tags in web apps.
28
+ bugReporting.enableSourceMapsLoader()
29
+
30
+ bugReporting.connectOnError()
31
+ bugReporting.connectUnhandledRejection()
32
+ ```
33
+
34
+ ## Node error listeners
35
+
36
+ ```js
37
+ import {BugReporting} from "peakflow"
38
+
39
+ const bugReporting = new BugReporting({authToken: "your-token"})
40
+
41
+ bugReporting.connectNodeUncaughtException()
42
+ bugReporting.connectNodeUnhandledRejection()
43
+ ```
44
+
45
+ ## Expo / React Native error listeners
46
+
47
+ ```js
48
+ import {BugReporting} from "peakflow"
49
+
50
+ const bugReporting = new BugReporting({authToken: "your-token"})
51
+
52
+ bugReporting.connectExpoErrorHandlers()
53
+ ```
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Handles client-side error reporting for Peakflow.
3
+ */
4
+ export default class BugReporting {
5
+ /**
6
+ * @param {{authToken: string}} data
7
+ */
8
+ constructor(data: {
9
+ authToken: string;
10
+ });
11
+ authToken: string;
12
+ testing: boolean;
13
+ collectEnvironmentCallback: (args: object) => Promise<object> | object;
14
+ collectParamsCallback: (args: object) => Promise<object> | object;
15
+ sourceMapsLoader: SourceMapsLoader;
16
+ /**
17
+ * Register a callback to resolve environment metadata.
18
+ * @param {(args: object) => Promise<object>|object} callback
19
+ */
20
+ collectEnvironment(callback: (args: object) => Promise<object> | object): void;
21
+ /**
22
+ * Register a callback to resolve parameter metadata.
23
+ * @param {(args: object) => Promise<object>|object} callback
24
+ */
25
+ collectParams(callback: (args: object) => Promise<object> | object): void;
26
+ /**
27
+ * Initialize global error handlers.
28
+ */
29
+ /**
30
+ * Initialize error handlers based on the runtime.
31
+ */
32
+ connect(): void;
33
+ isHandlingError: boolean;
34
+ /**
35
+ * Check if the runtime looks like a browser environment.
36
+ * @returns {boolean}
37
+ */
38
+ isBrowser(): boolean;
39
+ /**
40
+ * Check if the runtime looks like Expo/React Native.
41
+ * @returns {boolean}
42
+ */
43
+ isExpo(): boolean;
44
+ /**
45
+ * Check if the runtime looks like a Node environment.
46
+ * @returns {boolean}
47
+ */
48
+ isNode(): boolean;
49
+ /**
50
+ * Enable source map loading for environments with script tags.
51
+ * @returns {boolean}
52
+ */
53
+ enableSourceMapsLoader(): boolean;
54
+ /**
55
+ * Wire the window error handler.
56
+ */
57
+ connectOnError(): void;
58
+ /**
59
+ * Wire the unhandled rejection handler.
60
+ */
61
+ connectUnhandledRejection(): void;
62
+ /**
63
+ * Wire Expo/React Native global error handlers.
64
+ * @returns {boolean}
65
+ */
66
+ connectExpoErrorHandlers(): boolean;
67
+ /**
68
+ * Wire Node.js uncaught error handlers.
69
+ */
70
+ connectNodeUncaughtException(): void;
71
+ connectNodeUnhandledRejection(): void;
72
+ /**
73
+ * Send a formatted error report to Peakflow.
74
+ * @typedef {Error & {peakflowParameters?: object, peakflowEnvironment?: object}} PeakflowError
75
+ * @param {object} data
76
+ * @param {PeakflowError} [data.error]
77
+ * @param {string} [data.errorClass]
78
+ * @param {string|null} [data.file]
79
+ * @param {number|null} [data.line]
80
+ * @param {string} data.message
81
+ * @param {string|null} data.url
82
+ */
83
+ handleError(data: {
84
+ error?: Error & {
85
+ peakflowParameters?: object;
86
+ peakflowEnvironment?: object;
87
+ };
88
+ errorClass?: string;
89
+ file?: string | null;
90
+ line?: number | null;
91
+ message: string;
92
+ url: string | null;
93
+ }): Promise<void>;
94
+ /**
95
+ * Register a hook for loading source maps from script tags.
96
+ * @param {(script: HTMLScriptElement) => Promise<unknown>|unknown} callback
97
+ */
98
+ loadSourceMapForScriptTags(callback: (script: HTMLScriptElement) => Promise<unknown> | unknown): void;
99
+ loadSourceMapForScriptTagsCallback: (script: HTMLScriptElement) => Promise<unknown> | unknown;
100
+ /**
101
+ * Parse a URL using a DOM anchor element.
102
+ * @param {string} url
103
+ * @returns {HTMLAnchorElement|null}
104
+ */
105
+ loadUrl(url: string): HTMLAnchorElement | null;
106
+ /**
107
+ * Send JSON data through an XHR instance.
108
+ * @param {XMLHttpRequest} xhr
109
+ * @param {string} postData
110
+ * @returns {Promise<void>}
111
+ */
112
+ loadXhr(xhr: XMLHttpRequest, postData: string): Promise<void>;
113
+ /**
114
+ * Resolve environment data for a report.
115
+ * @param {object} args
116
+ * @returns {Promise<object|undefined>}
117
+ */
118
+ resolveEnvironment(args: object): Promise<object | undefined>;
119
+ /**
120
+ * Resolve request parameters for a report.
121
+ * @param {object} args
122
+ * @returns {Promise<object|undefined>}
123
+ */
124
+ resolveParams(args: object): Promise<object | undefined>;
125
+ /**
126
+ * Toggle testing mode for posting to local paths.
127
+ * @param {boolean} value
128
+ */
129
+ setTesting(value: boolean): void;
130
+ }
131
+ import SourceMapsLoader from "@kaspernj/api-maker/build/source-maps-loader.js";
132
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bug-reporting/index.js"],"names":[],"mappings":"AAMA;;GAEG;AACH;IACE;;OAEG;IACH,kBAFW;QAAC,SAAS,EAAE,MAAM,CAAA;KAAC,EAQ7B;IALC,kBAA+B;IAC/B,iBAAoB;IACpB,mCAOgB,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAC,MAAM,CAPN;IAC3C,8BAcgB,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAC,MAAM,CAdX;IACtC,mCAA4B;IAG9B;;;OAGG;IACH,6BAFW,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAC,MAAM,QAIlD;IAED;;;OAGG;IACH,wBAFW,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAC,MAAM,QAIlD;IAED;;OAEG;IACH;;OAEG;IACH,gBAmBC;IAjBC,yBAA4B;IAmB9B;;;OAGG;IACH,aAFa,OAAO,CAInB;IAED;;;OAGG;IACH,UAFa,OAAO,CAInB;IAED;;;OAGG;IACH,UAFa,OAAO,CAInB;IAED;;;OAGG;IACH,0BAFa,OAAO,CA0BnB;IAED;;OAEG;IACH,uBAyBC;IAED;;OAEG;IACH,kCAqBC;IAED;;;OAGG;IACH,4BAFa,OAAO,CAqCnB;IAED;;OAEG;IACH,qCAqBC;IAED,sCAqBC;IAED;;;;;;;;;;OAUG;IACH,kBAPG;QAA6B,KAAK;iCAFM,MAAM;kCAAwB,MAAM;;QAGtD,UAAU,GAAxB,MAAM;QACa,IAAI,GAAvB,MAAM,GAAC,IAAI;QACQ,IAAI,GAAvB,MAAM,GAAC,IAAI;QACE,OAAO,EAApB,MAAM;QACY,GAAG,EAArB,MAAM,GAAC,IAAI;KACrB,iBA8EA;IAED;;;OAGG;IACH,qCAFW,CAAC,MAAM,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,GAAC,OAAO,QAIjE;IADC,6CAHkB,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,GAAC,OAAO,CAGd;IAGpD;;;;OAIG;IACH,aAHW,MAAM,GACJ,iBAAiB,GAAC,IAAI,CAYlC;IAED;;;;;OAKG;IACH,aAJW,cAAc,YACd,MAAM,GACJ,OAAO,CAAC,IAAI,CAAC,CAOzB;IAED;;;;OAIG;IACH,yBAHW,MAAM,GACJ,OAAO,CAAC,MAAM,GAAC,SAAS,CAAC,CAMrC;IAED;;;;OAIG;IACH,oBAHW,MAAM,GACJ,OAAO,CAAC,MAAM,GAAC,SAAS,CAAC,CASrC;IAED;;;OAGG;IACH,kBAFW,OAAO,QAIjB;CACF;6BApa4B,iDAAiD"}
@@ -0,0 +1,2 @@
1
+ import{digg as r}from"diggerize";import e from"qs";import o from"./variables.js";import{debuggerInstance as n}from"../debugger.js";import s from"@kaspernj/api-maker/build/source-maps-loader.js";export default class a{constructor(r){this.authToken=r.authToken,this.testing=!1,this.collectEnvironmentCallback=void 0,this.collectParamsCallback=void 0,this.sourceMapsLoader=null}collectEnvironment(r){this.collectEnvironmentCallback=r}collectParams(r){this.collectParamsCallback=r}connect(){n.debug("Connecting handler"),this.isHandlingError=!1,this.isBrowser()?(this.connectOnError(),this.connectUnhandledRejection()):n.debug("Skipping browser error handlers outside the browser"),this.isExpo()&&this.connectExpoErrorHandlers(),this.isNode()&&(this.connectNodeUncaughtException(),this.connectNodeUnhandledRejection())}isBrowser(){return void 0!==globalThis.document&&"function"==typeof globalThis.addEventListener}isExpo(){return"function"==typeof globalThis.ErrorUtils?.setGlobalHandler}isNode(){return void 0!==globalThis.process&&"function"==typeof globalThis.process.on}enableSourceMapsLoader(){return"undefined"!=typeof window&&"undefined"!=typeof document&&(this.sourceMapsLoader||(this.sourceMapsLoader=new s),this.sourceMapsLoader.loadSourceMapsForScriptTags(r=>{const e=r.getAttribute("src"),o=r.getAttribute("type");if(this.loadSourceMapForScriptTagsCallback&&e&&("text/javascript"==o||!o)){n.debug(`Loading source map for ${e}`);const o=this.loadSourceMapForScriptTagsCallback(r);if(o)return o}}),!0)}connectOnError(){globalThis.addEventListener("error",async r=>{if(n.debug(`Error cought with message: ${r.message}`),n.debug(`Message: ${r.message}`),n.debug(`File: ${r.filename}`),n.debug(`Line: ${r.lineno}`),n.debug(`Error: ${r.error}`),!this.isHandlingError){this.isHandlingError=!0;try{await this.handleError({error:r.error,errorClass:r.error?.name,file:r.filename,line:r.lineno,message:r.message||"Unknown error",url:globalThis.location?.href})}finally{this.isHandlingError=!1}}})}connectUnhandledRejection(){globalThis.addEventListener("unhandledrejection",async r=>{if(n.debug(`Unhandled rejection: ${JSON.stringify(r.reason.message||r.reason)}`),!this.isHandlingError){this.isHandlingError=!0;try{await this.handleError({error:r.reason,errorClass:"UnhandledRejection",file:null,line:null,message:r.reason.message||r.reason||"Unhandled promise rejection",url:globalThis.location?.href})}finally{this.isHandlingError=!1}}})}connectExpoErrorHandlers(){if(!this.isExpo())return!1;const r="function"==typeof globalThis.ErrorUtils.getGlobalHandler?globalThis.ErrorUtils.getGlobalHandler():null;return globalThis.ErrorUtils.setGlobalHandler((e,o)=>{n.debug(`Expo error: ${e?.message||e}`),this.isHandlingError||(this.isHandlingError=!0,Promise.resolve(this.handleError({error:e,errorClass:e?.name||(o?"FatalError":"ExpoError"),file:null,line:null,message:e?.message||String(e),url:null})).finally(()=>{this.isHandlingError=!1})),r&&r(e,o)}),!0}connectNodeUncaughtException(){globalThis.process.on("uncaughtException",async r=>{if(n.debug(`Uncaught exception: ${r?.message||r}`),!this.isHandlingError){this.isHandlingError=!0;try{await this.handleError({error:r,errorClass:r?.name||"UncaughtException",file:null,line:null,message:r?.message||String(r),url:null})}finally{this.isHandlingError=!1}}})}connectNodeUnhandledRejection(){globalThis.process.on("unhandledRejection",async r=>{if(n.debug(`Unhandled rejection: ${JSON.stringify(r?.message||r)}`),!this.isHandlingError){this.isHandlingError=!0;try{await this.handleError({error:r instanceof Error?r:void 0,errorClass:"UnhandledRejection",file:null,line:null,message:r?.message||String(r),url:null})}finally{this.isHandlingError=!1}}})}async handleError(e){let s,a;n.debug(`Handle error: ${JSON.stringify(e)}`),e.error&&e.error.stack&&this.sourceMapsLoader?(n.debug("Parse stacktrace"),await this.sourceMapsLoader.loadSourceMaps(e.error),s=this.sourceMapsLoader.parseStackTrace(e.error.stack)):e.error&&e.error.stack?s=[e.error.stack]:e.file&&(n.debug("No stacktrace was present on the error so cant parse it"),s=[`${e.file}:${e.line}`]),n.debug(`AuthToken: ${this.authToken}`),n.debug(`Backtrace: ${s}`);const t=e.errorClass||"UnknownError",i={auth_token:this.authToken,error:{backtrace:s,error_class:t,message:e.message,url:e.url,user_agent:void 0!==globalThis.navigator&&globalThis.navigator?globalThis.navigator.userAgent:null}};if(a=this.testing?o.bugReportsPath:o.bugReportsUrl,n.debug(`Sending error report to: ${a}`),e.error?.peakflowParameters?i.error.parameters=e.error.peakflowParameters:(n.debug("Resolving params"),i.error.parameters=await this.resolveParams(e),n.debug("Resolved params",i.error.parameters)),e.error?.peakflowEnvironment?i.error.environment=e.error.peakflowEnvironment:(n.debug("Resolving environment"),i.error.environment=await this.resolveEnvironment(e),n.debug("Resolved environment",i.error.environment)),"undefined"==typeof XMLHttpRequest)return void n.debug("Skipping error report because XMLHttpRequest is unavailable");const l=new XMLHttpRequest;l.open("POST",a,!0),l.setRequestHeader("Content-Type","application/json"),await this.loadXhr(l,JSON.stringify(i)),n.debug(`Data received: ${l.responseText}`);const c=JSON.parse(r(l,"responseText"));c.url&&console.log(`Peakflow: Error was reported: ${r(c,"url")}`)}loadSourceMapForScriptTags(r){this.loadSourceMapForScriptTagsCallback=r}loadUrl(r){if(!this.isBrowser())return null;const e=globalThis.document.createElement("a");return e.href=r,e}loadXhr(r,e){return new Promise(o=>{r.onload=()=>o(),r.send(e)})}async resolveEnvironment(r){if(this.collectEnvironmentCallback)return await this.collectEnvironmentCallback(r)}async resolveParams(r){if(this.collectParamsCallback)return await this.collectParamsCallback(r);if("object"==typeof globalThis.location&&globalThis.location){const r="string"==typeof globalThis.location.search?globalThis.location.search:"";return e.parse(r.substr(1))}}setTesting(r){this.testing=r}}
2
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJkaWdnIiwicXMiLCJSYWlsc1ZhcmlhYmxlcyIsImRlYnVnZ2VySW5zdGFuY2UiLCJTb3VyY2VNYXBzTG9hZGVyIiwiQnVnUmVwb3J0aW5nIiwiY29uc3RydWN0b3IiLCJkYXRhIiwidGhpcyIsImF1dGhUb2tlbiIsInRlc3RpbmciLCJjb2xsZWN0RW52aXJvbm1lbnRDYWxsYmFjayIsInVuZGVmaW5lZCIsImNvbGxlY3RQYXJhbXNDYWxsYmFjayIsInNvdXJjZU1hcHNMb2FkZXIiLCJjb2xsZWN0RW52aXJvbm1lbnQiLCJjYWxsYmFjayIsImNvbGxlY3RQYXJhbXMiLCJjb25uZWN0IiwiZGVidWciLCJpc0hhbmRsaW5nRXJyb3IiLCJpc0Jyb3dzZXIiLCJjb25uZWN0T25FcnJvciIsImNvbm5lY3RVbmhhbmRsZWRSZWplY3Rpb24iLCJpc0V4cG8iLCJjb25uZWN0RXhwb0Vycm9ySGFuZGxlcnMiLCJpc05vZGUiLCJjb25uZWN0Tm9kZVVuY2F1Z2h0RXhjZXB0aW9uIiwiY29ubmVjdE5vZGVVbmhhbmRsZWRSZWplY3Rpb24iLCJnbG9iYWxUaGlzIiwiZG9jdW1lbnQiLCJhZGRFdmVudExpc3RlbmVyIiwiRXJyb3JVdGlscyIsInNldEdsb2JhbEhhbmRsZXIiLCJwcm9jZXNzIiwib24iLCJlbmFibGVTb3VyY2VNYXBzTG9hZGVyIiwid2luZG93IiwibG9hZFNvdXJjZU1hcHNGb3JTY3JpcHRUYWdzIiwic2NyaXB0Iiwic3JjIiwiZ2V0QXR0cmlidXRlIiwidHlwZSIsImxvYWRTb3VyY2VNYXBGb3JTY3JpcHRUYWdzQ2FsbGJhY2siLCJyZXN1bHQiLCJhc3luYyIsImV2ZW50IiwibWVzc2FnZSIsImZpbGVuYW1lIiwibGluZW5vIiwiZXJyb3IiLCJoYW5kbGVFcnJvciIsImVycm9yQ2xhc3MiLCJuYW1lIiwiZmlsZSIsImxpbmUiLCJ1cmwiLCJsb2NhdGlvbiIsImhyZWYiLCJKU09OIiwic3RyaW5naWZ5IiwicmVhc29uIiwicHJldmlvdXNIYW5kbGVyIiwiZ2V0R2xvYmFsSGFuZGxlciIsImlzRmF0YWwiLCJQcm9taXNlIiwicmVzb2x2ZSIsIlN0cmluZyIsImZpbmFsbHkiLCJFcnJvciIsImJhY2t0cmFjZSIsInBvc3RVcmwiLCJzdGFjayIsImxvYWRTb3VyY2VNYXBzIiwicGFyc2VTdGFja1RyYWNlIiwicG9zdERhdGEiLCJhdXRoX3Rva2VuIiwiZXJyb3JfY2xhc3MiLCJ1c2VyX2FnZW50IiwibmF2aWdhdG9yIiwidXNlckFnZW50IiwiYnVnUmVwb3J0c1BhdGgiLCJidWdSZXBvcnRzVXJsIiwicGVha2Zsb3dQYXJhbWV0ZXJzIiwicGFyYW1ldGVycyIsInJlc29sdmVQYXJhbXMiLCJwZWFrZmxvd0Vudmlyb25tZW50IiwiZW52aXJvbm1lbnQiLCJyZXNvbHZlRW52aXJvbm1lbnQiLCJYTUxIdHRwUmVxdWVzdCIsInhociIsIm9wZW4iLCJzZXRSZXF1ZXN0SGVhZGVyIiwibG9hZFhociIsInJlc3BvbnNlVGV4dCIsInJlc3BvbnNlIiwicGFyc2UiLCJjb25zb2xlIiwibG9nIiwibG9hZFNvdXJjZU1hcEZvclNjcmlwdFRhZ3MiLCJsb2FkVXJsIiwicGFyc2VyIiwiY3JlYXRlRWxlbWVudCIsIm9ubG9hZCIsInNlbmQiLCJhcmdzIiwic2VhcmNoIiwic3Vic3RyIiwic2V0VGVzdGluZyIsInZhbHVlIl0sInNvdXJjZXMiOlsiLi4vLi4vc3JjL2J1Zy1yZXBvcnRpbmcvaW5kZXguanMiXSwibWFwcGluZ3MiOiJlQUFRQSxNQUFXLG1CQUNaQyxNQUFRLFlBQ1JDLE1BQW9CLDRDQUNuQkMsTUFBdUIsd0JBQ3hCQyxNQUFzQixpRUFLZixNQUFPQyxFQUluQixXQUFBQyxDQUFZQyxHQUNWQyxLQUFLQyxVQUFZRixFQUFLRSxVQUN0QkQsS0FBS0UsU0FBVSxFQUNmRixLQUFLRyxnQ0FBNkJDLEVBQ2xDSixLQUFLSywyQkFBd0JELEVBQzdCSixLQUFLTSxpQkFBbUIsSUFDMUIsQ0FNQSxrQkFBQUMsQ0FBbUJDLEdBQ2pCUixLQUFLRywyQkFBNkJLLENBQ3BDLENBTUEs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
@@ -0,0 +1,6 @@
1
+ declare namespace _default {
2
+ let bugReportsPath: string;
3
+ let bugReportsUrl: string;
4
+ }
5
+ export default _default;
6
+ //# sourceMappingURL=variables.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"variables.d.ts","sourceRoot":"","sources":["../../src/bug-reporting/variables.js"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export default{bugReportsPath:"/errors/reports",bugReportsUrl:"https://www.peakflow.io/errors/reports"};
2
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJidWdSZXBvcnRzUGF0aCIsImJ1Z1JlcG9ydHNVcmwiXSwic291cmNlcyI6WyIuLi8uLi9zcmMvYnVnLXJlcG9ydGluZy92YXJpYWJsZXMuanMiXSwibWFwcGluZ3MiOiJjQUFlLENBQ2JBLGVBQWdCLGtCQUNoQkMsY0FBZSIsImlnbm9yZUxpc3QiOltdfQ==
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Simple logger for toggling Peakflow client debug output.
3
+ */
4
+ export default class Debugger {
5
+ debugging: boolean;
6
+ /**
7
+ * Log messages when debugging is enabled.
8
+ * @param {...unknown} messages
9
+ */
10
+ debug(...messages: unknown[]): void;
11
+ /**
12
+ * Enable or disable debug logging.
13
+ * @param {boolean} value
14
+ */
15
+ setDebugging(value: boolean): void;
16
+ }
17
+ /**
18
+ * Shared debugger instance for the package.
19
+ */
20
+ export const debuggerInstance: Debugger;
21
+ //# sourceMappingURL=debugger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"debugger.d.ts","sourceRoot":"","sources":["../src/debugger.js"],"names":[],"mappings":"AAAA;;GAEG;AACH;IAKI,mBAAsB;IAGxB;;;OAGG;IACH,mBAFc,OAAO,EAAA,QAMpB;IAED;;;OAGG;IACH,oBAFW,OAAO,QAIjB;CACF;AAED;;GAEG;AACH,wCAA8C"}
@@ -0,0 +1,2 @@
1
+ export default class e{constructor(){this.debugging=!1}debug(...e){this.debugging&&console.log("Peakflow JS errors:",...e)}setDebugging(e){this.debugging=e}}export const debuggerInstance=new e;
2
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJEZWJ1Z2dlciIsImNvbnN0cnVjdG9yIiwidGhpcyIsImRlYnVnZ2luZyIsImRlYnVnIiwibWVzc2FnZXMiLCJjb25zb2xlIiwibG9nIiwic2V0RGVidWdnaW5nIiwidmFsdWUiLCJkZWJ1Z2dlckluc3RhbmNlIl0sInNvdXJjZXMiOlsiLi4vc3JjL2RlYnVnZ2VyLmpzIl0sIm1hcHBpbmdzIjoiZUFHYyxNQUFPQSxFQUluQixXQUFBQyxHQUNFQyxLQUFLQyxXQUFZLENBQ25CLENBTUEsS0FBQUMsSUFBU0MsR0FDSEgsS0FBS0MsV0FDUEcsUUFBUUMsSUFBSSx5QkFBMEJGLEVBRTFDLENBTUEsWUFBQUcsQ0FBYUMsR0FDWFAsS0FBS0MsVUFBWU0sQ0FDbkIsU0FNSyxNQUFNQyxpQkFBbUIsSUFBSVYiLCJpZ25vcmVMaXN0IjpbXX0=
@@ -0,0 +1,5 @@
1
+ import BugReporting from "./bug-reporting/index.js";
2
+ import Debugger from "./debugger.js";
3
+ import { debuggerInstance } from "./debugger.js";
4
+ export { BugReporting, Debugger, debuggerInstance };
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"yBAAyB,0BAA0B;qBACV,eAAe;iCAAf,eAAe"}
package/build/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import r from"./bug-reporting/index.js";import o,{debuggerInstance as e}from"./debugger.js";export{r as BugReporting,o as Debugger,e as debuggerInstance};
2
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJCdWdSZXBvcnRpbmciLCJEZWJ1Z2dlciIsImRlYnVnZ2VySW5zdGFuY2UiXSwic291cmNlcyI6WyIuLi9zcmMvaW5kZXguanMiXSwibWFwcGluZ3MiOiJPQUFPQSxNQUFrQixrQ0FDbEJDLHVCQUFXQyxNQUF1Qix1QkFFakNGLGtCQUFjQyxjQUFVQyIsImlnbm9yZUxpc3QiOltdfQ==
@@ -0,0 +1,14 @@
1
+ import jsdoc from "eslint-plugin-jsdoc"
2
+
3
+ export default [
4
+ {
5
+ files: ["**/*.js"],
6
+ languageOptions: {
7
+ ecmaVersion: "latest",
8
+ sourceType: "module"
9
+ },
10
+ plugins: {
11
+ jsdoc
12
+ }
13
+ }
14
+ ]
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "peakflow-api",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc --project tsconfig.json && node scripts/minify.mjs",
9
+ "lint": "eslint .",
10
+ "release:patch": "npm version patch -m \"chore: release patch\" && git push origin master --follow-tags && npm publish",
11
+ "typecheck": "tsc --project tsconfig.json --noEmit",
12
+ "test": "jasmine"
13
+ },
14
+ "devDependencies": {
15
+ "@kaspernj/api-maker": "^1.0.2062",
16
+ "@types/node": "^20.0.0",
17
+ "eslint": "^9.0.0",
18
+ "eslint-plugin-jsdoc": "^48.0.0",
19
+ "jasmine": "^5.1.0",
20
+ "terser": "^5.31.0",
21
+ "typescript": "^5.4.0"
22
+ },
23
+ "peerDependencies": {
24
+ "@kaspernj/api-maker": "^1.0.2062"
25
+ },
26
+ "dependencies": {
27
+ "diggerize": "^1.0.10",
28
+ "qs": "^6.14.0"
29
+ }
30
+ }
package/peak_flow.yml ADDED
@@ -0,0 +1,6 @@
1
+ before_script:
2
+ - npm install
3
+ script:
4
+ - npm run typecheck
5
+ - npm run lint
6
+ - npm run test
@@ -0,0 +1,54 @@
1
+ import {promises as fs} from "node:fs"
2
+ import path from "node:path"
3
+ import {fileURLToPath} from "node:url"
4
+ import {minify} from "terser"
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+ const buildDir = path.join(__dirname, "..", "build")
8
+
9
+ async function collectJsFiles(dir) {
10
+ const entries = await fs.readdir(dir, {withFileTypes: true})
11
+ const files = []
12
+
13
+ for (const entry of entries) {
14
+ const fullPath = path.join(dir, entry.name)
15
+
16
+ if (entry.isDirectory()) {
17
+ files.push(...await collectJsFiles(fullPath))
18
+ } else if (entry.isFile() && fullPath.endsWith(".js")) {
19
+ files.push(fullPath)
20
+ }
21
+ }
22
+
23
+ return files
24
+ }
25
+
26
+ async function minifyFile(filePath) {
27
+ const code = await fs.readFile(filePath, "utf8")
28
+ const result = await minify(code, {
29
+ module: true,
30
+ sourceMap: {
31
+ content: "inline",
32
+ url: "inline"
33
+ }
34
+ })
35
+
36
+ if (!result.code) {
37
+ throw new Error(`Failed to minify ${filePath}`)
38
+ }
39
+
40
+ await fs.writeFile(filePath, result.code)
41
+ }
42
+
43
+ async function run() {
44
+ try {
45
+ await fs.access(buildDir)
46
+ } catch {
47
+ return
48
+ }
49
+
50
+ const files = await collectJsFiles(buildDir)
51
+ await Promise.all(files.map((filePath) => minifyFile(filePath)))
52
+ }
53
+
54
+ await run()
@@ -0,0 +1,161 @@
1
+ import BugReporting from "../src/bug-reporting/index.js"
2
+ import RailsVariables from "../src/bug-reporting/variables.js"
3
+
4
+ describe("BugReporting", () => {
5
+ const originalGlobals = {
6
+ window: globalThis.window,
7
+ document: globalThis.document,
8
+ navigator: globalThis.navigator,
9
+ location: globalThis.location,
10
+ XMLHttpRequest: globalThis.XMLHttpRequest,
11
+ ErrorUtils: globalThis.ErrorUtils
12
+ }
13
+
14
+ let lastXhr = null
15
+
16
+ class FakeXHR {
17
+ constructor() {
18
+ lastXhr = this
19
+ this.headers = {}
20
+ this.responseText = ""
21
+ }
22
+
23
+ open(method, url, async) {
24
+ this.method = method
25
+ this.url = url
26
+ this.async = async
27
+ }
28
+
29
+ setRequestHeader(key, value) {
30
+ this.headers[key] = value
31
+ }
32
+
33
+ send(body) {
34
+ this.requestBody = body
35
+ this.responseText = JSON.stringify({})
36
+ if (this.onload) {
37
+ this.onload()
38
+ }
39
+ }
40
+ }
41
+
42
+ beforeEach(() => {
43
+ globalThis.window = {
44
+ addEventListener: () => {},
45
+ location: {href: "https://example.test/page"}
46
+ }
47
+ globalThis.document = {
48
+ createElement: () => ({href: ""})
49
+ }
50
+ Object.defineProperty(globalThis, "navigator", {
51
+ configurable: true,
52
+ value: {userAgent: "test-agent"}
53
+ })
54
+ globalThis.location = globalThis.window.location
55
+ globalThis.XMLHttpRequest = FakeXHR
56
+ lastXhr = null
57
+ })
58
+
59
+ afterEach(() => {
60
+ globalThis.window = originalGlobals.window
61
+ globalThis.document = originalGlobals.document
62
+ if (typeof originalGlobals.navigator === "undefined") {
63
+ delete globalThis.navigator
64
+ } else {
65
+ Object.defineProperty(globalThis, "navigator", {
66
+ configurable: true,
67
+ value: originalGlobals.navigator
68
+ })
69
+ }
70
+ globalThis.location = originalGlobals.location
71
+ globalThis.XMLHttpRequest = originalGlobals.XMLHttpRequest
72
+ if (typeof originalGlobals.ErrorUtils === "undefined") {
73
+ delete globalThis.ErrorUtils
74
+ } else {
75
+ globalThis.ErrorUtils = originalGlobals.ErrorUtils
76
+ }
77
+ })
78
+
79
+ it("posts to the bug reports url by default", async () => {
80
+ const reporter = new BugReporting({authToken: "token"})
81
+
82
+ await reporter.handleError({
83
+ error: new Error("boom"),
84
+ errorClass: "Error",
85
+ file: null,
86
+ line: null,
87
+ message: "boom",
88
+ url: globalThis.window.location.href
89
+ })
90
+
91
+ expect(lastXhr.url).toBe(RailsVariables.bugReportsUrl)
92
+ })
93
+
94
+ it("posts to the bug reports path when testing is enabled", async () => {
95
+ const reporter = new BugReporting({authToken: "token"})
96
+
97
+ reporter.setTesting(true)
98
+
99
+ await reporter.handleError({
100
+ error: new Error("boom"),
101
+ errorClass: "Error",
102
+ file: null,
103
+ line: null,
104
+ message: "boom",
105
+ url: globalThis.window.location.href
106
+ })
107
+
108
+ expect(lastXhr.url).toBe(RailsVariables.bugReportsPath)
109
+ })
110
+
111
+ it("uses error-provided parameters and environment", async () => {
112
+ const reporter = new BugReporting({authToken: "token"})
113
+ const error = new Error("boom")
114
+
115
+ error.peakflowParameters = {foo: "bar"}
116
+ error.peakflowEnvironment = {env: "test"}
117
+
118
+ await reporter.handleError({
119
+ error,
120
+ errorClass: "Error",
121
+ file: null,
122
+ line: null,
123
+ message: "boom",
124
+ url: globalThis.window.location.href
125
+ })
126
+
127
+ const payload = JSON.parse(lastXhr.requestBody)
128
+
129
+ expect(payload.error.parameters).toEqual({foo: "bar"})
130
+ expect(payload.error.environment).toEqual({env: "test"})
131
+ })
132
+
133
+ it("connects Expo error handlers and forwards errors", async () => {
134
+ const reporter = new BugReporting({authToken: "token"})
135
+ const handleErrorSpy = spyOn(reporter, "handleError").and.returnValue(Promise.resolve())
136
+ let previousHandlerCalled = false
137
+ let globalHandler = null
138
+
139
+ globalThis.ErrorUtils = {
140
+ getGlobalHandler: () => (error, isFatal) => {
141
+ previousHandlerCalled = true
142
+ },
143
+ setGlobalHandler: (handler) => {
144
+ globalHandler = handler
145
+ }
146
+ }
147
+
148
+ expect(reporter.connectExpoErrorHandlers()).toBeTrue()
149
+
150
+ const error = new Error("expo")
151
+ globalHandler(error, true)
152
+
153
+ expect(handleErrorSpy).toHaveBeenCalled()
154
+ expect(handleErrorSpy.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({
155
+ error,
156
+ errorClass: "Error",
157
+ message: "expo"
158
+ }))
159
+ expect(previousHandlerCalled).toBeTrue()
160
+ })
161
+ })
@@ -0,0 +1,29 @@
1
+ import Debugger, {debuggerInstance} from "../src/debugger.js"
2
+
3
+ describe("Debugger", () => {
4
+ it("logs when debugging is enabled", () => {
5
+ const instance = new Debugger()
6
+ const logSpy = spyOn(console, "log")
7
+
8
+ instance.setDebugging(true)
9
+ instance.debug("hello", "world")
10
+
11
+ expect(logSpy).toHaveBeenCalled()
12
+ })
13
+
14
+ it("does not log when debugging is disabled", () => {
15
+ const instance = new Debugger()
16
+ const logSpy = spyOn(console, "log")
17
+
18
+ instance.setDebugging(false)
19
+ instance.debug("nope")
20
+
21
+ expect(logSpy).not.toHaveBeenCalled()
22
+ })
23
+ })
24
+
25
+ describe("debuggerInstance", () => {
26
+ it("is a shared Debugger instance", () => {
27
+ expect(debuggerInstance instanceof Debugger).toBeTrue()
28
+ })
29
+ })
@@ -0,0 +1,4 @@
1
+ {
2
+ "spec_dir": "spec",
3
+ "spec_files": ["**/*[sS]pec.js"]
4
+ }
@@ -0,0 +1,425 @@
1
+ import {digg} from "diggerize"
2
+ import qs from "qs"
3
+ import RailsVariables from "./variables.js"
4
+ import {debuggerInstance} from "../debugger.js"
5
+ import SourceMapsLoader from "@kaspernj/api-maker/build/source-maps-loader.js"
6
+
7
+ /**
8
+ * Handles client-side error reporting for Peakflow.
9
+ */
10
+ export default class BugReporting {
11
+ /**
12
+ * @param {{authToken: string}} data
13
+ */
14
+ constructor(data) {
15
+ this.authToken = data.authToken
16
+ this.testing = false
17
+ this.collectEnvironmentCallback = undefined
18
+ this.collectParamsCallback = undefined
19
+ this.sourceMapsLoader = null
20
+ }
21
+
22
+ /**
23
+ * Register a callback to resolve environment metadata.
24
+ * @param {(args: object) => Promise<object>|object} callback
25
+ */
26
+ collectEnvironment(callback) {
27
+ this.collectEnvironmentCallback = callback
28
+ }
29
+
30
+ /**
31
+ * Register a callback to resolve parameter metadata.
32
+ * @param {(args: object) => Promise<object>|object} callback
33
+ */
34
+ collectParams(callback) {
35
+ this.collectParamsCallback = callback
36
+ }
37
+
38
+ /**
39
+ * Initialize global error handlers.
40
+ */
41
+ /**
42
+ * Initialize error handlers based on the runtime.
43
+ */
44
+ connect() {
45
+ debuggerInstance.debug("Connecting handler")
46
+ this.isHandlingError = false
47
+
48
+ if (this.isBrowser()) {
49
+ this.connectOnError()
50
+ this.connectUnhandledRejection()
51
+ } else {
52
+ debuggerInstance.debug("Skipping browser error handlers outside the browser")
53
+ }
54
+
55
+ if (this.isExpo()) {
56
+ this.connectExpoErrorHandlers()
57
+ }
58
+
59
+ if (this.isNode()) {
60
+ this.connectNodeUncaughtException()
61
+ this.connectNodeUnhandledRejection()
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Check if the runtime looks like a browser environment.
67
+ * @returns {boolean}
68
+ */
69
+ isBrowser() {
70
+ return typeof globalThis.document !== "undefined" && typeof globalThis.addEventListener === "function"
71
+ }
72
+
73
+ /**
74
+ * Check if the runtime looks like Expo/React Native.
75
+ * @returns {boolean}
76
+ */
77
+ isExpo() {
78
+ return typeof globalThis.ErrorUtils?.setGlobalHandler === "function"
79
+ }
80
+
81
+ /**
82
+ * Check if the runtime looks like a Node environment.
83
+ * @returns {boolean}
84
+ */
85
+ isNode() {
86
+ return typeof globalThis.process !== "undefined" && typeof globalThis.process.on === "function"
87
+ }
88
+
89
+ /**
90
+ * Enable source map loading for environments with script tags.
91
+ * @returns {boolean}
92
+ */
93
+ enableSourceMapsLoader() {
94
+ if (typeof window === "undefined" || typeof document === "undefined") {
95
+ return false
96
+ }
97
+
98
+ if (!this.sourceMapsLoader) {
99
+ this.sourceMapsLoader = new SourceMapsLoader()
100
+ }
101
+
102
+ this.sourceMapsLoader.loadSourceMapsForScriptTags((script) => {
103
+ const src = script.getAttribute("src")
104
+ const type = script.getAttribute("type")
105
+
106
+ if (this.loadSourceMapForScriptTagsCallback && src && (type == "text/javascript" || !type)) {
107
+ debuggerInstance.debug(`Loading source map for ${src}`)
108
+ const result = this.loadSourceMapForScriptTagsCallback(script)
109
+
110
+ if (result) {
111
+ return result
112
+ }
113
+ }
114
+ })
115
+
116
+ return true
117
+ }
118
+
119
+ /**
120
+ * Wire the window error handler.
121
+ */
122
+ connectOnError() {
123
+ globalThis.addEventListener("error", async (event) => {
124
+ debuggerInstance.debug(`Error cought with message: ${event.message}`)
125
+ debuggerInstance.debug(`Message: ${event.message}`)
126
+ debuggerInstance.debug(`File: ${event.filename}`)
127
+ debuggerInstance.debug(`Line: ${event.lineno}`)
128
+ debuggerInstance.debug(`Error: ${event.error}`)
129
+
130
+ if (!this.isHandlingError) {
131
+ this.isHandlingError = true
132
+
133
+ try {
134
+ await this.handleError({
135
+ error: event.error,
136
+ errorClass: event.error?.name,
137
+ file: event.filename,
138
+ line: event.lineno,
139
+ message: event.message || "Unknown error",
140
+ url: globalThis.location?.href
141
+ })
142
+ } finally {
143
+ this.isHandlingError = false
144
+ }
145
+ }
146
+ })
147
+ }
148
+
149
+ /**
150
+ * Wire the unhandled rejection handler.
151
+ */
152
+ connectUnhandledRejection() {
153
+ globalThis.addEventListener("unhandledrejection", async (event) => {
154
+ debuggerInstance.debug(`Unhandled rejection: ${JSON.stringify(event.reason.message || event.reason)}`)
155
+
156
+ if (!this.isHandlingError) {
157
+ this.isHandlingError = true
158
+
159
+ try {
160
+ await this.handleError({
161
+ error: event.reason,
162
+ errorClass: "UnhandledRejection",
163
+ file: null,
164
+ line: null,
165
+ message: event.reason.message || event.reason || "Unhandled promise rejection",
166
+ url: globalThis.location?.href
167
+ })
168
+ } finally {
169
+ this.isHandlingError = false
170
+ }
171
+ }
172
+ })
173
+ }
174
+
175
+ /**
176
+ * Wire Expo/React Native global error handlers.
177
+ * @returns {boolean}
178
+ */
179
+ connectExpoErrorHandlers() {
180
+ if (!this.isExpo()) {
181
+ return false
182
+ }
183
+
184
+ const previousHandler = typeof globalThis.ErrorUtils.getGlobalHandler === "function"
185
+ ? globalThis.ErrorUtils.getGlobalHandler()
186
+ : null
187
+
188
+ globalThis.ErrorUtils.setGlobalHandler((error, isFatal) => {
189
+ debuggerInstance.debug(`Expo error: ${error?.message || error}`)
190
+
191
+ if (!this.isHandlingError) {
192
+ this.isHandlingError = true
193
+
194
+ Promise.resolve(
195
+ this.handleError({
196
+ error,
197
+ errorClass: error?.name || (isFatal ? "FatalError" : "ExpoError"),
198
+ file: null,
199
+ line: null,
200
+ message: error?.message || String(error),
201
+ url: null
202
+ })
203
+ ).finally(() => {
204
+ this.isHandlingError = false
205
+ })
206
+ }
207
+
208
+ if (previousHandler) {
209
+ previousHandler(error, isFatal)
210
+ }
211
+ })
212
+
213
+ return true
214
+ }
215
+
216
+ /**
217
+ * Wire Node.js uncaught error handlers.
218
+ */
219
+ connectNodeUncaughtException() {
220
+ globalThis.process.on("uncaughtException", async (error) => {
221
+ debuggerInstance.debug(`Uncaught exception: ${error?.message || error}`)
222
+
223
+ if (!this.isHandlingError) {
224
+ this.isHandlingError = true
225
+
226
+ try {
227
+ await this.handleError({
228
+ error,
229
+ errorClass: error?.name || "UncaughtException",
230
+ file: null,
231
+ line: null,
232
+ message: error?.message || String(error),
233
+ url: null
234
+ })
235
+ } finally {
236
+ this.isHandlingError = false
237
+ }
238
+ }
239
+ })
240
+ }
241
+
242
+ connectNodeUnhandledRejection() {
243
+ globalThis.process.on("unhandledRejection", async (reason) => {
244
+ debuggerInstance.debug(`Unhandled rejection: ${JSON.stringify(reason?.message || reason)}`)
245
+
246
+ if (!this.isHandlingError) {
247
+ this.isHandlingError = true
248
+
249
+ try {
250
+ await this.handleError({
251
+ error: reason instanceof Error ? reason : undefined,
252
+ errorClass: "UnhandledRejection",
253
+ file: null,
254
+ line: null,
255
+ message: reason?.message || String(reason),
256
+ url: null
257
+ })
258
+ } finally {
259
+ this.isHandlingError = false
260
+ }
261
+ }
262
+ })
263
+ }
264
+
265
+ /**
266
+ * Send a formatted error report to Peakflow.
267
+ * @typedef {Error & {peakflowParameters?: object, peakflowEnvironment?: object}} PeakflowError
268
+ * @param {object} data
269
+ * @param {PeakflowError} [data.error]
270
+ * @param {string} [data.errorClass]
271
+ * @param {string|null} [data.file]
272
+ * @param {number|null} [data.line]
273
+ * @param {string} data.message
274
+ * @param {string|null} data.url
275
+ */
276
+ async handleError(data) {
277
+ debuggerInstance.debug(`Handle error: ${JSON.stringify(data)}`)
278
+
279
+ let backtrace, postUrl
280
+
281
+ if (data.error && data.error.stack && this.sourceMapsLoader) {
282
+ debuggerInstance.debug("Parse stacktrace")
283
+ await this.sourceMapsLoader.loadSourceMaps(data.error)
284
+ backtrace = this.sourceMapsLoader.parseStackTrace(data.error.stack)
285
+ } else if (data.error && data.error.stack) {
286
+ backtrace = [data.error.stack]
287
+ } else if (data.file) {
288
+ debuggerInstance.debug("No stacktrace was present on the error so cant parse it")
289
+ backtrace = [`${data.file}:${data.line}`]
290
+ }
291
+
292
+ debuggerInstance.debug(`AuthToken: ${this.authToken}`)
293
+ debuggerInstance.debug(`Backtrace: ${backtrace}`)
294
+
295
+ const errorClass = data.errorClass || "UnknownError"
296
+ const postData = {
297
+ auth_token: this.authToken,
298
+ error: {
299
+ backtrace,
300
+ error_class: errorClass,
301
+ message: data.message,
302
+ url: data.url,
303
+ user_agent: typeof globalThis.navigator !== "undefined" && globalThis.navigator
304
+ ? globalThis.navigator.userAgent
305
+ : null
306
+ }
307
+ }
308
+
309
+ if (this.testing) {
310
+ postUrl = RailsVariables.bugReportsPath
311
+ } else {
312
+ postUrl = RailsVariables.bugReportsUrl
313
+ }
314
+
315
+ debuggerInstance.debug(`Sending error report to: ${postUrl}`)
316
+
317
+ if (data.error?.peakflowParameters) {
318
+ postData.error.parameters = data.error.peakflowParameters
319
+ } else {
320
+ debuggerInstance.debug("Resolving params")
321
+ postData.error.parameters = await this.resolveParams(data)
322
+ debuggerInstance.debug("Resolved params", postData.error.parameters)
323
+ }
324
+
325
+ if (data.error?.peakflowEnvironment) {
326
+ postData.error.environment = data.error.peakflowEnvironment
327
+ } else {
328
+ debuggerInstance.debug("Resolving environment")
329
+ postData.error.environment = await this.resolveEnvironment(data)
330
+ debuggerInstance.debug("Resolved environment", postData.error.environment)
331
+ }
332
+
333
+ if (typeof XMLHttpRequest === "undefined") {
334
+ debuggerInstance.debug("Skipping error report because XMLHttpRequest is unavailable")
335
+ return
336
+ }
337
+
338
+ const xhr = new XMLHttpRequest()
339
+
340
+ xhr.open("POST", postUrl, true)
341
+ xhr.setRequestHeader("Content-Type", "application/json")
342
+
343
+ await this.loadXhr(xhr, JSON.stringify(postData))
344
+
345
+ debuggerInstance.debug(`Data received: ${xhr.responseText}`)
346
+
347
+ const response = JSON.parse(digg(xhr, "responseText"))
348
+
349
+ // If the account has run out of bug reports, then this can potentially crash because 'url' wont be present.
350
+ if (response.url) {
351
+ console.log(`Peakflow: Error was reported: ${digg(response, "url")}`)
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Register a hook for loading source maps from script tags.
357
+ * @param {(script: HTMLScriptElement) => Promise<unknown>|unknown} callback
358
+ */
359
+ loadSourceMapForScriptTags(callback) {
360
+ this.loadSourceMapForScriptTagsCallback = callback
361
+ }
362
+
363
+ /**
364
+ * Parse a URL using a DOM anchor element.
365
+ * @param {string} url
366
+ * @returns {HTMLAnchorElement|null}
367
+ */
368
+ loadUrl(url) {
369
+ if (!this.isBrowser()) {
370
+ return null
371
+ }
372
+
373
+ const parser = globalThis.document.createElement("a")
374
+
375
+ parser.href = url
376
+
377
+ return parser
378
+ }
379
+
380
+ /**
381
+ * Send JSON data through an XHR instance.
382
+ * @param {XMLHttpRequest} xhr
383
+ * @param {string} postData
384
+ * @returns {Promise<void>}
385
+ */
386
+ loadXhr(xhr, postData) {
387
+ return new Promise((resolve) => {
388
+ xhr.onload = () => resolve()
389
+ xhr.send(postData)
390
+ })
391
+ }
392
+
393
+ /**
394
+ * Resolve environment data for a report.
395
+ * @param {object} args
396
+ * @returns {Promise<object|undefined>}
397
+ */
398
+ async resolveEnvironment(args) {
399
+ if (this.collectEnvironmentCallback) {
400
+ return await this.collectEnvironmentCallback(args)
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Resolve request parameters for a report.
406
+ * @param {object} args
407
+ * @returns {Promise<object|undefined>}
408
+ */
409
+ async resolveParams(args) {
410
+ if (this.collectParamsCallback) {
411
+ return await this.collectParamsCallback(args)
412
+ } else if (typeof globalThis.location === "object" && globalThis.location) {
413
+ const search = typeof globalThis.location.search === "string" ? globalThis.location.search : ""
414
+ return qs.parse(search.substr(1))
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Toggle testing mode for posting to local paths.
420
+ * @param {boolean} value
421
+ */
422
+ setTesting(value) {
423
+ this.testing = value
424
+ }
425
+ }
@@ -0,0 +1,4 @@
1
+ export default {
2
+ bugReportsPath: "/errors/reports",
3
+ bugReportsUrl: "https://www.peakflow.io/errors/reports"
4
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Simple logger for toggling Peakflow client debug output.
3
+ */
4
+ export default class Debugger {
5
+ /**
6
+ * Create a debugger instance.
7
+ */
8
+ constructor() {
9
+ this.debugging = false
10
+ }
11
+
12
+ /**
13
+ * Log messages when debugging is enabled.
14
+ * @param {...unknown} messages
15
+ */
16
+ debug(...messages) {
17
+ if (this.debugging) {
18
+ console.log("Peakflow JS errors:", ...messages)
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Enable or disable debug logging.
24
+ * @param {boolean} value
25
+ */
26
+ setDebugging(value) {
27
+ this.debugging = value
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Shared debugger instance for the package.
33
+ */
34
+ export const debuggerInstance = new Debugger()
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import BugReporting from "./bug-reporting/index.js"
2
+ import Debugger, {debuggerInstance} from "./debugger.js"
3
+
4
+ export {BugReporting, Debugger, debuggerInstance}
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ES2020",
5
+ "moduleResolution": "node",
6
+ "allowJs": true,
7
+ "checkJs": true,
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "lib": ["ES2020", "DOM"],
11
+ "outDir": "build",
12
+ "rootDir": "src",
13
+ "inlineSourceMap": true,
14
+ "noEmitOnError": true,
15
+ "strict": false,
16
+ "types": []
17
+ },
18
+ "include": ["src/**/*.js"]
19
+ }