@fixback/sdk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,6 +62,8 @@ global:
62
62
  | `reporterId` | `string` | — | The handle returned when a reporter redeems an invite. |
63
63
  | `anonymousId` | `string` | a persisted per-browser id | A stable first-party id for an anonymous reporter. |
64
64
  | `target` | `HTMLElement` | `document.body` | Where to mount the launcher. |
65
+ | `reduceMotion` | `boolean` | `false` | Still the launcher's pulse and motion (see [The launcher](#the-launcher)). |
66
+ | `autoCapture` | `boolean` | `true` | Automatic error capture — file uncaught errors with no prompt (see [Automatic error capture](#automatic-error-capture)). Set `false` to turn it off. |
65
67
 
66
68
  None of the identity fields is a trust tier — Fixback derives trust on the
67
69
  server and never honours a self-declared tier.
@@ -72,6 +74,24 @@ const fixback = await init({ key: "pk_live_..." });
72
74
  fixback.destroy();
73
75
  ```
74
76
 
77
+ ## The launcher
78
+
79
+ The launcher is a bottom-right **Feedback** pill, and it stays out of the way:
80
+
81
+ - **Hover-peek & tuck-away** — the pill's caret tucks it off-screen behind a
82
+ small edge nub. Hovering the bottom-right corner (or the nub) peeks it back;
83
+ clicking the nub — or pressing Enter/Space on it — brings it fully back, which
84
+ also covers pointers that can't hover (touch, keyboard). A brief hint appears
85
+ the first time it's tucked, pointing at the corner.
86
+ - **First-visit welcome** — a one-time toast greets a new visitor, drawing the
87
+ eye with a gentle pulse. It shows once per publishable key per browser.
88
+ - **Reduce motion** — pass `reduceMotion: true` to still the pulse and the
89
+ slide/fade transitions. The launcher also honours the visitor's OS-level
90
+ `prefers-reduced-motion: reduce` on its own, with no configuration.
91
+
92
+ All of this chrome lives inside the launcher's Shadow DOM, so it never adds a
93
+ global style or touches the host page's markup.
94
+
75
95
  ## The launch event
76
96
 
77
97
  Activating the launcher opens the SDK's own report overlay. It also dispatches a
@@ -86,6 +106,57 @@ document.addEventListener(LAUNCH_EVENT, () => {
86
106
  });
87
107
  ```
88
108
 
109
+ ## Automatic error capture
110
+
111
+ The SDK's signature capability: **errors report themselves, with no prompt.** Two
112
+ capture-phase global handlers (`error` + `unhandledrejection`) turn uncaught
113
+ exceptions and unhandled promise rejections into `source: auto`, `Kind = bug`
114
+ Feedback for the current session's reporter — carrying the same masked screenshot
115
+ and trace buffer a manual report does, plus a per-session fingerprint. It is
116
+ **on by default across every Gate**; pass `autoCapture: false` to turn it off.
117
+
118
+ - **Deduped & rate-limited.** The same error reported many times collapses to one
119
+ Feedback with a rising occurrence count. A token-bucket burst limiter and a
120
+ per-session cap keep a runaway error loop from flooding the queue; the excess is
121
+ dropped and kept only as a local count. On a `429` + `Retry-After` from ingest,
122
+ auto-reporting backs off until the window clears.
123
+ - **Gated like a manual report.** Auto-capture only runs where a submission would
124
+ be accepted (boot's `canSubmit`), so a public visitor's crash is tracked at their
125
+ server-derived tier — never auto-shipped. `console.error` is **not** promoted to a
126
+ report; it stays breadcrumb-only Evidence.
127
+ - **Private-by-default.** Every auto-report passes through the same client-side
128
+ `beforeSend` scrub choke point as a manual one — input values and request/response
129
+ bodies never leave the browser, and your hook can drop an auto-report entirely.
130
+
131
+ ## Invite redemption
132
+
133
+ An invited tester needs no account and no setup. When a page loads with an invite
134
+ token in its URL (`?fixback_invite=<token>`), `init` reads the invite's status and
135
+ renders the SDK's own **onboarding modal** — the site you were invited to, the
136
+ **access tier** the redemption grants (server-derived, never guessed on the
137
+ client), a private-by-default note, and a "Continue as" name / email:
138
+
139
+ ```ts
140
+ init({ key: "pk_live_..." }); // auto-detects ?fixback_invite= on the page
141
+ ```
142
+
143
+ On confirm the SDK redeems the invite, persists the returned `reporterId` in
144
+ `localStorage` (scoped to your publishable key), strips the token from the URL
145
+ (one-time consumption), and mounts the launcher. On later visits `init` presents
146
+ that stored `reporterId`, so a returning tester is recognised at their correct
147
+ tier without re-onboarding — and on an **Invited** or **Internal** Gate the
148
+ launcher appears only after redemption, while an **Open** Gate shows it to anyone.
149
+
150
+ The name / email are **self-provided display fields** — never a trust signal; the
151
+ tier is always the server's to derive. If the token reaches the page some way
152
+ other than the URL, redeem it explicitly:
153
+
154
+ ```ts
155
+ import { redeem } from "@fixback/sdk";
156
+
157
+ redeem({ key: "pk_live_...", token: "the-invite-token" });
158
+ ```
159
+
89
160
  ## How the boot gate works
90
161
 
91
162
  On `init`, the SDK `POST`s to `${apiUrl}/api/ingest/boot` with your key (the
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The Annotation & vector-marks model (spec 0003 §B/§D).
3
+ *
4
+ * An Annotation is everything a Reporter marked on the page, as three
5
+ * **composable, optional** layers over one full masked screenshot: the picked
6
+ * `element` (from the element-picker), a drag-captured `region`, and vector
7
+ * `marks` — arrow / box / pen / text. Marks live in the **screenshot's coordinate
8
+ * space** (the full viewport the screenshot is rasterised at), never baked into
9
+ * the PNG: the dashboard composites them at view time (#91).
10
+ *
11
+ * This module is the marks' single home — the pure types and geometry used by the
12
+ * region-capture and draw controllers and by the overlay's Send assembly. Like the
13
+ * rest of the SDK's wire types it is vendored (no `@fixback/shared` import); keep
14
+ * `Annotation` in lock-step with the server's ingest contract (spec §D/§H).
15
+ */
16
+ import type { SelectedElement } from "./report";
17
+ /** A point in screenshot (viewport) coordinate space. */
18
+ export interface Point {
19
+ readonly x: number;
20
+ readonly y: number;
21
+ }
22
+ /** A rectangle in screenshot coordinates — the same shape as an element's rect. */
23
+ export interface Rect {
24
+ readonly x: number;
25
+ readonly y: number;
26
+ readonly width: number;
27
+ readonly height: number;
28
+ }
29
+ /** The draw tools the overlay offers, in toolbar order (spec §B, prototype). */
30
+ export type DrawTool = "arrow" | "box" | "pen" | "text";
31
+ /** Fields shared by every vector mark. */
32
+ interface MarkBase {
33
+ /** Stroke/fill colour, as a CSS colour string. */
34
+ readonly color: string;
35
+ }
36
+ /** A directional arrow from `(x0,y0)` to its tip at `(x1,y1)`. */
37
+ export interface ArrowMark extends MarkBase {
38
+ readonly type: "arrow";
39
+ readonly x0: number;
40
+ readonly y0: number;
41
+ readonly x1: number;
42
+ readonly y1: number;
43
+ }
44
+ /** A rectangle spanning the drag from `(x0,y0)` to `(x1,y1)`. */
45
+ export interface BoxMark extends MarkBase {
46
+ readonly type: "box";
47
+ readonly x0: number;
48
+ readonly y0: number;
49
+ readonly x1: number;
50
+ readonly y1: number;
51
+ }
52
+ /** A freehand polyline through `points` (in order). */
53
+ export interface PenMark extends MarkBase {
54
+ readonly type: "pen";
55
+ readonly points: ReadonlyArray<Point>;
56
+ }
57
+ /** A text label anchored at `(x,y)` (its baseline-left, as SVG text). */
58
+ export interface TextMark extends MarkBase {
59
+ readonly type: "text";
60
+ readonly x: number;
61
+ readonly y: number;
62
+ readonly text: string;
63
+ }
64
+ /** A single vector mark, in screenshot coordinates. */
65
+ export type Mark = ArrowMark | BoxMark | PenMark | TextMark;
66
+ /**
67
+ * The structured Annotation carried on a report's content (spec §D): the three
68
+ * optional layers. Every field is optional — a report may carry any subset or
69
+ * none (a bare Kind + comment is a valid Send).
70
+ */
71
+ export interface Annotation {
72
+ readonly element?: SelectedElement;
73
+ readonly region?: Rect;
74
+ readonly marks?: ReadonlyArray<Mark>;
75
+ }
76
+ /**
77
+ * The default mark colour — Signal's danger red, matching the frozen Reporter
78
+ * prototype's draw layer (`docs/design/Fixback Reporter.dc.html`).
79
+ */
80
+ export declare const MARK_COLOR = "#e5484d";
81
+ /**
82
+ * Normalise a drag from a start to an end point into a positive-extent rect
83
+ * (top-left origin, non-negative width/height), rounded to whole pixels — the
84
+ * screenshot is a pixel raster, so sub-pixel extents carry no meaning. Shared by
85
+ * region-capture and the box mark.
86
+ */
87
+ export declare function normalizeRect(x0: number, y0: number, x1: number, y1: number): Rect;
88
+ /**
89
+ * The three points of an arrow's head, given its line `(x0,y0)→(x1,y1)` and a head
90
+ * length: the tip, then the two barbs splayed ±30° behind it. Pure geometry the
91
+ * draw surface renders as a filled triangle at the arrow's tip.
92
+ */
93
+ export declare function arrowHeadPoints(x0: number, y0: number, x1: number, y1: number, size?: number): [Point, Point, Point];
94
+ /**
95
+ * Assemble the structured Annotation from whatever a Reporter marked, dropping
96
+ * empty layers: no element, no region, and an empty marks list are omitted, so the
97
+ * result is `undefined` when nothing was marked. Marks are copied into a plain,
98
+ * independent array so the annotation is a serialisable snapshot decoupled from the
99
+ * live draw state.
100
+ */
101
+ export declare function assembleAnnotation(parts: {
102
+ readonly element?: SelectedElement | null;
103
+ readonly region?: Rect | null;
104
+ readonly marks?: ReadonlyArray<Mark> | null;
105
+ }): Annotation | undefined;
106
+ export {};
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Client-side backpressure for **automatic** error reports (spec 0003 §E/§H,
3
+ * ticket #89). When ingest sheds `source: auto` load it answers `429` with a
4
+ * `Retry-After`; the SDK honours it by holding a pause window during which further
5
+ * `source: auto` reports are dropped without touching the network. **Manual**
6
+ * reports — a human clicking Send — never consult this gate.
7
+ *
8
+ * This is the transport's counterpart to the server's per-Project token bucket
9
+ * (`apps/api/src/ingest/auto-report-rate-limiter.ts`): one shared window per page,
10
+ * so a 429 from one auto-report shed applies to the auto-reports that follow it.
11
+ * The clock is injectable so the window is unit-tested deterministically, never on
12
+ * wall time — mirroring the server limiter's `Clock`.
13
+ */
14
+ /** A source of the current time in epoch milliseconds — injectable for tests. */
15
+ export type Clock = () => number;
16
+ /** The hold window applied when a `429` carries no usable `Retry-After` (spec §E). */
17
+ export declare const DEFAULT_RETRY_AFTER_SECONDS = 60;
18
+ /**
19
+ * Parse a `Retry-After` header into whole seconds to hold for. Handles both HTTP
20
+ * forms — a delta-seconds integer and an HTTP-date (measured from `now`, rounded up
21
+ * and clamped at zero) — and falls back to {@link DEFAULT_RETRY_AFTER_SECONDS} when
22
+ * the header is absent, blank, or unparseable. Ingest sends the delta-seconds form;
23
+ * the date form is handled for spec-completeness.
24
+ */
25
+ export declare function parseRetryAfter(header: string | null | undefined, now: number): number;
26
+ /**
27
+ * A single pause window for `source: auto` reports. `hold` opens (or extends) it
28
+ * from a `429`'s `Retry-After`; `isPaused` reports whether it is still open. The
29
+ * default instance in `submit.ts` is shared across a page's reports so the hold
30
+ * persists across successive auto submissions.
31
+ */
32
+ export declare class AutoReportBackoff {
33
+ private readonly now;
34
+ /** Epoch ms until which `source: auto` reports are held; `0` when clear. */
35
+ private pausedUntil;
36
+ constructor(now?: Clock);
37
+ /** Is the `source: auto` pause window currently open? */
38
+ isPaused(): boolean;
39
+ /** Whole seconds remaining in the pause window (`0` when clear). */
40
+ retryAfterSeconds(): number;
41
+ /**
42
+ * Open (or extend) the window from a `429`'s `Retry-After` value, returning the
43
+ * seconds it will hold for. The window only ever grows — a shorter later hold
44
+ * never clips a longer one already in effect.
45
+ */
46
+ hold(retryAfterHeader: string | null | undefined): number;
47
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The thin trace **breadcrumb ring buffer** and its capture instrumentation
3
+ * (spec 0003 §C; research `sentry-error-capture-findings.md` §7.5).
4
+ *
5
+ * A fixed-size FIFO buffer of the most recent activity — console, navigation,
6
+ * network metadata, masked user actions, and the failing error — that rides on a
7
+ * report as Evidence. It is trimmed exactly like Sentry (`crumbs.slice(-N)`),
8
+ * defaults to a deliberately thin `N ≈ 30` because it ships on **every** payload,
9
+ * and takes an optional age cap. A `beforeBreadcrumb(crumb) => crumb | null`
10
+ * filter lets a Project mute or edit crumbs before they enter the buffer.
11
+ *
12
+ * Everything private is kept out **at the source**: `ui.input` records that an
13
+ * input changed, never its value; `fetch`/`xhr` crumbs carry method + URL +
14
+ * status only, **never** bodies; URLs are scrubbed as the crumb is built. The
15
+ * `beforeSend` choke point (`scrub.ts`) is the final gate over the whole report.
16
+ *
17
+ * The SDK stays dependency-free and must never throw into the host page, so every
18
+ * instrumentation hook is wrapped: a capture failure is swallowed and the original
19
+ * behaviour (the real `console`, `fetch`, navigation) always runs.
20
+ */
21
+ /** Console-style severity a `console` crumb records. */
22
+ export type BreadcrumbLevel = "log" | "info" | "warn" | "error" | "assert" | "debug";
23
+ /** The kind of activity a crumb records. */
24
+ export type BreadcrumbCategory = "console" | "navigation" | "fetch" | "xhr" | "ui.click" | "ui.input" | "error";
25
+ /**
26
+ * A crumb's structured detail. Deliberately narrow: there is **no** field for a
27
+ * request/response body or an input value, so those can never be recorded.
28
+ */
29
+ export interface BreadcrumbData {
30
+ readonly url?: string;
31
+ readonly method?: string;
32
+ readonly status?: number;
33
+ /** A masked CSS selector for a `ui.*` target — never its text or value. */
34
+ readonly target?: string;
35
+ readonly from?: string;
36
+ readonly to?: string;
37
+ readonly errorType?: string;
38
+ }
39
+ /** One entry in the trace buffer. */
40
+ export interface Breadcrumb {
41
+ readonly category: BreadcrumbCategory;
42
+ readonly message?: string;
43
+ readonly level?: BreadcrumbLevel;
44
+ /** Epoch milliseconds when the crumb was recorded. */
45
+ readonly timestamp: number;
46
+ readonly data?: BreadcrumbData;
47
+ }
48
+ /** Default ring size — thinner than Sentry's 100 (it rides on every payload). */
49
+ export declare const DEFAULT_MAX_BREADCRUMBS = 30;
50
+ /** Console levels captured by default (warn/error/assert first, per research). */
51
+ export declare const DEFAULT_CONSOLE_LEVELS: readonly BreadcrumbLevel[];
52
+ /** Filters or edits each crumb before it enters the buffer; `null` drops it. */
53
+ export type BeforeBreadcrumb = (crumb: Breadcrumb) => Breadcrumb | null;
54
+ /** Configuration for {@link createBreadcrumbBuffer}. All values are optional. */
55
+ export interface BreadcrumbBufferConfig {
56
+ /** Keep at most this many crumbs (oldest drop). Defaults to {@link DEFAULT_MAX_BREADCRUMBS}. */
57
+ readonly maxBreadcrumbs?: number;
58
+ /** Optional age cap in ms: crumbs older than this are dropped. Off by default. */
59
+ readonly maxAgeMs?: number;
60
+ /** A per-crumb filter (mute a category, edit, or drop by returning `null`). */
61
+ readonly beforeBreadcrumb?: BeforeBreadcrumb | null;
62
+ /** Clock source, injectable for tests. Defaults to `Date.now`. */
63
+ readonly now?: () => number;
64
+ }
65
+ /** A live trace buffer. */
66
+ export interface BreadcrumbBuffer {
67
+ /** Record a crumb (subject to `beforeBreadcrumb`, size, and age trimming). */
68
+ add(crumb: Breadcrumb): void;
69
+ /** The current crumbs, oldest first — a fresh array, safe to keep. */
70
+ snapshot(): Breadcrumb[];
71
+ /** Drop every crumb. */
72
+ clear(): void;
73
+ }
74
+ /**
75
+ * Create a FIFO ring buffer. On `add`, the crumb passes through
76
+ * `beforeBreadcrumb`, is appended, then the buffer is trimmed to the newest
77
+ * `maxBreadcrumbs` (`slice(-N)`) and — when an age cap is set — pruned of stale
78
+ * crumbs. `snapshot` prunes by age again at read time so an idle tab never ships
79
+ * stale context.
80
+ */
81
+ export declare function createBreadcrumbBuffer(config?: BreadcrumbBufferConfig): BreadcrumbBuffer;
82
+ /** A `console` crumb from a captured call's level and arguments. */
83
+ export declare function consoleCrumb(level: BreadcrumbLevel, args: readonly unknown[], timestamp: number): Breadcrumb;
84
+ /** A `navigation` crumb; both URLs are scrubbed as the crumb is built. */
85
+ export declare function navigationCrumb(from: string, to: string, timestamp: number): Breadcrumb;
86
+ /** A `fetch` crumb: method + scrubbed URL + status only. No body, ever. */
87
+ export declare function fetchCrumb(method: string, url: string, status: number | undefined, timestamp: number): Breadcrumb;
88
+ /** An `xhr` crumb: method + scrubbed URL + status only. No body, ever. */
89
+ export declare function xhrCrumb(method: string, url: string, status: number | undefined, timestamp: number): Breadcrumb;
90
+ /** A `ui.click` crumb: a masked target selector only — no text or value. */
91
+ export declare function clickCrumb(target: Element, timestamp: number): Breadcrumb;
92
+ /**
93
+ * A `ui.input` crumb: records **that** an input changed and which field, never
94
+ * the value typed into it. The `target` element's `.value` is never read.
95
+ */
96
+ export declare function inputCrumb(target: Element, timestamp: number): Breadcrumb;
97
+ /** An `error` crumb for the failing exception or rejection that ends the trace. */
98
+ export declare function errorCrumb(error: unknown, timestamp: number): Breadcrumb;
99
+ /** Detaches an installed instrumentation, restoring the original behaviour. */
100
+ export type Teardown = () => void;
101
+ type AnyFn = (...args: unknown[]) => unknown;
102
+ type ConsoleLike = Partial<Record<BreadcrumbLevel, AnyFn>>;
103
+ type FetchFn = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
104
+ interface HistoryLike {
105
+ pushState(data: unknown, unused: string, url?: string | URL | null): void;
106
+ replaceState(data: unknown, unused: string, url?: string | URL | null): void;
107
+ }
108
+ interface XhrInstance {
109
+ status: number;
110
+ open(method: string, url: string | URL, ...rest: unknown[]): void;
111
+ send(body?: unknown): void;
112
+ addEventListener(type: string, listener: () => void): void;
113
+ removeEventListener(type: string, listener: () => void): void;
114
+ }
115
+ interface XhrConstructor {
116
+ new (): XhrInstance;
117
+ prototype: XhrInstance;
118
+ }
119
+ /** The structural window surface the instrumentation reaches into. */
120
+ export interface InstrumentWindow {
121
+ fetch?: FetchFn;
122
+ history?: HistoryLike;
123
+ location?: {
124
+ href: string;
125
+ };
126
+ XMLHttpRequest?: XhrConstructor;
127
+ addEventListener(type: string, listener: (event: Event) => void, options?: boolean | AddEventListenerOptions): void;
128
+ removeEventListener(type: string, listener: (event: Event) => void, options?: boolean | EventListenerOptions): void;
129
+ }
130
+ /** Options for {@link instrumentBreadcrumbs} and the individual installers. */
131
+ export interface InstrumentOptions {
132
+ readonly win?: InstrumentWindow;
133
+ readonly doc?: Document;
134
+ readonly consoleObj?: ConsoleLike;
135
+ readonly consoleLevels?: readonly BreadcrumbLevel[];
136
+ /** Skip URLs (e.g. the SDK's own ingest calls) so they never become crumbs. */
137
+ readonly ignoreUrl?: (url: string) => boolean;
138
+ readonly now?: () => number;
139
+ }
140
+ /** Wrap `console` methods so calls at the captured levels become crumbs. */
141
+ export declare function instrumentConsole(buffer: BreadcrumbBuffer, consoleObj: ConsoleLike, levels: readonly BreadcrumbLevel[], now: () => number): Teardown;
142
+ /**
143
+ * Wrap `fetch` to record a crumb on settlement. The **original** outcome is
144
+ * returned untouched — the response is passed through without its body being
145
+ * read, and a rejection is re-thrown so the caller's `unhandledrejection`
146
+ * semantics are preserved.
147
+ */
148
+ export declare function instrumentFetch(buffer: BreadcrumbBuffer, win: InstrumentWindow, ignoreUrl: (url: string) => boolean, now: () => number): Teardown;
149
+ /**
150
+ * Patch `XMLHttpRequest` to record a crumb when a request settles. Only the
151
+ * method, URL, and final status are read; the `send` body argument is ignored,
152
+ * so a body can never reach the buffer.
153
+ */
154
+ export declare function instrumentXhr(buffer: BreadcrumbBuffer, win: InstrumentWindow, ignoreUrl: (url: string) => boolean, now: () => number): Teardown;
155
+ /** Record a `navigation` crumb on `pushState`/`replaceState`/pop/hash changes. */
156
+ export declare function instrumentNavigation(buffer: BreadcrumbBuffer, win: InstrumentWindow, now: () => number): Teardown;
157
+ /** Listen (capture-phase) for clicks and input changes as masked crumbs. */
158
+ export declare function instrumentUiEvents(buffer: BreadcrumbBuffer, doc: Document, now: () => number): Teardown;
159
+ /**
160
+ * Install every capture hook onto a window/document and return a single teardown
161
+ * that removes them all. Each hook is independent and defensive: a failure in one
162
+ * never blocks the others, and none can throw into the host page.
163
+ */
164
+ export declare function instrumentBreadcrumbs(buffer: BreadcrumbBuffer, options?: InstrumentOptions): Teardown;
165
+ export {};
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The draw surface — arrow / box / pen / text marking over the captured frame
3
+ * (spec 0003 §B, the Reporter prototype's draw toolbar). It is the interactive
4
+ * editor behind the overlay's draw mode: a tool, a live preview, a committed list
5
+ * of {@link Mark}s with undo, and an SVG it renders into. Marks are recorded in
6
+ * **screenshot (viewport) coordinates** so they compose over the full masked
7
+ * screenshot; the overlay reads {@link DrawSurface.marks} when the Reporter
8
+ * attaches.
9
+ *
10
+ * Pointer listeners are capture-phase on the document, so a stroke is intercepted
11
+ * before the host page; a stroke that begins on interactive chrome (the toolbar,
12
+ * the label input) is ignored so the toolbar stays clickable. The surface draws
13
+ * nothing into the host page — only into the SVG the overlay hands it inside its
14
+ * own Shadow DOM.
15
+ */
16
+ import { type DrawTool, type Mark } from "./annotation";
17
+ /** Options for {@link startDrawSurface}. */
18
+ export interface DrawSurfaceOptions {
19
+ /** Document to attach to. Defaults to the global `document`. */
20
+ readonly doc?: Document;
21
+ /** The (viewport-filling) SVG the surface renders committed marks + preview into. */
22
+ readonly svg: SVGSVGElement;
23
+ /** The inline label input the text tool shows, positions, and reads. */
24
+ readonly textInput: HTMLInputElement;
25
+ /** Mark colour. Defaults to {@link MARK_COLOR}. */
26
+ readonly color?: string;
27
+ /** Marks to seed from, so re-entering draw continues an existing annotation. */
28
+ readonly initialMarks?: ReadonlyArray<Mark>;
29
+ /** Notified with a fresh snapshot whenever the committed marks change. */
30
+ readonly onChange?: (marks: ReadonlyArray<Mark>) => void;
31
+ }
32
+ /** A running draw surface. */
33
+ export interface DrawSurface {
34
+ /** Switch the active tool. */
35
+ setTool(tool: DrawTool): void;
36
+ /** The active tool. */
37
+ getTool(): DrawTool;
38
+ /** Remove the most recently committed mark. */
39
+ undo(): void;
40
+ /** A snapshot of the committed marks, in screenshot coordinates. */
41
+ marks(): ReadonlyArray<Mark>;
42
+ /** Detach all listeners and hide the label input. */
43
+ stop(): void;
44
+ }
45
+ /**
46
+ * Begin drawing. The returned surface tracks a tool, a live preview stroke, and the
47
+ * committed marks; `marks()` snapshots them for the overlay's Send assembly. Text
48
+ * labels are entered through the provided input (Enter commits, Escape/blur-empty
49
+ * discards). `stop()` detaches everything.
50
+ */
51
+ export declare function startDrawSurface(options: DrawSurfaceOptions): DrawSurface;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Automatic error capture — the SDK's signature capability: **errors report
3
+ * themselves, no prompt** (spec 0003 §E, ADR-0011; research
4
+ * `sentry-error-capture-findings.md` §7.1–7.3, §7.6).
5
+ *
6
+ * Exactly **two capture-phase listeners** (`window` `error` +
7
+ * `unhandledrejection`) turn uncaught exceptions and unhandled rejections into
8
+ * `source: auto`, `Kind = bug` Feedback for the current session's Reporter — no
9
+ * native-API monkeypatching, no library. Each firing is deduped by a per-session
10
+ * fingerprint, rate-limited by a token-bucket burst limiter and a per-session cap,
11
+ * scrubbed through the same `beforeSend` choke point as manual reports (§C), and
12
+ * shipped through the same transport (which honours ingest's `429` / `Retry-After`
13
+ * backpressure, ticket #89). `console.error` is **not** promoted — it stays
14
+ * breadcrumb-only Evidence.
15
+ *
16
+ * Per-firing order (research §7.1): **`canSubmit`/Gate → dedup → rate-limit/cap →
17
+ * `beforeSend` scrub → enqueue Feedback**. The Gate is honoured by construction:
18
+ * `init` installs this only when boot returned `canSubmit`, so an auto-error is
19
+ * never filed where a manual report would be refused, and it inherits the session
20
+ * Reporter's server-derived tier.
21
+ *
22
+ * The whole module is defensive — every handler is wrapped so a Fixback problem
23
+ * (or an error thrown while capturing an error) never surfaces on the host page.
24
+ * The fingerprint/`normalize` shape and the limiter numbers are **starting points**
25
+ * from research (ticket #92), exposed as config — tunable, not frozen.
26
+ */
27
+ import type { IdentityInputs } from "./boot";
28
+ import { type BreadcrumbBuffer, type Teardown } from "./breadcrumbs";
29
+ import { type BeforeSend } from "./scrub";
30
+ import { type Capture, type CaptureOptions } from "./screenshot";
31
+ import { type SubmitInput, type SubmitResult } from "./submit";
32
+ export type { Teardown };
33
+ /** Burst limiter capacity — how many auto-reports may fire back-to-back (§E). */
34
+ export declare const DEFAULT_BURST_CAPACITY = 5;
35
+ /** Burst limiter refill — one token returns every this-many ms (§E). */
36
+ export declare const DEFAULT_BURST_REFILL_MS = 2000;
37
+ /** Per-session ceiling on distinct auto-Feedback; beyond it, only a dropped-count (§E). */
38
+ export declare const DEFAULT_MAX_DISTINCT_AUTO = 20;
39
+ /**
40
+ * Collapse the volatile parts of an error message so a changing string doesn't
41
+ * split one bug: UUIDs, URLs, `0x…` and long hex runs, and long digit runs are
42
+ * replaced with stable placeholders. Short numbers and stable text are kept so
43
+ * genuinely distinct bugs stay distinct. A small, dependency-free regex set —
44
+ * tunable per ticket #92, never a frozen magic set.
45
+ */
46
+ export declare function normalize(value: string): string;
47
+ /**
48
+ * A dependency-free FNV-1a hash rendered in base-36. It only has to be stable and
49
+ * well-distributed within one session (the client key is a flood guard; the server
50
+ * does canonical cross-session clustering), so a non-cryptographic hash is right.
51
+ */
52
+ export declare function hashString(input: string): string;
53
+ /**
54
+ * Extract a compact, stable signature of the top in-app frames of a stack: up to
55
+ * {@link FINGERPRINT_FRAME_LIMIT} frames as `function@basename:line:col`, origin
56
+ * and cache-busting query stripped so a per-deploy asset hash doesn't matter within
57
+ * a session. Returns `""` when there is no usable stack (message-only fallback).
58
+ */
59
+ export declare function extractTopFrames(stack: string | undefined, limit?: number): string;
60
+ /**
61
+ * The per-session fingerprint (research §7.2):
62
+ * `hash(errorType + "|" + normalize(value) + "|" + topFrames)`. Stack frames
63
+ * dominate when present; otherwise it falls back to type + normalized value.
64
+ */
65
+ export declare function computeFingerprint(type: string, value: string, stack?: string): string;
66
+ /** Configuration for {@link TokenBucket}. */
67
+ export interface TokenBucketOptions {
68
+ readonly capacity: number;
69
+ readonly refillIntervalMs: number;
70
+ /** Clock source, injectable for tests. Defaults to `Date.now`. */
71
+ readonly now?: () => number;
72
+ }
73
+ /**
74
+ * A token bucket: starts full at `capacity`, refills one token every
75
+ * `refillIntervalMs`, and refuses (`take() === false`) when empty. So a fast error
76
+ * loop that dodges dedup with distinct fingerprints still can't machine-gun ingest.
77
+ * The clock is injectable so the window is unit-tested deterministically.
78
+ */
79
+ export declare class TokenBucket {
80
+ private readonly options;
81
+ private tokens;
82
+ private last;
83
+ private readonly now;
84
+ constructor(options: TokenBucketOptions);
85
+ /** Consume a token if one is available (refilling first), else refuse. */
86
+ take(): boolean;
87
+ }
88
+ /** Injectable collaborators, defaulted to the real implementations. */
89
+ export interface AutoCaptureDeps {
90
+ readonly captureView: (options?: CaptureOptions) => Promise<Capture | null>;
91
+ readonly submitReport: (apiUrl: string, input: SubmitInput, fetchImpl?: typeof fetch) => Promise<SubmitResult>;
92
+ }
93
+ /** Configuration for {@link installErrorCapture}. */
94
+ export interface AutoCaptureConfig {
95
+ readonly apiUrl: string;
96
+ readonly key: string;
97
+ readonly identity?: IdentityInputs;
98
+ /** The window whose global handlers are installed. Defaults to `window`. */
99
+ readonly win?: Window;
100
+ /** The document used for capture + environment. Defaults to the window's. */
101
+ readonly doc?: Document;
102
+ readonly sdkVersion?: string;
103
+ /** The shared trace buffer; the failing error is added to it before filing. */
104
+ readonly buffer?: BreadcrumbBuffer | null;
105
+ /** Per-project client scrub hook, run at the `beforeSend` choke point (§C). */
106
+ readonly beforeSend?: BeforeSend;
107
+ /** Run the built-in default scrubbers. Defaults to `true` (private-by-default). */
108
+ readonly scrub?: boolean;
109
+ readonly deps?: Partial<AutoCaptureDeps>;
110
+ /** Burst limiter capacity. Defaults to {@link DEFAULT_BURST_CAPACITY}. */
111
+ readonly burstCapacity?: number;
112
+ /** Burst limiter refill interval (ms). Defaults to {@link DEFAULT_BURST_REFILL_MS}. */
113
+ readonly burstRefillMs?: number;
114
+ /** Per-session distinct-Feedback cap. Defaults to {@link DEFAULT_MAX_DISTINCT_AUTO}. */
115
+ readonly maxDistinct?: number;
116
+ /** Clock source, injectable for tests. Defaults to `Date.now`. */
117
+ readonly now?: () => number;
118
+ }
119
+ /** A running auto-capture: its teardown, plus the local dropped-count (spec §E). */
120
+ export interface AutoCaptureHandle {
121
+ /** Remove the two global handlers and the flush hooks. Safe to call repeatedly. */
122
+ readonly destroy: Teardown;
123
+ /**
124
+ * Distinct auto-Feedback dropped by the burst limiter or the per-session cap —
125
+ * the "keep only a local dropped-count" fallback beyond the guardrails (spec §E).
126
+ */
127
+ droppedCount(): number;
128
+ }
129
+ /**
130
+ * Install automatic error capture on a window and return a handle whose `destroy`
131
+ * removes every listener it added (the two global handlers plus the flush hooks).
132
+ * The caller (`init`) installs this only after boot returned `canSubmit`, so the
133
+ * Gate is respected and the auto-error inherits the session Reporter's tier.
134
+ */
135
+ export declare function installErrorCapture(config: AutoCaptureConfig): AutoCaptureHandle;