@flareapp/core 2.5.0 → 2.6.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/dist/index.cjs CHANGED
@@ -30,7 +30,7 @@ let error_stack_parser = require("error-stack-parser");
30
30
  error_stack_parser = __toESM(error_stack_parser);
31
31
 
32
32
  //#region src/env/index.ts
33
- const CLIENT_VERSION = typeof process !== "undefined" && true ? "2.5.0" : "?";
33
+ const CLIENT_VERSION = typeof process !== "undefined" && true ? "2.6.0" : "?";
34
34
  const KEY = typeof FLARE_JS_KEY === "undefined" ? "" : FLARE_JS_KEY;
35
35
  const SOURCEMAP_VERSION = typeof FLARE_SOURCEMAP_VERSION === "undefined" ? "" : FLARE_SOURCEMAP_VERSION;
36
36
 
@@ -171,6 +171,49 @@ function safeDecode(value) {
171
171
  }
172
172
  }
173
173
 
174
+ //#endregion
175
+ //#region src/util/rejection.ts
176
+ /** Best-effort human-readable description of an arbitrary rejection reason. */
177
+ function describeRejectionReason(reason) {
178
+ if (typeof reason === "string") return reason;
179
+ if (reason && typeof reason === "object") {
180
+ const message = reason.message;
181
+ if (typeof message === "string" && message) return message;
182
+ try {
183
+ return JSON.stringify(reason);
184
+ } catch {
185
+ return "Unhandled promise rejection (non-serializable reason)";
186
+ }
187
+ }
188
+ return String(reason);
189
+ }
190
+ function hasStack$1(reason) {
191
+ return !!reason && typeof reason === "object" && typeof reason.stack === "string";
192
+ }
193
+ /**
194
+ * Route a rejection reason to the reporter: an Error (or any stack-bearing
195
+ * object) goes to `reportSilently` so the STACK survives; only a stackless
196
+ * reason falls back to `reportUnhandledRejection` (string message, empty-stack
197
+ * `UnhandledRejection` class). Any rejection from `reportUnhandledRejection`'s
198
+ * returned promise is swallowed so a transport failure cannot itself surface as
199
+ * an unhandled rejection. `reportSilently` is assumed not to throw synchronously
200
+ * (core's does its work asynchronously); it is intentionally not wrapped, so a
201
+ * synchronous throw there would propagate.
202
+ */
203
+ function routeRejection(reporter, reason) {
204
+ if (reason instanceof Error) {
205
+ reporter.reportSilently(reason);
206
+ return;
207
+ }
208
+ if (hasStack$1(reason)) {
209
+ const error = new Error(describeRejectionReason(reason));
210
+ error.stack = reason.stack;
211
+ reporter.reportSilently(error);
212
+ return;
213
+ }
214
+ Promise.resolve(reporter.reportUnhandledRejection(describeRejectionReason(reason))).catch(() => {});
215
+ }
216
+
174
217
  //#endregion
175
218
  //#region src/api/Api.ts
