@flareapp/js 1.1.0 → 1.2.1

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.
@@ -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
@@ -77,7 +77,7 @@ type Glow = {
77
77
  type MessageLevel = 'info' | 'debug' | 'warning' | 'error' | 'critical';
78
78
 
79
79
  declare class Api {
80
- report(report: Report, url: string, key: string | null, reportBrowserExtensionErrors: boolean): Promise<void>;
80
+ report(report: Report, config: Config): Promise<void>;
81
81
  }
82
82
 
83
83
  declare class Flare {
package/dist/index.d.ts CHANGED
@@ -77,7 +77,7 @@ type Glow = {
77
77
  type MessageLevel = 'info' | 'debug' | 'warning' | 'error' | 'critical';
78
78
 
79
79
  declare class Api {
80
- report(report: Report, url: string, key: string | null, reportBrowserExtensionErrors: boolean): Promise<void>;
80
+ report(report: Report, config: Config): Promise<void>;
81
81
  }
82
82
 
83
83
  declare class Flare {
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 src_exports = {};
32
- __export(src_exports, {
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
33
  Flare: () => Flare,
34
34
  flare: () => flare
35
35
  });
36
- module.exports = __toCommonJS(src_exports);
36
+ module.exports = __toCommonJS(index_exports);
37
37
 
38
38
  // src/env/index.ts
39
- var CLIENT_VERSION = false ? "?" : '"1.1.0"';
39
+ var CLIENT_VERSION = false ? "?" : "1.2.1";
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
- let cache = [];
72
- const flattenedStringifiedJson = JSON.stringify(json, function(_, value) {
73
- if (typeof value === "object" && value !== null) {
74
- if (cache.indexOf(value) !== -1) {
75
- try {
76
- return JSON.parse(JSON.stringify(value));
77
- } catch (error) {
78
- return;
79
- }
80
- }
81
- cache.push(value);
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, url, key, reportBrowserExtensionErrors) {
104
- return fetch(url, {
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-Requested-With": "XMLHttpRequest",
110
- "X-Report-Browser-Extension-Errors": JSON.stringify(reportBrowserExtensionErrors)
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
- return {
133
- cookies: window.document.cookie.split("; ").reduce(
134
- (cookies, cookie2) => {
135
- const [cookieName, cookieValue] = cookie2.split(/=/);
136
- cookies[cookieName] = cookieValue;
137
- return cookies;
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
- line_number: 0,
285
- column_number: 0,
286
- method: "unknown",
287
- file: "unknown",
288
- code_snippet: {
289
- 0: "Could not read from file: stacktrace missing"
290
- },
291
- trimmed_column_number: null,
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
- import_error_stack_parser.default.parse(error).map((frame) => {
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,6 +416,17 @@ 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
  }
@@ -326,7 +441,7 @@ var Flare = class {
326
441
  sourcemapVersion: SOURCEMAP_VERSION,
327
442
  stage: "",
328
443
  maxGlowsPerReport: 30,
329
- reportingUrl: "https://reporting.flareapp.io/api/reports",
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.api.report(
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,50 @@ function catchWindowErrors() {
471
581
  if (!window || !flare2) {
472
582
  return;
473
583
  }
474
- const originalOnerrorHandler = window.onerror;
475
- const originalOnunhandledrejectionHandler = window.onunhandledrejection;
476
- window.onerror = (_1, _2, _3, _4, error) => {
477
- if (error) {
478
- flare2.report(error);
584
+ window.addEventListener("error", (event) => {
585
+ if (event.error) {
586
+ flare2.report(event.error);
479
587
  }
480
- if (typeof originalOnerrorHandler === "function") {
481
- originalOnerrorHandler(_1, _2, _3, _4, error);
588
+ });
589
+ window.addEventListener("unhandledrejection", (event) => {
590
+ const reason = event.reason;
591
+ if (reason instanceof Error) {
592
+ flare2.report(reason);
593
+ return;
482
594
  }
483
- };
484
- window.onunhandledrejection = (error) => {
485
- if (error.reason instanceof Error) {
486
- flare2.report(error.reason);
595
+ if (hasStack2(reason)) {
596
+ const error = new Error(rejectionReasonToMessage(reason));
597
+ error.stack = reason.stack;
598
+ flare2.report(error);
599
+ return;
487
600
  }
488
- if (typeof originalOnunhandledrejectionHandler === "function") {
489
- originalOnunhandledrejectionHandler(error);
601
+ flare2.reportMessage(rejectionReasonToMessage(reason), {}, "UnhandledPromiseRejection");
602
+ });
603
+ }
604
+ function rejectionReasonToMessage(reason) {
605
+ if (typeof reason === "string") {
606
+ return reason;
607
+ }
608
+ if (reason == null) {
609
+ return `Unhandled promise rejection (${reason})`;
610
+ }
611
+ if (typeof reason === "object") {
612
+ if ("message" in reason && typeof reason.message === "string") {
613
+ return reason.message;
490
614
  }
491
- };
615
+ try {
616
+ const json = JSON.stringify(reason);
617
+ if (json && json !== "{}") {
618
+ return `Unhandled promise rejection: ${json}`;
619
+ }
620
+ } catch {
621
+ }
622
+ return "Unhandled promise rejection with non-serializable object";
623
+ }
624
+ return `Unhandled promise rejection: ${String(reason)}`;
625
+ }
626
+ function hasStack2(value) {
627
+ return typeof value === "object" && value !== null && "stack" in value && typeof value.stack === "string";
492
628
  }
493
629
 
494
630
  // src/index.ts
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/env/index.ts
2
- var CLIENT_VERSION = false ? "?" : '"1.1.0"';
2
+ var CLIENT_VERSION = false ? "?" : "1.2.1";
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
- let cache = [];
35
- const flattenedStringifiedJson = JSON.stringify(json, function(_, value) {
36
- if (typeof value === "object" && value !== null) {
37
- if (cache.indexOf(value) !== -1) {
38
- try {
39
- return JSON.parse(JSON.stringify(value));
40
- } catch (error) {
41
- return;
42
- }
43
- }
44
- cache.push(value);
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, url, key, reportBrowserExtensionErrors) {
67
- return fetch(url, {
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-Requested-With": "XMLHttpRequest",
73
- "X-Report-Browser-Extension-Errors": JSON.stringify(reportBrowserExtensionErrors)
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
- return {
96
- cookies: window.document.cookie.split("; ").reduce(
97
- (cookies, cookie2) => {
98
- const [cookieName, cookieValue] = cookie2.split(/=/);
99
- cookies[cookieName] = cookieValue;
100
- return cookies;
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
- line_number: 0,
248
- column_number: 0,
249
- method: "unknown",
250
- file: "unknown",
251
- code_snippet: {
252
- 0: "Could not read from file: stacktrace missing"
253
- },
254
- trimmed_column_number: null,
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
- ErrorStackParser.parse(error).map((frame) => {
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,6 +379,17 @@ 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
  }
@@ -289,7 +404,7 @@ var Flare = class {
289
404
  sourcemapVersion: SOURCEMAP_VERSION,
290
405
  stage: "",
291
406
  maxGlowsPerReport: 30,
292
- reportingUrl: "https://reporting.flareapp.io/api/reports",
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.api.report(
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,50 @@ function catchWindowErrors() {
434
544
  if (!window || !flare2) {
435
545
  return;
436
546
  }
437
- const originalOnerrorHandler = window.onerror;
438
- const originalOnunhandledrejectionHandler = window.onunhandledrejection;
439
- window.onerror = (_1, _2, _3, _4, error) => {
440
- if (error) {
441
- flare2.report(error);
547
+ window.addEventListener("error", (event) => {
548
+ if (event.error) {
549
+ flare2.report(event.error);
442
550
  }
443
- if (typeof originalOnerrorHandler === "function") {
444
- originalOnerrorHandler(_1, _2, _3, _4, error);
551
+ });
552
+ window.addEventListener("unhandledrejection", (event) => {
553
+ const reason = event.reason;
554
+ if (reason instanceof Error) {
555
+ flare2.report(reason);
556
+ return;
445
557
  }
446
- };
447
- window.onunhandledrejection = (error) => {
448
- if (error.reason instanceof Error) {
449
- flare2.report(error.reason);
558
+ if (hasStack2(reason)) {
559
+ const error = new Error(rejectionReasonToMessage(reason));
560
+ error.stack = reason.stack;
561
+ flare2.report(error);
562
+ return;
450
563
  }
451
- if (typeof originalOnunhandledrejectionHandler === "function") {
452
- originalOnunhandledrejectionHandler(error);
564
+ flare2.reportMessage(rejectionReasonToMessage(reason), {}, "UnhandledPromiseRejection");
565
+ });
566
+ }
567
+ function rejectionReasonToMessage(reason) {
568
+ if (typeof reason === "string") {
569
+ return reason;
570
+ }
571
+ if (reason == null) {
572
+ return `Unhandled promise rejection (${reason})`;
573
+ }
574
+ if (typeof reason === "object") {
575
+ if ("message" in reason && typeof reason.message === "string") {
576
+ return reason.message;
453
577
  }
454
- };
578
+ try {
579
+ const json = JSON.stringify(reason);
580
+ if (json && json !== "{}") {
581
+ return `Unhandled promise rejection: ${json}`;
582
+ }
583
+ } catch {
584
+ }
585
+ return "Unhandled promise rejection with non-serializable object";
586
+ }
587
+ return `Unhandled promise rejection: ${String(reason)}`;
588
+ }
589
+ function hasStack2(value) {
590
+ return typeof value === "object" && value !== null && "stack" in value && typeof value.stack === "string";
455
591
  }
456
592
 
457
593
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flareapp/js",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
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=\\\"$(node -p \"require('./package.json').version\")\\\" --clean",
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
@@ -21,7 +21,7 @@ export class Flare {
21
21
  sourcemapVersion: SOURCEMAP_VERSION,
22
22
  stage: '',
23
23
  maxGlowsPerReport: 30,
24
- reportingUrl: 'https://reporting.flareapp.io/api/reports',
24
+ reportingUrl: 'https://ingress.flareapp.io/v1/errors',
25
25
  reportBrowserExtensionErrors: false,
26
26
  debug: false,
27
27
  beforeEvaluate: (error) => error,
@@ -184,12 +184,7 @@ export class Flare {
184
184
  return;
185
185
  }
186
186
 
187
- return this.api.report(
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, url: string, key: string | null, reportBrowserExtensionErrors: boolean): Promise<void> {
6
- return fetch(url, {
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-Requested-With': 'XMLHttpRequest',
12
- 'X-Report-Browser-Extension-Errors': JSON.stringify(reportBrowserExtensionErrors),
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
@@ -1 +1,2 @@
1
1
  export * from './Api';
2
+ export * from './mapToV2Wire';
@@ -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,63 @@ export function catchWindowErrors() {
10
10
  return;
11
11
  }
12
12
 
13
- const originalOnerrorHandler = window.onerror;
14
- const originalOnunhandledrejectionHandler = window.onunhandledrejection;
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
- if (typeof originalOnerrorHandler === 'function') {
22
- originalOnerrorHandler(_1, _2, _3, _4, error);
19
+ window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {
20
+ const reason = event.reason;
21
+
22
+ if (reason instanceof Error) {
23
+ flare.report(reason);
24
+ return;
23
25
  }
24
- };
25
26
 
26
- window.onunhandledrejection = (error: PromiseRejectionEvent) => {
27
- if (error.reason instanceof Error) {
28
- flare.report(error.reason);
27
+ if (hasStack(reason)) {
28
+ const error = new Error(rejectionReasonToMessage(reason));
29
+ error.stack = (reason as { stack: string }).stack;
30
+ flare.report(error);
31
+ return;
29
32
  }
30
33
 
31
- if (typeof originalOnunhandledrejectionHandler === 'function') {
32
- // @ts-ignore
33
- originalOnunhandledrejectionHandler(error);
34
+ flare.reportMessage(rejectionReasonToMessage(reason), {}, 'UnhandledPromiseRejection');
35
+ });
36
+ }
37
+
38
+ function rejectionReasonToMessage(reason: unknown): string {
39
+ if (typeof reason === 'string') {
40
+ return reason;
41
+ }
42
+
43
+ if (reason == null) {
44
+ return `Unhandled promise rejection (${reason})`;
45
+ }
46
+
47
+ if (typeof reason === 'object') {
48
+ if ('message' in reason && typeof (reason as Record<string, unknown>).message === 'string') {
49
+ return (reason as Record<string, unknown>).message as string;
34
50
  }
35
- };
51
+
52
+ try {
53
+ const json = JSON.stringify(reason);
54
+ if (json && json !== '{}') {
55
+ return `Unhandled promise rejection: ${json}`;
56
+ }
57
+ } catch {}
58
+
59
+ return 'Unhandled promise rejection with non-serializable object';
60
+ }
61
+
62
+ return `Unhandled promise rejection: ${String(reason)}`;
63
+ }
64
+
65
+ function hasStack(value: unknown): boolean {
66
+ return (
67
+ typeof value === 'object' &&
68
+ value !== null &&
69
+ 'stack' in value &&
70
+ typeof (value as Record<string, unknown>).stack === 'string'
71
+ );
36
72
  }
@@ -3,15 +3,22 @@ export default function cookie() {
3
3
  return {};
4
4
  }
5
5
 
6
- return {
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
- return cookies;
13
- },
14
- {} as { [key: string]: string }
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
- line_number: 0,
20
- column_number: 0,
21
- method: 'unknown',
22
- file: 'unknown',
23
- code_snippet: {
24
- 0: 'Could not read from file: stacktrace missing',
25
- },
26
- trimmed_column_number: null,
27
- class: 'unknown',
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
- ErrorStackParser.parse(error).map((frame) => {
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 &&
@@ -1,22 +1,25 @@
1
- // https://stackoverflow.com/a/11616993/6374824
2
- export function flatJsonStringify(json: Object): string {
3
- let cache: any = [];
1
+ export function flatJsonStringify(json: object): string {
2
+ const ancestors = new WeakSet<object>();
3
+ const path: object[] = [];
4
4
 
5
- const flattenedStringifiedJson = JSON.stringify(json, function (_, value) {
6
- if (typeof value === 'object' && value !== null) {
7
- if (cache.indexOf(value) !== -1) {
8
- try {
9
- return JSON.parse(JSON.stringify(value));
10
- } catch (error) {
11
- return;
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
- cache = null;
16
+ if (ancestors.has(value)) {
17
+ return '[Circular]';
18
+ }
19
+
20
+ ancestors.add(value);
21
+ path.push(value);
20
22
 
21
- return flattenedStringifiedJson;
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
+ }
package/src/util/index.ts CHANGED
@@ -3,4 +3,5 @@ export * from './assertKey';
3
3
  export * from './assertSolutionProvider';
4
4
  export * from './flatJsonStringify';
5
5
  export * from './flattenOnce';
6
+ export * from './glowsToEvents';
6
7
  export * from './now';