@flareapp/js 1.1.0-rc.1 → 1.2.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/.release-it.json +17 -0
- package/CHANGELOG.md +30 -0
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +190 -77
- package/dist/index.mjs +187 -74
- package/package.json +5 -3
- package/src/Flare.ts +4 -9
- package/src/api/Api.ts +11 -11
- package/src/api/index.ts +1 -0
- package/src/api/mapToV2Wire.ts +120 -0
- package/src/api/v2WireTypes.ts +39 -0
- package/src/browser/catchWindowErrors.ts +20 -18
- package/src/context/cookie.ts +17 -10
- package/src/stacktrace/createStackTrace.ts +24 -14
- package/src/types.ts +1 -1
- package/src/util/flatJsonStringify.ts +20 -17
- package/src/util/glowsToEvents.ts +17 -0
- package/src/util/index.ts +1 -0
package/.release-it.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"git": {
|
|
3
|
+
"commitMessage": "Release @flareapp/js v${version}",
|
|
4
|
+
"tagName": "@flareapp/js@${version}",
|
|
5
|
+
"tagAnnotation": "Release @flareapp/js v${version}"
|
|
6
|
+
},
|
|
7
|
+
"github": {
|
|
8
|
+
"release": false
|
|
9
|
+
},
|
|
10
|
+
"npm": {
|
|
11
|
+
"publish": true
|
|
12
|
+
},
|
|
13
|
+
"hooks": {
|
|
14
|
+
"before:init": ["npm run test", "npm run typescript"],
|
|
15
|
+
"after:bump": "npm run build"
|
|
16
|
+
}
|
|
17
|
+
}
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @flareapp/js
|
|
2
|
+
|
|
3
|
+
## 1.2.0
|
|
4
|
+
|
|
5
|
+
This is the **final v1 release**. Subsequent work ships under v2.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Reports are now mapped to the Flare v2 wire format on egress. The mapper sits at the `Api.report()` boundary; the public API and the `beforeSubmit` hook still see the v1 `Report` shape.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Default `reportingUrl` now points to the new ingestion endpoint: `https://ingress.flareapp.io/v1/errors`. Consumers with allowlist firewall rules on outbound HTTP must add this host to their allowlist. Consumers passing a custom `reportingUrl` to `flare.configure({...})` are unaffected.
|
|
12
|
+
- HTTP response is now treated as success on status `200`, `201`, or `204` (previously only `204`).
|
|
13
|
+
- Headers updated: `Accept: application/json` and `X-Flare-Client-Version: 1` added; `X-Requested-With` removed. `X-Api-Token` and `X-Report-Browser-Extension-Errors` retained.
|
|
14
|
+
- Stack frames omit the `class` field when empty (previously emitted as `""`). The field is still emitted when populated.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- `cookie` context collector preserves `=` characters inside cookie values (previously truncated base64-padded values at the first `=`).
|
|
18
|
+
- Global error and unhandled-rejection handlers attach via `addEventListener`, so user code reassigning `window.onerror` no longer detaches Flare's handlers.
|
|
19
|
+
- Non-Error rejection reasons (strings, plain objects, Symbols) now produce reports instead of being silently dropped.
|
|
20
|
+
- `flatJsonStringify` decycles via a `WeakSet` of ancestor objects: real cycles become the literal `'[Circular]'`; non-cyclic shared sub-objects are preserved without recursion blowup.
|
|
21
|
+
- `createStackTrace` wraps `ErrorStackParser.parse` in `try/catch`. Parser failures resolve to a single fallback frame instead of dropping the report.
|
|
22
|
+
|
|
23
|
+
### Internal
|
|
24
|
+
- New `mapToV2Wire(report, config)` pure function in `src/api/mapToV2Wire.ts`.
|
|
25
|
+
- New `glowsToEvents(glows)` helper in `src/util/glowsToEvents.ts`.
|
|
26
|
+
- `Api.report()` signature changed from `(report, url, key, reportBrowserExtensionErrors)` to `(report, config)`. This is internal — it is not part of the package's public exports.
|
|
27
|
+
|
|
28
|
+
## 1.1.0
|
|
29
|
+
|
|
30
|
+
Prior history is in git.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
type Config = {
|
|
2
|
-
key: string;
|
|
2
|
+
key: string | null;
|
|
3
3
|
version: string;
|
|
4
4
|
sourcemapVersion: string;
|
|
5
5
|
stage: string;
|
|
@@ -77,16 +77,16 @@ type Glow = {
|
|
|
77
77
|
type MessageLevel = 'info' | 'debug' | 'warning' | 'error' | 'critical';
|
|
78
78
|
|
|
79
79
|
declare class Api {
|
|
80
|
-
report(report: Report,
|
|
80
|
+
report(report: Report, config: Config): Promise<void>;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
declare class Flare {
|
|
84
|
-
|
|
84
|
+
api: Api;
|
|
85
85
|
config: Config;
|
|
86
86
|
glows: Glow[];
|
|
87
87
|
context: Context;
|
|
88
88
|
solutionProviders: SolutionProvider[];
|
|
89
|
-
constructor(
|
|
89
|
+
constructor(api?: Api);
|
|
90
90
|
light(key?: string, debug?: boolean): Flare;
|
|
91
91
|
configure(config: Partial<Config>): Flare;
|
|
92
92
|
test(): Promise<void>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
type Config = {
|
|
2
|
-
key: string;
|
|
2
|
+
key: string | null;
|
|
3
3
|
version: string;
|
|
4
4
|
sourcemapVersion: string;
|
|
5
5
|
stage: string;
|
|
@@ -77,16 +77,16 @@ type Glow = {
|
|
|
77
77
|
type MessageLevel = 'info' | 'debug' | 'warning' | 'error' | 'critical';
|
|
78
78
|
|
|
79
79
|
declare class Api {
|
|
80
|
-
report(report: Report,
|
|
80
|
+
report(report: Report, config: Config): Promise<void>;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
declare class Flare {
|
|
84
|
-
|
|
84
|
+
api: Api;
|
|
85
85
|
config: Config;
|
|
86
86
|
glows: Glow[];
|
|
87
87
|
context: Context;
|
|
88
88
|
solutionProviders: SolutionProvider[];
|
|
89
|
-
constructor(
|
|
89
|
+
constructor(api?: Api);
|
|
90
90
|
light(key?: string, debug?: boolean): Flare;
|
|
91
91
|
configure(config: Partial<Config>): Flare;
|
|
92
92
|
test(): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -28,15 +28,15 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
29
|
|
|
30
30
|
// src/index.ts
|
|
31
|
-
var
|
|
32
|
-
__export(
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
33
|
Flare: () => Flare,
|
|
34
34
|
flare: () => flare
|
|
35
35
|
});
|
|
36
|
-
module.exports = __toCommonJS(
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
37
|
|
|
38
38
|
// src/env/index.ts
|
|
39
|
-
var CLIENT_VERSION = false ? "?" :
|
|
39
|
+
var CLIENT_VERSION = false ? "?" : "1.2.0";
|
|
40
40
|
var KEY = typeof FLARE_JS_KEY === "undefined" ? "" : FLARE_JS_KEY;
|
|
41
41
|
var SOURCEMAP_VERSION = typeof FLARE_SOURCEMAP_VERSION === "undefined" ? "" : FLARE_SOURCEMAP_VERSION;
|
|
42
42
|
|
|
@@ -68,22 +68,23 @@ function assertSolutionProvider(solutionProvider, debug) {
|
|
|
68
68
|
|
|
69
69
|
// src/util/flatJsonStringify.ts
|
|
70
70
|
function flatJsonStringify(json) {
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
71
|
+
const ancestors = /* @__PURE__ */ new WeakSet();
|
|
72
|
+
const path = [];
|
|
73
|
+
return JSON.stringify(json, function(_key, value) {
|
|
74
|
+
if (typeof value !== "object" || value === null) {
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
while (path.length > 0 && path[path.length - 1] !== this) {
|
|
78
|
+
const popped = path.pop();
|
|
79
|
+
ancestors.delete(popped);
|
|
80
|
+
}
|
|
81
|
+
if (ancestors.has(value)) {
|
|
82
|
+
return "[Circular]";
|
|
82
83
|
}
|
|
84
|
+
ancestors.add(value);
|
|
85
|
+
path.push(value);
|
|
83
86
|
return value;
|
|
84
87
|
});
|
|
85
|
-
cache = null;
|
|
86
|
-
return flattenedStringifiedJson;
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
// src/util/flattenOnce.ts
|
|
@@ -93,29 +94,131 @@ function flattenOnce(array) {
|
|
|
93
94
|
}, []);
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
// src/util/glowsToEvents.ts
|
|
98
|
+
function glowsToEvents(glows) {
|
|
99
|
+
return glows.map((glow) => ({
|
|
100
|
+
type: "php_glow",
|
|
101
|
+
startTimeUnixNano: Math.round(glow.microtime * 1e9),
|
|
102
|
+
endTimeUnixNano: null,
|
|
103
|
+
attributes: {
|
|
104
|
+
"glow.name": String(glow.name),
|
|
105
|
+
"glow.level": glow.message_level,
|
|
106
|
+
"glow.context": glow.meta_data ?? {}
|
|
107
|
+
}
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
|
|
96
111
|
// src/util/now.ts
|
|
97
112
|
function now() {
|
|
98
113
|
return Math.round(Date.now() / 1e3);
|
|
99
114
|
}
|
|
100
115
|
|
|
116
|
+
// src/api/mapToV2Wire.ts
|
|
117
|
+
var KNOWN_CONTEXT_BUCKETS = /* @__PURE__ */ new Set(["request", "request_data", "cookies", "context"]);
|
|
118
|
+
var NON_APPLICATION_FRAME_PATTERN = /node_modules|vendor|chunk-/;
|
|
119
|
+
function mapToV2Wire(report, config) {
|
|
120
|
+
const wire = {
|
|
121
|
+
seenAtUnixNano: Math.round(report.seen_at * 1e9),
|
|
122
|
+
stacktrace: report.stacktrace.map(mapStackFrame),
|
|
123
|
+
events: glowsToEvents(report.glows),
|
|
124
|
+
attributes: buildAttributes(report, config)
|
|
125
|
+
};
|
|
126
|
+
if (report.exception_class) {
|
|
127
|
+
wire.exceptionClass = report.exception_class;
|
|
128
|
+
}
|
|
129
|
+
if (report.message != null) {
|
|
130
|
+
wire.message = report.message;
|
|
131
|
+
}
|
|
132
|
+
if (report.sourcemap_version_id) {
|
|
133
|
+
wire.sourcemapVersionId = report.sourcemap_version_id;
|
|
134
|
+
}
|
|
135
|
+
return wire;
|
|
136
|
+
}
|
|
137
|
+
function mapStackFrame(frame) {
|
|
138
|
+
const out = {
|
|
139
|
+
file: frame.file,
|
|
140
|
+
lineNumber: frame.line_number,
|
|
141
|
+
isApplicationFrame: !NON_APPLICATION_FRAME_PATTERN.test(frame.file)
|
|
142
|
+
};
|
|
143
|
+
if (frame.column_number != null) {
|
|
144
|
+
out.columnNumber = frame.column_number;
|
|
145
|
+
}
|
|
146
|
+
if (frame.method) {
|
|
147
|
+
out.method = frame.method;
|
|
148
|
+
}
|
|
149
|
+
if (frame.class) {
|
|
150
|
+
out.class = frame.class;
|
|
151
|
+
}
|
|
152
|
+
if (frame.code_snippet) {
|
|
153
|
+
out.codeSnippet = frame.code_snippet;
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
function buildAttributes(report, config) {
|
|
158
|
+
const attrs = {
|
|
159
|
+
"telemetry.sdk.language": "javascript",
|
|
160
|
+
"telemetry.sdk.name": "@flareapp/js",
|
|
161
|
+
"telemetry.sdk.version": CLIENT_VERSION,
|
|
162
|
+
"flare.language.name": "javascript",
|
|
163
|
+
"flare.entry_point.type": "web"
|
|
164
|
+
};
|
|
165
|
+
if (typeof window !== "undefined" && window.location && window.location.href) {
|
|
166
|
+
attrs["flare.entry_point.value"] = window.location.href;
|
|
167
|
+
}
|
|
168
|
+
if (config.stage) {
|
|
169
|
+
attrs["service.stage"] = config.stage;
|
|
170
|
+
}
|
|
171
|
+
if (config.version) {
|
|
172
|
+
attrs["service.version"] = config.version;
|
|
173
|
+
}
|
|
174
|
+
const context = report.context ?? {};
|
|
175
|
+
if (context.request?.url) attrs["url.full"] = String(context.request.url);
|
|
176
|
+
if (context.request?.useragent) attrs["user_agent.original"] = String(context.request.useragent);
|
|
177
|
+
if (context.request?.referrer) attrs["http.request.referrer"] = String(context.request.referrer);
|
|
178
|
+
if (context.request?.readyState) attrs["document.ready_state"] = String(context.request.readyState);
|
|
179
|
+
if (context.request_data?.queryString) {
|
|
180
|
+
attrs["url.query"] = context.request_data.queryString;
|
|
181
|
+
}
|
|
182
|
+
if (context.cookies) {
|
|
183
|
+
attrs["http.request.cookies"] = context.cookies;
|
|
184
|
+
}
|
|
185
|
+
const custom = buildCustomContext(context);
|
|
186
|
+
if (Object.keys(custom).length > 0) {
|
|
187
|
+
attrs["context.custom"] = custom;
|
|
188
|
+
}
|
|
189
|
+
return attrs;
|
|
190
|
+
}
|
|
191
|
+
function buildCustomContext(context) {
|
|
192
|
+
const custom = {};
|
|
193
|
+
for (const key of Object.keys(context)) {
|
|
194
|
+
if (KNOWN_CONTEXT_BUCKETS.has(key)) continue;
|
|
195
|
+
custom[key] = context[key];
|
|
196
|
+
}
|
|
197
|
+
const added = context.context;
|
|
198
|
+
if (added && typeof added === "object") {
|
|
199
|
+
for (const key of Object.keys(added)) {
|
|
200
|
+
custom[key] = added[key];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return custom;
|
|
204
|
+
}
|
|
205
|
+
|
|
101
206
|
// src/api/Api.ts
|
|
102
207
|
var Api = class {
|
|
103
|
-
report(report,
|
|
104
|
-
return fetch(
|
|
208
|
+
report(report, config) {
|
|
209
|
+
return fetch(config.reportingUrl, {
|
|
105
210
|
method: "POST",
|
|
106
211
|
headers: {
|
|
212
|
+
"Accept": "application/json",
|
|
107
213
|
"Content-Type": "application/json",
|
|
108
|
-
"X-Api-Token": key,
|
|
109
|
-
"X-
|
|
110
|
-
"X-
|
|
214
|
+
"X-Api-Token": config.key ?? "",
|
|
215
|
+
"X-Report-Browser-Extension-Errors": JSON.stringify(config.reportBrowserExtensionErrors),
|
|
216
|
+
"X-Flare-Client-Version": "1"
|
|
111
217
|
},
|
|
112
|
-
body: flatJsonStringify(
|
|
113
|
-
...report,
|
|
114
|
-
key
|
|
115
|
-
})
|
|
218
|
+
body: flatJsonStringify(mapToV2Wire(report, config))
|
|
116
219
|
}).then(
|
|
117
220
|
(response) => {
|
|
118
|
-
if (response.status !== 204) {
|
|
221
|
+
if (response.status !== 200 && response.status !== 201 && response.status !== 204) {
|
|
119
222
|
console.error(`Received response with status ${response.status} from Flare`);
|
|
120
223
|
}
|
|
121
224
|
},
|
|
@@ -129,16 +232,20 @@ function cookie() {
|
|
|
129
232
|
if (!window.document.cookie) {
|
|
130
233
|
return {};
|
|
131
234
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
235
|
+
const cookies = {};
|
|
236
|
+
for (const raw of window.document.cookie.split("; ")) {
|
|
237
|
+
const eq = raw.indexOf("=");
|
|
238
|
+
if (eq === -1) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const name = raw.slice(0, eq);
|
|
242
|
+
const value = raw.slice(eq + 1);
|
|
243
|
+
cookies[name] = value;
|
|
244
|
+
}
|
|
245
|
+
if (Object.keys(cookies).length === 0) {
|
|
246
|
+
return {};
|
|
247
|
+
}
|
|
248
|
+
return { cookies };
|
|
142
249
|
}
|
|
143
250
|
|
|
144
251
|
// src/context/request.ts
|
|
@@ -279,22 +386,19 @@ function createStackTrace(error, debug) {
|
|
|
279
386
|
if (debug) {
|
|
280
387
|
console.error(error);
|
|
281
388
|
}
|
|
282
|
-
return resolve([
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
class: "unknown"
|
|
293
|
-
}
|
|
294
|
-
]);
|
|
389
|
+
return resolve([fallbackFrame("stacktrace missing")]);
|
|
390
|
+
}
|
|
391
|
+
let parsed;
|
|
392
|
+
try {
|
|
393
|
+
parsed = import_error_stack_parser.default.parse(error);
|
|
394
|
+
} catch (parseError) {
|
|
395
|
+
if (debug) {
|
|
396
|
+
console.error("Flare: failed to parse stacktrace", parseError);
|
|
397
|
+
}
|
|
398
|
+
return resolve([fallbackFrame("Could not parse stacktrace")]);
|
|
295
399
|
}
|
|
296
400
|
Promise.all(
|
|
297
|
-
|
|
401
|
+
parsed.map((frame) => {
|
|
298
402
|
return new Promise((resolve2) => {
|
|
299
403
|
getCodeSnippet(frame.fileName, frame.lineNumber, frame.columnNumber).then((snippet) => {
|
|
300
404
|
resolve2({
|
|
@@ -312,21 +416,32 @@ function createStackTrace(error, debug) {
|
|
|
312
416
|
).then(resolve);
|
|
313
417
|
});
|
|
314
418
|
}
|
|
419
|
+
function fallbackFrame(message) {
|
|
420
|
+
return {
|
|
421
|
+
line_number: 0,
|
|
422
|
+
column_number: 0,
|
|
423
|
+
method: "unknown",
|
|
424
|
+
file: "unknown",
|
|
425
|
+
code_snippet: { 0: message },
|
|
426
|
+
trimmed_column_number: null,
|
|
427
|
+
class: "unknown"
|
|
428
|
+
};
|
|
429
|
+
}
|
|
315
430
|
function hasStack(err) {
|
|
316
431
|
return !!err && (!!err.stack || !!err.stacktrace || !!err["opera#sourceloc"]) && typeof (err.stack || err.stacktrace || err["opera#sourceloc"]) === "string" && err.stack !== `${err.name}: ${err.message}`;
|
|
317
432
|
}
|
|
318
433
|
|
|
319
434
|
// src/Flare.ts
|
|
320
435
|
var Flare = class {
|
|
321
|
-
constructor(
|
|
322
|
-
this.
|
|
436
|
+
constructor(api = new Api()) {
|
|
437
|
+
this.api = api;
|
|
323
438
|
this.config = {
|
|
324
|
-
key:
|
|
439
|
+
key: null,
|
|
325
440
|
version: CLIENT_VERSION,
|
|
326
441
|
sourcemapVersion: SOURCEMAP_VERSION,
|
|
327
442
|
stage: "",
|
|
328
443
|
maxGlowsPerReport: 30,
|
|
329
|
-
reportingUrl: "https://
|
|
444
|
+
reportingUrl: "https://ingress.flareapp.io/v1/errors",
|
|
330
445
|
reportBrowserExtensionErrors: false,
|
|
331
446
|
debug: false,
|
|
332
447
|
beforeEvaluate: (error) => error,
|
|
@@ -443,12 +558,7 @@ var Flare = class {
|
|
|
443
558
|
if (!reportToSubmit) {
|
|
444
559
|
return;
|
|
445
560
|
}
|
|
446
|
-
return this.
|
|
447
|
-
reportToSubmit,
|
|
448
|
-
this.config.reportingUrl,
|
|
449
|
-
this.config.key,
|
|
450
|
-
this.config.reportBrowserExtensionErrors
|
|
451
|
-
);
|
|
561
|
+
return this.api.report(reportToSubmit, this.config);
|
|
452
562
|
}
|
|
453
563
|
// Deprecated, the following methods exist for backwards compatibility.
|
|
454
564
|
set beforeEvaluate(beforeEvaluate) {
|
|
@@ -471,24 +581,27 @@ function catchWindowErrors() {
|
|
|
471
581
|
if (!window || !flare2) {
|
|
472
582
|
return;
|
|
473
583
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
if (error) {
|
|
478
|
-
flare2.report(error);
|
|
479
|
-
}
|
|
480
|
-
if (typeof originalOnerrorHandler === "function") {
|
|
481
|
-
originalOnerrorHandler(_1, _2, _3, _4, error);
|
|
482
|
-
}
|
|
483
|
-
};
|
|
484
|
-
window.onunhandledrejection = (error) => {
|
|
485
|
-
if (error.reason instanceof Error) {
|
|
486
|
-
flare2.report(error.reason);
|
|
584
|
+
window.addEventListener("error", (event) => {
|
|
585
|
+
if (event.error) {
|
|
586
|
+
flare2.report(event.error);
|
|
487
587
|
}
|
|
488
|
-
|
|
489
|
-
|
|
588
|
+
});
|
|
589
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
590
|
+
const reason = event.reason;
|
|
591
|
+
if (reason instanceof Error) {
|
|
592
|
+
flare2.report(reason);
|
|
593
|
+
return;
|
|
490
594
|
}
|
|
491
|
-
|
|
595
|
+
const message = typeof reason === "string" ? reason : safeStringify(reason);
|
|
596
|
+
flare2.report(new Error(message));
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
function safeStringify(value) {
|
|
600
|
+
try {
|
|
601
|
+
return JSON.stringify(value) ?? String(value);
|
|
602
|
+
} catch {
|
|
603
|
+
return String(value);
|
|
604
|
+
}
|
|
492
605
|
}
|
|
493
606
|
|
|
494
607
|
// src/index.ts
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/env/index.ts
|
|
2
|
-
var CLIENT_VERSION = false ? "?" :
|
|
2
|
+
var CLIENT_VERSION = false ? "?" : "1.2.0";
|
|
3
3
|
var KEY = typeof FLARE_JS_KEY === "undefined" ? "" : FLARE_JS_KEY;
|
|
4
4
|
var SOURCEMAP_VERSION = typeof FLARE_SOURCEMAP_VERSION === "undefined" ? "" : FLARE_SOURCEMAP_VERSION;
|
|
5
5
|
|
|
@@ -31,22 +31,23 @@ function assertSolutionProvider(solutionProvider, debug) {
|
|
|
31
31
|
|
|
32
32
|
// src/util/flatJsonStringify.ts
|
|
33
33
|
function flatJsonStringify(json) {
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
34
|
+
const ancestors = /* @__PURE__ */ new WeakSet();
|
|
35
|
+
const path = [];
|
|
36
|
+
return JSON.stringify(json, function(_key, value) {
|
|
37
|
+
if (typeof value !== "object" || value === null) {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
while (path.length > 0 && path[path.length - 1] !== this) {
|
|
41
|
+
const popped = path.pop();
|
|
42
|
+
ancestors.delete(popped);
|
|
43
|
+
}
|
|
44
|
+
if (ancestors.has(value)) {
|
|
45
|
+
return "[Circular]";
|
|
45
46
|
}
|
|
47
|
+
ancestors.add(value);
|
|
48
|
+
path.push(value);
|
|
46
49
|
return value;
|
|
47
50
|
});
|
|
48
|
-
cache = null;
|
|
49
|
-
return flattenedStringifiedJson;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
// src/util/flattenOnce.ts
|
|
@@ -56,29 +57,131 @@ function flattenOnce(array) {
|
|
|
56
57
|
}, []);
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
// src/util/glowsToEvents.ts
|
|
61
|
+
function glowsToEvents(glows) {
|
|
62
|
+
return glows.map((glow) => ({
|
|
63
|
+
type: "php_glow",
|
|
64
|
+
startTimeUnixNano: Math.round(glow.microtime * 1e9),
|
|
65
|
+
endTimeUnixNano: null,
|
|
66
|
+
attributes: {
|
|
67
|
+
"glow.name": String(glow.name),
|
|
68
|
+
"glow.level": glow.message_level,
|
|
69
|
+
"glow.context": glow.meta_data ?? {}
|
|
70
|
+
}
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
|
|
59
74
|
// src/util/now.ts
|
|
60
75
|
function now() {
|
|
61
76
|
return Math.round(Date.now() / 1e3);
|
|
62
77
|
}
|
|
63
78
|
|
|
79
|
+
// src/api/mapToV2Wire.ts
|
|
80
|
+
var KNOWN_CONTEXT_BUCKETS = /* @__PURE__ */ new Set(["request", "request_data", "cookies", "context"]);
|
|
81
|
+
var NON_APPLICATION_FRAME_PATTERN = /node_modules|vendor|chunk-/;
|
|
82
|
+
function mapToV2Wire(report, config) {
|
|
83
|
+
const wire = {
|
|
84
|
+
seenAtUnixNano: Math.round(report.seen_at * 1e9),
|
|
85
|
+
stacktrace: report.stacktrace.map(mapStackFrame),
|
|
86
|
+
events: glowsToEvents(report.glows),
|
|
87
|
+
attributes: buildAttributes(report, config)
|
|
88
|
+
};
|
|
89
|
+
if (report.exception_class) {
|
|
90
|
+
wire.exceptionClass = report.exception_class;
|
|
91
|
+
}
|
|
92
|
+
if (report.message != null) {
|
|
93
|
+
wire.message = report.message;
|
|
94
|
+
}
|
|
95
|
+
if (report.sourcemap_version_id) {
|
|
96
|
+
wire.sourcemapVersionId = report.sourcemap_version_id;
|
|
97
|
+
}
|
|
98
|
+
return wire;
|
|
99
|
+
}
|
|
100
|
+
function mapStackFrame(frame) {
|
|
101
|
+
const out = {
|
|
102
|
+
file: frame.file,
|
|
103
|
+
lineNumber: frame.line_number,
|
|
104
|
+
isApplicationFrame: !NON_APPLICATION_FRAME_PATTERN.test(frame.file)
|
|
105
|
+
};
|
|
106
|
+
if (frame.column_number != null) {
|
|
107
|
+
out.columnNumber = frame.column_number;
|
|
108
|
+
}
|
|
109
|
+
if (frame.method) {
|
|
110
|
+
out.method = frame.method;
|
|
111
|
+
}
|
|
112
|
+
if (frame.class) {
|
|
113
|
+
out.class = frame.class;
|
|
114
|
+
}
|
|
115
|
+
if (frame.code_snippet) {
|
|
116
|
+
out.codeSnippet = frame.code_snippet;
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
function buildAttributes(report, config) {
|
|
121
|
+
const attrs = {
|
|
122
|
+
"telemetry.sdk.language": "javascript",
|
|
123
|
+
"telemetry.sdk.name": "@flareapp/js",
|
|
124
|
+
"telemetry.sdk.version": CLIENT_VERSION,
|
|
125
|
+
"flare.language.name": "javascript",
|
|
126
|
+
"flare.entry_point.type": "web"
|
|
127
|
+
};
|
|
128
|
+
if (typeof window !== "undefined" && window.location && window.location.href) {
|
|
129
|
+
attrs["flare.entry_point.value"] = window.location.href;
|
|
130
|
+
}
|
|
131
|
+
if (config.stage) {
|
|
132
|
+
attrs["service.stage"] = config.stage;
|
|
133
|
+
}
|
|
134
|
+
if (config.version) {
|
|
135
|
+
attrs["service.version"] = config.version;
|
|
136
|
+
}
|
|
137
|
+
const context = report.context ?? {};
|
|
138
|
+
if (context.request?.url) attrs["url.full"] = String(context.request.url);
|
|
139
|
+
if (context.request?.useragent) attrs["user_agent.original"] = String(context.request.useragent);
|
|
140
|
+
if (context.request?.referrer) attrs["http.request.referrer"] = String(context.request.referrer);
|
|
141
|
+
if (context.request?.readyState) attrs["document.ready_state"] = String(context.request.readyState);
|
|
142
|
+
if (context.request_data?.queryString) {
|
|
143
|
+
attrs["url.query"] = context.request_data.queryString;
|
|
144
|
+
}
|
|
145
|
+
if (context.cookies) {
|
|
146
|
+
attrs["http.request.cookies"] = context.cookies;
|
|
147
|
+
}
|
|
148
|
+
const custom = buildCustomContext(context);
|
|
149
|
+
if (Object.keys(custom).length > 0) {
|
|
150
|
+
attrs["context.custom"] = custom;
|
|
151
|
+
}
|
|
152
|
+
return attrs;
|
|
153
|
+
}
|
|
154
|
+
function buildCustomContext(context) {
|
|
155
|
+
const custom = {};
|
|
156
|
+
for (const key of Object.keys(context)) {
|
|
157
|
+
if (KNOWN_CONTEXT_BUCKETS.has(key)) continue;
|
|
158
|
+
custom[key] = context[key];
|
|
159
|
+
}
|
|
160
|
+
const added = context.context;
|
|
161
|
+
if (added && typeof added === "object") {
|
|
162
|
+
for (const key of Object.keys(added)) {
|
|
163
|
+
custom[key] = added[key];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return custom;
|
|
167
|
+
}
|
|
168
|
+
|
|
64
169
|
// src/api/Api.ts
|
|
65
170
|
var Api = class {
|
|
66
|
-
report(report,
|
|
67
|
-
return fetch(
|
|
171
|
+
report(report, config) {
|
|
172
|
+
return fetch(config.reportingUrl, {
|
|
68
173
|
method: "POST",
|
|
69
174
|
headers: {
|
|
175
|
+
"Accept": "application/json",
|
|
70
176
|
"Content-Type": "application/json",
|
|
71
|
-
"X-Api-Token": key,
|
|
72
|
-
"X-
|
|
73
|
-
"X-
|
|
177
|
+
"X-Api-Token": config.key ?? "",
|
|
178
|
+
"X-Report-Browser-Extension-Errors": JSON.stringify(config.reportBrowserExtensionErrors),
|
|
179
|
+
"X-Flare-Client-Version": "1"
|
|
74
180
|
},
|
|
75
|
-
body: flatJsonStringify(
|
|
76
|
-
...report,
|
|
77
|
-
key
|
|
78
|
-
})
|
|
181
|
+
body: flatJsonStringify(mapToV2Wire(report, config))
|
|
79
182
|
}).then(
|
|
80
183
|
(response) => {
|
|
81
|
-
if (response.status !== 204) {
|
|
184
|
+
if (response.status !== 200 && response.status !== 201 && response.status !== 204) {
|
|
82
185
|
console.error(`Received response with status ${response.status} from Flare`);
|
|
83
186
|
}
|
|
84
187
|
},
|
|
@@ -92,16 +195,20 @@ function cookie() {
|
|
|
92
195
|
if (!window.document.cookie) {
|
|
93
196
|
return {};
|
|
94
197
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
198
|
+
const cookies = {};
|
|
199
|
+
for (const raw of window.document.cookie.split("; ")) {
|
|
200
|
+
const eq = raw.indexOf("=");
|
|
201
|
+
if (eq === -1) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const name = raw.slice(0, eq);
|
|
205
|
+
const value = raw.slice(eq + 1);
|
|
206
|
+
cookies[name] = value;
|
|
207
|
+
}
|
|
208
|
+
if (Object.keys(cookies).length === 0) {
|
|
209
|
+
return {};
|
|
210
|
+
}
|
|
211
|
+
return { cookies };
|
|
105
212
|
}
|
|
106
213
|
|
|
107
214
|
// src/context/request.ts
|
|
@@ -242,22 +349,19 @@ function createStackTrace(error, debug) {
|
|
|
242
349
|
if (debug) {
|
|
243
350
|
console.error(error);
|
|
244
351
|
}
|
|
245
|
-
return resolve([
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
class: "unknown"
|
|
256
|
-
}
|
|
257
|
-
]);
|
|
352
|
+
return resolve([fallbackFrame("stacktrace missing")]);
|
|
353
|
+
}
|
|
354
|
+
let parsed;
|
|
355
|
+
try {
|
|
356
|
+
parsed = ErrorStackParser.parse(error);
|
|
357
|
+
} catch (parseError) {
|
|
358
|
+
if (debug) {
|
|
359
|
+
console.error("Flare: failed to parse stacktrace", parseError);
|
|
360
|
+
}
|
|
361
|
+
return resolve([fallbackFrame("Could not parse stacktrace")]);
|
|
258
362
|
}
|
|
259
363
|
Promise.all(
|
|
260
|
-
|
|
364
|
+
parsed.map((frame) => {
|
|
261
365
|
return new Promise((resolve2) => {
|
|
262
366
|
getCodeSnippet(frame.fileName, frame.lineNumber, frame.columnNumber).then((snippet) => {
|
|
263
367
|
resolve2({
|
|
@@ -275,21 +379,32 @@ function createStackTrace(error, debug) {
|
|
|
275
379
|
).then(resolve);
|
|
276
380
|
});
|
|
277
381
|
}
|
|
382
|
+
function fallbackFrame(message) {
|
|
383
|
+
return {
|
|
384
|
+
line_number: 0,
|
|
385
|
+
column_number: 0,
|
|
386
|
+
method: "unknown",
|
|
387
|
+
file: "unknown",
|
|
388
|
+
code_snippet: { 0: message },
|
|
389
|
+
trimmed_column_number: null,
|
|
390
|
+
class: "unknown"
|
|
391
|
+
};
|
|
392
|
+
}
|
|
278
393
|
function hasStack(err) {
|
|
279
394
|
return !!err && (!!err.stack || !!err.stacktrace || !!err["opera#sourceloc"]) && typeof (err.stack || err.stacktrace || err["opera#sourceloc"]) === "string" && err.stack !== `${err.name}: ${err.message}`;
|
|
280
395
|
}
|
|
281
396
|
|
|
282
397
|
// src/Flare.ts
|
|
283
398
|
var Flare = class {
|
|
284
|
-
constructor(
|
|
285
|
-
this.
|
|
399
|
+
constructor(api = new Api()) {
|
|
400
|
+
this.api = api;
|
|
286
401
|
this.config = {
|
|
287
|
-
key:
|
|
402
|
+
key: null,
|
|
288
403
|
version: CLIENT_VERSION,
|
|
289
404
|
sourcemapVersion: SOURCEMAP_VERSION,
|
|
290
405
|
stage: "",
|
|
291
406
|
maxGlowsPerReport: 30,
|
|
292
|
-
reportingUrl: "https://
|
|
407
|
+
reportingUrl: "https://ingress.flareapp.io/v1/errors",
|
|
293
408
|
reportBrowserExtensionErrors: false,
|
|
294
409
|
debug: false,
|
|
295
410
|
beforeEvaluate: (error) => error,
|
|
@@ -406,12 +521,7 @@ var Flare = class {
|
|
|
406
521
|
if (!reportToSubmit) {
|
|
407
522
|
return;
|
|
408
523
|
}
|
|
409
|
-
return this.
|
|
410
|
-
reportToSubmit,
|
|
411
|
-
this.config.reportingUrl,
|
|
412
|
-
this.config.key,
|
|
413
|
-
this.config.reportBrowserExtensionErrors
|
|
414
|
-
);
|
|
524
|
+
return this.api.report(reportToSubmit, this.config);
|
|
415
525
|
}
|
|
416
526
|
// Deprecated, the following methods exist for backwards compatibility.
|
|
417
527
|
set beforeEvaluate(beforeEvaluate) {
|
|
@@ -434,24 +544,27 @@ function catchWindowErrors() {
|
|
|
434
544
|
if (!window || !flare2) {
|
|
435
545
|
return;
|
|
436
546
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if (error) {
|
|
441
|
-
flare2.report(error);
|
|
442
|
-
}
|
|
443
|
-
if (typeof originalOnerrorHandler === "function") {
|
|
444
|
-
originalOnerrorHandler(_1, _2, _3, _4, error);
|
|
445
|
-
}
|
|
446
|
-
};
|
|
447
|
-
window.onunhandledrejection = (error) => {
|
|
448
|
-
if (error.reason instanceof Error) {
|
|
449
|
-
flare2.report(error.reason);
|
|
547
|
+
window.addEventListener("error", (event) => {
|
|
548
|
+
if (event.error) {
|
|
549
|
+
flare2.report(event.error);
|
|
450
550
|
}
|
|
451
|
-
|
|
452
|
-
|
|
551
|
+
});
|
|
552
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
553
|
+
const reason = event.reason;
|
|
554
|
+
if (reason instanceof Error) {
|
|
555
|
+
flare2.report(reason);
|
|
556
|
+
return;
|
|
453
557
|
}
|
|
454
|
-
|
|
558
|
+
const message = typeof reason === "string" ? reason : safeStringify(reason);
|
|
559
|
+
flare2.report(new Error(message));
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
function safeStringify(value) {
|
|
563
|
+
try {
|
|
564
|
+
return JSON.stringify(value) ?? String(value);
|
|
565
|
+
} catch {
|
|
566
|
+
return String(value);
|
|
567
|
+
}
|
|
455
568
|
}
|
|
456
569
|
|
|
457
570
|
// src/index.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flareapp/js",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "JavaScript client for flareapp.io",
|
|
5
5
|
"homepage": "https://flareapp.io",
|
|
6
6
|
"bugs": {
|
|
@@ -23,14 +23,16 @@
|
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"prepublishOnly": "npm run build",
|
|
26
|
-
"build": "tsup src/index.ts --format cjs,esm --dts --env.FLARE_JS_CLIENT_VERSION
|
|
26
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --env.FLARE_JS_CLIENT_VERSION=$(node -p \"require('./package.json').version\") --clean",
|
|
27
27
|
"test": "vitest run",
|
|
28
|
-
"typescript": "tsc"
|
|
28
|
+
"typescript": "tsc",
|
|
29
|
+
"release": "npx release-it"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
31
32
|
"error-stack-parser": "^2.0.2"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
35
|
+
"jsdom": "^25.0.0",
|
|
34
36
|
"tsup": "^8.0.1",
|
|
35
37
|
"typescript": "^5.3.3",
|
|
36
38
|
"vitest": "^1.0.4"
|
package/src/Flare.ts
CHANGED
|
@@ -16,12 +16,12 @@ import { assert, assertKey, assertSolutionProvider, now } from './util';
|
|
|
16
16
|
|
|
17
17
|
export class Flare {
|
|
18
18
|
config: Config = {
|
|
19
|
-
key:
|
|
19
|
+
key: null,
|
|
20
20
|
version: CLIENT_VERSION,
|
|
21
21
|
sourcemapVersion: SOURCEMAP_VERSION,
|
|
22
22
|
stage: '',
|
|
23
23
|
maxGlowsPerReport: 30,
|
|
24
|
-
reportingUrl: 'https://
|
|
24
|
+
reportingUrl: 'https://ingress.flareapp.io/v1/errors',
|
|
25
25
|
reportBrowserExtensionErrors: false,
|
|
26
26
|
debug: false,
|
|
27
27
|
beforeEvaluate: (error) => error,
|
|
@@ -32,7 +32,7 @@ export class Flare {
|
|
|
32
32
|
context: Context = { context: {} };
|
|
33
33
|
solutionProviders: SolutionProvider[] = [];
|
|
34
34
|
|
|
35
|
-
constructor(public
|
|
35
|
+
constructor(public api: Api = new Api()) {}
|
|
36
36
|
|
|
37
37
|
light(key: string = KEY, debug: boolean = false): Flare {
|
|
38
38
|
this.config.key = key;
|
|
@@ -184,12 +184,7 @@ export class Flare {
|
|
|
184
184
|
return;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
return this.
|
|
188
|
-
reportToSubmit,
|
|
189
|
-
this.config.reportingUrl,
|
|
190
|
-
this.config.key,
|
|
191
|
-
this.config.reportBrowserExtensionErrors
|
|
192
|
-
);
|
|
187
|
+
return this.api.report(reportToSubmit, this.config);
|
|
193
188
|
}
|
|
194
189
|
|
|
195
190
|
// Deprecated, the following methods exist for backwards compatibility.
|
package/src/api/Api.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import { Report } from '../types';
|
|
1
|
+
import { Config, Report } from '../types';
|
|
2
2
|
import { flatJsonStringify } from '../util';
|
|
3
3
|
|
|
4
|
+
import { mapToV2Wire } from './mapToV2Wire';
|
|
5
|
+
|
|
4
6
|
export class Api {
|
|
5
|
-
report(report: Report,
|
|
6
|
-
return fetch(
|
|
7
|
+
report(report: Report, config: Config): Promise<void> {
|
|
8
|
+
return fetch(config.reportingUrl, {
|
|
7
9
|
method: 'POST',
|
|
8
10
|
headers: {
|
|
11
|
+
'Accept': 'application/json',
|
|
9
12
|
'Content-Type': 'application/json',
|
|
10
|
-
'X-Api-Token': key,
|
|
11
|
-
'X-
|
|
12
|
-
'X-
|
|
13
|
+
'X-Api-Token': config.key ?? '',
|
|
14
|
+
'X-Report-Browser-Extension-Errors': JSON.stringify(config.reportBrowserExtensionErrors),
|
|
15
|
+
'X-Flare-Client-Version': '1',
|
|
13
16
|
},
|
|
14
|
-
body: flatJsonStringify(
|
|
15
|
-
...report,
|
|
16
|
-
key: key,
|
|
17
|
-
}),
|
|
17
|
+
body: flatJsonStringify(mapToV2Wire(report, config)),
|
|
18
18
|
}).then(
|
|
19
19
|
(response) => {
|
|
20
|
-
if (response.status !== 204) {
|
|
20
|
+
if (response.status !== 200 && response.status !== 201 && response.status !== 204) {
|
|
21
21
|
console.error(`Received response with status ${response.status} from Flare`);
|
|
22
22
|
}
|
|
23
23
|
},
|
package/src/api/index.ts
CHANGED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { CLIENT_VERSION } from '../env';
|
|
2
|
+
import { Config, Context, Report, StackFrame } from '../types';
|
|
3
|
+
import { glowsToEvents } from '../util/glowsToEvents';
|
|
4
|
+
|
|
5
|
+
import { V2AttributeValue, V2Attributes, V2StackFrame, V2WirePayload } from './v2WireTypes';
|
|
6
|
+
|
|
7
|
+
const KNOWN_CONTEXT_BUCKETS = new Set(['request', 'request_data', 'cookies', 'context']);
|
|
8
|
+
|
|
9
|
+
const NON_APPLICATION_FRAME_PATTERN = /node_modules|vendor|chunk-/;
|
|
10
|
+
|
|
11
|
+
export function mapToV2Wire(report: Report, config: Config): V2WirePayload {
|
|
12
|
+
const wire: V2WirePayload = {
|
|
13
|
+
seenAtUnixNano: Math.round(report.seen_at * 1_000_000_000),
|
|
14
|
+
stacktrace: report.stacktrace.map(mapStackFrame),
|
|
15
|
+
events: glowsToEvents(report.glows),
|
|
16
|
+
attributes: buildAttributes(report, config),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
if (report.exception_class) {
|
|
20
|
+
wire.exceptionClass = report.exception_class;
|
|
21
|
+
}
|
|
22
|
+
if (report.message != null) {
|
|
23
|
+
wire.message = report.message;
|
|
24
|
+
}
|
|
25
|
+
if (report.sourcemap_version_id) {
|
|
26
|
+
wire.sourcemapVersionId = report.sourcemap_version_id;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return wire;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function mapStackFrame(frame: StackFrame): V2StackFrame {
|
|
33
|
+
const out: V2StackFrame = {
|
|
34
|
+
file: frame.file,
|
|
35
|
+
lineNumber: frame.line_number,
|
|
36
|
+
isApplicationFrame: !NON_APPLICATION_FRAME_PATTERN.test(frame.file),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
if (frame.column_number != null) {
|
|
40
|
+
out.columnNumber = frame.column_number;
|
|
41
|
+
}
|
|
42
|
+
if (frame.method) {
|
|
43
|
+
out.method = frame.method;
|
|
44
|
+
}
|
|
45
|
+
if (frame.class) {
|
|
46
|
+
out.class = frame.class;
|
|
47
|
+
}
|
|
48
|
+
if (frame.code_snippet) {
|
|
49
|
+
out.codeSnippet = frame.code_snippet;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildAttributes(report: Report, config: Config): V2Attributes {
|
|
56
|
+
const attrs: V2Attributes = {
|
|
57
|
+
'telemetry.sdk.language': 'javascript',
|
|
58
|
+
'telemetry.sdk.name': '@flareapp/js',
|
|
59
|
+
'telemetry.sdk.version': CLIENT_VERSION,
|
|
60
|
+
'flare.language.name': 'javascript',
|
|
61
|
+
'flare.entry_point.type': 'web',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (typeof window !== 'undefined' && window.location && window.location.href) {
|
|
65
|
+
attrs['flare.entry_point.value'] = window.location.href;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (config.stage) {
|
|
69
|
+
attrs['service.stage'] = config.stage;
|
|
70
|
+
}
|
|
71
|
+
if (config.version) {
|
|
72
|
+
attrs['service.version'] = config.version;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const context: Context = report.context ?? {};
|
|
76
|
+
|
|
77
|
+
if (context.request?.url) attrs['url.full'] = String(context.request.url);
|
|
78
|
+
if (context.request?.useragent) attrs['user_agent.original'] = String(context.request.useragent);
|
|
79
|
+
if (context.request?.referrer) attrs['http.request.referrer'] = String(context.request.referrer);
|
|
80
|
+
if (context.request?.readyState) attrs['document.ready_state'] = String(context.request.readyState);
|
|
81
|
+
|
|
82
|
+
if (context.request_data?.queryString) {
|
|
83
|
+
attrs['url.query'] = context.request_data.queryString as V2AttributeValue;
|
|
84
|
+
}
|
|
85
|
+
if (context.cookies) {
|
|
86
|
+
attrs['http.request.cookies'] = context.cookies as V2AttributeValue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const custom = buildCustomContext(context);
|
|
90
|
+
if (Object.keys(custom).length > 0) {
|
|
91
|
+
attrs['context.custom'] = custom;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return attrs;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildCustomContext(context: Context): { [k: string]: V2AttributeValue } {
|
|
98
|
+
const custom: { [k: string]: V2AttributeValue } = {};
|
|
99
|
+
|
|
100
|
+
// Passed-context: any top-level key not in known buckets.
|
|
101
|
+
for (const key of Object.keys(context)) {
|
|
102
|
+
if (KNOWN_CONTEXT_BUCKETS.has(key)) continue;
|
|
103
|
+
custom[key] = context[key] as V2AttributeValue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// addContext('foo', value) populates context.context.foo. Flatten its
|
|
107
|
+
// children into siblings under context.custom. We process this AFTER the
|
|
108
|
+
// passed-context loop above so that on a collision (e.g. user calls both
|
|
109
|
+
// addContext('vue', X) and flare.report(err, {vue: Y})), the addContext
|
|
110
|
+
// value wins — same outcome as the {...passed, ...this.context} merge in
|
|
111
|
+
// Flare.report's createReportFromError.
|
|
112
|
+
const added = (context as any).context;
|
|
113
|
+
if (added && typeof added === 'object') {
|
|
114
|
+
for (const key of Object.keys(added)) {
|
|
115
|
+
custom[key] = added[key] as V2AttributeValue;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return custom;
|
|
120
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Internal types — NOT re-exported from packages/js/src/index.ts.
|
|
2
|
+
// These describe the v2 wire shape posted to https://ingress.flareapp.io/v1/errors.
|
|
3
|
+
|
|
4
|
+
export type V2AttributeValue =
|
|
5
|
+
| string
|
|
6
|
+
| number
|
|
7
|
+
| boolean
|
|
8
|
+
| null
|
|
9
|
+
| V2AttributeValue[]
|
|
10
|
+
| { [key: string]: V2AttributeValue };
|
|
11
|
+
|
|
12
|
+
export type V2Attributes = Record<string, V2AttributeValue>;
|
|
13
|
+
|
|
14
|
+
export type V2StackFrame = {
|
|
15
|
+
file: string;
|
|
16
|
+
lineNumber: number;
|
|
17
|
+
columnNumber?: number;
|
|
18
|
+
method?: string;
|
|
19
|
+
class?: string;
|
|
20
|
+
codeSnippet?: { [line: number]: string };
|
|
21
|
+
isApplicationFrame?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type V2SpanEvent = {
|
|
25
|
+
type: string;
|
|
26
|
+
startTimeUnixNano: number;
|
|
27
|
+
endTimeUnixNano: number | null;
|
|
28
|
+
attributes: V2Attributes;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type V2WirePayload = {
|
|
32
|
+
exceptionClass?: string;
|
|
33
|
+
message?: string;
|
|
34
|
+
seenAtUnixNano: number;
|
|
35
|
+
sourcemapVersionId?: string;
|
|
36
|
+
stacktrace: V2StackFrame[];
|
|
37
|
+
events: V2SpanEvent[];
|
|
38
|
+
attributes: V2Attributes;
|
|
39
|
+
};
|
|
@@ -10,27 +10,29 @@ export function catchWindowErrors() {
|
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
window.onerror = (_1, _2, _3, _4, error) => {
|
|
17
|
-
if (error) {
|
|
18
|
-
flare.report(error);
|
|
13
|
+
window.addEventListener('error', (event: ErrorEvent) => {
|
|
14
|
+
if (event.error) {
|
|
15
|
+
flare.report(event.error);
|
|
19
16
|
}
|
|
17
|
+
});
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
};
|
|
19
|
+
window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {
|
|
20
|
+
const reason = event.reason;
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
if (reason instanceof Error) {
|
|
23
|
+
flare.report(reason);
|
|
24
|
+
return;
|
|
29
25
|
}
|
|
30
26
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
const message = typeof reason === 'string' ? reason : safeStringify(reason);
|
|
28
|
+
flare.report(new Error(message));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function safeStringify(value: unknown): string {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.stringify(value) ?? String(value);
|
|
35
|
+
} catch {
|
|
36
|
+
return String(value);
|
|
37
|
+
}
|
|
36
38
|
}
|
package/src/context/cookie.ts
CHANGED
|
@@ -3,15 +3,22 @@ export default function cookie() {
|
|
|
3
3
|
return {};
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
cookies: window.document.cookie.split('; ').reduce(
|
|
8
|
-
(cookies, cookie) => {
|
|
9
|
-
const [cookieName, cookieValue] = cookie.split(/=/);
|
|
10
|
-
cookies[cookieName] = cookieValue;
|
|
6
|
+
const cookies: { [key: string]: string } = {};
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
for (const raw of window.document.cookie.split('; ')) {
|
|
9
|
+
const eq = raw.indexOf('=');
|
|
10
|
+
if (eq === -1) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const name = raw.slice(0, eq);
|
|
15
|
+
const value = raw.slice(eq + 1);
|
|
16
|
+
cookies[name] = value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (Object.keys(cookies).length === 0) {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return { cookies };
|
|
17
24
|
}
|
|
@@ -14,23 +14,21 @@ export function createStackTrace(error: Error, debug: boolean): Promise<Array<St
|
|
|
14
14
|
console.error(error);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
return resolve([
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
},
|
|
29
|
-
]);
|
|
17
|
+
return resolve([fallbackFrame('stacktrace missing')]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let parsed: ReturnType<typeof ErrorStackParser.parse>;
|
|
21
|
+
try {
|
|
22
|
+
parsed = ErrorStackParser.parse(error);
|
|
23
|
+
} catch (parseError) {
|
|
24
|
+
if (debug) {
|
|
25
|
+
console.error('Flare: failed to parse stacktrace', parseError);
|
|
26
|
+
}
|
|
27
|
+
return resolve([fallbackFrame('Could not parse stacktrace')]);
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
Promise.all(
|
|
33
|
-
|
|
31
|
+
parsed.map((frame) => {
|
|
34
32
|
return new Promise<StackFrame>((resolve) => {
|
|
35
33
|
getCodeSnippet(frame.fileName, frame.lineNumber, frame.columnNumber).then((snippet) => {
|
|
36
34
|
resolve({
|
|
@@ -49,6 +47,18 @@ export function createStackTrace(error: Error, debug: boolean): Promise<Array<St
|
|
|
49
47
|
});
|
|
50
48
|
}
|
|
51
49
|
|
|
50
|
+
function fallbackFrame(message: string): StackFrame {
|
|
51
|
+
return {
|
|
52
|
+
line_number: 0,
|
|
53
|
+
column_number: 0,
|
|
54
|
+
method: 'unknown',
|
|
55
|
+
file: 'unknown',
|
|
56
|
+
code_snippet: { 0: message },
|
|
57
|
+
trimmed_column_number: null,
|
|
58
|
+
class: 'unknown',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
52
62
|
function hasStack(err: any): boolean {
|
|
53
63
|
return (
|
|
54
64
|
!!err &&
|
package/src/types.ts
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export function flatJsonStringify(json: object): string {
|
|
2
|
+
const ancestors = new WeakSet<object>();
|
|
3
|
+
const path: object[] = [];
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
if (typeof value
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
cache.push(value);
|
|
5
|
+
return JSON.stringify(json, function (this: object, _key, value) {
|
|
6
|
+
if (typeof value !== 'object' || value === null) {
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Pop ancestors that are no longer on the path to `this`.
|
|
11
|
+
while (path.length > 0 && path[path.length - 1] !== this) {
|
|
12
|
+
const popped = path.pop()!;
|
|
13
|
+
ancestors.delete(popped);
|
|
15
14
|
}
|
|
16
|
-
return value;
|
|
17
|
-
});
|
|
18
15
|
|
|
19
|
-
|
|
16
|
+
if (ancestors.has(value)) {
|
|
17
|
+
return '[Circular]';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
ancestors.add(value);
|
|
21
|
+
path.push(value);
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
return value;
|
|
24
|
+
});
|
|
22
25
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { V2AttributeValue, V2SpanEvent } from '../api/v2WireTypes';
|
|
2
|
+
import { Glow } from '../types';
|
|
3
|
+
|
|
4
|
+
// glow.microtime is seconds since epoch with sub-second fraction (see util/now.ts).
|
|
5
|
+
// V2SpanEvent.startTimeUnixNano is unix nanoseconds.
|
|
6
|
+
export function glowsToEvents(glows: Glow[]): V2SpanEvent[] {
|
|
7
|
+
return glows.map((glow) => ({
|
|
8
|
+
type: 'php_glow',
|
|
9
|
+
startTimeUnixNano: Math.round(glow.microtime * 1_000_000_000),
|
|
10
|
+
endTimeUnixNano: null,
|
|
11
|
+
attributes: {
|
|
12
|
+
'glow.name': String(glow.name),
|
|
13
|
+
'glow.level': glow.message_level,
|
|
14
|
+
'glow.context': (glow.meta_data ?? {}) as V2AttributeValue,
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
}
|