Skip to content

Runtime architecture ​

Flare guarantees how a report is constructed, isolated, sanitized, routed and handed to a destination. It claims nothing about what a provider does afterwards unless the destination gives evidence for that exact boundary. This page describes the parts that keep that guarantee and names the test that guards each rule.

Owners ​

Every piece of state has one owner, and only the first two rows below are exported.

OwnerResponsibility
FlareThe facade. Holds configuration, runs the report pipeline, routes, and owns the receipts.
createMockAdapter, testReporterAdapterThe deliberately badly behaved test double, and the contract every adapter must keep.
SessionStateThe identity generation, session tags, session contexts and the breadcrumb ring.
DestinationRuntimeOne destination: its session, startup buffer, the deadline of each submission, flush and disposal.
DedupeIndex, RateWindowWhat each destination has already been sent, and the per-minute storm limit.
DiagnosticsThe passive snapshot and event stream.

Report intake is separate from lifecycle orchestration: prepareReportLayer composes metadata preparation and prepareReportPayload normalizes an exception or message. Their behavior is exercised through the public capture and privacy tests. Leaf operations such as normalizeException, sanitizeValue, validateDeclared, composeReport, fitReport and copySubmissionOutcome have focused tests. Numeric options are resolved once, at construction, and a value that cannot work throws there.

Exception text and metadata strings share sanitizeString, which scrubs before truncating and records losses in the caller's list. Exception normalization collects those losses directly instead of merging intermediate result wrappers. Array sanitization snapshots descriptors only for the retained prefix, so its work is bounded by the breadth limit even when the input array is large. Capture metadata options and error-like name/message fields are read once before their values are validated and used.

Object sanitization collects enumerable string-key descriptors until it can determine the retained prefix and truncation. It avoids copying all descriptors, while key enumeration still depends on input size. Retained descriptors are snapshotted before application scrubbers run.

Normalized exceptions, their members and mapping losses are frozen before routing and fan-out. Aggregate members are read into a core-owned array, so application array methods cannot bypass normalization. Receipt completion derives from its frozen outcome snapshot. An adapter's answer is copied before it is published: only the fields its status allows, with frozen copies of its event reference and mapping losses. An answer the contract does not allow becomes a failure with INVALID_ANSWER. fitReport measures each frozen breadcrumb and context once in its life, so the size check costs the same however many reports share them.

Report, receipt, session and lifecycle status types expose their frozen fields as readonly. Runtime and destination statuses are frozen before observers can read them. Diagnostic snapshots and events are frozen too, so observation cannot change submission policy, another observer's counts or what the next listener receives. Adapters copy fields into provider-owned objects when they need to edit them. Ambient integrations share a frozen snapshot, replaced only when metadata changes. Breadcrumb-only updates reuse it. Repeating the same user traits or tag value, or removing absent metadata, preserves the session snapshot and sends no ambient update; identity comparison still uses the private, unredacted id.

The path of a report ​

  1. Refusals that need no work: disposed, reentrant, stale scope, rate limited. A stale report spends none of the rate budget.
  2. normalizeException turns the thrown value into bounded plain data. It reads only name, message, stack, cause and errors, each behind a guard.
  3. The capture options are validated against the schema and sanitized. An invalid piece is dropped and recorded as a loss; the report continues.
  4. composeReport merges application defaults, the session, the scope and the options, in that order.
  5. fitReport sheds the oldest breadcrumbs, then the newest contexts, until the report fits.
  6. Routing selects destinations, reusing the runtimes resolved at construction for a defaults.to list. A failure here drops the report rather than widening its audience.
  7. Per destination: dedupe, then the startup buffer or an immediate submission under a deadline.
  8. Each outcome lands on the receipt. The first answer of a destination stands.

Session data takes the same validate and sanitize steps when it is set, not when it is reported. That is what keeps unsanitized data out of the breadcrumb ring, the startup buffer, diagnostics and ambient integrations.

A report snapshots its session before preparation calls application validators or scrubbers. Those callbacks cannot relabel it by switching accounts. A session mutation prepared across an account switch is discarded. Ambient updates stop when a provider callback replaces their snapshot, and breadcrumbs stop when their account changes, so later providers cannot receive an older update over the newer session.

