@fixback/sdk 0.3.0 → 0.5.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 +11 -10
- package/dist/annotation.d.ts +10 -73
- package/dist/boot.d.ts +6 -0
- package/dist/breadcrumbs.d.ts +2 -123
- package/dist/error-capture.d.ts +30 -29
- package/dist/fixback.umd.js +102 -68
- package/dist/fixback.umd.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.mjs +4468 -2401
- package/dist/index.mjs.map +1 -1
- package/dist/init.d.ts +23 -3
- package/dist/launcher.d.ts +2 -2
- package/dist/overlay-styles.d.ts +1 -1
- package/dist/overlay.d.ts +21 -11
- package/dist/replay.d.ts +101 -0
- package/dist/report.d.ts +23 -66
- package/dist/styles.d.ts +2 -2
- package/dist/submit.d.ts +13 -11
- package/dist/version.d.ts +1 -1
- package/package.json +4 -2
- package/dist/auto-report-backoff.d.ts +0 -47
- package/dist/screenshot.d.ts +0 -73
- package/dist/scrub.d.ts +0 -61
package/dist/init.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { BeforeSend } from "@fixback/sdk-core";
|
|
1
2
|
import { type IdentityInputs } from "./boot";
|
|
2
3
|
import { type BeforeBreadcrumb, type BreadcrumbLevel, type TraceStream } from "./breadcrumbs";
|
|
3
|
-
import type
|
|
4
|
+
import { type ReplayOptions } from "./replay";
|
|
4
5
|
/**
|
|
5
6
|
* The hosted Fixback API origin the SDK talks to by default. A self-hosted or
|
|
6
7
|
* local deployment overrides it with `apiUrl` (the dashboard's install snippet
|
|
@@ -69,26 +70,45 @@ export interface InitOptions extends IdentityInputs {
|
|
|
69
70
|
readonly scrub?: boolean;
|
|
70
71
|
/** Trace buffer tuning, or `false` to turn the buffer off entirely. */
|
|
71
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;
|
|
72
79
|
/**
|
|
73
80
|
* The dev-side override of the Project's server-served capture config (spec #122
|
|
74
81
|
* §L; ticket #138). Capture is **default-on** and normally governed per-project
|
|
75
82
|
* from the dashboard, surfaced on the boot answer; set a stream here to override
|
|
76
83
|
* what the server serves for it — `{ network: false }` turns network capture off
|
|
77
84
|
* even where the Project leaves it on, and a stream left unset follows the served
|
|
78
|
-
* config. Independent of {@link trace} `false`, which turns the whole buffer off
|
|
85
|
+
* config. Independent of {@link trace} `false`, which turns the whole buffer off,
|
|
86
|
+
* and of {@link replay} `false`, which turns the recorder off.
|
|
79
87
|
*/
|
|
80
88
|
readonly capture?: {
|
|
81
89
|
readonly console?: boolean;
|
|
82
90
|
readonly network?: boolean;
|
|
91
|
+
readonly replay?: boolean;
|
|
83
92
|
};
|
|
84
93
|
/**
|
|
85
94
|
* Automatic error capture — the SDK files uncaught exceptions / unhandled
|
|
86
|
-
* rejections as `source:
|
|
95
|
+
* rejections as `source: error` Feedback with no prompt (spec §E, ADR-0011).
|
|
87
96
|
* **Default-on across all Gates**; set `false` for the per-project toggle that
|
|
88
97
|
* turns it off. It is gated by boot's `canSubmit` either way, so an auto-error is
|
|
89
98
|
* never filed where a manual report would be refused.
|
|
90
99
|
*/
|
|
91
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;
|
|
92
112
|
}
|
|
93
113
|
/**
|
|
94
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,17 +1,16 @@
|
|
|
1
|
+
import { type BeforeSend } from "@fixback/sdk-core";
|
|
1
2
|
import type { IdentityInputs, ProjectGate, ReporterTier } from "./boot";
|
|
2
3
|
import type { Breadcrumb } from "./breadcrumbs";
|
|
4
|
+
import type { ReporterDisplay } from "./invite";
|
|
3
5
|
import { type ElementPicker, type ElementPickerOptions } from "./element-picker";
|
|
4
|
-
import {
|
|
5
|
-
import { type Capture, type CaptureOptions } from "./screenshot";
|
|
6
|
+
import type { ReplaySource } from "./replay";
|
|
6
7
|
import { type SubmitInput, type SubmitResult } from "./submit";
|
|
7
8
|
/** Marks the overlay's host element in the light DOM (skipped by capture/picker). */
|
|
8
9
|
export declare const OVERLAY_ATTRIBUTE = "data-fixback-overlay";
|
|
9
|
-
type CaptureViewFn = (options?: CaptureOptions) => Promise<Capture | null>;
|
|
10
10
|
type SubmitReportFn = (apiUrl: string, input: SubmitInput, fetchImpl?: typeof fetch) => Promise<SubmitResult>;
|
|
11
11
|
type StartPickerFn = (options: ElementPickerOptions) => ElementPicker;
|
|
12
12
|
/** Injectable collaborators, defaulted to the real implementations. */
|
|
13
13
|
export interface OverlayDeps {
|
|
14
|
-
readonly captureView: CaptureViewFn;
|
|
15
14
|
readonly submitReport: SubmitReportFn;
|
|
16
15
|
readonly startElementPicker: StartPickerFn;
|
|
17
16
|
}
|
|
@@ -38,6 +37,14 @@ export interface OverlayConfig {
|
|
|
38
37
|
*/
|
|
39
38
|
readonly tier?: ReporterTier | null;
|
|
40
39
|
readonly sdkVersion?: string;
|
|
40
|
+
/** The host app's Release (#117, ADR-0024), validated by `init`; rides in the environment. */
|
|
41
|
+
readonly release?: string;
|
|
42
|
+
/**
|
|
43
|
+
* The Reporter's self-provided display name / email (spec §F), when the SDK holds
|
|
44
|
+
* them (from a redemption in this session or a prior one). They ride along with the
|
|
45
|
+
* report so a Member sees who filed it — display only, never a trust signal.
|
|
46
|
+
*/
|
|
47
|
+
readonly display?: ReporterDisplay;
|
|
41
48
|
/** Where to mount the overlay host. Defaults to `document.body`. */
|
|
42
49
|
readonly target?: HTMLElement;
|
|
43
50
|
readonly doc?: Document;
|
|
@@ -45,6 +52,8 @@ export interface OverlayConfig {
|
|
|
45
52
|
readonly deps?: Partial<OverlayDeps>;
|
|
46
53
|
/** The trace buffer whose snapshot rides on each report (spec §C). */
|
|
47
54
|
readonly buffer?: BreadcrumbSource | null;
|
|
55
|
+
/** The replay recorder whose buffered window rides on each report (ADR-0024). */
|
|
56
|
+
readonly replay?: ReplaySource | null;
|
|
48
57
|
/** Per-project client scrub hook, run at the `beforeSend` choke point. */
|
|
49
58
|
readonly beforeSend?: BeforeSend;
|
|
50
59
|
/** Run the built-in default scrubbers. Defaults to `true` (private-by-default). */
|
|
@@ -61,14 +70,15 @@ export interface OverlayController {
|
|
|
61
70
|
* Create the report overlay — the on-page panel a Reporter files a report from,
|
|
62
71
|
* built to the frozen Signal Reporter prototype (`docs/design/Fixback Reporter.dc.html`).
|
|
63
72
|
* It mounts lazily inside its own Shadow DOM (isolated from the host page, and
|
|
64
|
-
* marked so the
|
|
73
|
+
* marked so the replay recorder and element-picker skip it), opens on the launcher's
|
|
65
74
|
* `fixback:launch` seam, and offers three **composable, optional** marking layers
|
|
66
|
-
* over
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
75
|
+
* over the current view (spec §B): **element-pick**, **region-capture** (drag), and
|
|
76
|
+
* **draw** (arrow / box / pen / text, with undo / cancel / attach). The marks are
|
|
77
|
+
* viewport-space vectors composited at view time (spec §D), never baked into an
|
|
78
|
+
* image; the still they annotate is a frame of the buffered rrweb window
|
|
79
|
+
* (reconstructed downstream since ADR-0027), so the SDK rasterises nothing. On Send
|
|
80
|
+
* it assembles the structured Annotation (`{ element?, region?, marks? }`) and
|
|
81
|
+
* submits to ingest — showing a confirmation on success and failing quietly otherwise.
|
|
72
82
|
*/
|
|
73
83
|
export declare function createOverlay(config: OverlayConfig): OverlayController;
|
|
74
84
|
export {};
|
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
|
@@ -1,80 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Assembling the ingest **feedback** content from what a Reporter composed in the
|
|
3
|
+
* overlay, plus the browser-side environment capture and Release validation.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* the
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* The shared wire shapes it produces — `ReportContent`, `CaptureEnvironment`,
|
|
6
|
+
* `CapturedFrame`, `SelectedElement`, `ElementRect`, `FeedbackSource` — live in
|
|
7
|
+
* `@fixback/sdk-core` (ADR-0028), so the browser and backend SDKs speak the same
|
|
8
|
+
* shape; this module re-exports them for the SDK's public surface and holds only
|
|
9
|
+
* the browser-specific assembly (`assembleContent`, `collectEnvironment`) and the
|
|
10
|
+
* `release` validation. Keep the shapes in lock-step with the server: the JSON
|
|
11
|
+
* `payload` accepted by `POST /api/ingest/feedback`
|
|
12
|
+
* (`apps/api/src/ingest/ingest.controller.ts`).
|
|
10
13
|
*/
|
|
11
|
-
import { type
|
|
14
|
+
import { type CaptureEnvironment, type CapturedFrame, type ElementRect, type FeedbackSource, type ReportContent, type SelectedElement } from "@fixback/sdk-core";
|
|
15
|
+
import { type Mark, type Rect } from "./annotation";
|
|
12
16
|
import type { Breadcrumb } from "./breadcrumbs";
|
|
13
|
-
|
|
14
|
-
export type IssueKind = "bug" | "improvement" | "idea";
|
|
17
|
+
export type { CaptureEnvironment, CapturedFrame, ElementRect, FeedbackSource, ReportContent, SelectedElement, };
|
|
15
18
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
+
* Validate + trim a builder-supplied release (#117, ADR-0024), mirroring the
|
|
20
|
+
* server's rules by value: 1–100 visible characters, no whitespace, control
|
|
21
|
+
* characters, or path separators, and not a reserved name. Returns `undefined`
|
|
22
|
+
* for an invalid value — the SDK drops it (with a warning at `init`) rather than
|
|
23
|
+
* shipping a value the server would discard.
|
|
19
24
|
*/
|
|
20
|
-
export
|
|
21
|
-
/** The picked element's viewport rectangle, as the server's annotation `rect`. */
|
|
22
|
-
export interface ElementRect {
|
|
23
|
-
readonly x: number;
|
|
24
|
-
readonly y: number;
|
|
25
|
-
readonly width: number;
|
|
26
|
-
readonly height: number;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* The picked element — the `element` layer of an {@link Annotation}: a stable CSS
|
|
30
|
-
* selector, a readable DOM path, the tag, and the bounding rect. Exactly the
|
|
31
|
-
* server's annotation `element` object shape.
|
|
32
|
-
*/
|
|
33
|
-
export interface SelectedElement {
|
|
34
|
-
readonly selector: string;
|
|
35
|
-
readonly domPath: string;
|
|
36
|
-
readonly tag: string;
|
|
37
|
-
readonly rect: ElementRect;
|
|
38
|
-
}
|
|
39
|
-
/** The capture environment recorded alongside a report. */
|
|
40
|
-
export interface CaptureEnvironment {
|
|
41
|
-
readonly viewportWidth?: number;
|
|
42
|
-
readonly viewportHeight?: number;
|
|
43
|
-
readonly browser?: string;
|
|
44
|
-
readonly sdkVersion?: string;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* The JSON content of a feedback submission — the object serialised into the
|
|
48
|
-
* multipart `payload` part next to the `key` and identity evidence. Every field
|
|
49
|
-
* is optional: none of it feeds the server's trust decision, so a submission may
|
|
50
|
-
* carry any subset. `annotation` is the structured `{ element?, region?, marks? }`
|
|
51
|
-
* (spec §D); the screenshot is a separate binary part, never part of this JSON.
|
|
52
|
-
*/
|
|
53
|
-
export interface ReportContent {
|
|
54
|
-
readonly comment?: string;
|
|
55
|
-
readonly kind?: IssueKind;
|
|
56
|
-
readonly url?: string;
|
|
57
|
-
readonly environment?: CaptureEnvironment;
|
|
58
|
-
readonly annotation?: Annotation;
|
|
59
|
-
/** The masked breadcrumb trace buffer that rode on this report (spec §C). */
|
|
60
|
-
readonly trace?: readonly Breadcrumb[];
|
|
61
|
-
/**
|
|
62
|
-
* Provenance (spec §D/§E). Omitted for a manual report — the transport stamps the
|
|
63
|
-
* `reporter` default on the wire; set to `auto` by the SDK's error capture.
|
|
64
|
-
*/
|
|
65
|
-
readonly source?: FeedbackSource;
|
|
66
|
-
/** For `source: auto` only — the SDK's per-session error fingerprint (spec §E). */
|
|
67
|
-
readonly errorSignature?: string;
|
|
68
|
-
/** For `source: auto` only — the running occurrence count within the session (spec §E). */
|
|
69
|
-
readonly occurrences?: number;
|
|
70
|
-
}
|
|
25
|
+
export declare function normaliseRelease(value: string): string | undefined;
|
|
71
26
|
/**
|
|
72
27
|
* What the overlay hands to {@link assembleContent} when the Reporter sends. The
|
|
73
28
|
* three marking layers arrive flat (`element` / `region` / `marks`); `assembleContent`
|
|
74
29
|
* folds whatever is present into the structured {@link Annotation}.
|
|
75
30
|
*/
|
|
76
31
|
export interface ReportDraft {
|
|
77
|
-
readonly kind?: IssueKind;
|
|
78
32
|
readonly comment?: string;
|
|
79
33
|
readonly element?: SelectedElement;
|
|
80
34
|
readonly region?: Rect;
|
|
@@ -82,6 +36,9 @@ export interface ReportDraft {
|
|
|
82
36
|
readonly url?: string;
|
|
83
37
|
readonly environment?: CaptureEnvironment;
|
|
84
38
|
readonly trace?: readonly Breadcrumb[];
|
|
39
|
+
/** The Reporter's self-provided display name / email (spec §F), if the SDK holds them. */
|
|
40
|
+
readonly reporterName?: string;
|
|
41
|
+
readonly reporterEmail?: string;
|
|
85
42
|
}
|
|
86
43
|
/**
|
|
87
44
|
* Read the capture environment off a window: the viewport size, the browser's
|
|
@@ -89,7 +46,7 @@ export interface ReportDraft {
|
|
|
89
46
|
* positive number (a headless/zero viewport records nothing rather than an
|
|
90
47
|
* invalid `0`, which the server would reject).
|
|
91
48
|
*/
|
|
92
|
-
export declare function collectEnvironment(win: Window, sdkVersion: string): CaptureEnvironment;
|
|
49
|
+
export declare function collectEnvironment(win: Window, sdkVersion: string, release?: string): CaptureEnvironment;
|
|
93
50
|
/**
|
|
94
51
|
* Assemble the ingest content from what the Reporter composed. Empty pieces are
|
|
95
52
|
* dropped rather than sent as blanks: a whitespace-only comment, an absent
|
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AutoReportBackoff } from "
|
|
1
|
+
import { AutoReportBackoff } from "@fixback/sdk-core";
|
|
2
2
|
import type { IdentityInputs, ReporterTier } from "./boot";
|
|
3
3
|
import type { ReportContent } from "./report";
|
|
4
4
|
/** What ingest returns for an accepted submission (vendored server shape). */
|
|
@@ -8,17 +8,19 @@ export interface RecordedFeedback {
|
|
|
8
8
|
readonly reporterId: string;
|
|
9
9
|
readonly tier: ReporterTier;
|
|
10
10
|
}
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
/**
|
|
12
|
+
* The buffered replay window to attach, if any — the rrweb events a
|
|
13
|
+
* `ReplaySnapshot` holds (replay.ts). Serialized into its own binary part.
|
|
14
|
+
*/
|
|
15
|
+
export interface SubmitReplay {
|
|
16
|
+
readonly events: readonly unknown[];
|
|
15
17
|
}
|
|
16
18
|
/** Everything a single feedback submission carries. */
|
|
17
19
|
export interface SubmitInput {
|
|
18
20
|
readonly key: string;
|
|
19
21
|
readonly identity?: IdentityInputs;
|
|
20
22
|
readonly content: ReportContent;
|
|
21
|
-
readonly
|
|
23
|
+
readonly replay?: SubmitReplay | null;
|
|
22
24
|
}
|
|
23
25
|
/** The outcome of a submission — never an exception. */
|
|
24
26
|
export type SubmitResult = {
|
|
@@ -30,7 +32,7 @@ export type SubmitResult = {
|
|
|
30
32
|
readonly status?: number;
|
|
31
33
|
} | {
|
|
32
34
|
/**
|
|
33
|
-
* A `source:
|
|
35
|
+
* A `source: error` report shed under ingest backpressure (spec §E/§H) — either
|
|
34
36
|
* held locally because the window is still open, or answered `429` by ingest,
|
|
35
37
|
* which opened/extended the window. Carries the seconds left to wait. Manual
|
|
36
38
|
* reports never produce this.
|
|
@@ -43,11 +45,11 @@ export type SubmitResult = {
|
|
|
43
45
|
export declare function feedbackEndpoint(apiUrl: string): string;
|
|
44
46
|
/**
|
|
45
47
|
* Submit a report to ingest. Assembles the `payload` JSON (key + identity +
|
|
46
|
-
* content, with an explicit `source` stamped) and the optional
|
|
47
|
-
*
|
|
48
|
-
*
|
|
48
|
+
* content, with an explicit `source` stamped) and the optional `replay` window
|
|
49
|
+
* into a `FormData`, posts it, and resolves to the recorded Feedback on success
|
|
50
|
+
* or a named failure otherwise.
|
|
49
51
|
*
|
|
50
|
-
* A `source:
|
|
52
|
+
* A `source: error` report first consults the shared backpressure window: while it
|
|
51
53
|
* is open the report is dropped without a request (spec §E/§H). Ingest's `429` +
|
|
52
54
|
* `Retry-After` opens/extends that window (default 60 s if the header is absent).
|
|
53
55
|
* Manual reports (`source: reporter`, the default) never consult the window and a
|
package/dist/version.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fixback/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -41,7 +41,9 @@
|
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"
|
|
44
|
+
"@rrweb/record": "^2.1.1",
|
|
45
|
+
"@rrweb/types": "^2.1.1",
|
|
46
|
+
"@fixback/sdk-core": "0.2.0"
|
|
45
47
|
},
|
|
46
48
|
"devDependencies": {
|
|
47
49
|
"jsdom": "^30.0.1",
|
|
@@ -1,47 +0,0 @@
|
|
|
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
|
-
}
|