@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 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), and
32
- * whether a submission would be accepted right now. The launcher shows only when
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;