Principles ​

  1. The thrown value never leaves the core. An adapter receives frozen, sanitized plain data and nothing else.
  2. Privacy outranks delivery. A redactor or scrubber that throws costs the data it was given; a defaults.to that throws costs the report.
  3. Delivery outranks metadata. A tag, context or breadcrumb that fails its schema costs only itself.
  4. Flare is a best-effort feature of its host. Reporting and session calls never throw into the application. Two programming mistakes do throw: a misconfigured constructor, and asking destination() for a name that was never configured.
  5. Every change replaces a frozen snapshot. A report composed earlier cannot be reached by a later change.
  6. A scope belongs to the identity it was created under and never adopts a later one.
  7. Every entry a destination accepts is settled exactly once, whatever the adapter does, and through one place, so every outcome is announced to diagnostics whichever path it took.
  8. The runtime owns each session's lifecycle. It opens a destination once, disposes it once, and calls nothing on it afterwards, so an adapter needs no guards of its own.
  9. A timeout is indeterminate, never failed: the provider may still have sent the report.
  10. Observation is passive. Reading a status, a native handle or diagnostics opens nothing and creates no report.
  11. The core never branches on an adapter name. Adding a provider is one new package.

Adapter layout ​

Each package has one entry point. Where the provider's browser and React Native SDKs share an API, as Sentry's and Bugsnag's do, one package and one factory take either: the report mapping is the same on both, and what differs is the SDK the application passes in, whose queue, flush and native layer belong to the SDK, not to the adapter. Where the two APIs differ, as PostHog's and Datadog's do, React Native has its own -react-native package, whose factory has the same name as the browser one. Crashlytics is React Native only.

The packages import no provider SDK at runtime, so a platform package does not reduce bundled SDK code. It exists because the platform's SDK has its own API and its own peer dependency.

Invariants and their tests ​

