Skip to content

Commit 3cf6400

Browse files
committed
refactor: trim comments to why-only
Removes signature restatements, numbered gate lists that narrate the code below them, and the four copies of the destination-url rationale now documented once on RouteName.url. Framework quirks, ordering invariants and deliberate deviations stay, compressed. Also merges the two stacked JSDoc blocks on createIdentityTagger and re-homes the resolveRejectionEnabler doc that the previous commit's extraction left sitting above the wrong function.
1 parent 04ccd8c commit 3cf6400

34 files changed

Lines changed: 329 additions & 654 deletions

packages/core/src/Flare.ts

Lines changed: 24 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,11 @@ export class Flare {
6969
private framework: Framework | null = null;
7070

7171
/**
72-
* @param contextCollector returns per-report attributes (browser DOM, Node process, etc). No-op by default.
73-
* @param fileReader reads source files for stack-trace snippets. Default returns null (no snippets);
72+
* @param contextCollector per-report attributes (browser DOM, Node process). No-op by default.
73+
* @param fileReader source files for stack-trace snippets. Defaults to no snippets;
7474
* `@flareapp/js` injects a fetch reader, `@flareapp/node` a disk reader.
75-
* @param scopeProvider returns the current `Scope` (glows, pendingAttributes, entryPoint). Browser uses one
76-
* global scope; Node uses an AsyncLocalStorage-backed provider so each request gets its own.
75+
* @param scopeProvider the current `Scope`. Browser uses one global scope; Node an
76+
* AsyncLocalStorage-backed provider so each request gets its own.
7777
*/
7878
constructor(
7979
public api: Api = new Api(),
@@ -107,14 +107,12 @@ export class Flare {
107107
}
108108

109109
/**
110-
* Register an in-flight report so `flush()` can wait for it. Called by every public report entry point, each
111-
* wrapping its full async pipeline (beforeEvaluate -> stack trace -> beforeSubmit -> api.report) so the whole
112-
* roundtrip is tracked, not just the HTTP send.
110+
* Register an in-flight report so `flush()` can wait for it. Every entry point wraps its whole async
111+
* pipeline (beforeEvaluate -> stack trace -> beforeSubmit -> api.report), not just the HTTP send.
113112
*
114-
* Stores a shadow promise that mirrors `p`'s timing but cannot reject (the `() => undefined` rejection handler
115-
* consumes any failure), so an unhandled report rejection never surfaces as a Node warning / console error. The
116-
* shadow self-removes from the Set via `.finally`; `delete` never throws, so wrap any richer cleanup in try/catch.
117-
* Returns the original `p` so the caller still observes real success/failure; tracking is invisible to them.
113+
* What goes in the Set is a shadow promise that mirrors `p`'s timing but cannot reject, so a failed
114+
* report never surfaces as an unhandled rejection warning. `p` itself is returned untouched, so the
115+
* caller still observes real success or failure.
118116
*/
119117
private track<T>(p: Promise<T>): Promise<T> {
120118
const tracked = p.then(
@@ -127,14 +125,12 @@ export class Flare {
127125
}
128126

129127
/**
130-
* Wait until every in-flight report settles or `timeoutMs` elapses, whichever comes first. Always resolves, never
131-
* rejects; no retry. Main consumer is `@flareapp/node`'s fatal handler, which awaits the fatal report explicitly
132-
* then calls flush to drain any OTHER concurrent reports before `process.exit`.
128+
* Wait until every in-flight report settles or `timeoutMs` elapses. Always resolves, never rejects.
129+
* Written for `@flareapp/node`'s fatal handler, which awaits the fatal report itself then flushes to
130+
* drain any other concurrent reports before `process.exit`.
133131
*
134-
* `[...this.inflight]` snapshots the Set: reports started after this line are NOT awaited, which bounds the wait so
135-
* a handler emitting reports during shutdown cannot block the process forever. Call flush again to drain those.
136-
* `allSettled` (not `all`) waits for every report regardless of HTTP success/failure; `all` would short-circuit on
137-
* the first rejection. Flush does not stop new reports; the instance stays usable afterward.
132+
* Snapshotting the Set bounds the wait: reports started after this line are not awaited, so a handler
133+
* that keeps emitting during shutdown cannot block the process forever. Call flush again for those.
138134
*/
139135
flush(timeoutMs = 2000): Promise<void> {
140136
this._logger.flush();
@@ -200,9 +196,8 @@ export class Flare {
200196
this._config.tracesSampleRate = Math.max(0, Math.min(1, config.tracesSampleRate));
201197
}
202198

203-
// Only re-resolve the denylist when this call actually carries denylist config. Otherwise the spread
204-
// above already preserved the previously resolved denylist, and re-resolving with an undefined `custom`
205-
// would clobber a custom denylist back to the default, silently re-exposing data the user asked to redact.
199+
// Only when this call carries denylist config. Re-resolving with an undefined `custom` would reset a
200+
// custom denylist to the default, silently re-exposing data the user asked to redact.
206201
if (config.urlDenylist !== undefined || config.replaceDefaultUrlDenylist !== undefined) {
207202
this._config.urlDenylist = resolveDenylist(
208203
config.urlDenylist,
@@ -278,11 +273,8 @@ export class Flare {
278273
}
279274

280275
/**
281-
* Attach an identified user to the active scope. Fields are projected to the
282-
* keys the Flare backend reads: `user.id`, `user.email`, `user.full_name`,
283-
* and `client.address`. Any extra keys are bundled into `user.attributes`.
284-
* Pass `null` to clear the user. Scope-aware: in Node this targets the
285-
* per-request scope via the scope provider.
276+
* Projects the known fields to the keys the Flare backend reads (see `USER_FIELD_KEYS`) and bundles
277+
* anything else into `user.attributes`. Pass `null` to clear. In Node this targets the per-request scope.
286278
*/
287279
setUser(user: User | null): this {
288280
const scope = this.scopeProvider.active();
@@ -536,16 +528,15 @@ export class Flare {
536528
};
537529
}
538530

531+
/** Local roots only, snapshotted by the Tracer at span START so a long-lived root does not drift into
532+
* the next page's scope. Children get none, and no span ever runs the DOM collector. */
539533
private getScopeAttributes(): Attributes {
540-
// Scope-derived record a LOCAL ROOT span carries: user context, entry-point overrides,
541-
// framework-in-context.custom. Tracer snapshots this at span START so a long-lived root does not drift into the
542-
// next page's scope. Spans never run the DOM collector; children get no scope. Mirrors the PHP client.
543534
return this.assembleAttributes({}, {}, false);
544535
}
545536

546537
private spanResourceAttributes(): Attributes {
547-
// Resource is stable per page (host.name). Keep only the collector's resource partition, dropping record-level
548-
// context (cookies/url) so nothing heavy or drifting reaches spans. Evaluated once per flush, not per span.
538+
// Only the resource partition: record-level context (cookies, url) is heavy and drifts, and this is
539+
// evaluated once per flush rather than per span.
549540
return partitionAttributes(this.contextCollector(this._config)).resource;
550541
}
551542

@@ -562,9 +553,8 @@ export class Flare {
562553
const activeScope = this.scopeProvider.active();
563554
const attributes = this.assembleAttributes(this.contextCollector(this._config), input.extraAttributes, true);
564555

565-
// seenAtUnixNano in real nanoseconds. Date.now() * 1_000_000 exceeds MAX_SAFE_INTEGER by ~3 bits (~256 ns), but
566-
// browser clocks are millisecond-precision so the lost bits are below source resolution. PHP's json_decode
567-
// reads the 19-digit literal as a 64-bit int (PHP_INT_MAX ~9.22e18 vs our ~1.78e18).
556+
// seenAtUnixNano overflows MAX_SAFE_INTEGER by ~3 bits (~256ns), which is below the millisecond
557+
// resolution browser clocks actually have. PHP reads the 19-digit literal as a 64-bit int.
568558
const report: Report = {
569559
exceptionClass: input.exceptionClass,
570560
message: input.message,

packages/core/src/Scope.ts

Lines changed: 12 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,18 @@
11
import type { Attributes, AttributeValue, EntryPointHandler, Glow } from './types';
22

3-
/**
4-
* Maps each `User` identity field to the flat report attribute key it projects to. `Flare.setUser` writes through
5-
* these; `USER_IDENTITY_KEYS` (the clear pass) derives from them, so a new field can never leave the clear pass stale.
6-
*/
3+
/** `USER_IDENTITY_KEYS` derives from this, so adding a field here can never leave the clear pass stale. */
74
export const USER_FIELD_KEYS = {
85
id: 'user.id',
96
email: 'user.email',
107
fullName: 'user.full_name',
118
ipAddress: 'client.address',
129
} as const;
1310

14-
/**
15-
* Attribute keys `Flare.setUser` owns: the four projected identity fields plus the `user.attributes` bag. Single
16-
* source of truth so the set/clear passes cannot drift, and so consumers stamping identity outside core's report
17-
* pipeline (Electron's forwarded-renderer path) reuse the exact same set.
18-
*/
11+
/** Every key `Flare.setUser` owns. Consumers stamping identity outside core's report pipeline (Electron's
12+
* forwarded-renderer path) reuse this exact set. */
1913
export const USER_IDENTITY_KEYS = [...Object.values(USER_FIELD_KEYS), 'user.attributes'] as const;
2014

21-
/**
22-
* Pick the user-identity attributes currently set on a scope. Used where identity must be copied onto a report that
23-
* does not flow through `Flare.report()` (which would otherwise spread `pendingAttributes` automatically).
24-
*/
15+
/** For reports that do not flow through `Flare.report()`, which would spread `pendingAttributes` itself. */
2516
export function userIdentityAttributes(scope: Scope): Attributes {
2617
const attrs: Attributes = {};
2718
for (const key of USER_IDENTITY_KEYS) {
@@ -34,23 +25,16 @@ export function userIdentityAttributes(scope: Scope): Attributes {
3425
}
3526

3627
/**
37-
* Per-call mutable state: breadcrumbs (`glows`), custom attributes (`pendingAttributes`), and the current entry-point
38-
* handler. Split out of `Flare` so the consumer can choose a single global `Scope` (browser, one user at a time) or one
39-
* `Scope` per request via AsyncLocalStorage (Node, concurrent requests must not leak into each other). `Flare` reaches
40-
* it through `scopeProvider.active()`, so per-request behavior lives in the provider.
41-
*
42-
* `NodeScope` (in `@flareapp/node`) extends this with a `request` bucket (method, path, headers). User identity goes to
43-
* `pendingAttributes` via `Flare.setUser`, so it needs no dedicated field.
28+
* Per-call mutable state, split out of `Flare` so the consumer can choose one global `Scope` (browser, one
29+
* user at a time) or one per request via AsyncLocalStorage (Node, where concurrent requests must not leak
30+
* into each other). `@flareapp/node`'s `NodeScope` extends this with a `request` bucket.
4431
*/
4532
export class Scope {
4633
glows: Glow[] = [];
4734
pendingAttributes: Attributes = {};
4835
entryPoint: EntryPointHandler | null = null;
4936

50-
/**
51-
* Append a breadcrumb, capping the list at `maxGlowsPerReport` by dropping the oldest entries. Keeps the payload
52-
* bounded while preserving the most recent events leading up to an error.
53-
*/
37+
/** Caps at `maxGlowsPerReport` by dropping the oldest, so the payload stays bounded. */
5438
addGlow(glow: Glow, maxGlowsPerReport: number): void {
5539
this.glows.push(glow);
5640
if (this.glows.length > maxGlowsPerReport) {
@@ -62,34 +46,26 @@ export class Scope {
6246
this.glows = [];
6347
}
6448

65-
/** Set a single attribute (`Flare.addContext` / `addContextGroup`). Last write wins. */
6649
setAttribute(key: string, value: AttributeValue): void {
6750
this.pendingAttributes[key] = value;
6851
}
6952

70-
/**
71-
* Shallow-merge attributes into this scope. Used by Node's provider when patching live request context via
72-
* `flare.mergeContext({ ... })`. Last write wins per key; nested objects are not deep-merged.
73-
*/
53+
/** Shallow: last write wins per key, nested objects are not deep-merged. */
7454
mergeAttributes(partial: Attributes): void {
7555
Object.assign(this.pendingAttributes, partial);
7656
}
7757
}
7858

7959
/**
8060
* The seam through which `Flare` reaches its current `Scope`; implementations decide what "current" means.
81-
* `GlobalScopeProvider` always returns the same instance (browser); `@flareapp/node`'s provider returns the
82-
* per-request `NodeScope` from `node:async_hooks`, falling back to a shared scope outside any `runWithContext(...)`.
83-
* Consumers may supply their own.
61+
* `@flareapp/node`'s returns the per-request `NodeScope` from `node:async_hooks`, falling back to a shared
62+
* scope outside any `runWithContext(...)`.
8463
*/
8564
export interface ScopeProvider {
8665
active(): Scope;
8766
}
8867

89-
/**
90-
* One `Scope` for the provider's lifetime, shared by every caller. Right default for single-context environments
91-
* (browser tab, CLI script) and the fallback `Flare`'s constructor uses when no provider is supplied.
92-
*/
68+
/** One `Scope` for the provider's lifetime. The right default for a browser tab or a CLI script. */
9369
export class GlobalScopeProvider implements ScopeProvider {
9470
private scope = new Scope();
9571
active(): Scope {

packages/core/src/util/createIdentityTagger.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@ export interface SdkTaggable {
88
}
99

1010
/**
11-
* Builds a per-package SDK/framework identity tagger. Holds its own WeakSet guards so each Flare
12-
* instance (singleton or injected renderer) is tagged at most once, on each of the two axes.
13-
*/
14-
/**
15-
* `frameworkName` is typed as `FrameworkName`, not `string`: this is the wire vocabulary the backend
16-
* keys off, so a first-party package cannot invent a value here. A host app that genuinely needs its
17-
* own name calls `setFramework` directly.
11+
* A per-package SDK/framework identity tagger. Holds its own WeakSet guards, so each Flare instance
12+
* (singleton or injected renderer) is tagged at most once on each of the two axes.
13+
*
14+
* `frameworkName` is `FrameworkName` rather than `string` because that is the wire vocabulary the backend
15+
* keys off, so a first-party package cannot invent a value. A host app that needs its own name calls
16+
* `setFramework` directly.
1817
*/
1918
export function createIdentityTagger(config: { sdkName: string; sdkVersion: string; frameworkName: FrameworkName }): {
2019
registerSdkIdentity(flare: SdkTaggable): void;

packages/core/src/util/rejection.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
1-
/**
2-
* Shared unhandled-rejection routing. A rejection reason can be anything a promise rejected with: an Error, a
3-
* stack-bearing object, a string, or a plain object. The browser `unhandledrejection` listener (`@flareapp/js`) and the
4-
* React Native rejection tracker (`@flareapp/react-native`) share this routing so reports look identical across SDKs.
5-
*/
1+
// Shared by the browser `unhandledrejection` listener and the React Native rejection tracker, so a
2+
// rejection reports identically across SDKs whatever it was rejected with.
63

74
export type RejectionReporter = {
8-
// Error / stack-bearing reasons: preserve the stack.
5+
/** Error / stack-bearing reasons: preserve the stack. */
96
reportSilently: (error: Error) => void;
10-
// Stackless reasons: empty-stack `UnhandledRejection` shaping. May return a promise (core's does);
11-
// `routeRejection` swallows any rejection from it.
7+
/** Stackless reasons. May return a promise; `routeRejection` swallows any rejection from it. */
128
reportUnhandledRejection: (message: string) => unknown;
139
};
1410

@@ -19,7 +15,7 @@ export function describeRejectionReason(reason: unknown): string {
1915
}
2016
if (reason && typeof reason === 'object') {
2117
const message = (reason as { message?: unknown }).message;
22-
// An empty `.message` carries no signal; fall through to JSON.stringify so the report shows the object's shape.
18+
// An empty `.message` carries no signal, so fall through and let JSON.stringify show the shape.
2319
if (typeof message === 'string' && message) {
2420
return message;
2521
}
@@ -37,10 +33,8 @@ function hasStack(reason: unknown): reason is { stack: string } {
3733
}
3834

3935
/**
40-
* Route a rejection reason: an Error (or any stack-bearing object) goes to `reportSilently` so the stack survives; a
41-
* stackless reason falls back to `reportUnhandledRejection` (string message, empty-stack `UnhandledRejection`). Any
42-
* rejection from `reportUnhandledRejection`'s promise is swallowed so a transport failure cannot itself surface as an
43-
* unhandled rejection. `reportSilently` is assumed async and not wrapped, so a synchronous throw there would propagate.
36+
* The `.catch` is what stops a transport failure from surfacing as a second unhandled rejection.
37+
* `reportSilently` is assumed async and left unwrapped, so a synchronous throw there still propagates.
4438
*/
4539
export function routeRejection(reporter: RejectionReporter, reason: unknown): void {
4640
if (reason instanceof Error) {

0 commit comments

Comments
 (0)