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 +27 -0
- package/README.md +53 -0
- package/build/bug-reporting/index.d.ts +132 -0
- package/build/bug-reporting/index.d.ts.map +1 -0
- package/build/bug-reporting/index.js +2 -0
- package/build/bug-reporting/variables.d.ts +6 -0
- package/build/bug-reporting/variables.d.ts.map +1 -0
- package/build/bug-reporting/variables.js +2 -0
- package/build/debugger.d.ts +21 -0
- package/build/debugger.d.ts.map +1 -0
- package/build/debugger.js +2 -0
- package/build/index.d.ts +5 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +2 -0
- package/eslint.config.js +14 -0
- package/package.json +30 -0
- package/peak_flow.yml +6 -0
- package/scripts/minify.mjs +54 -0
- package/spec/bug-reporting.spec.js +161 -0
- package/spec/debugger.spec.js +29 -0
- package/spec/support/jasmine.json +4 -0
- package/src/bug-reporting/index.js +425 -0
- package/src/bug-reporting/variables.js +4 -0
- package/src/debugger.js +34 -0
- package/src/index.js +4 -0
- package/tsconfig.json +19 -0
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 @@
|
|
|
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=
|
package/build/index.d.ts
ADDED
|
@@ -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==
|
package/eslint.config.js
ADDED
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,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,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
|
+
}
|
package/src/debugger.js
ADDED
|
@@ -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
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
|
+
}
|