176
219
  var Api = class {
@@ -500,6 +543,39 @@ function partitionAttributes(attributes) {
500
543
  //#endregion
501
544
  //#region src/Scope.ts
502
545
  /**
546
+ * Maps each `User` identity field to the flat report attribute key it projects to.
547
+ * `Flare.setUser`'s set pass writes through these so the literal key strings live in
548
+ * exactly one place; `USER_IDENTITY_KEYS` (the clear pass) derives from them, so adding
549
+ * a field here can never silently leave the clear pass out of date.
550
+ */
551
+ const USER_FIELD_KEYS = {
552
+ id: "user.id",
553
+ email: "user.email",
554
+ fullName: "user.full_name",
555
+ ipAddress: "client.address"
556
+ };
557
+ /**
558
+ * The report attribute keys that `Flare.setUser` owns: the four projected identity
559
+ * fields plus the `user.attributes` bag for extras. Single source of truth so the
560
+ * clear pass and the set pass in `setUser` cannot drift, and so consumers that must
561
+ * stamp identity outside core's report pipeline (Electron's forwarded-renderer path)
562
+ * pick up the exact same set instead of re-hardcoding it.
563
+ */
564
+ const USER_IDENTITY_KEYS = [...Object.values(USER_FIELD_KEYS), "user.attributes"];
565
+ /**
566
+ * Pick the user-identity attributes currently set on a scope. Used where identity must
567
+ * be copied onto a report that does not flow through `Flare.report()` (which would
568
+ * otherwise spread `pendingAttributes` automatically).
569
+ */
570
+ function userIdentityAttributes(scope) {
571
+ const attrs = {};
572
+ for (const key of USER_IDENTITY_KEYS) {
573
+ const value = scope.pendingAttributes[key];
574
+ if (value !== void 0) attrs[key] = value;
575
+ }
576
+ return attrs;
577
+ }
578
+ /**
503
579
  * Holds the per-call mutable state that used to live on the `Flare` instance:
504
580
  * breadcrumbs (`glows`), custom attributes (`pendingAttributes`), and the
505
581
  * current entry-point handler.
@@ -516,9 +592,9 @@ function partitionAttributes(attributes) {
516
592
  * holding the state directly, so the per-request behavior comes from the
517
593
  * provider, not from the class itself.
518
594
  *
519
- * `NodeScope` (in `@flareapp/node`) extends this with two more buckets:
520
- * `request` (HTTP method, path, headers) and `user` (id, email, ...). Browser
521
- * does not need those.
595
+ * `NodeScope` (in `@flareapp/node`) extends this with a `request` bucket
596
+ * (HTTP method, path, headers). User identity is written to `pendingAttributes`
597
+ * by `Flare.setUser`, so it needs no dedicated field. Browser does not need `request`.
522
598
  */
523
599
  var Scope = class {
524
600
  glows = [];
@@ -641,18 +717,24 @@ function createStackTrace(error, debug, fileReader) {
641
717
  return resolve([fallbackFrame("stacktrace could not be parsed")]);
642
718
  }
643
719
  Promise.all(parsedFrames.map((frame) => {
644
- return getCodeSnippet(fileReader, frame.fileName, frame.lineNumber, frame.columnNumber).then((snippet) => ({
720
+ const fileName = normalizeFileName(frame.fileName);
721
+ return getCodeSnippet(fileReader, fileName, frame.lineNumber, frame.columnNumber).then((snippet) => ({
645
722
  lineNumber: frame.lineNumber || 1,
646
723
  columnNumber: frame.columnNumber || 1,
647
724
  method: frame.functionName || "Anonymous or unknown function",
648
- file: frame.fileName || "Unknown file",
725
+ file: fileName || "Unknown file",
649
726
  codeSnippet: snippet.codeSnippet,
650
727
  class: "",
651
- isApplicationFrame: isApplicationFrame(frame.fileName)
728
+ isApplicationFrame: isApplicationFrame(fileName)
652
729
  }));
653
730
  })).then(resolve);
654
731
  });
655
732
  }
733
+ const HERMES_ADDRESS_PREFIX = "address at ";
734
+ function normalizeFileName(fileName) {
735
+ if (fileName?.startsWith(HERMES_ADDRESS_PREFIX)) return fileName.slice(11);
736
+ return fileName;
737
+ }
656
738
  function fallbackFrame(reason) {
657
739
  return {
658
740
  lineNumber: 0,
@@ -975,6 +1057,26 @@ var Flare = class {
975
1057
  this.scopeProvider.active().setAttribute(`context.${groupName}`, value);
976
1058
  return this;
977
1059
  }
1060
+ /**
1061
+ * Attach an identified user to the active scope. Fields are projected to the
1062
+ * keys the Flare backend reads: `user.id`, `user.email`, `user.full_name`,
1063
+ * and `client.address`. Any extra keys are bundled into `user.attributes`.
1064
+ * Pass `null` to clear the user. Scope-aware: in Node this targets the
1065
+ * per-request scope via the scope provider.
1066
+ */
1067
+ setUser(user) {
1068
+ const scope = this.scopeProvider.active();
1069
+ for (const key of USER_IDENTITY_KEYS) delete scope.pendingAttributes[key];
1070
+ if (!user) return this;
1071
+ const { id, email, fullName, ipAddress, ...rest } = user;
1072
+ if (id !== void 0 && id !== null) scope.setAttribute(USER_FIELD_KEYS.id, String(id));
1073
+ if (email !== void 0) scope.setAttribute(USER_FIELD_KEYS.email, email);
1074
+ if (fullName !== void 0) scope.setAttribute(USER_FIELD_KEYS.fullName, fullName);
1075
+ if (ipAddress !== void 0) scope.setAttribute(USER_FIELD_KEYS.ipAddress, ipAddress);
1076
+ const extras = Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== void 0));
1077
+ if (Object.keys(extras).length > 0) scope.setAttribute("user.attributes", extras);
1078
+ return this;
1079
+ }
978
1080
  setEntryPoint(handler) {
979
1081
  this.scopeProvider.active().entryPoint = handler;
980
1082
  return this;
@@ -1138,10 +1240,12 @@ exports.Logger = Logger;
1138
1240
  exports.NoopFlushScheduler = NoopFlushScheduler;
1139
1241
  exports.NullFileReader = NullFileReader;
1140
1242
  exports.Scope = Scope;
1243
+ exports.USER_IDENTITY_KEYS = USER_IDENTITY_KEYS;
1141
1244
  exports.assert = assert;
1142
1245
  exports.assertKey = assertKey;
1143
1246
  exports.convertToError = convertToError;
1144
1247
  exports.createStackTrace = createStackTrace;
1248
+ exports.describeRejectionReason = describeRejectionReason;
1145
1249
  exports.extractCode = extractCode;
1146
1250
  exports.flatJsonStringify = flatJsonStringify;
1147
1251
  exports.getCodeSnippet = getCodeSnippet;
@@ -1149,4 +1253,6 @@ exports.glowsToEvents = glowsToEvents;
1149
1253
  exports.now = now;
1150
1254
  exports.readLinesFromFile = readLinesFromFile;
1151
1255
  exports.redactUrlQuery = redactUrlQuery;
1152
- exports.resolveDenylist = resolveDenylist;
1256
+ exports.resolveDenylist = resolveDenylist;
1257
+ exports.routeRejection = routeRejection;
1258
+ exports.userIdentityAttributes = userIdentityAttributes;
package/dist/index.d.cts CHANGED
@@ -4,6 +4,23 @@ type AttributeValue = string | number | boolean | null | AttributeValue[] | {
4
4
  [key: string]: AttributeValue;
5
5
  };
6
6
  type Attributes = Record<string, AttributeValue>;
7
+ /**
8
+ * An identified user passed to `Flare.setUser`. The four known fields project to the
9
+ * report keys the Flare backend reads: `id`→`user.id`, `email`→`user.email`,
10
+ * `fullName`→`user.full_name`, `ipAddress`→`client.address`. Any OTHER key is bundled
11
+ * into `user.attributes`.
12
+ *
13
+ * Caveat: the open index signature means a misspelled known field (e.g. `fullname` or
14
+ * `full_name` instead of `fullName`) does NOT raise a type error — it silently lands in
15
+ * `user.attributes` rather than the identity key. Spell the four known fields exactly.
16
+ */
17
+ type User = {
18
+ id?: string | number;
19
+ email?: string;
20
+ fullName?: string;
21
+ ipAddress?: string;
22
+ [key: string]: AttributeValue | undefined;
23
+ };
7
24
  type Config = {
8
25
  key: string | null;
9
26
  version: string;
@@ -168,6 +185,34 @@ declare const DEFAULT_URL_DENYLIST: RegExp;
168
185
  declare function resolveDenylist(custom?: RegExp, replaceDefault?: boolean, defaultDenylist?: RegExp): RegExp;
169
186
  declare function redactUrlQuery(fullPath: string, denylist?: RegExp): string;
170
187
  //#endregion
188
+ //#region src/util/rejection.d.ts
189
+ /**
190
+ * Shared unhandled-rejection routing. A rejection "reason" can be anything a
191
+ * promise was rejected with: an Error, an Error-like object carrying a `.stack`,
192
+ * a string, or a plain object. The browser `unhandledrejection` listener
193
+ * (`@flareapp/js`) and the React Native engine rejection tracker
194
+ * (`@flareapp/react-native`) need the SAME routing so a report looks identical
195
+ * across SDKs, so it lives here instead of being copy-pasted (and drifting) per
196
+ * client.
197
+ */
198
+ type RejectionReporter = {
199
+ reportSilently: (error: Error) => void;
200
+ reportUnhandledRejection: (message: string) => unknown;
201
+ };
202
+ /** Best-effort human-readable description of an arbitrary rejection reason. */
203
+ declare function describeRejectionReason(reason: unknown): string;
204
+ /**
205
+ * Route a rejection reason to the reporter: an Error (or any stack-bearing
206
+ * object) goes to `reportSilently` so the STACK survives; only a stackless
207
+ * reason falls back to `reportUnhandledRejection` (string message, empty-stack
208
+ * `UnhandledRejection` class). Any rejection from `reportUnhandledRejection`'s
209
+ * returned promise is swallowed so a transport failure cannot itself surface as
210
+ * an unhandled rejection. `reportSilently` is assumed not to throw synchronously
211
+ * (core's does its work asynchronously); it is intentionally not wrapped, so a
212
+ * synchronous throw there would propagate.
213
+ */
214
+ declare function routeRejection(reporter: RejectionReporter, reason: unknown): void;
215
+ //#endregion
171
216
  //#region src/api/Api.d.ts
172
217
  declare class Api {
173
218
  report(report: Report, url: string, key: string | null, reportBrowserExtensionErrors: boolean, debug?: boolean): Promise<void>;
@@ -236,6 +281,20 @@ declare class Logger {
236
281
  }
237
282
  //#endregion
238
283
  //#region src/Scope.d.ts
284
+ /**
285
+ * The report attribute keys that `Flare.setUser` owns: the four projected identity
286
+ * fields plus the `user.attributes` bag for extras. Single source of truth so the
287
+ * clear pass and the set pass in `setUser` cannot drift, and so consumers that must
288
+ * stamp identity outside core's report pipeline (Electron's forwarded-renderer path)
289
+ * pick up the exact same set instead of re-hardcoding it.
290
+ */
291
+ declare const USER_IDENTITY_KEYS: readonly [...("user.id" | "user.email" | "user.full_name" | "client.address")[], "user.attributes"];
292
+ /**
293
+ * Pick the user-identity attributes currently set on a scope. Used where identity must
294
+ * be copied onto a report that does not flow through `Flare.report()` (which would
295
+ * otherwise spread `pendingAttributes` automatically).
296
+ */
297
+ declare function userIdentityAttributes(scope: Scope): Attributes;
239
298
  /**
240
299
  * Holds the per-call mutable state that used to live on the `Flare` instance:
241
300
  * breadcrumbs (`glows`), custom attributes (`pendingAttributes`), and the
@@ -253,9 +312,9 @@ declare class Logger {
253
312
  * holding the state directly, so the per-request behavior comes from the
254
313
  * provider, not from the class itself.
255
314
  *
256
- * `NodeScope` (in `@flareapp/node`) extends this with two more buckets:
257
- * `request` (HTTP method, path, headers) and `user` (id, email, ...). Browser
258
- * does not need those.
315
+ * `NodeScope` (in `@flareapp/node`) extends this with a `request` bucket
316
+ * (HTTP method, path, headers). User identity is written to `pendingAttributes`
317
+ * by `Flare.setUser`, so it needs no dedicated field. Browser does not need `request`.
259
318
  */
260
319
  declare class Scope {
261
320
  glows: Glow[];
@@ -494,6 +553,14 @@ declare class Flare {
494
553
  clearGlows(): this;
495
554
  addContext(name: string, value: AttributeValue): this;
496
555
  addContextGroup(groupName: string, value: Record<string, AttributeValue>): this;
556
+ /**
557
+ * Attach an identified user to the active scope. Fields are projected to the
558
+ * keys the Flare backend reads: `user.id`, `user.email`, `user.full_name`,
559
+ * and `client.address`. Any extra keys are bundled into `user.attributes`.
560
+ * Pass `null` to clear the user. Scope-aware: in Node this targets the
561
+ * per-request scope via the scope provider.
562
+ */
563
+ setUser(user: User | null): this;
497
564
  setEntryPoint(handler: EntryPointHandler): this;
498
565
  setSdkInfo(info: SdkInfo): this;
499
566
  setFramework(framework: Framework): this;
@@ -541,4 +608,4 @@ declare class NullFileReader implements FileReader {
541
608
  //#region src/stacktrace/createStackTrace.d.ts
542
609
  declare function createStackTrace(error: Error, debug: boolean, fileReader: FileReader): Promise<Array<StackFrame>>;
543
610
  //#endregion
544
- export { type AnyValue, Api, type AttributeValue, type Attributes, type BufferedLog, type Config, type ContextCollector, DEFAULT_URL_DENYLIST, type EntryPointHandler, type FileReader, Flare, type FlushFn, type FlushScheduler, type Framework, GlobalScopeProvider, type Glow, type KeyValue, Logger, type LoggerDeps, type LogsEnvelope, type MessageLevel, NoopFlushScheduler, NullFileReader, type OtelLogRecord, type OverriddenGrouping, type Report, Scope, type ScopeProvider, type SdkInfo, type SpanEvent, type StackFrame, assert, assertKey, convertToError, createStackTrace, extractCode, flatJsonStringify, getCodeSnippet, glowsToEvents, now, readLinesFromFile, redactUrlQuery, resolveDenylist };
611
+ export { type AnyValue, Api, type AttributeValue, type Attributes, type BufferedLog, type Config, type ContextCollector, DEFAULT_URL_DENYLIST, type EntryPointHandler, type FileReader, Flare, type FlushFn, type FlushScheduler, type Framework, GlobalScopeProvider, type Glow, type KeyValue, Logger, type LoggerDeps, type LogsEnvelope, type MessageLevel, NoopFlushScheduler, NullFileReader, type OtelLogRecord, type OverriddenGrouping, type RejectionReporter, type Report, Scope, type ScopeProvider, type SdkInfo, type SpanEvent, type StackFrame, USER_IDENTITY_KEYS, type User, assert, assertKey, convertToError, createStackTrace, describeRejectionReason, extractCode, flatJsonStringify, getCodeSnippet, glowsToEvents, now, readLinesFromFile, redactUrlQuery, resolveDenylist, routeRejection, userIdentityAttributes };
package/dist/index.d.mts CHANGED
@@ -4,6 +4,23 @@ type AttributeValue = string | number | boolean | null | AttributeValue[] | {
4
4
  [key: string]: AttributeValue;
5
5
  };
6
6
  type Attributes = Record<string, AttributeValue>;
7
+ /**
8
+ * An identified user passed to `Flare.setUser`. The four known fields project to the
9
+ * report keys the Flare backend reads: `id`→`user.id`, `email`→`user.email`,
10
+ * `fullName`→`user.full_name`, `ipAddress`→`client.address`. Any OTHER key is bundled
11
+ * into `user.attributes`.
12
+ *
13
+ * Caveat: the open index signature means a misspelled known field (e.g. `fullname` or
14
+ * `full_name` instead of `fullName`) does NOT raise a type error — it silently lands in
15
+ * `user.attributes` rather than the identity key. Spell the four known fields exactly.
16
+ */
17
+ type User = {
18
+ id?: string | number;
19
+ email?: string;
20
+ fullName?: string;
21
+ ipAddress?: string;
22
+ [key: string]: AttributeValue | undefined;
23
+ };
7
24
  type Config = {
8
25
  key: string | null;
9
26
  version: string;
@@ -168,6 +185,34 @@ declare const DEFAULT_URL_DENYLIST: RegExp;
168
185
  declare function resolveDenylist(custom?: RegExp, replaceDefault?: boolean, defaultDenylist?: RegExp): RegExp;
169
186
  declare function redactUrlQuery(fullPath: string, denylist?: RegExp): string;
170
187
  //#endregion
188
+ //#region src/util/rejection.d.ts
189
+ /**
190
+ * Shared unhandled-rejection routing. A rejection "reason" can be anything a
191
+ * promise was rejected with: an Error, an Error-like object carrying a `.stack`,
192
+ * a string, or a plain object. The browser `unhandledrejection` listener
193
+ * (`@flareapp/js`) and the React Native engine rejection tracker
194
+ * (`@flareapp/react-native`) need the SAME routing so a report looks identical
195
+ * across SDKs, so it lives here instead of being copy-pasted (and drifting) per
196
+ * client.
197
+ */
198
+ type RejectionReporter = {
199
+ reportSilently: (error: Error) => void;
200
+ reportUnhandledRejection: (message: string) => unknown;
201
+ };
202
+ /** Best-effort human-readable description of an arbitrary rejection reason. */
203
+ declare function describeRejectionReason(reason: unknown): string;
204
+ /**
205
+ * Route a rejection reason to the reporter: an Error (or any stack-bearing
206
+ * object) goes to `reportSilently` so the STACK survives; only a stackless
207
+ * reason falls back to `reportUnhandledRejection` (string message, empty-stack
208
+ * `UnhandledRejection` class). Any rejection from `reportUnhandledRejection`'s
209
+ * returned promise is swallowed so a transport failure cannot itself surface as
210
+ * an unhandled rejection. `reportSilently` is assumed not to throw synchronously
211
+ * (core's does its work asynchronously); it is intentionally not wrapped, so a
212
+ * synchronous throw there would propagate.
213
+ */
214
+ declare function routeRejection(reporter: RejectionReporter, reason: unknown): void;
215
+ //#endregion
171
216
  //#region src/api/Api.d.ts
172
217
  declare class Api {
173
218
  report(report: Report, url: string, key: string | null, reportBrowserExtensionErrors: boolean, debug?: boolean): Promise<void>;
@@ -236,6 +281,20 @@ declare class Logger {
236
281
  }
237
282
  //#endregion
238
283
  //#region src/Scope.d.ts
284
+ /**
285
+ * The report attribute keys that `Flare.setUser` owns: the four projected identity
286
+ * fields plus the `user.attributes` bag for extras. Single source of truth so the
287
+ * clear pass and the set pass in `setUser` cannot drift, and so consumers that must
288
+ * stamp identity outside core's report pipeline (Electron's forwarded-renderer path)
289
+ * pick up the exact same set instead of re-hardcoding it.
290
+ */
291
+ declare const USER_IDENTITY_KEYS: readonly [...("user.id" | "user.email" | "user.full_name" | "client.address")[], "user.attributes"];
292
+ /**
293
+ * Pick the user-identity attributes currently set on a scope. Used where identity must
294
+ * be copied onto a report that does not flow through `Flare.report()` (which would
295
+ * otherwise spread `pendingAttributes` automatically).
296
+ */
297
+ declare function userIdentityAttributes(scope: Scope): Attributes;
239
298
  /**
240
299
  * Holds the per-call mutable state that used to live on the `Flare` instance:
241
300
  * breadcrumbs (`glows`), custom attributes (`pendingAttributes`), and the
@@ -253,9 +312,9 @@ declare class Logger {
253
312
  * holding the state directly, so the per-request behavior comes from the
254
313
  * provider, not from the class itself.
255
314
  *
256
- * `NodeScope` (in `@flareapp/node`) extends this with two more buckets:
257
- * `request` (HTTP method, path, headers) and `user` (id, email, ...). Browser
258
- * does not need those.
315
+ * `NodeScope` (in `@flareapp/node`) extends this with a `request` bucket
316
+ * (HTTP method, path, headers). User identity is written to `pendingAttributes`
317
+ * by `Flare.setUser`, so it needs no dedicated field. Browser does not need `request`.
259
318
  */
260
319
  declare class Scope {
261
320
  glows: Glow[];
@@ -494,6 +553,14 @@ declare class Flare {
494
553
  clearGlows(): this;
495
554
  addContext(name: string, value: AttributeValue): this;
496
555
  addContextGroup(groupName: string, value: Record<string, AttributeValue>): this;
556
+ /**
557
+ * Attach an identified user to the active scope. Fields are projected to the
558
+ * keys the Flare backend reads: `user.id`, `user.email`, `user.full_name`,
559
+ * and `client.address`. Any extra keys are bundled into `user.attributes`.
560
+ * Pass `null` to clear the user. Scope-aware: in Node this targets the
561
+ * per-request scope via the scope provider.
562
+ */
563
+ setUser(user: User | null): this;
497
564
  setEntryPoint(handler: EntryPointHandler): this;
498
565
  setSdkInfo(info: SdkInfo): this;
499
566
  setFramework(framework: Framework): this;
@@ -541,4 +608,4 @@ declare class NullFileReader implements FileReader {
541
608
  //#region src/stacktrace/createStackTrace.d.ts
542
609
  declare function createStackTrace(error: Error, debug: boolean, fileReader: FileReader): Promise<Array<StackFrame>>;
543
610
  //#endregion
544
- export { type AnyValue, Api, type AttributeValue, type Attributes, type BufferedLog, type Config, type ContextCollector, DEFAULT_URL_DENYLIST, type EntryPointHandler, type FileReader, Flare, type FlushFn, type FlushScheduler, type Framework, GlobalScopeProvider, type Glow, type KeyValue, Logger, type LoggerDeps, type LogsEnvelope, type MessageLevel, NoopFlushScheduler, NullFileReader, type OtelLogRecord, type OverriddenGrouping, type Report, Scope, type ScopeProvider, type SdkInfo, type SpanEvent, type StackFrame, assert, assertKey, convertToError, createStackTrace, extractCode, flatJsonStringify, getCodeSnippet, glowsToEvents, now, readLinesFromFile, redactUrlQuery, resolveDenylist };
611
+ export { type AnyValue, Api, type AttributeValue, type Attributes, type BufferedLog, type Config, type ContextCollector, DEFAULT_URL_DENYLIST, type EntryPointHandler, type FileReader, Flare, type FlushFn, type FlushScheduler, type Framework, GlobalScopeProvider, type Glow, type KeyValue, Logger, type LoggerDeps, type LogsEnvelope, type MessageLevel, NoopFlushScheduler, NullFileReader, type OtelLogRecord, type OverriddenGrouping, type RejectionReporter, type Report, Scope, type ScopeProvider, type SdkInfo, type SpanEvent, type StackFrame, USER_IDENTITY_KEYS, type User, assert, assertKey, convertToError, createStackTrace, describeRejectionReason, extractCode, flatJsonStringify, getCodeSnippet, glowsToEvents, now, readLinesFromFile, redactUrlQuery, resolveDenylist, routeRejection, userIdentityAttributes };
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import ErrorStackParser from "error-stack-parser";
2
2
 
3
3
  //#region src/env/index.ts
4
- const CLIENT_VERSION = typeof process !== "undefined" && true ? "2.5.0" : "?";
4
+ const CLIENT_VERSION = typeof process !== "undefined" && true ? "2.6.0" : "?";
5
5
  const KEY = typeof FLARE_JS_KEY === "undefined" ? "" : FLARE_JS_KEY;
6
6
  const SOURCEMAP_VERSION = typeof FLARE_SOURCEMAP_VERSION === "undefined" ? "" : FLARE_SOURCEMAP_VERSION;
7
7
 
@@ -142,6 +142,49 @@ function safeDecode(value) {
142
142
  }
143
143
  }
144
144
 
145
+ //#endregion
146
+ //#region src/util/rejection.ts
147
+ /** Best-effort human-readable description of an arbitrary rejection reason. */
148
+ function describeRejectionReason(reason) {
149
+ if (typeof reason === "string") return reason;
150
+ if (reason && typeof reason === "object") {
151
+ const message = reason.message;
152
+ if (typeof message === "string" && message) return message;
153
+ try {
154
+ return JSON.stringify(reason);
155
+ } catch {
156
+ return "Unhandled promise rejection (non-serializable reason)";
157
+ }
158
+ }
159
+ return String(reason);
160
+ }
161
+ function hasStack$1(reason) {
162
+ return !!reason && typeof reason === "object" && typeof reason.stack === "string";
163
+ }
164
+ /**
165
+ * Route a rejection reason to the reporter: an Error (or any stack-bearing
166
+ * object) goes to `reportSilently` so the STACK survives; only a stackless
167
+ * reason falls back to `reportUnhandledRejection` (string message, empty-stack
168
+ * `UnhandledRejection` class). Any rejection from `reportUnhandledRejection`'s
169
+ * returned promise is swallowed so a transport failure cannot itself surface as
170
+ * an unhandled rejection. `reportSilently` is assumed not to throw synchronously
171
+ * (core's does its work asynchronously); it is intentionally not wrapped, so a
172
+ * synchronous throw there would propagate.
173
+ */
174
+ function routeRejection(reporter, reason) {
175
+ if (reason instanceof Error) {
176
+ reporter.reportSilently(reason);
177
+ return;
178
+ }
179
+ if (hasStack$1(reason)) {
180
+ const error = new Error(describeRejectionReason(reason));
181
+ error.stack = reason.stack;
182
+ reporter.reportSilently(error);
183
+ return;
184
+ }
185
+ Promise.resolve(reporter.reportUnhandledRejection(describeRejectionReason(reason))).catch(() => {});
186
+ }
187
+
145
188
  //#endregion
146
189
  //#region src/api/Api.ts
147
190
  var Api = class {
@@ -471,6 +514,39 @@ function partitionAttributes(attributes) {
471
514
  //#endregion
472
515
  //#region src/Scope.ts
473
516
  /**
517
+ * Maps each `User` identity field to the flat report attribute key it projects to.
518
+ * `Flare.setUser`'s set pass writes through these so the literal key strings live in
519
+ * exactly one place; `USER_IDENTITY_KEYS` (the clear pass) derives from them, so adding
520
+ * a field here can never silently leave the clear pass out of date.
521
+ */
522
+ const USER_FIELD_KEYS = {
523
+ id: "user.id",
524
+ email: "user.email",
525
+ fullName: "user.full_name",
526
+ ipAddress: "client.address"
527
+ };
528
+ /**
529
+ * The report attribute keys that `Flare.setUser` owns: the four projected identity
530
+ * fields plus the `user.attributes` bag for extras. Single source of truth so the
531
+ * clear pass and the set pass in `setUser` cannot drift, and so consumers that must
532
+ * stamp identity outside core's report pipeline (Electron's forwarded-renderer path)
533
+ * pick up the exact same set instead of re-hardcoding it.
534
+ */
535
+ const USER_IDENTITY_KEYS = [...Object.values(USER_FIELD_KEYS), "user.attributes"];
536
+ /**
537
+ * Pick the user-identity attributes currently set on a scope. Used where identity must
538
+ * be copied onto a report that does not flow through `Flare.report()` (which would
539
+ * otherwise spread `pendingAttributes` automatically).
540
+ */
541
+ function userIdentityAttributes(scope) {
542
+ const attrs = {};
543
+ for (const key of USER_IDENTITY_KEYS) {
544
+ const value = scope.pendingAttributes[key];
545
+ if (value !== void 0) attrs[key] = value;
546
+ }
547
+ return attrs;
548
+ }
549
+ /**
474
550
  * Holds the per-call mutable state that used to live on the `Flare` instance:
475
551
  * breadcrumbs (`glows`), custom attributes (`pendingAttributes`), and the
476
552
  * current entry-point handler.
@@ -487,9 +563,9 @@ function partitionAttributes(attributes) {
487
563
  * holding the state directly, so the per-request behavior comes from the
488
564
  * provider, not from the class itself.
489
565
  *
490
- * `NodeScope` (in `@flareapp/node`) extends this with two more buckets:
491
- * `request` (HTTP method, path, headers) and `user` (id, email, ...). Browser
492
- * does not need those.
566
+ * `NodeScope` (in `@flareapp/node`) extends this with a `request` bucket
567
+ * (HTTP method, path, headers). User identity is written to `pendingAttributes`
568
+ * by `Flare.setUser`, so it needs no dedicated field. Browser does not need `request`.
493
569
  */
494
570
  var Scope = class {
495
571
  glows = [];
@@ -612,18 +688,24 @@ function createStackTrace(error, debug, fileReader) {
612
688
  return resolve([fallbackFrame("stacktrace could not be parsed")]);
613
689
  }
614
690
  Promise.all(parsedFrames.map((frame) => {
615
- return getCodeSnippet(fileReader, frame.fileName, frame.lineNumber, frame.columnNumber).then((snippet) => ({
691
+ const fileName = normalizeFileName(frame.fileName);
692
+ return getCodeSnippet(fileReader, fileName, frame.lineNumber, frame.columnNumber).then((snippet) => ({
616
693
  lineNumber: frame.lineNumber || 1,
617
694
  columnNumber: frame.columnNumber || 1,
618
695
  method: frame.functionName || "Anonymous or unknown function",
619
- file: frame.fileName || "Unknown file",
696
+ file: fileName || "Unknown file",
620
697
  codeSnippet: snippet.codeSnippet,
621
698
  class: "",
622
- isApplicationFrame: isApplicationFrame(frame.fileName)
699
+ isApplicationFrame: isApplicationFrame(fileName)
623
700
  }));
624
701
  })).then(resolve);
625
702
  });
626
703
  }
704
+ const HERMES_ADDRESS_PREFIX = "address at ";
705
+ function normalizeFileName(fileName) {
706
+ if (fileName?.startsWith(HERMES_ADDRESS_PREFIX)) return fileName.slice(11);
707
+ return fileName;
708
+ }
627
709
  function fallbackFrame(reason) {
628
710
  return {
629
711
  lineNumber: 0,
@@ -946,6 +1028,26 @@ var Flare = class {
946
1028
  this.scopeProvider.active().setAttribute(`context.${groupName}`, value);
947
1029
  return this;
948
1030
  }
1031
+ /**
1032
+ * Attach an identified user to the active scope. Fields are projected to the
1033
+ * keys the Flare backend reads: `user.id`, `user.email`, `user.full_name`,
1034
+ * and `client.address`. Any extra keys are bundled into `user.attributes`.
1035
+ * Pass `null` to clear the user. Scope-aware: in Node this targets the
1036
+ * per-request scope via the scope provider.
1037
+ */
1038
+ setUser(user) {
1039
+ const scope = this.scopeProvider.active();
1040
+ for (const key of USER_IDENTITY_KEYS) delete scope.pendingAttributes[key];
1041
+ if (!user) return this;
1042
+ const { id, email, fullName, ipAddress, ...rest } = user;
1043
+ if (id !== void 0 && id !== null) scope.setAttribute(USER_FIELD_KEYS.id, String(id));
1044
+ if (email !== void 0) scope.setAttribute(USER_FIELD_KEYS.email, email);
1045
+ if (fullName !== void 0) scope.setAttribute(USER_FIELD_KEYS.fullName, fullName);
1046
+ if (ipAddress !== void 0) scope.setAttribute(USER_FIELD_KEYS.ipAddress, ipAddress);
1047
+ const extras = Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== void 0));
1048
+ if (Object.keys(extras).length > 0) scope.setAttribute("user.attributes", extras);
1049
+ return this;
1050
+ }
949
1051
  setEntryPoint(handler) {
950
1052
  this.scopeProvider.active().entryPoint = handler;
951
1053
  return this;
@@ -1101,4 +1203,4 @@ var Flare = class {
1101
1203
  };
1102
1204
 
1103
1205
  //#endregion
1104
- export { Api, DEFAULT_URL_DENYLIST, Flare, GlobalScopeProvider, Logger, NoopFlushScheduler, NullFileReader, Scope, assert, assertKey, convertToError, createStackTrace, extractCode, flatJsonStringify, getCodeSnippet, glowsToEvents, now, readLinesFromFile, redactUrlQuery, resolveDenylist };
1206
+ export { Api, DEFAULT_URL_DENYLIST, Flare, GlobalScopeProvider, Logger, NoopFlushScheduler, NullFileReader, Scope, USER_IDENTITY_KEYS, assert, assertKey, convertToError, createStackTrace, describeRejectionReason, extractCode, flatJsonStringify, getCodeSnippet, glowsToEvents, now, readLinesFromFile, redactUrlQuery, resolveDenylist, routeRejection, userIdentityAttributes };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flareapp/core",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "Environment-agnostic core for the Flare JS SDK",
5
5
  "homepage": "https://flareapp.io",
6
6
  "bugs": {