@fixback/sdk 0.2.0 → 0.4.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 +12 -10
- package/dist/annotation.d.ts +1 -1
- package/dist/boot.d.ts +23 -3
- package/dist/breadcrumbs.d.ts +315 -32
- package/dist/dom.d.ts +7 -0
- package/dist/error-capture.d.ts +31 -1
- package/dist/fixback.umd.js +115 -54
- package/dist/fixback.umd.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.mjs +5544 -1258
- package/dist/index.mjs.map +1 -1
- package/dist/init.d.ts +62 -8
- package/dist/launcher.d.ts +2 -2
- package/dist/overlay-styles.d.ts +1 -1
- package/dist/overlay.d.ts +12 -0
- package/dist/replay.d.ts +101 -0
- package/dist/report.d.ts +47 -5
- package/dist/screenshot.d.ts +37 -18
- package/dist/scrub.d.ts +5 -4
- package/dist/styles.d.ts +2 -2
- package/dist/submit.d.ts +8 -0
- package/dist/version.d.ts +10 -4
- package/package.json +6 -1
package/dist/init.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type IdentityInputs } from "./boot";
|
|
2
|
-
import { type BeforeBreadcrumb, type BreadcrumbLevel } from "./breadcrumbs";
|
|
2
|
+
import { type BeforeBreadcrumb, type BreadcrumbLevel, type TraceStream } from "./breadcrumbs";
|
|
3
|
+
import { type ReplayOptions } from "./replay";
|
|
3
4
|
import type { BeforeSend } from "./scrub";
|
|
4
5
|
/**
|
|
5
6
|
* The hosted Fixback API origin the SDK talks to by default. A self-hosted or
|
|
@@ -8,17 +9,39 @@ import type { BeforeSend } from "./scrub";
|
|
|
8
9
|
*/
|
|
9
10
|
export declare const DEFAULT_API_URL = "https://api.fixback.dev";
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Does `url` address one of the SDK's **own** API endpoints? Every request the SDK
|
|
13
|
+
* issues lives under two namespaces beneath the API origin: `/api/ingest/*` — boot
|
|
14
|
+
* (`bootEndpoint`, boot.ts) and feedback (`feedbackEndpoint`, submit.ts) — and
|
|
15
|
+
* `/api/invites/*` (`inviteStatusEndpoint` / `redeemEndpoint`, invite.ts).
|
|
16
|
+
*
|
|
17
|
+
* The trace's network capture ignores **only** these, never the whole origin. That
|
|
18
|
+
* distinction matters when the instrumented app and the Fixback API share an origin — a
|
|
19
|
+
* self-hosted deployment, or Fixback running its own SDK on its own dashboard — where
|
|
20
|
+
* ignoring the origin wholesale would swallow all of the app's own requests and leave the
|
|
21
|
+
* Network tab empty. When the app and the API sit on different origins (the common case)
|
|
22
|
+
* this matches nothing extra, since the app's origin is never `apiUrl`. `url` is the raw
|
|
23
|
+
* request URL — absolute for every call the SDK makes — so a prefix test over the
|
|
24
|
+
* normalised origin is exact.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isFixbackApiRequest(apiUrl: string, url: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Trace buffer tuning (spec #122 §A). These are configurable **starting points**
|
|
29
|
+
* from the grill — three per-stream ring budgets (network ≈ 100 / console ≈ 80 /
|
|
30
|
+
* breadcrumbs ≈ 40), a shared age cap (~3 min, on), and **all** console levels
|
|
31
|
+
* captured (eviction priority, not capture, keeps warn/error above the chatter) —
|
|
32
|
+
* never frozen magic numbers. Pass `false` for {@link InitOptions.trace} to disable
|
|
14
33
|
* capture entirely.
|
|
15
34
|
*/
|
|
16
35
|
export interface TraceOptions {
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Per-stream ring budgets (network / console / breadcrumbs); each stream evicts
|
|
38
|
+
* independently, so a chatty stream can't drop another's lead-up. Any stream
|
|
39
|
+
* omitted uses its default.
|
|
40
|
+
*/
|
|
41
|
+
readonly budgets?: Partial<Record<TraceStream, number>>;
|
|
42
|
+
/** Shared age cap in ms. Defaults to ~3 min (on); pass `0` to disable age pruning. */
|
|
20
43
|
readonly maxAgeMs?: number;
|
|
21
|
-
/** Console levels captured. Defaults to
|
|
44
|
+
/** Console levels captured. Defaults to **all** levels (log/info/warn/error/assert/debug). */
|
|
22
45
|
readonly consoleLevels?: readonly BreadcrumbLevel[];
|
|
23
46
|
/** Per-crumb filter: mute a category, edit a crumb, or drop it (`null`). */
|
|
24
47
|
readonly beforeBreadcrumb?: BeforeBreadcrumb;
|
|
@@ -47,6 +70,26 @@ export interface InitOptions extends IdentityInputs {
|
|
|
47
70
|
readonly scrub?: boolean;
|
|
48
71
|
/** Trace buffer tuning, or `false` to turn the buffer off entirely. */
|
|
49
72
|
readonly trace?: TraceOptions | false;
|
|
73
|
+
/**
|
|
74
|
+
* Buffered session-replay tuning (issue #189, ADR-0024) — masking defaults on:
|
|
75
|
+
* all inputs, all text, SDK chrome blocked — or `false` to turn the recorder
|
|
76
|
+
* off entirely regardless of the served config.
|
|
77
|
+
*/
|
|
78
|
+
readonly replay?: ReplayOptions | false;
|
|
79
|
+
/**
|
|
80
|
+
* The dev-side override of the Project's server-served capture config (spec #122
|
|
81
|
+
* §L; ticket #138). Capture is **default-on** and normally governed per-project
|
|
82
|
+
* from the dashboard, surfaced on the boot answer; set a stream here to override
|
|
83
|
+
* what the server serves for it — `{ network: false }` turns network capture off
|
|
84
|
+
* even where the Project leaves it on, and a stream left unset follows the served
|
|
85
|
+
* config. Independent of {@link trace} `false`, which turns the whole buffer off,
|
|
86
|
+
* and of {@link replay} `false`, which turns the recorder off.
|
|
87
|
+
*/
|
|
88
|
+
readonly capture?: {
|
|
89
|
+
readonly console?: boolean;
|
|
90
|
+
readonly network?: boolean;
|
|
91
|
+
readonly replay?: boolean;
|
|
92
|
+
};
|
|
50
93
|
/**
|
|
51
94
|
* Automatic error capture — the SDK files uncaught exceptions / unhandled
|
|
52
95
|
* rejections as `source: auto` Feedback with no prompt (spec §E, ADR-0011).
|
|
@@ -55,6 +98,17 @@ export interface InitOptions extends IdentityInputs {
|
|
|
55
98
|
* never filed where a manual report would be refused.
|
|
56
99
|
*/
|
|
57
100
|
readonly autoCapture?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* The host app's **Release** — a build identifier (a git SHA, a semver, a
|
|
103
|
+
* `name@version`) stamped into every submission's environment (#117,
|
|
104
|
+
* ADR-0024). Set it to the same value the build uploaded sourcemaps under
|
|
105
|
+
* (`npx fixback sourcemaps upload --release <v>`), and auto-captured errors
|
|
106
|
+
* gain a symbolicated **code-area pointer** on their Issues. Optional: without
|
|
107
|
+
* it (or without uploaded sourcemaps) everything behaves exactly as before.
|
|
108
|
+
* An invalid value (whitespace, slashes, over 100 chars) is ignored with a
|
|
109
|
+
* console warning.
|
|
110
|
+
*/
|
|
111
|
+
readonly release?: string;
|
|
58
112
|
}
|
|
59
113
|
/**
|
|
60
114
|
* Options for {@link redeem} — the explicit form of the `?fixback_invite=` URL
|
package/dist/launcher.d.ts
CHANGED
|
@@ -29,8 +29,8 @@ export interface Launcher {
|
|
|
29
29
|
* Mount the launcher into `target` (typically `document.body`). Everything the
|
|
30
30
|
* launcher draws — the **Feedback** pill, its **tuck** control, the edge **nub**
|
|
31
31
|
* and corner **hover-zone** that peek it back, and the first-visit **welcome**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* toast — lives inside one open Shadow DOM, so its styles are fully isolated
|
|
33
|
+
* from the host page and vice-versa. The host element is
|
|
34
34
|
* fixed-positioned and out of flow, so mounting never shifts the host page's
|
|
35
35
|
* layout. Only one launcher can exist at a time — an earlier one is removed first.
|
|
36
36
|
*
|
package/dist/overlay-styles.d.ts
CHANGED
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
* vendored Signal tokens (copied from `packages/ui/src/tokens.css`, not imported —
|
|
7
7
|
* the SDK must not depend on `@fixback/ui` at runtime). Keep them in sync by value.
|
|
8
8
|
*/
|
|
9
|
-
export declare const OVERLAY_STYLES = "\n:host {\n --fb-color-accent: #2f6fed;\n --fb-color-accent-hover: #245fd0;\n --fb-color-on-emphasis: #ffffff;\n --fb-color-ink: #0f1720;\n --fb-color-text: #1a2530;\n --fb-color-muted: #5a6875;\n --fb-color-faint: #9aa7b2;\n --fb-color-border: #e0e6ec;\n --fb-color-border-soft: #e6ebf0;\n --fb-color-surface: #ffffff;\n --fb-color-bug: #e5484d;\n --fb-color-bug-bg: #fdecec;\n --fb-color-impr: #2f6fed;\n --fb-color-impr-bg: #eaf1fe;\n --fb-color-idea: #8b5cf6;\n --fb-color-idea-bg: #f2ecfe;\n --fb-color-success: #2f9e5b;\n --fb-font-sans: \"IBM Plex Sans\", system-ui, -apple-system, \"Segoe UI\", Roboto,\n Helvetica, Arial, sans-serif;\n --fb-font-mono: \"IBM Plex Mono\", ui-monospace, \"SFMono-Regular\", Menlo, Consolas,\n monospace;\n\n display: block;\n color: var(--fb-color-text);\n font-family: var(--fb-font-sans);\n font-size: 13px;\n line-height: 1.45;\n -webkit-font-smoothing: antialiased;\n}\n\n* { box-sizing: border-box; }\n\n.fb-ov-panel {\n width: 300px;\n max-width: calc(100vw - 40px);\n background: var(--fb-color-surface);\n border: 1px solid var(--fb-color-border);\n border-radius: 13px;\n box-shadow: 0 18px 44px rgba(20, 40, 70, 0.2);\n overflow: hidden;\n}\n\n.fb-ov-head {\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--fb-color-border-soft);\n}\n.fb-ov-mark-sq {\n width: 13px;\n height: 13px;\n border-radius: 4px;\n background: var(--fb-color-accent);\n flex: none;\n}\n.fb-ov-brand { font-size: 14px; font-weight: 600; color: var(--fb-color-ink); }\n.fb-ov-tier {\n flex: none;\n font-family: var(--fb-font-mono);\n font-size: 9.5px;\n color: var(--fb-color-faint);\n border: 1px solid var(--fb-color-border-soft);\n border-radius: 5px;\n padding: 1px 6px;\n}\n.fb-ov-close {\n margin-left: auto;\n border: 0;\n background: none;\n color: var(--fb-color-faint);\n font-size: 16px;\n line-height: 1;\n padding: 2px 4px;\n cursor: pointer;\n border-radius: 6px;\n}\n.fb-ov-close:hover { color: var(--fb-color-muted); background: #f4f7fa; }\n\n.fb-ov-tabs { display: flex; gap: 6px; padding: 12px 14px 6px; }\n.fb-ov-tab {\n flex: 1;\n text-align: center;\n font-size: 11px;\n font-weight: 600;\n padding: 6px;\n border-radius: 7px;\n border: 1px solid var(--fb-color-border);\n background: var(--fb-color-surface);\n color: var(--fb-color-muted);\n cursor: pointer;\n font-family: inherit;\n}\n.fb-ov-tab:hover { border-color: #cfd8e2; }\n.fb-ov-tab.is-active[data-kind=\"bug\"] {\n background: var(--fb-color-bug-bg); color: var(--fb-color-bug); border-color: transparent;\n}\n.fb-ov-tab.is-active[data-kind=\"improvement\"] {\n background: var(--fb-color-impr-bg); color: var(--fb-color-impr); border-color: transparent;\n}\n.fb-ov-tab.is-active[data-kind=\"idea\"] {\n background: var(--fb-color-idea-bg); color: var(--fb-color-idea); border-color: transparent;\n}\n\n.fb-ov-mark {\n margin: 10px 14px;\n min-height: 52px;\n border-radius: 9px;\n border: 1px solid var(--fb-color-border-soft);\n background: repeating-linear-gradient(135deg, #f4f7fa, #f4f7fa 7px, #eaeff4 7px, #eaeff4 14px);\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n gap: 6px;\n padding: 10px 12px;\n}\n.fb-ov-selector {\n display: none;\n max-width: 100%;\n font-family: var(--fb-font-mono);\n font-size: 10px;\n color: var(--fb-color-on-emphasis);\n background: var(--fb-color-accent);\n padding: 3px 8px;\n border-radius: 5px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.fb-ov-mark.has-element .fb-ov-selector { display: inline-block; }\n.fb-ov-mark-caption { font-size: 10px; color: var(--fb-color-faint); font-family: var(--fb-font-mono); }\n\n.fb-ov-comment {\n display: block;\n width: calc(100% - 28px);\n margin: 0 14px 10px;\n min-height: 62px;\n resize: vertical;\n font-family: inherit;\n font-size: 13px;\n color: var(--fb-color-text);\n border: 1px solid var(--fb-color-border-soft);\n border-radius: 9px;\n padding: 10px 11px;\n}\n.fb-ov-comment::placeholder { color: var(--fb-color-faint); }\n.fb-ov-comment:focus-visible { outline: 2px solid var(--fb-color-accent); outline-offset: 1px; }\n\n.fb-ov-status { padding: 0 14px; font-size: 11px; min-height: 0; }\n.fb-ov-status.is-error { color: var(--fb-color-bug); }\n\n.fb-ov-tools { display: flex; align-items: center; gap: 7px; padding: 8px 14px 14px; }\n.fb-ov-pickbtn,\n.fb-ov-capturebtn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-family: inherit;\n font-size: 12px;\n color: var(--fb-color-muted);\n background: var(--fb-color-surface);\n border: 1px solid var(--fb-color-border);\n border-radius: 8px;\n padding: 8px 11px;\n cursor: pointer;\n}\n.fb-ov-capturebtn { padding: 8px 9px; }\n.fb-ov-pickbtn:hover,\n.fb-ov-capturebtn:hover { border-color: #cfd8e2; }\n.fb-ov-pickbtn.is-active,\n.fb-ov-capturebtn.is-active,\n.fb-ov-capturebtn.is-attached {\n color: var(--fb-color-accent);\n border-color: var(--fb-color-accent);\n background: var(--fb-color-impr-bg);\n}\n.fb-ov-pickbtn__glyph { font-size: 14px; line-height: 1; }\n.fb-ov-icon { display: block; }\n\n.fb-ov-send {\n margin-left: auto;\n font-family: inherit;\n font-size: 13px;\n font-weight: 600;\n color: var(--fb-color-on-emphasis);\n background: var(--fb-color-accent);\n border: 0;\n border-radius: 8px;\n padding: 9px 18px;\n cursor: pointer;\n}\n.fb-ov-send:hover { background: var(--fb-color-accent-hover); }\n.fb-ov-send:disabled { opacity: 0.6; cursor: default; }\n\n.fb-ov-done { display: none; padding: 24px 18px; text-align: center; }\n.fb-ov-panel.is-sent .fb-ov-form { display: none; }\n.fb-ov-panel.is-sent .fb-ov-done { display: block; }\n.fb-ov-done__check {\n width: 40px; height: 40px; margin: 0 auto 12px;\n border-radius: 50%;\n background: #e7f6ee; color: var(--fb-color-success);\n display: flex; align-items: center; justify-content: center;\n font-size: 20px; font-weight: 700;\n}\n.fb-ov-done__title { font-size: 15px; font-weight: 600; color: var(--fb-color-ink); }\n.fb-ov-done__sub { font-size: 12px; color: var(--fb-color-muted); margin-top: 4px; }\n\n/* The panel is hidden while any full-screen marking layer is active. */\n.fb-ov-panel.is-marking { visibility: hidden; }\n\n.fb-ov-pick,\n.fb-ov-capture,\n.fb-ov-draw { position: fixed; inset: 0; z-index: 2147483002; display: none; }\n.fb-ov-pick.is-visible,\n.fb-ov-capture.is-visible,\n.fb-ov-draw.is-visible { display: block; }\n\n/* Element-pick highlight layer \u2014 visual only, never intercepts host events. */\n.fb-ov-pick { pointer-events: none; }\n.fb-ov-highlight {\n position: absolute;\n border: 2px dashed var(--fb-color-accent);\n border-radius: 6px;\n box-shadow: 0 0 0 3px rgba(47, 111, 237, 0.14);\n transition: all 60ms ease;\n}\n.fb-ov-hint {\n position: absolute;\n top: 16px;\n left: 50%;\n transform: translateX(-50%);\n font-family: var(--fb-font-mono);\n font-size: 11px;\n color: var(--fb-color-on-emphasis);\n background: var(--fb-color-ink);\n padding: 6px 12px;\n border-radius: 7px;\n box-shadow: 0 8px 20px rgba(20, 40, 70, 0.25);\n}\n\n/* Region-capture layer \u2014 a dim wash with a live selection box. */\n.fb-ov-capture { cursor: crosshair; }\n.fb-ov-capture-dim { position: absolute; inset: 0; background: rgba(15, 23, 32, 0.28); }\n.fb-ov-capture-sel {\n position: absolute;\n border: 2px solid var(--fb-color-accent);\n border-radius: 4px;\n box-shadow: 0 0 0 100vmax rgba(15, 23, 32, 0.32);\n}\n\n/* Draw layer \u2014 the mark SVG, the region frame, the label input, and the toolbar. */\n.fb-ov-draw-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }\n.fb-ov-draw-frame {\n position: absolute;\n border: 1px solid rgba(47, 111, 237, 0.5);\n border-radius: 4px;\n box-shadow: 0 0 0 100vmax rgba(15, 23, 32, 0.18);\n pointer-events: none;\n}\n.fb-ov-draw-text {\n position: absolute;\n display: none;\n font-family: var(--fb-font-sans);\n font-size: 15px;\n font-weight: 600;\n color: var(--fb-color-bug);\n background: rgba(255, 255, 255, 0.92);\n border: 1px dashed var(--fb-color-bug);\n border-radius: 4px;\n padding: 2px 6px;\n outline: none;\n z-index: 3;\n}\n.fb-ov-draw-toolbar {\n position: fixed;\n left: 50%;\n bottom: 26px;\n transform: translateX(-50%);\n display: flex;\n align-items: center;\n gap: 5px;\n background: var(--fb-color-ink);\n border-radius: 12px;\n padding: 7px;\n box-shadow: 0 16px 40px rgba(15, 40, 70, 0.4);\n}\n.fb-ov-drawtool {\n width: 34px;\n height: 34px;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 0;\n border-radius: 8px;\n background: transparent;\n color: #c4ccd4;\n cursor: pointer;\n font-family: inherit;\n}\n.fb-ov-drawtool:hover { background: rgba(255, 255, 255, 0.08); color: #fff; }\n.fb-ov-drawtool.is-active { background: var(--fb-color-accent); color: var(--fb-color-on-emphasis); }\n.fb-ov-drawtool__t { font-weight: 700; font-size: 14px; line-height: 1; }\n.fb-ov-draw-divider { width: 1px; height: 22px; background: #2a343e; margin: 0 3px; }\n.fb-ov-draw-undo {\n width: 34px;\n height: 34px;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 0;\n border-radius: 8px;\n background: transparent;\n color: #c4ccd4;\n cursor: pointer;\n}\n.fb-ov-draw-undo:hover:not(:disabled) { background: rgba(255, 255, 255, 0.08); color: #fff; }\n.fb-ov-draw-undo:disabled { opacity: 0.4; cursor: default; }\n.fb-ov-draw-cancel {\n height: 34px;\n padding: 0 12px;\n border: 0;\n border-radius: 8px;\n background: transparent;\n color: var(--fb-color-faint);\n font-family: inherit;\n font-size: 12.5px;\n cursor: pointer;\n}\n.fb-ov-draw-cancel:hover { color: #fff; }\n.fb-ov-draw-attach {\n height: 34px;\n padding: 0 15px;\n border: 0;\n border-radius: 8px;\n background: var(--fb-color-accent);\n color: var(--fb-color-on-emphasis);\n font-family: inherit;\n font-size: 12.5px;\n font-weight: 600;\n cursor: pointer;\n}\n.fb-ov-draw-attach:hover { background: var(--fb-color-accent-hover); }\n\n@media (prefers-reduced-motion: reduce) {\n .fb-ov-highlight { transition: none; }\n}\n";
|
|
9
|
+
export declare const OVERLAY_STYLES = "\n:host {\n --fb-color-accent: #2f6fed;\n --fb-color-accent-hover: #245fd0;\n --fb-color-on-emphasis: #ffffff;\n --fb-color-ink: #0f1720;\n --fb-color-text: #1a2530;\n --fb-color-muted: #5a6875;\n --fb-color-faint: #9aa7b2;\n --fb-color-border: #e0e6ec;\n --fb-color-border-soft: #e6ebf0;\n --fb-color-surface: #ffffff;\n --fb-color-bug: #e5484d;\n --fb-color-bug-bg: #fdecec;\n --fb-color-impr: #2f6fed;\n --fb-color-impr-bg: #eaf1fe;\n --fb-color-success: #2f9e5b;\n --fb-font-sans: \"IBM Plex Sans\", system-ui, -apple-system, \"Segoe UI\", Roboto,\n Helvetica, Arial, sans-serif;\n --fb-font-mono: \"IBM Plex Mono\", ui-monospace, \"SFMono-Regular\", Menlo, Consolas,\n monospace;\n\n display: block;\n color: var(--fb-color-text);\n font-family: var(--fb-font-sans);\n font-size: 13px;\n line-height: 1.45;\n -webkit-font-smoothing: antialiased;\n}\n\n* { box-sizing: border-box; }\n\n.fb-ov-panel {\n width: 300px;\n max-width: calc(100vw - 40px);\n background: var(--fb-color-surface);\n border: 1px solid var(--fb-color-border);\n border-radius: 13px;\n box-shadow: 0 18px 44px rgba(20, 40, 70, 0.2);\n overflow: hidden;\n}\n\n.fb-ov-head {\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--fb-color-border-soft);\n}\n.fb-ov-mark-sq {\n width: 13px;\n height: 13px;\n border-radius: 4px;\n background: var(--fb-color-accent);\n flex: none;\n}\n.fb-ov-brand { font-size: 14px; font-weight: 600; color: var(--fb-color-ink); }\n.fb-ov-tier {\n flex: none;\n font-family: var(--fb-font-mono);\n font-size: 9.5px;\n color: var(--fb-color-faint);\n border: 1px solid var(--fb-color-border-soft);\n border-radius: 5px;\n padding: 1px 6px;\n}\n.fb-ov-close {\n margin-left: auto;\n border: 0;\n background: none;\n color: var(--fb-color-faint);\n font-size: 16px;\n line-height: 1;\n padding: 2px 4px;\n cursor: pointer;\n border-radius: 6px;\n}\n.fb-ov-close:hover { color: var(--fb-color-muted); background: #f4f7fa; }\n\n.fb-ov-mark {\n margin: 12px 14px 10px;\n min-height: 52px;\n border-radius: 9px;\n border: 1px solid var(--fb-color-border-soft);\n background: repeating-linear-gradient(135deg, #f4f7fa, #f4f7fa 7px, #eaeff4 7px, #eaeff4 14px);\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n gap: 6px;\n padding: 10px 12px;\n}\n.fb-ov-selector {\n display: none;\n max-width: 100%;\n font-family: var(--fb-font-mono);\n font-size: 10px;\n color: var(--fb-color-on-emphasis);\n background: var(--fb-color-accent);\n padding: 3px 8px;\n border-radius: 5px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.fb-ov-mark.has-element .fb-ov-selector { display: inline-block; }\n.fb-ov-mark-caption { font-size: 10px; color: var(--fb-color-faint); font-family: var(--fb-font-mono); }\n\n.fb-ov-comment {\n display: block;\n width: calc(100% - 28px);\n margin: 0 14px 10px;\n min-height: 62px;\n resize: vertical;\n font-family: inherit;\n font-size: 13px;\n color: var(--fb-color-text);\n border: 1px solid var(--fb-color-border-soft);\n border-radius: 9px;\n padding: 10px 11px;\n}\n.fb-ov-comment::placeholder { color: var(--fb-color-faint); }\n.fb-ov-comment:focus-visible { outline: 2px solid var(--fb-color-accent); outline-offset: 1px; }\n\n.fb-ov-status { padding: 0 14px; font-size: 11px; min-height: 0; }\n.fb-ov-status.is-error { color: var(--fb-color-bug); }\n\n.fb-ov-tools { display: flex; align-items: center; gap: 7px; padding: 8px 14px 14px; }\n.fb-ov-pickbtn,\n.fb-ov-capturebtn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-family: inherit;\n font-size: 12px;\n color: var(--fb-color-muted);\n background: var(--fb-color-surface);\n border: 1px solid var(--fb-color-border);\n border-radius: 8px;\n padding: 8px 11px;\n cursor: pointer;\n}\n.fb-ov-capturebtn { padding: 8px 9px; }\n.fb-ov-pickbtn:hover,\n.fb-ov-capturebtn:hover { border-color: #cfd8e2; }\n.fb-ov-pickbtn.is-active,\n.fb-ov-capturebtn.is-active,\n.fb-ov-capturebtn.is-attached {\n color: var(--fb-color-accent);\n border-color: var(--fb-color-accent);\n background: var(--fb-color-impr-bg);\n}\n.fb-ov-pickbtn__glyph { font-size: 14px; line-height: 1; }\n.fb-ov-icon { display: block; }\n\n.fb-ov-send {\n margin-left: auto;\n font-family: inherit;\n font-size: 13px;\n font-weight: 600;\n color: var(--fb-color-on-emphasis);\n background: var(--fb-color-accent);\n border: 0;\n border-radius: 8px;\n padding: 9px 18px;\n cursor: pointer;\n}\n.fb-ov-send:hover { background: var(--fb-color-accent-hover); }\n.fb-ov-send:disabled { opacity: 0.6; cursor: default; }\n\n.fb-ov-done { display: none; padding: 24px 18px; text-align: center; }\n.fb-ov-panel.is-sent .fb-ov-form { display: none; }\n.fb-ov-panel.is-sent .fb-ov-done { display: block; }\n.fb-ov-done__check {\n width: 40px; height: 40px; margin: 0 auto 12px;\n border-radius: 50%;\n background: #e7f6ee; color: var(--fb-color-success);\n display: flex; align-items: center; justify-content: center;\n font-size: 20px; font-weight: 700;\n}\n.fb-ov-done__title { font-size: 15px; font-weight: 600; color: var(--fb-color-ink); }\n.fb-ov-done__sub { font-size: 12px; color: var(--fb-color-muted); margin-top: 4px; }\n\n/* The panel is hidden while any full-screen marking layer is active. */\n.fb-ov-panel.is-marking { visibility: hidden; }\n\n.fb-ov-pick,\n.fb-ov-capture,\n.fb-ov-draw { position: fixed; inset: 0; z-index: 2147483002; display: none; }\n.fb-ov-pick.is-visible,\n.fb-ov-capture.is-visible,\n.fb-ov-draw.is-visible { display: block; }\n\n/* Element-pick highlight layer \u2014 visual only, never intercepts host events. */\n.fb-ov-pick { pointer-events: none; }\n.fb-ov-highlight {\n position: absolute;\n border: 2px dashed var(--fb-color-accent);\n border-radius: 6px;\n box-shadow: 0 0 0 3px rgba(47, 111, 237, 0.14);\n transition: all 60ms ease;\n}\n.fb-ov-hint {\n position: absolute;\n top: 16px;\n left: 50%;\n transform: translateX(-50%);\n font-family: var(--fb-font-mono);\n font-size: 11px;\n color: var(--fb-color-on-emphasis);\n background: var(--fb-color-ink);\n padding: 6px 12px;\n border-radius: 7px;\n box-shadow: 0 8px 20px rgba(20, 40, 70, 0.25);\n}\n\n/* Region-capture layer \u2014 a dim wash with a live selection box. */\n.fb-ov-capture { cursor: crosshair; }\n.fb-ov-capture-dim { position: absolute; inset: 0; background: rgba(15, 23, 32, 0.28); }\n.fb-ov-capture-sel {\n position: absolute;\n border: 2px solid var(--fb-color-accent);\n border-radius: 4px;\n box-shadow: 0 0 0 100vmax rgba(15, 23, 32, 0.32);\n}\n\n/* Draw layer \u2014 the mark SVG, the region frame, the label input, and the toolbar. */\n.fb-ov-draw-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }\n.fb-ov-draw-frame {\n position: absolute;\n border: 1px solid rgba(47, 111, 237, 0.5);\n border-radius: 4px;\n box-shadow: 0 0 0 100vmax rgba(15, 23, 32, 0.18);\n pointer-events: none;\n}\n.fb-ov-draw-text {\n position: absolute;\n display: none;\n font-family: var(--fb-font-sans);\n font-size: 15px;\n font-weight: 600;\n color: var(--fb-color-bug);\n background: rgba(255, 255, 255, 0.92);\n border: 1px dashed var(--fb-color-bug);\n border-radius: 4px;\n padding: 2px 6px;\n outline: none;\n z-index: 3;\n}\n.fb-ov-draw-toolbar {\n position: fixed;\n left: 50%;\n bottom: 26px;\n transform: translateX(-50%);\n display: flex;\n align-items: center;\n gap: 5px;\n background: var(--fb-color-ink);\n border-radius: 12px;\n padding: 7px;\n box-shadow: 0 16px 40px rgba(15, 40, 70, 0.4);\n}\n.fb-ov-drawtool {\n width: 34px;\n height: 34px;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 0;\n border-radius: 8px;\n background: transparent;\n color: #c4ccd4;\n cursor: pointer;\n font-family: inherit;\n}\n.fb-ov-drawtool:hover { background: rgba(255, 255, 255, 0.08); color: #fff; }\n.fb-ov-drawtool.is-active { background: var(--fb-color-accent); color: var(--fb-color-on-emphasis); }\n.fb-ov-drawtool__t { font-weight: 700; font-size: 14px; line-height: 1; }\n.fb-ov-draw-divider { width: 1px; height: 22px; background: #2a343e; margin: 0 3px; }\n.fb-ov-draw-undo {\n width: 34px;\n height: 34px;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 0;\n border-radius: 8px;\n background: transparent;\n color: #c4ccd4;\n cursor: pointer;\n}\n.fb-ov-draw-undo:hover:not(:disabled) { background: rgba(255, 255, 255, 0.08); color: #fff; }\n.fb-ov-draw-undo:disabled { opacity: 0.4; cursor: default; }\n.fb-ov-draw-cancel {\n height: 34px;\n padding: 0 12px;\n border: 0;\n border-radius: 8px;\n background: transparent;\n color: var(--fb-color-faint);\n font-family: inherit;\n font-size: 12.5px;\n cursor: pointer;\n}\n.fb-ov-draw-cancel:hover { color: #fff; }\n.fb-ov-draw-attach {\n height: 34px;\n padding: 0 15px;\n border: 0;\n border-radius: 8px;\n background: var(--fb-color-accent);\n color: var(--fb-color-on-emphasis);\n font-family: inherit;\n font-size: 12.5px;\n font-weight: 600;\n cursor: pointer;\n}\n.fb-ov-draw-attach:hover { background: var(--fb-color-accent-hover); }\n\n@media (prefers-reduced-motion: reduce) {\n .fb-ov-highlight { transition: none; }\n}\n";
|
package/dist/overlay.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { IdentityInputs, ProjectGate, ReporterTier } from "./boot";
|
|
2
2
|
import type { Breadcrumb } from "./breadcrumbs";
|
|
3
|
+
import type { ReporterDisplay } from "./invite";
|
|
3
4
|
import { type ElementPicker, type ElementPickerOptions } from "./element-picker";
|
|
5
|
+
import type { ReplaySource } from "./replay";
|
|
4
6
|
import { type BeforeSend } from "./scrub";
|
|
5
7
|
import { type Capture, type CaptureOptions } from "./screenshot";
|
|
6
8
|
import { type SubmitInput, type SubmitResult } from "./submit";
|
|
@@ -38,6 +40,14 @@ export interface OverlayConfig {
|
|
|
38
40
|
*/
|
|
39
41
|
readonly tier?: ReporterTier | null;
|
|
40
42
|
readonly sdkVersion?: string;
|
|
43
|
+
/** The host app's Release (#117, ADR-0024), validated by `init`; rides in the environment. */
|
|
44
|
+
readonly release?: string;
|
|
45
|
+
/**
|
|
46
|
+
* The Reporter's self-provided display name / email (spec §F), when the SDK holds
|
|
47
|
+
* them (from a redemption in this session or a prior one). They ride along with the
|
|
48
|
+
* report so a Member sees who filed it — display only, never a trust signal.
|
|
49
|
+
*/
|
|
50
|
+
readonly display?: ReporterDisplay;
|
|
41
51
|
/** Where to mount the overlay host. Defaults to `document.body`. */
|
|
42
52
|
readonly target?: HTMLElement;
|
|
43
53
|
readonly doc?: Document;
|
|
@@ -45,6 +55,8 @@ export interface OverlayConfig {
|
|
|
45
55
|
readonly deps?: Partial<OverlayDeps>;
|
|
46
56
|
/** The trace buffer whose snapshot rides on each report (spec §C). */
|
|
47
57
|
readonly buffer?: BreadcrumbSource | null;
|
|
58
|
+
/** The replay recorder whose buffered window rides on each report (ADR-0024). */
|
|
59
|
+
readonly replay?: ReplaySource | null;
|
|
48
60
|
/** Per-project client scrub hook, run at the `beforeSend` choke point. */
|
|
49
61
|
readonly beforeSend?: BeforeSend;
|
|
50
62
|
/** Run the built-in default scrubbers. Defaults to `true` (private-by-default). */
|
package/dist/replay.d.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { type eventWithTime } from "@rrweb/types";
|
|
2
|
+
/**
|
|
3
|
+
* Buffered session replay — the rrweb capture half of issue #189 (ADR-0024).
|
|
4
|
+
*
|
|
5
|
+
* The recorder runs continuously from mount ("the moment before matters",
|
|
6
|
+
* VISION principle 4) but never streams: rrweb events accumulate in **two
|
|
7
|
+
* checkout-delimited buckets**, and every {@link REPLAY_CHECKOUT_MS} rrweb takes
|
|
8
|
+
* a fresh full DOM snapshot (a "checkout") that rotates the buckets. A report's
|
|
9
|
+
* {@link ReplayRecorder.snapshot} is the concatenation of both buckets, so it
|
|
10
|
+
* always starts at a full snapshot and carries between one and two checkout
|
|
11
|
+
* intervals of lead-up — the bounded-window posture the Trace already follows,
|
|
12
|
+
* never an unbounded full-session stream.
|
|
13
|
+
*
|
|
14
|
+
* Masking is applied by rrweb **at serialization time, inside the page** — the
|
|
15
|
+
* same client-side, private-by-default boundary as the screenshot and the Trace
|
|
16
|
+
* (spec 0003 §C, VISION §5): every input is masked (`maskAllInputs`) and, by
|
|
17
|
+
* default, **all text** is masked (`maskTextSelector: "*"`), so the recording
|
|
18
|
+
* that leaves the browser carries layout and interaction, not content. A
|
|
19
|
+
* Project that accepts the trade-off can narrow the text mask via
|
|
20
|
+
* {@link ReplayOptions.maskTextSelector} (rrweb 2.1 has no per-element unmask
|
|
21
|
+
* selector, so narrowing the mask selector is the escape hatch). The SDK's own
|
|
22
|
+
* chrome — launcher and overlay hosts — is blocked from the recording entirely,
|
|
23
|
+
* mirroring how the screenshot strips it.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* How often rrweb takes a checkout full snapshot, which is also the bucket
|
|
27
|
+
* rotation period: a snapshot carries 1–2 of these intervals of lead-up
|
|
28
|
+
* (30–60 s), Sentry's buffered-replay ballpark.
|
|
29
|
+
*/
|
|
30
|
+
export declare const REPLAY_CHECKOUT_MS = 30000;
|
|
31
|
+
/**
|
|
32
|
+
* The SDK's own host elements (dom.ts markers), excluded from the recording the
|
|
33
|
+
* way the screenshot's clone walk strips them.
|
|
34
|
+
*/
|
|
35
|
+
export declare const FIXBACK_BLOCK_SELECTOR = "[data-fixback-root],[data-fixback-overlay]";
|
|
36
|
+
/**
|
|
37
|
+
* Media elements blocked from the recording by default (Sentry's `blockAllMedia`
|
|
38
|
+
* posture, adopted): text masking cannot redact what an image shows, so media
|
|
39
|
+
* renders as a same-size placeholder in replay unless a Project opts out via
|
|
40
|
+
* {@link ReplayOptions.blockMedia}.
|
|
41
|
+
*/
|
|
42
|
+
export declare const FIXBACK_MEDIA_SELECTOR = "img,image,svg,video,object,picture,embed,map,audio";
|
|
43
|
+
/** Tuning for the replay recorder. All optional; the defaults are the contract. */
|
|
44
|
+
export interface ReplayOptions {
|
|
45
|
+
/**
|
|
46
|
+
* Mask **all** text content (default `true`, private-by-default). `false`
|
|
47
|
+
* records text in the clear — inputs stay masked regardless.
|
|
48
|
+
*/
|
|
49
|
+
readonly maskText?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Which elements' text rrweb masks when {@link maskText} is on. Defaults to
|
|
52
|
+
* `"*"` (everything); narrow it to unmask known-safe regions.
|
|
53
|
+
*/
|
|
54
|
+
readonly maskTextSelector?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Block media content — images, video, audio, SVG — from the recording
|
|
57
|
+
* (default `true`, {@link FIXBACK_MEDIA_SELECTOR}): what a picture shows
|
|
58
|
+
* cannot be text-masked, so media is a placeholder in replay. `false` records
|
|
59
|
+
* it (an internal/beta Project trading privacy for fidelity, VISION §5).
|
|
60
|
+
*/
|
|
61
|
+
readonly blockMedia?: boolean;
|
|
62
|
+
/** Extra elements to exclude from the recording, alongside the SDK's own chrome. */
|
|
63
|
+
readonly blockSelector?: string;
|
|
64
|
+
}
|
|
65
|
+
/** One rrweb event as recorded; the wire payload is an array of these. */
|
|
66
|
+
export type ReplayEvent = eventWithTime;
|
|
67
|
+
/** The buffered window a report ships: rrweb events starting at a full snapshot. */
|
|
68
|
+
export interface ReplaySnapshot {
|
|
69
|
+
readonly events: readonly ReplayEvent[];
|
|
70
|
+
}
|
|
71
|
+
/** A read-only view of the recorder the report surfaces attach from. */
|
|
72
|
+
export interface ReplaySource {
|
|
73
|
+
/** The current buffered window, or `null` when nothing replayable exists. */
|
|
74
|
+
snapshot(): ReplaySnapshot | null;
|
|
75
|
+
}
|
|
76
|
+
/** A running replay recorder. */
|
|
77
|
+
export interface ReplayRecorder extends ReplaySource {
|
|
78
|
+
/** Stop recording and release the buffers. Safe to call more than once. */
|
|
79
|
+
stop(): void;
|
|
80
|
+
}
|
|
81
|
+
/** The slice of rrweb's record options the recorder sets. */
|
|
82
|
+
export interface ReplayRecordOptions {
|
|
83
|
+
readonly emit: (event: ReplayEvent, isCheckout?: boolean) => void;
|
|
84
|
+
readonly checkoutEveryNms?: number;
|
|
85
|
+
readonly maskAllInputs?: boolean;
|
|
86
|
+
readonly maskTextSelector?: string;
|
|
87
|
+
readonly blockSelector?: string;
|
|
88
|
+
readonly slimDOMOptions?: "all";
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The slice of rrweb's `record` the recorder consumes; injectable for tests.
|
|
92
|
+
* (A concrete signature — rrweb's own is generic, which mocks can't satisfy.)
|
|
93
|
+
*/
|
|
94
|
+
export type RecordFn = (options: ReplayRecordOptions) => (() => void) | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* Start the buffered replay recorder. Never throws: an rrweb failure (an exotic
|
|
97
|
+
* DOM, an unsupported environment) degrades to a recorder whose `snapshot()` is
|
|
98
|
+
* `null`, so a Fixback problem never surfaces on the host page and a report
|
|
99
|
+
* simply ships without replay.
|
|
100
|
+
*/
|
|
101
|
+
export declare function startReplayRecorder(options?: ReplayOptions, recordFn?: RecordFn): ReplayRecorder;
|
package/dist/report.d.ts
CHANGED
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { type Annotation, type Mark, type Rect } from "./annotation";
|
|
12
12
|
import type { Breadcrumb } from "./breadcrumbs";
|
|
13
|
-
/** The Kind a Reporter tags a report with. Mirrors the server's `ISSUE_KINDS`. */
|
|
14
|
-
export type IssueKind = "bug" | "improvement" | "idea";
|
|
15
13
|
/**
|
|
16
14
|
* Where a Feedback came from — a human in the overlay (`reporter`, the default) or
|
|
17
15
|
* the SDK's automatic error capture (`auto`). Mirrors the server's `FEEDBACK_SOURCES`;
|
|
@@ -42,7 +40,34 @@ export interface CaptureEnvironment {
|
|
|
42
40
|
readonly viewportHeight?: number;
|
|
43
41
|
readonly browser?: string;
|
|
44
42
|
readonly sdkVersion?: string;
|
|
43
|
+
/**
|
|
44
|
+
* The host app's **Release** — the build identifier the builder configured at
|
|
45
|
+
* `init` (#117, ADR-0024). Uploaded sourcemaps are keyed by it, so the server
|
|
46
|
+
* can symbolicate this session's stack traces against the exact build that
|
|
47
|
+
* produced them. Omitted when the Project doesn't set one.
|
|
48
|
+
*/
|
|
49
|
+
readonly release?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* One structured frame of a captured stack trace (#117, ADR-0024), top of stack
|
|
53
|
+
* first — the server's `errorFrames` wire shape (mirrors `@fixback/shared`'s
|
|
54
|
+
* `CapturedStackFrame` by value, per this file's vendoring rule). `file` is the
|
|
55
|
+
* scrubbed script URL; `line`/`column` are 1-based as browsers report them.
|
|
56
|
+
*/
|
|
57
|
+
export interface CapturedFrame {
|
|
58
|
+
readonly file: string;
|
|
59
|
+
readonly line: number;
|
|
60
|
+
readonly column: number | null;
|
|
61
|
+
readonly function: string | null;
|
|
45
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Validate + trim a builder-supplied release (#117, ADR-0024), mirroring the
|
|
65
|
+
* server's rules by value: 1–100 visible characters, no whitespace, control
|
|
66
|
+
* characters, or path separators, and not a reserved name. Returns `undefined`
|
|
67
|
+
* for an invalid value — the SDK drops it (with a warning at `init`) rather than
|
|
68
|
+
* shipping a value the server would discard.
|
|
69
|
+
*/
|
|
70
|
+
export declare function normaliseRelease(value: string): string | undefined;
|
|
46
71
|
/**
|
|
47
72
|
* The JSON content of a feedback submission — the object serialised into the
|
|
48
73
|
* multipart `payload` part next to the `key` and identity evidence. Every field
|
|
@@ -52,7 +77,15 @@ export interface CaptureEnvironment {
|
|
|
52
77
|
*/
|
|
53
78
|
export interface ReportContent {
|
|
54
79
|
readonly comment?: string;
|
|
55
|
-
|
|
80
|
+
/**
|
|
81
|
+
* The Reporter's self-provided display **name / email** (spec §F), captured in the
|
|
82
|
+
* invite onboarding step and riding along with every submission so a Member sees
|
|
83
|
+
* who reported an Issue. **Display only, never a trust signal** — the server derives
|
|
84
|
+
* the tier from identity evidence alone and ignores these. Omitted when the Reporter
|
|
85
|
+
* gave none (a Public/anonymous visitor).
|
|
86
|
+
*/
|
|
87
|
+
readonly reporterName?: string;
|
|
88
|
+
readonly reporterEmail?: string;
|
|
56
89
|
readonly url?: string;
|
|
57
90
|
readonly environment?: CaptureEnvironment;
|
|
58
91
|
readonly annotation?: Annotation;
|
|
@@ -67,6 +100,13 @@ export interface ReportContent {
|
|
|
67
100
|
readonly errorSignature?: string;
|
|
68
101
|
/** For `source: auto` only — the running occurrence count within the session (spec §E). */
|
|
69
102
|
readonly occurrences?: number;
|
|
103
|
+
/**
|
|
104
|
+
* For `source: auto` only — the captured error's parsed stack frames (#117,
|
|
105
|
+
* ADR-0024), top of stack first, capped client-side. The analysis worker
|
|
106
|
+
* matches them against the release's uploaded sourcemaps to write the Issue's
|
|
107
|
+
* Code-area pointer.
|
|
108
|
+
*/
|
|
109
|
+
readonly errorFrames?: readonly CapturedFrame[];
|
|
70
110
|
}
|
|
71
111
|
/**
|
|
72
112
|
* What the overlay hands to {@link assembleContent} when the Reporter sends. The
|
|
@@ -74,7 +114,6 @@ export interface ReportContent {
|
|
|
74
114
|
* folds whatever is present into the structured {@link Annotation}.
|
|
75
115
|
*/
|
|
76
116
|
export interface ReportDraft {
|
|
77
|
-
readonly kind?: IssueKind;
|
|
78
117
|
readonly comment?: string;
|
|
79
118
|
readonly element?: SelectedElement;
|
|
80
119
|
readonly region?: Rect;
|
|
@@ -82,6 +121,9 @@ export interface ReportDraft {
|
|
|
82
121
|
readonly url?: string;
|
|
83
122
|
readonly environment?: CaptureEnvironment;
|
|
84
123
|
readonly trace?: readonly Breadcrumb[];
|
|
124
|
+
/** The Reporter's self-provided display name / email (spec §F), if the SDK holds them. */
|
|
125
|
+
readonly reporterName?: string;
|
|
126
|
+
readonly reporterEmail?: string;
|
|
85
127
|
}
|
|
86
128
|
/**
|
|
87
129
|
* Read the capture environment off a window: the viewport size, the browser's
|
|
@@ -89,7 +131,7 @@ export interface ReportDraft {
|
|
|
89
131
|
* positive number (a headless/zero viewport records nothing rather than an
|
|
90
132
|
* invalid `0`, which the server would reject).
|
|
91
133
|
*/
|
|
92
|
-
export declare function collectEnvironment(win: Window, sdkVersion: string): CaptureEnvironment;
|
|
134
|
+
export declare function collectEnvironment(win: Window, sdkVersion: string, release?: string): CaptureEnvironment;
|
|
93
135
|
/**
|
|
94
136
|
* Assemble the ingest content from what the Reporter composed. Empty pieces are
|
|
95
137
|
* dropped rather than sent as blanks: a whitespace-only comment, an absent
|
package/dist/screenshot.d.ts
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Client-side masked screenshot capture (spec MVP §E; ticket #54).
|
|
2
|
+
* Client-side masked screenshot capture (spec MVP §E; ticket #54; ADR-0014).
|
|
3
3
|
*
|
|
4
|
-
* The capture is **private-by-default**: input values are masked and the SDK's
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
4
|
+
* The capture is **private-by-default**: input values are masked and the SDK's own
|
|
5
|
+
* UI is excluded from a *clone* of the view **before** anything is rasterised, so no
|
|
6
|
+
* unmasked text and none of Fixback's chrome ever reaches the image.
|
|
7
|
+
*
|
|
8
|
+
* The rasterisation itself is delegated to `modern-screenshot` (ADR-0014): it clones
|
|
9
|
+
* the target, inlines the page's real styles **and** its fonts and images as data
|
|
10
|
+
* URIs, then draws the result through an SVG `<foreignObject>` onto a `<canvas>`.
|
|
11
|
+
* That closes the fidelity gap a hand-rolled `<foreignObject>` left open — external
|
|
12
|
+
* stylesheets, web fonts, and same-origin images now render — so the shot matches
|
|
13
|
+
* what the Reporter saw and lines up with the annotation's viewport-space marks.
|
|
14
|
+
*
|
|
15
|
+
* We keep the privacy guarantee by driving it through two of its hooks: `filter`
|
|
16
|
+
* drops Fixback's own host elements, and `onCloneNode` masks the clone's inputs —
|
|
17
|
+
* both run on the library's internal clone, before it embeds or serialises anything,
|
|
18
|
+
* so the live page is never touched and no real value reaches the raster. The backend
|
|
19
|
+
* is injectable so the pipeline (and the masking-before-capture guarantee) is testable
|
|
20
|
+
* without a real canvas, and it fails quietly: any problem resolves to `null` and the
|
|
21
|
+
* report is simply sent without a screenshot.
|
|
13
22
|
*/
|
|
14
23
|
/** The character private input content is replaced with. */
|
|
15
24
|
export declare const MASK_CHAR = "\u2022";
|
|
@@ -29,12 +38,19 @@ export interface Capture {
|
|
|
29
38
|
}
|
|
30
39
|
/** The screenshot filename extension for a {@link Capture}'s content type (defaults to `png`). */
|
|
31
40
|
export declare function extensionFor(type: string): string;
|
|
32
|
-
/**
|
|
33
|
-
|
|
41
|
+
/** How the SDK prepares a capture: exclude Fixback's UI, mask the clone's inputs. */
|
|
42
|
+
interface CaptureHooks {
|
|
43
|
+
/** Keep a node in the shot? Returns `false` for Fixback's own host elements. */
|
|
44
|
+
readonly filter: (node: Node) => boolean;
|
|
45
|
+
/** Mask private input content on the library's internal clone, pre-raster. */
|
|
46
|
+
readonly onCloneNode: (clone: Node) => void;
|
|
47
|
+
}
|
|
48
|
+
/** Turns a DOM subtree into image bytes at a given size (injectable for tests). */
|
|
49
|
+
export type CaptureBackend = (target: Element, meta: {
|
|
34
50
|
width: number;
|
|
35
51
|
height: number;
|
|
36
52
|
type: string;
|
|
37
|
-
}) => Promise<Blob | null>;
|
|
53
|
+
} & CaptureHooks) => Promise<Blob | null>;
|
|
38
54
|
/** Options for {@link captureView}. */
|
|
39
55
|
export interface CaptureOptions {
|
|
40
56
|
/** The element to capture. Defaults to the document element (the full view). */
|
|
@@ -43,12 +59,15 @@ export interface CaptureOptions {
|
|
|
43
59
|
readonly win?: Window;
|
|
44
60
|
/** Output content type. Defaults to `image/png`. */
|
|
45
61
|
readonly type?: string;
|
|
46
|
-
/** Override the raster
|
|
47
|
-
readonly
|
|
62
|
+
/** Override the raster backend (the default is `modern-screenshot`). */
|
|
63
|
+
readonly capture?: CaptureBackend;
|
|
48
64
|
}
|
|
49
65
|
/**
|
|
50
|
-
* Capture the current view as a masked screenshot.
|
|
51
|
-
*
|
|
52
|
-
*
|
|
66
|
+
* Capture the current view as a masked screenshot. Delegates the raster to the
|
|
67
|
+
* backend ({@link captureViaModernScreenshot} by default), which excludes the SDK's
|
|
68
|
+
* own UI and masks input values on its internal clone — **before** it embeds or
|
|
69
|
+
* serialises anything — then returns the image bytes, or `null` if capture wasn't
|
|
70
|
+
* possible.
|
|
53
71
|
*/
|
|
54
72
|
export declare function captureView(options?: CaptureOptions): Promise<Capture | null>;
|
|
73
|
+
export {};
|
package/dist/scrub.d.ts
CHANGED
|
@@ -35,10 +35,11 @@ export interface BeforeSendOptions {
|
|
|
35
35
|
export declare function redactPii(text: string): string;
|
|
36
36
|
/**
|
|
37
37
|
* Strip the sensitive parts of a URL: userinfo credentials
|
|
38
|
-
* (`scheme://user:pass@host`), the entire query string,
|
|
39
|
-
* fragment (one that carries `key=value`)
|
|
40
|
-
*
|
|
41
|
-
* throw.
|
|
38
|
+
* (`scheme://user:pass@host`), the entire query string, a token-bearing
|
|
39
|
+
* fragment (one that carries `key=value`), and PII (emails, long digit runs) in
|
|
40
|
+
* the **path segments** (#139). Plain hash routes (`#/checkout`) are kept. Works
|
|
41
|
+
* on absolute and relative URLs alike, with no dependency and no throw. The host
|
|
42
|
+
* (authority) is never redacted — only the path and any surviving fragment route.
|
|
42
43
|
*/
|
|
43
44
|
export declare function scrubUrl(url: string): string;
|
|
44
45
|
/**
|
package/dist/styles.d.ts
CHANGED
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
* **Feedback** pill that **hover-peeks**, can be **tucked away** (sliding off
|
|
12
12
|
* behind an edge nub, with a corner hover-zone to bring it back), a first-visit
|
|
13
13
|
* **welcome toast**, and a **reduce-motion** mode that stills the pulse. Every
|
|
14
|
-
* piece — pill, nub, corner zone,
|
|
14
|
+
* piece — pill, nub, corner zone, welcome toast — lives in this one Shadow DOM.
|
|
15
15
|
*
|
|
16
16
|
* Motion is driven by two host-element attributes the mount toggles:
|
|
17
17
|
* `data-fb-hidden` (tucked away) and `data-fb-peeking` (peeked back on hover);
|
|
18
18
|
* `data-fb-reduce-motion` (or the OS `prefers-reduced-motion`) stills it all.
|
|
19
19
|
*/
|
|
20
|
-
export declare const LAUNCHER_STYLES = "\n:host {\n /* Vendored Signal tokens (packages/ui/src/tokens.css). */\n --fb-color-accent: #2f6fed;\n --fb-color-accent-hover: #245fd0;\n --fb-color-on-emphasis: #ffffff;\n --fb-color-text: #0f1720;\n /* Inverse (dark) surface for the toasts \u2014 Signal --fb-ink-900. */\n --fb-color-surface-inverse: #0f1720;\n --fb-font-sans: \"IBM Plex Sans\", system-ui, -apple-system, \"Segoe UI\", Roboto,\n Helvetica, Arial, sans-serif;\n\n display: block;\n color: var(--fb-color-text);\n font-family: var(--fb-font-sans);\n font-size: 13px;\n line-height: 1.4;\n -webkit-font-smoothing: antialiased;\n}\n\n/* The pill: Feedback button + divider + tuck control, anchored bottom-right. */\n.fb-launcher {\n position: fixed;\n right: 20px;\n bottom: 20px;\n z-index: 2;\n display: inline-flex;\n align-items: stretch;\n box-sizing: border-box;\n height: 40px;\n margin: 0;\n border-radius: 999px;\n background: var(--fb-color-accent);\n box-shadow:\n 0 6px 18px rgba(15, 23, 32, 0.16),\n 0 1px 2px rgba(15, 23, 32, 0.12);\n transform: translateX(0);\n transition:\n transform 340ms cubic-bezier(0.2, 0.8, 0.3, 1),\n background-color 120ms ease;\n}\n\n.fb-launcher:hover {\n background: var(--fb-color-accent-hover);\n}\n\n.fb-launcher__button {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n box-sizing: border-box;\n height: 100%;\n margin: 0;\n padding: 0 6px 0 16px;\n border: 0;\n background: transparent;\n color: var(--fb-color-on-emphasis);\n font-family: inherit;\n font-size: 13px;\n font-weight: 600;\n letter-spacing: 0.01em;\n cursor: pointer;\n}\n\n.fb-launcher__button:focus-visible,\n.fb-launcher__tuck:focus-visible {\n outline: 2px solid var(--fb-color-on-emphasis);\n outline-offset: -3px;\n border-radius: 999px;\n}\n\n.fb-launcher__icon {\n display: block;\n flex: none;\n width: 16px;\n height: 16px;\n}\n\n.fb-launcher__label {\n white-space: nowrap;\n}\n\n.fb-launcher__divider {\n width: 1px;\n margin: 9px 0;\n flex: none;\n background: rgba(255, 255, 255, 0.28);\n}\n\n.fb-launcher__tuck {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: 32px;\n height: 100%;\n padding: 0;\n border: 0;\n background: transparent;\n color: rgba(255, 255, 255, 0.82);\n cursor: pointer;\n}\n\n.fb-launcher__tuck:hover {\n color: var(--fb-color-on-emphasis);\n}\n\n.fb-launcher__tuck-icon {\n display: block;\n width: 14px;\n height: 14px;\n}\n\n/* The corner hover-zone that peeks a tucked launcher back \u2014 inert until tucked,\n * so it never swallows the host page's own bottom-right clicks. */\n.fb-launcher__peekzone {\n position: fixed;\n right: 0;\n bottom: 0;\n z-index: 1;\n width: 160px;\n height: 160px;\n pointer-events: none;\n}\n\n/* The edge nub: the visible re-reveal affordance, hidden until tucked. */\n.fb-launcher__nub {\n position: fixed;\n right: 0;\n bottom: 22px;\n z-index: 2;\n width: 13px;\n height: 42px;\n border-radius: 9px 0 0 9px;\n background: var(--fb-color-accent);\n box-shadow: -6px 5px 18px rgba(47, 111, 237, 0.4);\n cursor: pointer;\n opacity: 0;\n transform: translateX(10px);\n pointer-events: none;\n transition:\n opacity 220ms ease,\n transform 220ms ease;\n}\n\n/* Tucked and not peeking: slide the pill off, reveal the nub, arm the zone. */\n:host([data-fb-hidden]:not([data-fb-peeking])) .fb-launcher {\n transform: translateX(calc(100% + 30px));\n}\n\n:host([data-fb-hidden]:not([data-fb-peeking])) .fb-launcher__nub {\n opacity: 1;\n transform: none;\n pointer-events: auto;\n}\n\n:host([data-fb-hidden]) .fb-launcher__peekzone {\n pointer-events: auto;\n}\n\n/*
|
|
20
|
+
export declare const LAUNCHER_STYLES = "\n:host {\n /* Vendored Signal tokens (packages/ui/src/tokens.css). */\n --fb-color-accent: #2f6fed;\n --fb-color-accent-hover: #245fd0;\n --fb-color-on-emphasis: #ffffff;\n --fb-color-text: #0f1720;\n /* Inverse (dark) surface for the toasts \u2014 Signal --fb-ink-900. */\n --fb-color-surface-inverse: #0f1720;\n --fb-font-sans: \"IBM Plex Sans\", system-ui, -apple-system, \"Segoe UI\", Roboto,\n Helvetica, Arial, sans-serif;\n\n display: block;\n color: var(--fb-color-text);\n font-family: var(--fb-font-sans);\n font-size: 13px;\n line-height: 1.4;\n -webkit-font-smoothing: antialiased;\n}\n\n/* The pill: Feedback button + divider + tuck control, anchored bottom-right. */\n.fb-launcher {\n position: fixed;\n right: 20px;\n bottom: 20px;\n z-index: 2;\n display: inline-flex;\n align-items: stretch;\n box-sizing: border-box;\n height: 40px;\n margin: 0;\n border-radius: 999px;\n background: var(--fb-color-accent);\n box-shadow:\n 0 6px 18px rgba(15, 23, 32, 0.16),\n 0 1px 2px rgba(15, 23, 32, 0.12);\n transform: translateX(0);\n transition:\n transform 340ms cubic-bezier(0.2, 0.8, 0.3, 1),\n background-color 120ms ease;\n}\n\n.fb-launcher:hover {\n background: var(--fb-color-accent-hover);\n}\n\n.fb-launcher__button {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n box-sizing: border-box;\n height: 100%;\n margin: 0;\n padding: 0 6px 0 16px;\n border: 0;\n background: transparent;\n color: var(--fb-color-on-emphasis);\n font-family: inherit;\n font-size: 13px;\n font-weight: 600;\n letter-spacing: 0.01em;\n cursor: pointer;\n}\n\n.fb-launcher__button:focus-visible,\n.fb-launcher__tuck:focus-visible {\n outline: 2px solid var(--fb-color-on-emphasis);\n outline-offset: -3px;\n border-radius: 999px;\n}\n\n.fb-launcher__icon {\n display: block;\n flex: none;\n width: 16px;\n height: 16px;\n}\n\n.fb-launcher__label {\n white-space: nowrap;\n}\n\n.fb-launcher__divider {\n width: 1px;\n margin: 9px 0;\n flex: none;\n background: rgba(255, 255, 255, 0.28);\n}\n\n.fb-launcher__tuck {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: 32px;\n height: 100%;\n padding: 0;\n border: 0;\n background: transparent;\n color: rgba(255, 255, 255, 0.82);\n cursor: pointer;\n}\n\n.fb-launcher__tuck:hover {\n color: var(--fb-color-on-emphasis);\n}\n\n.fb-launcher__tuck-icon {\n display: block;\n width: 14px;\n height: 14px;\n transition: transform 340ms cubic-bezier(0.2, 0.8, 0.3, 1);\n}\n\n/* Tucked away: mirror the chevron so it points left \u2014 back into view. */\n:host([data-fb-hidden]) .fb-launcher__tuck-icon {\n transform: scaleX(-1);\n}\n\n/* The corner hover-zone that peeks a tucked launcher back \u2014 inert until tucked,\n * so it never swallows the host page's own bottom-right clicks. */\n.fb-launcher__peekzone {\n position: fixed;\n right: 0;\n bottom: 0;\n z-index: 1;\n width: 160px;\n height: 160px;\n pointer-events: none;\n}\n\n/* The edge nub: the visible re-reveal affordance, hidden until tucked. */\n.fb-launcher__nub {\n position: fixed;\n right: 0;\n bottom: 22px;\n z-index: 2;\n width: 13px;\n height: 42px;\n border-radius: 9px 0 0 9px;\n background: var(--fb-color-accent);\n box-shadow: -6px 5px 18px rgba(47, 111, 237, 0.4);\n cursor: pointer;\n opacity: 0;\n transform: translateX(10px);\n pointer-events: none;\n transition:\n opacity 220ms ease,\n transform 220ms ease;\n}\n\n/* Tucked and not peeking: slide the pill off, reveal the nub, arm the zone. */\n:host([data-fb-hidden]:not([data-fb-peeking])) .fb-launcher {\n transform: translateX(calc(100% + 30px));\n}\n\n:host([data-fb-hidden]:not([data-fb-peeking])) .fb-launcher__nub {\n opacity: 1;\n transform: none;\n pointer-events: auto;\n}\n\n:host([data-fb-hidden]) .fb-launcher__peekzone {\n pointer-events: auto;\n}\n\n/* The first-visit welcome toast, above the pill. */\n.fb-launcher__welcome {\n position: fixed;\n right: 20px;\n bottom: 70px;\n z-index: 5;\n box-sizing: border-box;\n max-width: 210px;\n padding: 9px 13px;\n color: #fff;\n font-size: 12px;\n line-height: 1.45;\n background: var(--fb-color-surface-inverse);\n border-radius: 10px;\n box-shadow: 0 12px 30px rgba(15, 40, 70, 0.32);\n animation: fbToast 300ms ease both;\n}\n\n.fb-launcher__welcome strong {\n color: #8fc0ff;\n font-weight: 600;\n}\n\n/* The pulse that draws the eye while the welcome shows. */\n.fb-launcher--pulse {\n animation: fbPulse 1.8s ease-in-out 2;\n}\n\n@keyframes fbPulse {\n 0%,\n 100% {\n box-shadow:\n 0 6px 18px rgba(15, 23, 32, 0.16),\n 0 0 0 0 rgba(47, 111, 237, 0.45);\n }\n 50% {\n box-shadow:\n 0 6px 18px rgba(15, 23, 32, 0.16),\n 0 0 0 12px rgba(47, 111, 237, 0);\n }\n}\n\n@keyframes fbToast {\n from {\n opacity: 0;\n transform: translateY(14px);\n }\n to {\n opacity: 1;\n transform: none;\n }\n}\n\n/* Reduce motion \u2014 the explicit opt-in and the OS setting both still it all. */\n:host([data-fb-reduce-motion]) .fb-launcher,\n:host([data-fb-reduce-motion]) .fb-launcher__nub,\n:host([data-fb-reduce-motion]) .fb-launcher__tuck-icon {\n transition: none;\n}\n\n:host([data-fb-reduce-motion]) .fb-launcher--pulse,\n:host([data-fb-reduce-motion]) .fb-launcher__welcome {\n animation: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .fb-launcher,\n .fb-launcher__nub,\n .fb-launcher__tuck-icon {\n transition: none;\n }\n .fb-launcher--pulse,\n .fb-launcher__welcome {\n animation: none;\n }\n}\n";
|
package/dist/submit.d.ts
CHANGED
|
@@ -13,12 +13,20 @@ export interface SubmitScreenshot {
|
|
|
13
13
|
readonly blob: Blob;
|
|
14
14
|
readonly filename?: string;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* The buffered replay window to attach, if any — the rrweb events a
|
|
18
|
+
* `ReplaySnapshot` holds (replay.ts). Serialized into its own binary part.
|
|
19
|
+
*/
|
|
20
|
+
export interface SubmitReplay {
|
|
21
|
+
readonly events: readonly unknown[];
|
|
22
|
+
}
|
|
16
23
|
/** Everything a single feedback submission carries. */
|
|
17
24
|
export interface SubmitInput {
|
|
18
25
|
readonly key: string;
|
|
19
26
|
readonly identity?: IdentityInputs;
|
|
20
27
|
readonly content: ReportContent;
|
|
21
28
|
readonly screenshot?: SubmitScreenshot | null;
|
|
29
|
+
readonly replay?: SubmitReplay | null;
|
|
22
30
|
}
|
|
23
31
|
/** The outcome of a submission — never an exception. */
|
|
24
32
|
export type SubmitResult = {
|
package/dist/version.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The SDK's own version string, reported to ingest as `environment.sdkVersion`
|
|
3
|
-
* (spec MVP §B/§E). Kept as
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* (spec MVP §B/§E). Kept as an inlined constant rather than a runtime `package.json`
|
|
4
|
+
* import, so the published bundle stays a single self-contained file.
|
|
5
|
+
*
|
|
6
|
+
* It must equal `package.json`'s `version`. Two things keep it there: the Changesets
|
|
7
|
+
* `version` step syncs it automatically (`scripts/sync-sdk-version.mjs`, wired into
|
|
8
|
+
* `version-packages`), and `version.test.ts` fails the build if the two ever drift —
|
|
9
|
+
* so a stale dogfood version (the SDK reporting an old number on our own dashboard)
|
|
10
|
+
* can't slip through. Do not hand-edit this line to a value other than
|
|
11
|
+
* `package.json`'s version.
|
|
6
12
|
*/
|
|
7
|
-
export declare const SDK_VERSION = "0.
|
|
13
|
+
export declare const SDK_VERSION = "0.4.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fixback/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "The Fixback capture SDK — a boot-gated, self-isolating on-page feedback launcher.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -40,6 +40,11 @@
|
|
|
40
40
|
"publishConfig": {
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@rrweb/record": "^2.1.1",
|
|
45
|
+
"@rrweb/types": "^2.1.1",
|
|
46
|
+
"modern-screenshot": "^4.7.0"
|
|
47
|
+
},
|
|
43
48
|
"devDependencies": {
|
|
44
49
|
"jsdom": "^30.0.1",
|
|
45
50
|
"typescript": "5.9.3",
|