InvariantTest
Construction is cold and safe on a serverFlare.test.ts: "constructing a Flare is cold: nothing opens, no timer starts, no global is touched"; index.test.ts: "importing and constructing on a server is inert: no timer, no listener, nothing opened"
A repeated start changes nothing and tells nobodyFlare.test.ts: "start opens every destination once, however often it is called"; "starting again tells no status observer anything, because nothing changed"
A report captured before start is delivered laterFlare.test.ts: "a report captured before start is delivered once the destination is ready"
A failed start can be retried without losing the bufferDestinationRuntime.test.ts: "a start that throws leaves the destination failed and keeps its buffer for a retry"
A session opened across disposal has no ownerDestinationRuntime.test.ts: "a session opened while its destination was disposed from inside open is released at once and never used"
Disposal settles what is in flightFlare.test.ts: "disposal during a submission settles its receipt as indeterminate"
A disposed session is released once and never called againFlare.test.ts: "disposal is idempotent, releases every session, and later captures are dropped"; DestinationRuntime.test.ts: "disposing from the ready notification releases the session before any buffered report is submitted"
A late answer is ignoredDestinationRuntime.test.ts: "a hanging provider is cut off at the deadline as indeterminate, and its late answer is ignored"; createReceipt.test.ts: "a late answer is ignored even while another destination is still pending"
Concurrent reports cannot share event-local contextisolation.test.ts: "concurrent reports never see each other's event-local context"
A pending report keeps the identity it was captured underisolation.test.ts: "a report keeps the context it was captured with, whatever the session does while it is pending"
A breadcrumb never crosses an account boundaryisolation.test.ts: "a breadcrumb that occurred before the current identity began is not kept"; "a breadcrumb recorded now is kept even when the wall clock steps backwards"
A stale scope adopts nobodyisolation.test.ts: "a scope created before an account switch is stale and adopts nobody"; "logging out makes an old asynchronous scope stale too"
No account can see another's dataisolation.test.ts: "aggressively interleaved users, scopes and captures never leak across accounts"
Identity follows the full id even when its report value is redacted or boundedSessionState.test.ts: "identity follows the real id even when the visible user is redacted"; isolation.test.ts: "accounts whose reported ids share a truncated prefix still have separate sessions"
Privacy runs before retention and fan-outprivacy.test.ts: "redaction runs before the startup buffer, the mock, diagnostics and fan-out ever see the data"
A failing redactor or scrubber fails closedprivacy.test.ts: "a scrubber that throws fails closed: the report is dropped, not sent unscrubbed"; "a predicate that throws drops the report rather than send it unredacted"
Text is scrubbed, never redactedprivacy.test.ts: "message and operation text is scrubbed, never redacted"; "a breadcrumb name is scrubbed and bounded like any other text"
A secret is scrubbed before it can be cut in halfsanitizeValue.test.ts: "a secret that straddles the cut is scrubbed whole before the string is cut"
Redaction asks about each key with its pathsanitizeValue.test.ts: "a key the predicate names is redacted at any depth"; "the predicate gets each key's dotted path, so it can name one path only"
Sanitization skips accessors and serialization hooks, and contains throwing proxy trapssanitizeValue.test.ts: "getters and toJSON are never run"; "array accessors and custom slice methods are never run"; normalizeException.test.ts: "a proxy whose every trap throws is still reported"
Prototype-named data remains own fieldsFlare.test.ts: "prototype-named destinations retain their receipts and flush results"; "prototype-named tags and contexts remain own report fields"
A schema failure costs only the invalid pieceprivacy.test.ts: "a schema failure costs only the invalid piece and is recorded on the report"
Routing fails closedrouting.test.ts: "a defaults.to that throws fails closed: the report goes nowhere"; "a defaults.to function or a to that names an unknown destination fails closed too"
A report's to replaces defaults.torouting.test.ts: "a report's own to replaces defaults.to and never merges with it"
One destination never blocks or repeats anotherrouting.test.ts: "one destination failing never blocks or repeats another"
Dedupe is per destination and per identityguards.test.ts: "an explicit dedupe key holds per destination and per identity"
The same Error is reported again laterguards.test.ts: "the same Error captured twice in quick succession is sent once, and again later"
A feedback loop cannot startguards.test.ts: "a capture made from inside an adapter's submit is refused, so a feedback loop cannot start"
An error storm is boundedguards.test.ts: "an error storm is cut off per minute, announced once, and let through again afterwards"; containment.test.ts: "reports dropped as stale spend none of the rate budget"
An option that cannot work is refused at constructionoptions.test.ts: "$name is refused when it is not a usable number"; "a nested option left undefined keeps its default"
An adapter's answer outside its contract is a failure, never a throwcontainment.test.ts: "a malformed session is a failed start, and the next destination still opens"; "an adapter answer with an unknown status is a failed outcome, never a throw"; "a flush answer that is not one the contract allows is a failed flush"
An answer is published with its own fields onlycontainment.test.ts: "an answer is published with its own fields only"
An unknown level is a losscontainment.test.ts: "a level that is not one of the four is a loss, and the report keeps its default"
Flush is a barrier, and a timeout proves nothingflush.test.ts: "flush is a barrier: captures made after the call do not extend it"; "a flush that times out says so, and neither cancels nor disproves the submission"
Event-local metadata never reaches provider globalsambient.test.ts: "event-local metadata never reaches the ambient integration"
Every outcome is announced, whichever path it tookdiagnostics.test.ts: "every outcome a destination gives is announced, whichever path it took"; "what happens to a buffered report is announced too: held, overflowed, expired, submitted and disposed"
Observation is passive and payload freediagnostics.test.ts: "observing diagnostics is passive: it opens nothing and creates no report"; "the snapshot holds counts and statuses, never report content"; containment.test.ts: "a diagnostic event cannot be changed by the listener that receives it"
A published outcome cannot changecopySubmissionOutcome.test.ts: "the event and the losses are copied, so the adapter cannot change a published outcome"
The public surface cannot drift silentlyindex.test.ts; types/schema.contracts.ts; utils/Flare.contracts.ts

Each of these tests has been watched failing against a deliberately broken implementation.

Released under the MIT License.