@flareapp/js 2.0.0-rc.1 → 2.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/CHANGELOG.md +33 -0
- package/README.md +26 -5
- package/dist/index.cjs +603 -0
- package/dist/index.d.cts +122 -0
- package/dist/index.d.mts +109 -96
- package/dist/index.mjs +524 -420
- package/package.json +29 -11
- package/dist/index.d.ts +0 -109
- package/dist/index.js +0 -504
- package/src/Flare.ts +0 -208
- package/src/api/Api.ts +0 -27
- package/src/api/index.ts +0 -1
- package/src/browser/catchWindowErrors.ts +0 -36
- package/src/browser/index.ts +0 -1
- package/src/context/collectContext.ts +0 -18
- package/src/context/cookie.ts +0 -17
- package/src/context/index.ts +0 -1
- package/src/context/request.ts +0 -10
- package/src/context/requestData.ts +0 -13
- package/src/env/index.ts +0 -12
- package/src/index.ts +0 -14
- package/src/solutions/getSolutions.ts +0 -35
- package/src/solutions/index.ts +0 -1
- package/src/stacktrace/createStackTrace.ts +0 -59
- package/src/stacktrace/fileReader.ts +0 -96
- package/src/stacktrace/index.ts +0 -1
- package/src/types.ts +0 -76
- package/src/util/assert.ts +0 -9
- package/src/util/assertKey.ts +0 -11
- package/src/util/assertSolutionProvider.ts +0 -12
- package/src/util/flatJsonStringify.ts +0 -22
- package/src/util/flattenOnce.ts +0 -5
- package/src/util/index.ts +0 -6
- package/src/util/now.ts +0 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
## 2.0.0
|
|
2
|
+
|
|
3
|
+
### Breaking changes
|
|
4
|
+
|
|
5
|
+
- **Endpoint changed.** SDK now POSTs to `https://ingress.flareapp.io/v1/errors`. Success response is `201 Created`. Auth header is `x-api-token`.
|
|
6
|
+
- **Wire format reshaped to match the server's canonical schema.**
|
|
7
|
+
- `Report` uses camelCase top-level fields (`exceptionClass`, `seenAtUnixNano`, `sourcemapVersionId`, `isLog`, `level`, `attributes`, `events`).
|
|
8
|
+
- `StackFrame` uses camelCase (`lineNumber`, `columnNumber`, `codeSnippet`, `isApplicationFrame`).
|
|
9
|
+
- `Context` is gone. User context is set via `addContext(name, value)` and `addContextGroup(group, value)`; both write into the flat `attributes` map under `context.custom` and `context.<group>`.
|
|
10
|
+
- Glows ride along as `php_glow`-typed entries in `events[]`.
|
|
11
|
+
- **`report()` second argument** is now an `Attributes` map (was a freeform context object). The third argument (solution provider parameters) is removed.
|
|
12
|
+
- **Config keys renamed:** `reportingUrl` → `ingestUrl`, `sourcemapVersion` → `sourcemapVersionId`.
|
|
13
|
+
- **Deprecated trailing setters removed:** `flare.beforeEvaluate = …`, `flare.beforeSubmit = …`, `flare.stage = …`. Use `flare.configure({ … })`.
|
|
14
|
+
- **`reportMessage` signature changed:** `reportMessage(message, level?, attributes?)`. The old `'Log INFO'` regex is gone, pass `level` directly.
|
|
15
|
+
- **Solutions API removed:** `registerSolutionProvider`, `Solution`, `SolutionProvider`, `SolutionProviderExtraParameters` are all deleted.
|
|
16
|
+
- **`flare.config` is now `Readonly<Config>`** and `flare.glows` is `readonly Glow[]`. Direct property mutation no longer works; use `configure()`.
|
|
17
|
+
|
|
18
|
+
### New
|
|
19
|
+
|
|
20
|
+
- **URL redaction.** Sensitive query-string parameters (`password`, `token`, `secret`, `authorization`, etc.) are automatically replaced with `[redacted]` in `url.full`, `url.query`, and `flare.entry_point.value` attributes. Configurable via `urlDenylist` (custom regex) and `replaceDefaultUrlDenylist` (boolean) config options.
|
|
21
|
+
- `DEFAULT_URL_DENYLIST`, `redactFullPath`, and `resolveDenylist` are exported for direct use by framework adapters.
|
|
22
|
+
- `setEntryPoint({ identifier?, name?, type? })` — mutable global setter for the entry-point handler. SPA framework adapters call this on every navigation.
|
|
23
|
+
- `setSdkInfo({ name, version })` — overridable SDK identity (default `@flareapp/js` + client version). Integrations override it.
|
|
24
|
+
- `setFramework({ name, version? })` — host framework attribution (e.g. React, Vue).
|
|
25
|
+
- `code` field auto-populated from `error.code` when present (string, ≤64 chars).
|
|
26
|
+
- Non-`Error` values passed to `report()` are coerced to `Error` instead of silently failing.
|
|
27
|
+
- Browser context attributes (`url.full`, `url.query`, `browser.user_agent`, `browser.viewport.*`, cookies, request data) are auto-collected on each report.
|
|
28
|
+
- New exported types: `SpanEvent`, `EntryPointHandler`, `Framework`, `SdkInfo`, `OverriddenGrouping`, `Attributes`, `AttributeValue`.
|
|
29
|
+
- **`sampleRate` config option.** Number between `0` and `1` (default `1`). Controls what fraction of errors are reported. Applies to `report()`, `reportMessage()`, and `reportUnhandledRejection()`.
|
|
30
|
+
|
|
31
|
+
### Notes
|
|
32
|
+
|
|
33
|
+
- `seenAtUnixNano` is now real nanoseconds (`Date.now() * 1_000_000`).
|
package/README.md
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @flareapp/js
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The core JavaScript/TypeScript client for [Flare](https://flareapp.io) error tracking. Captures frontend errors, parses
|
|
4
|
+
stack traces, collects browser context, and reports everything to the Flare backend.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
## Installation
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
```bash
|
|
9
|
+
npm install @flareapp/js
|
|
10
|
+
```
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import { flare } from '@flareapp/js';
|
|
16
|
+
|
|
17
|
+
flare.light('YOUR_FLARE_API_KEY');
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
That is all you need. The client automatically listens for uncaught exceptions and unhandled promise rejections,
|
|
21
|
+
collects browser context, and sends error reports to Flare.
|
|
22
|
+
|
|
23
|
+
## Documentation
|
|
24
|
+
|
|
25
|
+
Full documentation on configuration, hooks, context, breadcrumbs, solution providers, and more is available
|
|
26
|
+
at [flareapp.io/docs/javascript/general/installation](https://flareapp.io/docs/javascript/general/installation).
|
|
27
|
+
|
|
28
|
+
## License
|
|
29
|
+
|
|
30
|
+
The MIT License (MIT). Please see [License File](../../LICENSE.md) for more information.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
12
|
+
key = keys[i];
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
14
|
+
__defProp(to, key, {
|
|
15
|
+
get: ((k) => from[k]).bind(null, key),
|
|
16
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
24
|
+
value: mod,
|
|
25
|
+
enumerable: true
|
|
26
|
+
}) : target, mod));
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
let error_stack_parser = require("error-stack-parser");
|
|
30
|
+
error_stack_parser = __toESM(error_stack_parser);
|
|
31
|
+
|
|
32
|
+
//#region src/browser/catchWindowErrors.ts
|
|
33
|
+
function catchWindowErrors() {
|
|
34
|
+
if (typeof window === "undefined") return;
|
|
35
|
+
window.addEventListener("error", (event) => {
|
|
36
|
+
const flare = window.flare;
|
|
37
|
+
if (!flare) return;
|
|
38
|
+
if (event.error instanceof Error) Promise.resolve(flare.report(event.error)).catch(() => {});
|
|
39
|
+
});
|
|
40
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
41
|
+
const flare = window.flare;
|
|
42
|
+
if (!flare) return;
|
|
43
|
+
const reason = event.reason;
|
|
44
|
+
if (reason instanceof Error) {
|
|
45
|
+
Promise.resolve(flare.report(reason)).catch(() => {});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (hasStack$1(reason)) {
|
|
49
|
+
const error = new Error(describeRejectionReason(reason));
|
|
50
|
+
error.stack = reason.stack;
|
|
51
|
+
Promise.resolve(flare.report(error)).catch(() => {});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
Promise.resolve(flare.reportUnhandledRejection(describeRejectionReason(reason))).catch(() => {});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function describeRejectionReason(reason) {
|
|
58
|
+
if (typeof reason === "string") return reason;
|
|
59
|
+
if (reason && typeof reason === "object") {
|
|
60
|
+
const message = reason.message;
|
|
61
|
+
if (typeof message === "string") return message;
|
|
62
|
+
try {
|
|
63
|
+
return JSON.stringify(reason);
|
|
64
|
+
} catch {
|
|
65
|
+
return "Unhandled promise rejection (non-serializable reason)";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return String(reason);
|
|
69
|
+
}
|
|
70
|
+
function hasStack$1(value) {
|
|
71
|
+
return typeof value === "object" && value !== null && "stack" in value && typeof value.stack === "string";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/env/index.ts
|
|
76
|
+
const CLIENT_VERSION = typeof process !== "undefined" && true ? "2.0.0" : "?";
|
|
77
|
+
const KEY = typeof FLARE_JS_KEY === "undefined" ? "" : FLARE_JS_KEY;
|
|
78
|
+
const SOURCEMAP_VERSION = typeof FLARE_SOURCEMAP_VERSION === "undefined" ? "" : FLARE_SOURCEMAP_VERSION;
|
|
79
|
+
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/util/assert.ts
|
|
82
|
+
function assert(value, message, debug) {
|
|
83
|
+
if (debug && !value) console.error(`Flare JavaScript client v${CLIENT_VERSION}: ${message}`);
|
|
84
|
+
return !!value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/util/assertKey.ts
|
|
89
|
+
function assertKey(key, debug) {
|
|
90
|
+
return assert(key, "The client was not yet initialised with an API key. Run client.light('<flare-project-key>') when you initialise your app. If you are running in dev mode and didn't run the light command on purpose, you can ignore this error.", debug);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/util/extractCode.ts
|
|
95
|
+
const MAX_CODE_LENGTH = 64;
|
|
96
|
+
function extractCode(error) {
|
|
97
|
+
const code = error.code;
|
|
98
|
+
if (typeof code !== "string" || code.length === 0) return;
|
|
99
|
+
return code.slice(0, MAX_CODE_LENGTH);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/util/flatJsonStringify.ts
|
|
104
|
+
function flatJsonStringify(json) {
|
|
105
|
+
return JSON.stringify(decycle(json));
|
|
106
|
+
}
|
|
107
|
+
function isPlainObject(value) {
|
|
108
|
+
if (typeof value !== "object" || value === null) return false;
|
|
109
|
+
const proto = Object.getPrototypeOf(value);
|
|
110
|
+
return proto === Object.prototype || proto === null;
|
|
111
|
+
}
|
|
112
|
+
function decycle(root) {
|
|
113
|
+
const inPath = /* @__PURE__ */ new WeakSet();
|
|
114
|
+
function clone(node) {
|
|
115
|
+
if (Array.isArray(node)) {
|
|
116
|
+
if (inPath.has(node)) return "[Circular]";
|
|
117
|
+
inPath.add(node);
|
|
118
|
+
const result = node.map(clone);
|
|
119
|
+
inPath.delete(node);
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
if (isPlainObject(node)) {
|
|
123
|
+
if (inPath.has(node)) return "[Circular]";
|
|
124
|
+
inPath.add(node);
|
|
125
|
+
const result = {};
|
|
126
|
+
for (const [k, v] of Object.entries(node)) result[k] = clone(v);
|
|
127
|
+
inPath.delete(node);
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
return node;
|
|
131
|
+
}
|
|
132
|
+
return clone(root);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/util/glowsToEvents.ts
|
|
137
|
+
function glowsToEvents(glows) {
|
|
138
|
+
return glows.map((glow) => ({
|
|
139
|
+
type: "php_glow",
|
|
140
|
+
startTimeUnixNano: Math.round(glow.microtime * 1e9),
|
|
141
|
+
endTimeUnixNano: null,
|
|
142
|
+
attributes: {
|
|
143
|
+
"glow.name": String(glow.name),
|
|
144
|
+
"glow.level": glow.messageLevel,
|
|
145
|
+
"glow.context": glow.metaData ?? {}
|
|
146
|
+
}
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/util/now.ts
|
|
152
|
+
function now() {
|
|
153
|
+
return Math.round(Date.now() / 1e3);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/util/redactUrl.ts
|
|
158
|
+
const DEFAULT_URL_DENYLIST = /password|passwd|pwd|token|secret|authorization|\bauth\b|bearer|oauth|credentials?|cookie|api[-_]?key|private[-_]?key|session|csrf|xsrf|\bpin\b|\bssn\b|card[-_]?number|\bcvv\b/i;
|
|
159
|
+
function resolveDenylist(custom, replaceDefault = false, defaultDenylist = DEFAULT_URL_DENYLIST) {
|
|
160
|
+
if (!custom) return defaultDenylist;
|
|
161
|
+
if (replaceDefault) {
|
|
162
|
+
const safeFlags = custom.flags.replace(/[gy]/g, "");
|
|
163
|
+
return new RegExp(custom.source, safeFlags);
|
|
164
|
+
}
|
|
165
|
+
const flags = unionFlags(defaultDenylist.flags, custom.flags);
|
|
166
|
+
return new RegExp(`(?:${defaultDenylist.source})|(?:${custom.source})`, flags);
|
|
167
|
+
}
|
|
168
|
+
function unionFlags(a, b) {
|
|
169
|
+
const merged = /* @__PURE__ */ new Set();
|
|
170
|
+
for (const flag of a + b) {
|
|
171
|
+
if (flag === "g" || flag === "y") continue;
|
|
172
|
+
merged.add(flag);
|
|
173
|
+
}
|
|
174
|
+
return [...merged].join("");
|
|
175
|
+
}
|
|
176
|
+
function redactFullPath(fullPath, denylist = DEFAULT_URL_DENYLIST) {
|
|
177
|
+
const queryStart = fullPath.indexOf("?");
|
|
178
|
+
if (queryStart === -1) return fullPath;
|
|
179
|
+
const hashStart = fullPath.indexOf("#", queryStart);
|
|
180
|
+
const queryEnd = hashStart === -1 ? fullPath.length : hashStart;
|
|
181
|
+
const prefix = fullPath.slice(0, queryStart + 1);
|
|
182
|
+
const queryString = fullPath.slice(queryStart + 1, queryEnd);
|
|
183
|
+
const suffix = fullPath.slice(queryEnd);
|
|
184
|
+
return `${prefix}${queryString.split("&").map((pair) => {
|
|
185
|
+
if (pair === "") return pair;
|
|
186
|
+
const eq = pair.indexOf("=");
|
|
187
|
+
const rawKey = eq === -1 ? pair : pair.slice(0, eq);
|
|
188
|
+
const decodedKey = safeDecode(rawKey);
|
|
189
|
+
if (!denylist.test(decodedKey)) return pair;
|
|
190
|
+
return eq === -1 ? rawKey : `${rawKey}=[redacted]`;
|
|
191
|
+
}).join("&")}${suffix}`;
|
|
192
|
+
}
|
|
193
|
+
function safeDecode(value) {
|
|
194
|
+
try {
|
|
195
|
+
return decodeURIComponent(value);
|
|
196
|
+
} catch {
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
//#endregion
|
|
202
|
+
//#region src/api/Api.ts
|
|
203
|
+
var Api = class {
|
|
204
|
+
report(report, url, key, reportBrowserExtensionErrors, debug = false) {
|
|
205
|
+
return fetch(url, {
|
|
206
|
+
method: "POST",
|
|
207
|
+
headers: {
|
|
208
|
+
"Accept": "application/json",
|
|
209
|
+
"Content-Type": "application/json",
|
|
210
|
+
"X-Api-Token": key ?? "",
|
|
211
|
+
"X-Report-Browser-Extension-Errors": JSON.stringify(reportBrowserExtensionErrors),
|
|
212
|
+
"X-Flare-Client-Version": "2"
|
|
213
|
+
},
|
|
214
|
+
body: flatJsonStringify(report)
|
|
215
|
+
}).then((response) => {
|
|
216
|
+
if (debug && response.status !== 201) console.error(`Received response with status ${response.status} from Flare`);
|
|
217
|
+
}, (error) => {
|
|
218
|
+
if (debug) console.error(error);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/context/cookie.ts
|
|
225
|
+
function cookie() {
|
|
226
|
+
if (!window.document.cookie) return {};
|
|
227
|
+
const cookies = {};
|
|
228
|
+
window.document.cookie.split("; ").forEach((rawCookie) => {
|
|
229
|
+
const idx = rawCookie.indexOf("=");
|
|
230
|
+
if (idx === -1) {
|
|
231
|
+
cookies[rawCookie] = "";
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const name = rawCookie.slice(0, idx);
|
|
235
|
+
cookies[name] = rawCookie.slice(idx + 1);
|
|
236
|
+
});
|
|
237
|
+
return { "http.request.cookies": cookies };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region src/context/request.ts
|
|
242
|
+
function request(urlDenylist) {
|
|
243
|
+
return {
|
|
244
|
+
"url.full": redactFullPath(window.location.href, urlDenylist),
|
|
245
|
+
"user_agent.original": window.navigator.userAgent,
|
|
246
|
+
"http.request.referrer": redactFullPath(window.document.referrer, urlDenylist),
|
|
247
|
+
"document.ready_state": window.document.readyState
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/context/requestData.ts
|
|
253
|
+
function requestData(urlDenylist) {
|
|
254
|
+
if (!window.location.search) return {};
|
|
255
|
+
return { "url.query": redactFullPath(window.location.search, urlDenylist).replace(/^\?/, "") };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/context/collectAttributes.ts
|
|
260
|
+
function collectAttributes(urlDenylist) {
|
|
261
|
+
if (typeof window === "undefined") return {};
|
|
262
|
+
return {
|
|
263
|
+
...request(urlDenylist),
|
|
264
|
+
...requestData(urlDenylist),
|
|
265
|
+
...cookie()
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region src/stacktrace/fileReader.ts
|
|
271
|
+
const cachedFiles = {};
|
|
272
|
+
function getCodeSnippet(url, lineNumber, columnNumber) {
|
|
273
|
+
return new Promise((resolve) => {
|
|
274
|
+
if (!url || !lineNumber) return resolve({
|
|
275
|
+
codeSnippet: { 0: `Could not read from file: missing file URL or line number. URL: ${url} lineNumber: ${lineNumber}` },
|
|
276
|
+
trimmedColumnNumber: null
|
|
277
|
+
});
|
|
278
|
+
if (!isFetchableUrl(url)) return resolve({
|
|
279
|
+
codeSnippet: { 0: `Could not read from file: unsupported URL scheme. URL: ${url}` },
|
|
280
|
+
trimmedColumnNumber: null
|
|
281
|
+
});
|
|
282
|
+
readFile(url).then((fileText) => {
|
|
283
|
+
if (!fileText) return resolve({
|
|
284
|
+
codeSnippet: { 0: `Could not read from file: Error while opening file at URL ${url}` },
|
|
285
|
+
trimmedColumnNumber: null
|
|
286
|
+
});
|
|
287
|
+
return resolve(readLinesFromFile(fileText, lineNumber, columnNumber));
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
function isFetchableUrl(url) {
|
|
292
|
+
return /^https?:\/\//i.test(url);
|
|
293
|
+
}
|
|
294
|
+
function readFile(url) {
|
|
295
|
+
if (cachedFiles[url] !== void 0) return Promise.resolve(cachedFiles[url]);
|
|
296
|
+
return fetch(url).then((response) => {
|
|
297
|
+
if (response.status !== 200) return null;
|
|
298
|
+
return response.text();
|
|
299
|
+
}).then((text) => {
|
|
300
|
+
if (text !== null) cachedFiles[url] = text;
|
|
301
|
+
return text;
|
|
302
|
+
}).catch(() => null);
|
|
303
|
+
}
|
|
304
|
+
function readLinesFromFile(fileText, lineNumber, columnNumber, maxSnippetLineLength = 1e3, maxSnippetLines = 40) {
|
|
305
|
+
const codeSnippet = {};
|
|
306
|
+
let trimmedColumnNumber = null;
|
|
307
|
+
const lines = fileText.split("\n");
|
|
308
|
+
const errorLineIndex = lineNumber - 1;
|
|
309
|
+
const half = Math.floor(maxSnippetLines / 2);
|
|
310
|
+
for (let i = -half; i <= half; i++) {
|
|
311
|
+
const currentLineIndex = errorLineIndex + i;
|
|
312
|
+
if (currentLineIndex < 0 || !lines[currentLineIndex]) continue;
|
|
313
|
+
const displayLine = currentLineIndex + 1;
|
|
314
|
+
const line = lines[currentLineIndex];
|
|
315
|
+
if (line.length > maxSnippetLineLength) {
|
|
316
|
+
if (columnNumber && columnNumber > maxSnippetLineLength / 2) {
|
|
317
|
+
const start = columnNumber - Math.round(maxSnippetLineLength / 2);
|
|
318
|
+
codeSnippet[displayLine] = line.slice(start, start + maxSnippetLineLength);
|
|
319
|
+
if (displayLine === lineNumber) trimmedColumnNumber = Math.round(maxSnippetLineLength / 2);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
codeSnippet[displayLine] = line.slice(0, maxSnippetLineLength) + "…";
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
codeSnippet[displayLine] = line;
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
codeSnippet,
|
|
329
|
+
trimmedColumnNumber
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/stacktrace/createStackTrace.ts
|
|
335
|
+
function createStackTrace(error, debug) {
|
|
336
|
+
return new Promise((resolve) => {
|
|
337
|
+
if (!hasStack(error)) return resolve([fallbackFrame("stacktrace missing")]);
|
|
338
|
+
let parsedFrames;
|
|
339
|
+
try {
|
|
340
|
+
parsedFrames = error_stack_parser.default.parse(error);
|
|
341
|
+
} catch (parseError) {
|
|
342
|
+
assert(false, "Couldn't parse stacktrace of below error:", debug);
|
|
343
|
+
if (debug) {
|
|
344
|
+
console.error(parseError);
|
|
345
|
+
console.error(error);
|
|
346
|
+
}
|
|
347
|
+
return resolve([fallbackFrame("stacktrace could not be parsed")]);
|
|
348
|
+
}
|
|
349
|
+
Promise.all(parsedFrames.map((frame) => {
|
|
350
|
+
return getCodeSnippet(frame.fileName, frame.lineNumber, frame.columnNumber).then((snippet) => ({
|
|
351
|
+
lineNumber: frame.lineNumber || 1,
|
|
352
|
+
columnNumber: frame.columnNumber || 1,
|
|
353
|
+
method: frame.functionName || "Anonymous or unknown function",
|
|
354
|
+
file: frame.fileName || "Unknown file",
|
|
355
|
+
codeSnippet: snippet.codeSnippet,
|
|
356
|
+
class: "",
|
|
357
|
+
isApplicationFrame: isApplicationFrame(frame.fileName)
|
|
358
|
+
}));
|
|
359
|
+
})).then(resolve);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
function fallbackFrame(reason) {
|
|
363
|
+
return {
|
|
364
|
+
lineNumber: 0,
|
|
365
|
+
columnNumber: 0,
|
|
366
|
+
method: "unknown",
|
|
367
|
+
file: "unknown",
|
|
368
|
+
codeSnippet: { 0: `Could not read from file: ${reason}` },
|
|
369
|
+
class: "unknown"
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
function hasStack(err) {
|
|
373
|
+
if (!err || typeof err !== "object") return false;
|
|
374
|
+
const e = err;
|
|
375
|
+
const stack = e.stack ?? e.stacktrace ?? e["opera#sourceloc"];
|
|
376
|
+
return typeof stack === "string" && stack !== `${e.name}: ${e.message}`;
|
|
377
|
+
}
|
|
378
|
+
function isApplicationFrame(fileName) {
|
|
379
|
+
if (!fileName) return true;
|
|
380
|
+
if (/[/\\]node_modules[/\\]/.test(fileName)) return false;
|
|
381
|
+
if (/(^|[/\\])(vendor|vendors)[.~-][^/\\]*\.js/i.test(fileName)) return false;
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
//#endregion
|
|
386
|
+
//#region src/Flare.ts
|
|
387
|
+
const DEFAULT_SDK_NAME = "@flareapp/js";
|
|
388
|
+
var Flare = class {
|
|
389
|
+
_config = {
|
|
390
|
+
key: null,
|
|
391
|
+
version: "",
|
|
392
|
+
sourcemapVersionId: SOURCEMAP_VERSION,
|
|
393
|
+
stage: "",
|
|
394
|
+
maxGlowsPerReport: 30,
|
|
395
|
+
ingestUrl: "https://ingress.flareapp.io/v1/errors",
|
|
396
|
+
reportBrowserExtensionErrors: false,
|
|
397
|
+
debug: false,
|
|
398
|
+
urlDenylist: DEFAULT_URL_DENYLIST,
|
|
399
|
+
replaceDefaultUrlDenylist: false,
|
|
400
|
+
sampleRate: 1,
|
|
401
|
+
beforeEvaluate: (error) => error,
|
|
402
|
+
beforeSubmit: (report) => report
|
|
403
|
+
};
|
|
404
|
+
_glows = [];
|
|
405
|
+
pendingAttributes = {};
|
|
406
|
+
entryPoint = null;
|
|
407
|
+
sdkInfo = {
|
|
408
|
+
name: DEFAULT_SDK_NAME,
|
|
409
|
+
version: CLIENT_VERSION
|
|
410
|
+
};
|
|
411
|
+
framework = null;
|
|
412
|
+
constructor(api = new Api()) {
|
|
413
|
+
this.api = api;
|
|
414
|
+
}
|
|
415
|
+
get config() {
|
|
416
|
+
return this._config;
|
|
417
|
+
}
|
|
418
|
+
get glows() {
|
|
419
|
+
return this._glows;
|
|
420
|
+
}
|
|
421
|
+
light(key = KEY, debug) {
|
|
422
|
+
this._config.key = key;
|
|
423
|
+
if (debug !== void 0) this._config.debug = debug;
|
|
424
|
+
return this;
|
|
425
|
+
}
|
|
426
|
+
configure(config) {
|
|
427
|
+
this._config = {
|
|
428
|
+
...this._config,
|
|
429
|
+
...config
|
|
430
|
+
};
|
|
431
|
+
if (config.sampleRate !== void 0) this._config.sampleRate = Math.max(0, Math.min(1, config.sampleRate));
|
|
432
|
+
this._config.urlDenylist = resolveDenylist(config.urlDenylist, config.replaceDefaultUrlDenylist ?? this._config.replaceDefaultUrlDenylist);
|
|
433
|
+
return this;
|
|
434
|
+
}
|
|
435
|
+
async test() {
|
|
436
|
+
const report = await this.createReportFromError(/* @__PURE__ */ new Error("The Flare client is set up correctly!"));
|
|
437
|
+
if (!report) return;
|
|
438
|
+
return this.sendReport(report);
|
|
439
|
+
}
|
|
440
|
+
glow(name, level = "info", data = []) {
|
|
441
|
+
const time = now();
|
|
442
|
+
this._glows.push({
|
|
443
|
+
name,
|
|
444
|
+
messageLevel: level,
|
|
445
|
+
metaData: data,
|
|
446
|
+
time,
|
|
447
|
+
microtime: time
|
|
448
|
+
});
|
|
449
|
+
if (this._glows.length > this._config.maxGlowsPerReport) this._glows = this._glows.slice(this._glows.length - this._config.maxGlowsPerReport);
|
|
450
|
+
return this;
|
|
451
|
+
}
|
|
452
|
+
clearGlows() {
|
|
453
|
+
this._glows = [];
|
|
454
|
+
return this;
|
|
455
|
+
}
|
|
456
|
+
addContext(name, value) {
|
|
457
|
+
const existing = this.pendingAttributes["context.custom"] ?? {};
|
|
458
|
+
this.pendingAttributes["context.custom"] = {
|
|
459
|
+
...existing,
|
|
460
|
+
[name]: value
|
|
461
|
+
};
|
|
462
|
+
return this;
|
|
463
|
+
}
|
|
464
|
+
addContextGroup(groupName, value) {
|
|
465
|
+
this.pendingAttributes[`context.${groupName}`] = value;
|
|
466
|
+
return this;
|
|
467
|
+
}
|
|
468
|
+
setEntryPoint(handler) {
|
|
469
|
+
this.entryPoint = handler;
|
|
470
|
+
return this;
|
|
471
|
+
}
|
|
472
|
+
setSdkInfo(info) {
|
|
473
|
+
this.sdkInfo = info;
|
|
474
|
+
return this;
|
|
475
|
+
}
|
|
476
|
+
setFramework(framework) {
|
|
477
|
+
this.framework = framework;
|
|
478
|
+
return this;
|
|
479
|
+
}
|
|
480
|
+
async report(error, attributes = {}) {
|
|
481
|
+
if (this._config.sampleRate < 1 && Math.random() >= this._config.sampleRate) return;
|
|
482
|
+
const seenAtUnixNano = Date.now() * 1e6;
|
|
483
|
+
const coerced = error instanceof Error ? error : new Error(typeof error === "string" ? error : String(error));
|
|
484
|
+
const errorToReport = await this._config.beforeEvaluate(coerced);
|
|
485
|
+
if (!errorToReport) return;
|
|
486
|
+
const report = await this.createReportFromError(errorToReport, attributes, seenAtUnixNano);
|
|
487
|
+
if (!report) return;
|
|
488
|
+
return this.sendReport(report);
|
|
489
|
+
}
|
|
490
|
+
async reportUnhandledRejection(message, attributes = {}) {
|
|
491
|
+
if (Math.random() >= this._config.sampleRate) return;
|
|
492
|
+
const seenAtUnixNano = Date.now() * 1e6;
|
|
493
|
+
const report = this.buildReport({
|
|
494
|
+
exceptionClass: "UnhandledRejection",
|
|
495
|
+
message,
|
|
496
|
+
stacktrace: [],
|
|
497
|
+
isLog: false,
|
|
498
|
+
level: void 0,
|
|
499
|
+
extraAttributes: attributes,
|
|
500
|
+
code: void 0,
|
|
501
|
+
seenAtUnixNano
|
|
502
|
+
});
|
|
503
|
+
return this.sendReport(report);
|
|
504
|
+
}
|
|
505
|
+
async reportMessage(message, level, attributes = {}) {
|
|
506
|
+
if (this._config.sampleRate < 1 && Math.random() >= this._config.sampleRate) return;
|
|
507
|
+
const seenAtUnixNano = Date.now() * 1e6;
|
|
508
|
+
const stackTrace = await createStackTrace(/* @__PURE__ */ new Error(), this._config.debug);
|
|
509
|
+
stackTrace.shift();
|
|
510
|
+
const report = this.buildReport({
|
|
511
|
+
exceptionClass: "Log",
|
|
512
|
+
message,
|
|
513
|
+
stacktrace: stackTrace,
|
|
514
|
+
isLog: true,
|
|
515
|
+
level,
|
|
516
|
+
extraAttributes: attributes,
|
|
517
|
+
code: void 0,
|
|
518
|
+
seenAtUnixNano
|
|
519
|
+
});
|
|
520
|
+
return this.sendReport(report);
|
|
521
|
+
}
|
|
522
|
+
async createReportFromError(error, attributes = {}, seenAtUnixNano = Date.now() * 1e6) {
|
|
523
|
+
if (!assert(error, "No error provided.", this._config.debug)) return false;
|
|
524
|
+
const stacktrace = await createStackTrace(error, this._config.debug);
|
|
525
|
+
assert(stacktrace.length, "Couldn't generate stacktrace of this error: " + error, this._config.debug);
|
|
526
|
+
const exceptionClass = error.constructor && error.constructor.name ? error.constructor.name : "undefined";
|
|
527
|
+
return this.buildReport({
|
|
528
|
+
exceptionClass,
|
|
529
|
+
message: error.message,
|
|
530
|
+
stacktrace,
|
|
531
|
+
isLog: false,
|
|
532
|
+
level: void 0,
|
|
533
|
+
extraAttributes: attributes,
|
|
534
|
+
code: extractCode(error),
|
|
535
|
+
seenAtUnixNano
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
buildReport(input) {
|
|
539
|
+
const baseAttributes = {
|
|
540
|
+
"telemetry.sdk.language": "javascript",
|
|
541
|
+
"telemetry.sdk.name": this.sdkInfo.name,
|
|
542
|
+
"telemetry.sdk.version": this.sdkInfo.version,
|
|
543
|
+
"flare.language.name": "javascript",
|
|
544
|
+
"flare.entry_point.type": "web"
|
|
545
|
+
};
|
|
546
|
+
if (typeof window !== "undefined" && window?.location?.href) baseAttributes["flare.entry_point.value"] = redactFullPath(window.location.href, this._config.urlDenylist);
|
|
547
|
+
const handlerIdentifier = this.entryPoint?.identifier ?? (typeof window !== "undefined" && window?.location?.pathname ? window.location.pathname : void 0);
|
|
548
|
+
const handlerType = this.entryPoint?.type ?? (typeof window !== "undefined" && window ? "browser" : void 0);
|
|
549
|
+
if (handlerIdentifier !== void 0) baseAttributes["flare.entry_point.handler.identifier"] = handlerIdentifier;
|
|
550
|
+
if (handlerType !== void 0) baseAttributes["flare.entry_point.handler.type"] = handlerType;
|
|
551
|
+
if (this.entryPoint?.name !== void 0) baseAttributes["flare.entry_point.handler.name"] = this.entryPoint.name;
|
|
552
|
+
if (this.framework?.name) baseAttributes["flare.framework.name"] = this.framework.name;
|
|
553
|
+
if (this.framework?.version) baseAttributes["flare.framework.version"] = this.framework.version;
|
|
554
|
+
if (this._config.stage) baseAttributes["service.stage"] = this._config.stage;
|
|
555
|
+
if (this._config.version) baseAttributes["service.version"] = this._config.version;
|
|
556
|
+
const attributes = {
|
|
557
|
+
...baseAttributes,
|
|
558
|
+
...collectAttributes(this._config.urlDenylist),
|
|
559
|
+
...this.pendingAttributes,
|
|
560
|
+
...input.extraAttributes
|
|
561
|
+
};
|
|
562
|
+
const pendingCustom = this.pendingAttributes["context.custom"];
|
|
563
|
+
const extraCustom = input.extraAttributes["context.custom"];
|
|
564
|
+
if (pendingCustom && extraCustom && typeof pendingCustom === "object" && typeof extraCustom === "object" && !Array.isArray(pendingCustom) && !Array.isArray(extraCustom)) attributes["context.custom"] = {
|
|
565
|
+
...pendingCustom,
|
|
566
|
+
...extraCustom
|
|
567
|
+
};
|
|
568
|
+
const report = {
|
|
569
|
+
exceptionClass: input.exceptionClass,
|
|
570
|
+
message: input.message,
|
|
571
|
+
seenAtUnixNano: input.seenAtUnixNano,
|
|
572
|
+
stacktrace: input.stacktrace,
|
|
573
|
+
events: glowsToEvents(this._glows),
|
|
574
|
+
attributes
|
|
575
|
+
};
|
|
576
|
+
if (input.isLog) report.isLog = true;
|
|
577
|
+
if (input.level !== void 0) report.level = input.level;
|
|
578
|
+
if (this._config.sourcemapVersionId) report.sourcemapVersionId = this._config.sourcemapVersionId;
|
|
579
|
+
if (input.code !== void 0) report.code = input.code;
|
|
580
|
+
return report;
|
|
581
|
+
}
|
|
582
|
+
async sendReport(report) {
|
|
583
|
+
if (!assertKey(this._config.key, this._config.debug)) return;
|
|
584
|
+
const reportToSubmit = await this._config.beforeSubmit(report);
|
|
585
|
+
if (!reportToSubmit) return;
|
|
586
|
+
return this.api.report(reportToSubmit, this._config.ingestUrl, this._config.key, this._config.reportBrowserExtensionErrors, this._config.debug);
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
//#endregion
|
|
591
|
+
//#region src/index.ts
|
|
592
|
+
const flare = new Flare();
|
|
593
|
+
if (typeof window !== "undefined" && window) {
|
|
594
|
+
window.flare = flare;
|
|
595
|
+
catchWindowErrors();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
//#endregion
|
|
599
|
+
exports.DEFAULT_URL_DENYLIST = DEFAULT_URL_DENYLIST;
|
|
600
|
+
exports.Flare = Flare;
|
|
601
|
+
exports.flare = flare;
|
|
602
|
+
exports.redactFullPath = redactFullPath;
|
|
603
|
+
exports.resolveDenylist = resolveDenylist;
|