@fixback/sdk 0.1.0 → 0.3.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 +72 -0
- package/dist/annotation.d.ts +106 -0
- package/dist/auto-report-backoff.d.ts +47 -0
- package/dist/boot.d.ts +17 -3
- package/dist/breadcrumbs.d.ts +448 -0
- package/dist/dom.d.ts +7 -0
- package/dist/draw-surface.d.ts +51 -0
- package/dist/error-capture.d.ts +135 -0
- package/dist/fixback.umd.js +538 -28
- package/dist/fixback.umd.js.map +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.mjs +3645 -468
- package/dist/index.mjs.map +1 -1
- package/dist/init.d.ts +100 -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 +39 -18
- package/dist/scrub.d.ts +61 -0
- package/dist/styles.d.ts +11 -1
- package/dist/submit.d.ts +22 -4
- package/dist/version.d.ts +10 -4
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -62,6 +62,9 @@ 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. |
|
|
67
|
+
| `capture` | `{ console?: boolean; network?: boolean }` | served per-project | Console / network Trace capture. **On by default** and normally governed per-project from the dashboard; set a stream here to override what the server serves (e.g. `{ network: false }`). A stream you leave unset follows the Project's setting. |
|
|
65
68
|
|
|
66
69
|
None of the identity fields is a trust tier — Fixback derives trust on the
|
|
67
70
|
server and never honours a self-declared tier.
|
|
@@ -72,6 +75,24 @@ const fixback = await init({ key: "pk_live_..." });
|
|
|
72
75
|
fixback.destroy();
|
|
73
76
|
```
|
|
74
77
|
|
|
78
|
+
## The launcher
|
|
79
|
+
|
|
80
|
+
The launcher is a bottom-right **Feedback** pill, and it stays out of the way:
|
|
81
|
+
|
|
82
|
+
- **Hover-peek & tuck-away** — the pill's caret tucks it off-screen behind a
|
|
83
|
+
small edge nub. Hovering the bottom-right corner (or the nub) peeks it back;
|
|
84
|
+
clicking the nub — or pressing Enter/Space on it — brings it fully back, which
|
|
85
|
+
also covers pointers that can't hover (touch, keyboard). A brief hint appears
|
|
86
|
+
the first time it's tucked, pointing at the corner.
|
|
87
|
+
- **First-visit welcome** — a one-time toast greets a new visitor, drawing the
|
|
88
|
+
eye with a gentle pulse. It shows once per publishable key per browser.
|
|
89
|
+
- **Reduce motion** — pass `reduceMotion: true` to still the pulse and the
|
|
90
|
+
slide/fade transitions. The launcher also honours the visitor's OS-level
|
|
91
|
+
`prefers-reduced-motion: reduce` on its own, with no configuration.
|
|
92
|
+
|
|
93
|
+
All of this chrome lives inside the launcher's Shadow DOM, so it never adds a
|
|
94
|
+
global style or touches the host page's markup.
|
|
95
|
+
|
|
75
96
|
## The launch event
|
|
76
97
|
|
|
77
98
|
Activating the launcher opens the SDK's own report overlay. It also dispatches a
|
|
@@ -86,6 +107,57 @@ document.addEventListener(LAUNCH_EVENT, () => {
|
|
|
86
107
|
});
|
|
87
108
|
```
|
|
88
109
|
|
|
110
|
+
## Automatic error capture
|
|
111
|
+
|
|
112
|
+
The SDK's signature capability: **errors report themselves, with no prompt.** Two
|
|
113
|
+
capture-phase global handlers (`error` + `unhandledrejection`) turn uncaught
|
|
114
|
+
exceptions and unhandled promise rejections into `source: auto`, `Kind = bug`
|
|
115
|
+
Feedback for the current session's reporter — carrying the same masked screenshot
|
|
116
|
+
and trace buffer a manual report does, plus a per-session fingerprint. It is
|
|
117
|
+
**on by default across every Gate**; pass `autoCapture: false` to turn it off.
|
|
118
|
+
|
|
119
|
+
- **Deduped & rate-limited.** The same error reported many times collapses to one
|
|
120
|
+
Feedback with a rising occurrence count. A token-bucket burst limiter and a
|
|
121
|
+
per-session cap keep a runaway error loop from flooding the queue; the excess is
|
|
122
|
+
dropped and kept only as a local count. On a `429` + `Retry-After` from ingest,
|
|
123
|
+
auto-reporting backs off until the window clears.
|
|
124
|
+
- **Gated like a manual report.** Auto-capture only runs where a submission would
|
|
125
|
+
be accepted (boot's `canSubmit`), so a public visitor's crash is tracked at their
|
|
126
|
+
server-derived tier — never auto-shipped. `console.error` is **not** promoted to a
|
|
127
|
+
report; it stays breadcrumb-only Evidence.
|
|
128
|
+
- **Private-by-default.** Every auto-report passes through the same client-side
|
|
129
|
+
`beforeSend` scrub choke point as a manual one — input values and request/response
|
|
130
|
+
bodies never leave the browser, and your hook can drop an auto-report entirely.
|
|
131
|
+
|
|
132
|
+
## Invite redemption
|
|
133
|
+
|
|
134
|
+
An invited tester needs no account and no setup. When a page loads with an invite
|
|
135
|
+
token in its URL (`?fixback_invite=<token>`), `init` reads the invite's status and
|
|
136
|
+
renders the SDK's own **onboarding modal** — the site you were invited to, the
|
|
137
|
+
**access tier** the redemption grants (server-derived, never guessed on the
|
|
138
|
+
client), a private-by-default note, and a "Continue as" name / email:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
init({ key: "pk_live_..." }); // auto-detects ?fixback_invite= on the page
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
On confirm the SDK redeems the invite, persists the returned `reporterId` in
|
|
145
|
+
`localStorage` (scoped to your publishable key), strips the token from the URL
|
|
146
|
+
(one-time consumption), and mounts the launcher. On later visits `init` presents
|
|
147
|
+
that stored `reporterId`, so a returning tester is recognised at their correct
|
|
148
|
+
tier without re-onboarding — and on an **Invited** or **Internal** Gate the
|
|
149
|
+
launcher appears only after redemption, while an **Open** Gate shows it to anyone.
|
|
150
|
+
|
|
151
|
+
The name / email are **self-provided display fields** — never a trust signal; the
|
|
152
|
+
tier is always the server's to derive. If the token reaches the page some way
|
|
153
|
+
other than the URL, redeem it explicitly:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { redeem } from "@fixback/sdk";
|
|
157
|
+
|
|
158
|
+
redeem({ key: "pk_live_...", token: "the-invite-token" });
|
|
159
|
+
```
|
|
160
|
+
|
|
89
161
|
## How the boot gate works
|
|
90
162
|
|
|
91
163
|
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
|
+
}
|
package/dist/boot.d.ts
CHANGED
|
@@ -26,17 +26,31 @@ export interface IdentityInputs {
|
|
|
26
26
|
export interface BootRequest extends IdentityInputs {
|
|
27
27
|
readonly key: string;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* The Project's effective console/network capture config, served on the boot answer
|
|
31
|
+
* (spec #122 §L; ticket #138). Each flag is the per-project master toggle ANDed with
|
|
32
|
+
* that stream's own toggle, so the SDK gates instrumentation on one boolean per
|
|
33
|
+
* stream. Optional on the wire: an older server that does not send it (or a
|
|
34
|
+
* malformed value) is treated as **capture on** — default-on, matching the server
|
|
35
|
+
* default — and `init` options override whatever is served.
|
|
36
|
+
*/
|
|
37
|
+
export interface CaptureConfig {
|
|
38
|
+
readonly console: boolean;
|
|
39
|
+
readonly network: boolean;
|
|
40
|
+
}
|
|
29
41
|
/**
|
|
30
42
|
* The boot answer: whether this origin is allowlisted, the Project's Gate, the
|
|
31
|
-
* caller's derived tier (`null` when a presented identity was refused),
|
|
32
|
-
*
|
|
33
|
-
* `canSubmit` is true.
|
|
43
|
+
* caller's derived tier (`null` when a presented identity was refused), whether a
|
|
44
|
+
* submission would be accepted right now, and the Project's capture config. The
|
|
45
|
+
* launcher shows only when `canSubmit` is true.
|
|
34
46
|
*/
|
|
35
47
|
export interface BootAnswer {
|
|
36
48
|
readonly originAllowed: boolean;
|
|
37
49
|
readonly gate: ProjectGate;
|
|
38
50
|
readonly tier: ReporterTier | null;
|
|
39
51
|
readonly canSubmit: boolean;
|
|
52
|
+
/** The Project's console/network capture config; absent ⇒ default-on (#138). */
|
|
53
|
+
readonly capture?: CaptureConfig;
|
|
40
54
|
}
|
|
41
55
|
/** Join an API base URL with the boot path, tolerating a trailing slash. */
|
|
42
56
|
export declare function bootEndpoint(apiUrl: string): string;
|