@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 +71 -0
- package/dist/annotation.d.ts +106 -0
- package/dist/auto-report-backoff.d.ts +47 -0
- package/dist/breadcrumbs.d.ts +165 -0
- package/dist/draw-surface.d.ts +51 -0
- package/dist/error-capture.d.ts +135 -0
- package/dist/fixback.umd.js +524 -28
- package/dist/fixback.umd.js.map +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.mjs +2402 -467
- package/dist/index.mjs.map +1 -1
- package/dist/init.d.ts +66 -7
- package/dist/invite.d.ts +104 -0
- package/dist/launcher.d.ts +29 -9
- package/dist/onboarding-styles.d.ts +8 -0
- package/dist/onboarding.d.ts +44 -0
- package/dist/overlay-styles.d.ts +1 -1
- package/dist/overlay.d.ts +36 -6
- package/dist/region-capture.d.ts +40 -0
- package/dist/report.d.ts +33 -7
- package/dist/screenshot.d.ts +2 -0
- package/dist/scrub.d.ts +60 -0
- package/dist/styles.d.ts +11 -1
- package/dist/submit.d.ts +22 -4
- package/package.json +1 -1
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;
|