@fixback/sdk 0.5.0 → 0.6.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 -2
- package/dist/capture/extract-dom.d.ts +33 -0
- package/dist/connect.d.ts +47 -0
- package/dist/dom.d.ts +7 -19
- package/dist/element-picker.d.ts +1 -1
- package/dist/error-capture.d.ts +21 -64
- package/dist/fixback.umd.js +36 -202
- package/dist/fixback.umd.js.map +1 -1
- package/dist/identity.d.ts +6 -1
- package/dist/index.d.ts +17 -15
- package/dist/index.mjs +3577 -3683
- package/dist/index.mjs.map +1 -1
- package/dist/init.d.ts +62 -96
- package/dist/mount.d.ts +46 -0
- package/dist/overlay/dom.d.ts +16 -0
- package/dist/overlay/icons.d.ts +23 -0
- package/dist/overlay/marking.d.ts +64 -0
- package/dist/overlay/send.d.ts +65 -0
- package/dist/overlay/view.d.ts +78 -0
- package/dist/overlay-styles.d.ts +1 -1
- package/dist/overlay.d.ts +40 -41
- package/dist/package.json +3 -0
- package/dist/report.d.ts +9 -13
- package/dist/reporter-session-store.d.ts +17 -0
- package/dist/session.d.ts +27 -0
- package/dist/submit.d.ts +51 -8
- package/dist/trace/instrument/beacon.d.ts +16 -0
- package/dist/trace/instrument/console.d.ts +11 -0
- package/dist/trace/instrument/fetch.d.ts +16 -0
- package/dist/trace/instrument/index.d.ts +64 -0
- package/dist/trace/instrument/navigation.d.ts +12 -0
- package/dist/trace/instrument/ui.d.ts +23 -0
- package/dist/trace/instrument/window.d.ts +66 -0
- package/dist/trace/instrument/xhr.d.ts +15 -0
- package/dist/version.d.ts +1 -1
- package/package.json +8 -5
- package/dist/boot.d.ts +0 -74
- package/dist/breadcrumbs.d.ts +0 -327
- package/dist/invite.d.ts +0 -104
- package/dist/onboarding-styles.d.ts +0 -8
- package/dist/onboarding.d.ts +0 -44
package/dist/overlay.d.ts
CHANGED
|
@@ -1,28 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The report **overlay** — the on-page panel a Reporter files a report from, built
|
|
3
|
+
* to the frozen Signal Reporter prototype (`docs/design/Fixback Reporter.dc.html`).
|
|
4
|
+
*
|
|
5
|
+
* This module is the wiring: it mounts the view lazily inside its own Shadow DOM
|
|
6
|
+
* (isolated from the host page, and marked so the replay recorder and
|
|
7
|
+
* element-picker skip it), opens on the launcher's `fixback:launch` seam, and joins
|
|
8
|
+
* the three parts that do the work:
|
|
9
|
+
*
|
|
10
|
+
* - `./overlay/view` builds the markup and hands back typed refs;
|
|
11
|
+
* - `./overlay/marking` runs the three composable marking layers (element-pick,
|
|
12
|
+
* region-capture, draw) and owns what has been marked;
|
|
13
|
+
* - `./overlay/send` assembles, scrubs, and submits.
|
|
14
|
+
*
|
|
15
|
+
* What is left here is the panel's own state: open/closed, the compose → sending →
|
|
16
|
+
* sent phase, the status line, and the confirmation's auto-close. The marks are
|
|
17
|
+
* viewport-space vectors composited at view time (spec §D), never baked into an
|
|
18
|
+
* image; the still they annotate is a frame of the buffered rrweb window
|
|
19
|
+
* (reconstructed downstream since ADR-0027), so the SDK rasterises nothing.
|
|
20
|
+
*/
|
|
21
|
+
import type { BeforeSend, IdentityInputs, ProjectGate, ReporterIdentity } from "@fixback/sdk-core";
|
|
22
|
+
import { type StartPickerFn } from "./overlay/marking";
|
|
23
|
+
import { type BreadcrumbSource, type SubmitReportFn } from "./overlay/send";
|
|
6
24
|
import type { ReplaySource } from "./replay";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export declare const OVERLAY_ATTRIBUTE = "data-fixback-overlay";
|
|
10
|
-
type SubmitReportFn = (apiUrl: string, input: SubmitInput, fetchImpl?: typeof fetch) => Promise<SubmitResult>;
|
|
11
|
-
type StartPickerFn = (options: ElementPickerOptions) => ElementPicker;
|
|
25
|
+
export { OVERLAY_ATTRIBUTE } from "./overlay/dom";
|
|
26
|
+
export type { BreadcrumbSource } from "./overlay/send";
|
|
12
27
|
/** Injectable collaborators, defaulted to the real implementations. */
|
|
13
28
|
export interface OverlayDeps {
|
|
14
29
|
readonly submitReport: SubmitReportFn;
|
|
15
30
|
readonly startElementPicker: StartPickerFn;
|
|
16
31
|
}
|
|
17
|
-
/** A read-only view of the trace buffer the overlay attaches to a report. */
|
|
18
|
-
export interface BreadcrumbSource {
|
|
19
|
-
snapshot(): readonly Breadcrumb[];
|
|
20
|
-
}
|
|
21
32
|
/** Configuration for {@link createOverlay}. */
|
|
22
33
|
export interface OverlayConfig {
|
|
23
34
|
readonly apiUrl: string;
|
|
24
35
|
readonly key: string;
|
|
25
|
-
|
|
36
|
+
/** Identity evidence, read **live** at send time (ADR-0032) — see {@link MountContext}. */
|
|
37
|
+
readonly getIdentity?: () => IdentityInputs;
|
|
26
38
|
/**
|
|
27
39
|
* The Project's Gate, forwarded verbatim from the boot answer. Reserved for
|
|
28
40
|
* Gate-aware launcher / redemption behaviour (spec §A/§F); the overlay itself
|
|
@@ -30,21 +42,20 @@ export interface OverlayConfig {
|
|
|
30
42
|
*/
|
|
31
43
|
readonly gate?: ProjectGate;
|
|
32
44
|
/**
|
|
33
|
-
* The Reporter
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* and never sent back to the server. When `null`/absent no tier is asserted.
|
|
45
|
+
* The current Reporter identity — anonymous, or a connected Account with its
|
|
46
|
+
* server-derived tier — rendered in the header's identity chip (ADR-0032). Updated in
|
|
47
|
+
* place via {@link OverlayController.setIdentity} after a Connect or sign-out.
|
|
37
48
|
*/
|
|
38
|
-
readonly
|
|
49
|
+
readonly reporterIdentity: ReporterIdentity;
|
|
50
|
+
/** Start a Connect from the chip's **Sign in** (ADR-0032). */
|
|
51
|
+
readonly onSignIn: () => void;
|
|
52
|
+
/** Clear the Reporter session from the chip's **Sign out** (ADR-0032). */
|
|
53
|
+
readonly onSignOut: () => void;
|
|
39
54
|
readonly sdkVersion?: string;
|
|
40
55
|
/** The host app's Release (#117, ADR-0024), validated by `init`; rides in the environment. */
|
|
41
56
|
readonly release?: string;
|
|
42
|
-
/**
|
|
43
|
-
|
|
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;
|
|
57
|
+
/** The deploy environment (`production` / `staging` / …), stamped on every report. */
|
|
58
|
+
readonly deployEnvironment?: string;
|
|
48
59
|
/** Where to mount the overlay host. Defaults to `document.body`. */
|
|
49
60
|
readonly target?: HTMLElement;
|
|
50
61
|
readonly doc?: Document;
|
|
@@ -64,21 +75,9 @@ export interface OverlayController {
|
|
|
64
75
|
open(): void;
|
|
65
76
|
close(): void;
|
|
66
77
|
destroy(): void;
|
|
78
|
+
/** Update the identity chip in place after a Connect or sign-out (ADR-0032). */
|
|
79
|
+
setIdentity(identity: ReporterIdentity): void;
|
|
67
80
|
readonly isOpen: boolean;
|
|
68
81
|
}
|
|
69
|
-
/**
|
|
70
|
-
* Create the report overlay — the on-page panel a Reporter files a report from,
|
|
71
|
-
* built to the frozen Signal Reporter prototype (`docs/design/Fixback Reporter.dc.html`).
|
|
72
|
-
* It mounts lazily inside its own Shadow DOM (isolated from the host page, and
|
|
73
|
-
* marked so the replay recorder and element-picker skip it), opens on the launcher's
|
|
74
|
-
* `fixback:launch` seam, and offers three **composable, optional** marking layers
|
|
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.
|
|
82
|
-
*/
|
|
82
|
+
/** Create the report overlay. See the module doc. */
|
|
83
83
|
export declare function createOverlay(config: OverlayConfig): OverlayController;
|
|
84
|
-
export {};
|
package/dist/report.d.ts
CHANGED
|
@@ -11,18 +11,10 @@
|
|
|
11
11
|
* `payload` accepted by `POST /api/ingest/feedback`
|
|
12
12
|
* (`apps/api/src/ingest/ingest.controller.ts`).
|
|
13
13
|
*/
|
|
14
|
-
import { type CaptureEnvironment, type CapturedFrame, type ElementRect, type FeedbackSource, type ReportContent, type SelectedElement } from "@fixback/sdk-core";
|
|
14
|
+
import { type Breadcrumb, type CaptureEnvironment, type CapturedFrame, type ElementRect, type FeedbackSource, normaliseRelease, type ReportContent, type SelectedElement } from "@fixback/sdk-core";
|
|
15
15
|
import { type Mark, type Rect } from "./annotation";
|
|
16
|
-
import type { Breadcrumb } from "./breadcrumbs";
|
|
17
16
|
export type { CaptureEnvironment, CapturedFrame, ElementRect, FeedbackSource, ReportContent, SelectedElement, };
|
|
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.
|
|
24
|
-
*/
|
|
25
|
-
export declare function normaliseRelease(value: string): string | undefined;
|
|
17
|
+
export { normaliseRelease };
|
|
26
18
|
/**
|
|
27
19
|
* What the overlay hands to {@link assembleContent} when the Reporter sends. The
|
|
28
20
|
* three marking layers arrive flat (`element` / `region` / `marks`); `assembleContent`
|
|
@@ -36,9 +28,13 @@ export interface ReportDraft {
|
|
|
36
28
|
readonly url?: string;
|
|
37
29
|
readonly environment?: CaptureEnvironment;
|
|
38
30
|
readonly trace?: readonly Breadcrumb[];
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
31
|
+
/**
|
|
32
|
+
* The deploy **Environment** the SDK was configured with (ADR-0025) —
|
|
33
|
+
* `production` / `staging` / … — distinct from the build `release` in
|
|
34
|
+
* {@link CaptureEnvironment}. Named apart from `environment` (the capture bag)
|
|
35
|
+
* to avoid the collision, exactly as the wire does.
|
|
36
|
+
*/
|
|
37
|
+
readonly deployEnvironment?: string;
|
|
42
38
|
}
|
|
43
39
|
/**
|
|
44
40
|
* Read the capture environment off a window: the viewport size, the browser's
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser's **Reporter session** store (ADR-0032) — the storage binding only.
|
|
3
|
+
*
|
|
4
|
+
* After a Connect the SDK holds a compact signed token per origin and forwards it on
|
|
5
|
+
* every boot; the boot answer hands back a refreshed one (30-day sliding). The token
|
|
6
|
+
* lives in `localStorage` under a key **scoped by publishable key** (so two Projects on
|
|
7
|
+
* one origin keep separate sessions), the key format shared with every SDK
|
|
8
|
+
* (`@fixback/sdk-core`). Persistence is best-effort: a blocked or absent `localStorage`
|
|
9
|
+
* (private mode) simply means no stored session — the SDK falls back to anonymous
|
|
10
|
+
* rather than throwing, so it never disturbs the host page.
|
|
11
|
+
*/
|
|
12
|
+
/** Read the stored Reporter session token for this Project, or `null`. */
|
|
13
|
+
export declare function readReporterSession(publishableKey: string): string | null;
|
|
14
|
+
/** Persist a Reporter session token for this Project. Best-effort. */
|
|
15
|
+
export declare function writeReporterSession(publishableKey: string, token: string): void;
|
|
16
|
+
/** Clear the stored Reporter session token for this Project (sign out). Best-effort. */
|
|
17
|
+
export declare function clearReporterSession(publishableKey: string): void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **session flow** behind `init` (ADR-0032): recognise the Reporter through the
|
|
3
|
+
* Reporter session token it holds, ask ingest what it may do, and mount only when a
|
|
4
|
+
* submission would be accepted — while exposing `signIn` / `signOut` / `identity`.
|
|
5
|
+
*
|
|
6
|
+
* On boot the SDK forwards its stored **Reporter session** token (from a prior Connect,
|
|
7
|
+
* scoped per publishable key) and swaps in the refreshed one boot hands back (30-day
|
|
8
|
+
* sliding). It also detects a one-time code arriving on the `?fixback=` query parameter
|
|
9
|
+
* — the connect page's redirect fallback, and Fixback-issued links (ticket #250) — and
|
|
10
|
+
* exchanges it for a session before booting.
|
|
11
|
+
*
|
|
12
|
+
* **Connect** binds an Account to the site: `signIn` opens the platform's connect page
|
|
13
|
+
* (popup, or a full redirect when it is blocked), exchanges the returned code, and
|
|
14
|
+
* re-boots — so an Open-gated visitor's launcher gains the Account, and a gated
|
|
15
|
+
* Project's launcher **mounts** once the Account is eligible. The launcher mounts only
|
|
16
|
+
* when boot returns `canSubmit`, so a gated Project shows nothing to a signed-out
|
|
17
|
+
* visitor. Everything is wrapped so a Fixback problem resolves to a no-op and never
|
|
18
|
+
* surfaces on the host page.
|
|
19
|
+
*/
|
|
20
|
+
import type { FixbackInstance, InitOptions } from "./init";
|
|
21
|
+
/** A running SDK that mounted nothing — every failure path resolves to this. */
|
|
22
|
+
export declare const NOOP_INSTANCE: FixbackInstance;
|
|
23
|
+
/**
|
|
24
|
+
* Run one session: recognise the Reporter, boot, and mount when accepted — exchanging a
|
|
25
|
+
* `?fixback=` return code first, and exposing Connect (`signIn` / `signOut` / `identity`).
|
|
26
|
+
*/
|
|
27
|
+
export declare function runSession(options: InitOptions, apiUrl: string): Promise<FixbackInstance>;
|
package/dist/submit.d.ts
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The multipart feedback submission — the transport half of the capture loop.
|
|
3
|
+
*
|
|
4
|
+
* `POST /api/ingest/feedback` (spec MVP §C; tickets #50/#53) is a multipart
|
|
5
|
+
* request: a single text part `payload` carrying the JSON (publishable key +
|
|
6
|
+
* identity evidence + content) and an optional binary `replay` part — the buffered
|
|
7
|
+
* rrweb window (ADR-0024), from which the annotated still is reconstructed downstream
|
|
8
|
+
* (ADR-0027), so the browser SDK rasterises and sends no screenshot. The content-type
|
|
9
|
+
* header is deliberately not set, so the runtime writes the `multipart/form-data`
|
|
10
|
+
* boundary itself. Like `requestBoot`, this never throws: an unreachable Fixback or a
|
|
11
|
+
* refusal is a quiet result, so a Fixback problem never breaks the host page.
|
|
12
|
+
*
|
|
13
|
+
* The endpoint, the identity compaction, and the backpressure window are the shared
|
|
14
|
+
* core's (ADR-0028); what lives here is the browser envelope — a `FormData` with a
|
|
15
|
+
* `Blob` replay part.
|
|
16
|
+
*
|
|
17
|
+
* The `payload` carries the full report content — the structured `annotation`
|
|
18
|
+
* (element + region + marks, kept vector), the masked `trace` buffer, and the
|
|
19
|
+
* `source` provenance (spec §D). Automatic reports (`source: error`) additionally
|
|
20
|
+
* honour ingest's `429` + `Retry-After` backpressure: while the hold window is open
|
|
21
|
+
* they are dropped without touching the network. Manual reports are never gated
|
|
22
|
+
* (spec §E/§H).
|
|
23
|
+
*/
|
|
24
|
+
import { AutoReportBackoff, feedbackEndpoint, type FetchLike, type IdentityInputs, type ReportContent, type ReporterTier } from "@fixback/sdk-core";
|
|
25
|
+
export { feedbackEndpoint };
|
|
26
|
+
/**
|
|
27
|
+
* How long a submission may stay in flight before it is reported as
|
|
28
|
+
* `unreachable`. A hung connection — a captive portal, a proxy that never
|
|
29
|
+
* answers — would otherwise leave a report pending forever, and with it the
|
|
30
|
+
* overlay's "Sending…" state. The abandoned request keeps running in the
|
|
31
|
+
* background harmlessly. Matches `@fixback/expo`'s and `@fixback/node`'s window.
|
|
32
|
+
*/
|
|
33
|
+
export declare const DEFAULT_SUBMIT_TIMEOUT_MS = 30000;
|
|
4
34
|
/** What ingest returns for an accepted submission (vendored server shape). */
|
|
5
35
|
export interface RecordedFeedback {
|
|
6
36
|
readonly feedbackId: string;
|
|
@@ -41,8 +71,21 @@ export type SubmitResult = {
|
|
|
41
71
|
readonly reason: "backpressure";
|
|
42
72
|
readonly retryAfterSeconds: number;
|
|
43
73
|
};
|
|
44
|
-
/**
|
|
45
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Injectable transport collaborators, defaulted to the real implementations — the
|
|
76
|
+
* same deps-object shape `requestBoot` and `@fixback/expo`'s `submitReport` take.
|
|
77
|
+
*/
|
|
78
|
+
export interface SubmitDeps {
|
|
79
|
+
/** The `fetch` to call. Defaults to the page's global. */
|
|
80
|
+
readonly fetch?: FetchLike;
|
|
81
|
+
/**
|
|
82
|
+
* The shared `source: error` backpressure window. Defaults to the page-wide
|
|
83
|
+
* instance; tests inject their own to isolate the clock.
|
|
84
|
+
*/
|
|
85
|
+
readonly backoff?: AutoReportBackoff;
|
|
86
|
+
/** Overall request timeout in ms; `0` disables. Defaults to {@link DEFAULT_SUBMIT_TIMEOUT_MS}. */
|
|
87
|
+
readonly timeoutMs?: number;
|
|
88
|
+
}
|
|
46
89
|
/**
|
|
47
90
|
* Submit a report to ingest. Assembles the `payload` JSON (key + identity +
|
|
48
91
|
* content, with an explicit `source` stamped) and the optional `replay` window
|
|
@@ -53,7 +96,7 @@ export declare function feedbackEndpoint(apiUrl: string): string;
|
|
|
53
96
|
* is open the report is dropped without a request (spec §E/§H). Ingest's `429` +
|
|
54
97
|
* `Retry-After` opens/extends that window (default 60 s if the header is absent).
|
|
55
98
|
* Manual reports (`source: reporter`, the default) never consult the window and a
|
|
56
|
-
* non-2xx for them stays a plain refusal. The `
|
|
57
|
-
*
|
|
99
|
+
* non-2xx for them stays a plain refusal. The `deps` seams exist purely so the call
|
|
100
|
+
* is testable.
|
|
58
101
|
*/
|
|
59
|
-
export declare function submitReport(apiUrl: string, input: SubmitInput,
|
|
102
|
+
export declare function submitReport(apiUrl: string, input: SubmitInput, deps?: SubmitDeps): Promise<SubmitResult>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `navigator.sendBeacon` network hook (spec #122 §D, ticket #139).
|
|
3
|
+
*
|
|
4
|
+
* `sendBeacon` is fire-and-forget with a **synchronous boolean** return — `true`
|
|
5
|
+
* when the user agent queued the beacon, `false` when it declined — so wrapping
|
|
6
|
+
* must return that boolean unchanged: the original is called first and its result
|
|
7
|
+
* both classifies the crumb (`ok` / `network-error`) and is returned to the caller.
|
|
8
|
+
* Only the URL and the trivially-known request size are recorded (no response
|
|
9
|
+
* exists to read); capture failure is swallowed and the boolean still returned, and
|
|
10
|
+
* if the original itself throws we record best-effort and re-throw so the contract
|
|
11
|
+
* is preserved exactly.
|
|
12
|
+
*/
|
|
13
|
+
import { type BreadcrumbBuffer, type Teardown } from "@fixback/sdk-core";
|
|
14
|
+
import { type InstrumentWindow } from "./window";
|
|
15
|
+
/** Wrap `navigator.sendBeacon` to record a network crumb per beacon. See the module doc. */
|
|
16
|
+
export declare function instrumentBeacon(buffer: BreadcrumbBuffer, win: InstrumentWindow, ignoreUrl: (url: string) => boolean, now: () => number): Teardown;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser's binding for the shared **console** wrapper.
|
|
3
|
+
*
|
|
4
|
+
* `instrumentConsole` itself is `@fixback/sdk-core`'s (ADR-0028) — the browser and
|
|
5
|
+
* React Native wrap `console` identically, and the wrapper takes its target as an
|
|
6
|
+
* argument rather than reading a global. This module exists so `./trace/instrument`
|
|
7
|
+
* names every stream it installs, and so the page's `console` is resolved in one
|
|
8
|
+
* place.
|
|
9
|
+
*/
|
|
10
|
+
export { instrumentConsole, type ConsoleLike } from "@fixback/sdk-core";
|
|
11
|
+
export { globalConsole } from "./window";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `fetch` network hook (spec #122 §D, ticket #139).
|
|
3
|
+
*
|
|
4
|
+
* Wraps the page's `fetch` to record a rich network crumb on settlement: method,
|
|
5
|
+
* scrubbed URL, status + statusText, a `performance.now()` duration, request size
|
|
6
|
+
* (only when the body is trivially sized), response size (from the
|
|
7
|
+
* `content-length` header only), content type, and a failure outcome. The
|
|
8
|
+
* **original** outcome is returned untouched — the response is passed through
|
|
9
|
+
* **without its body being read** (only two metadata headers are inspected), and a
|
|
10
|
+
* rejection is re-thrown so the caller's `unhandledrejection` semantics are
|
|
11
|
+
* preserved.
|
|
12
|
+
*/
|
|
13
|
+
import { type BreadcrumbBuffer, type Teardown } from "@fixback/sdk-core";
|
|
14
|
+
import { type InstrumentWindow } from "./window";
|
|
15
|
+
/** Wrap `fetch` to record a rich network crumb on settlement. See the module doc. */
|
|
16
|
+
export declare function instrumentFetch(buffer: BreadcrumbBuffer, win: InstrumentWindow, ignoreUrl: (url: string) => boolean, now: () => number, mono?: () => number): Teardown;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser SDK's **capture instrumentation** — one hook per stream, plus the
|
|
3
|
+
* composite that installs them all (spec 0003 §C; spec #122 §A/§D).
|
|
4
|
+
*
|
|
5
|
+
* Each hook lives in its own module and takes exactly the surface it patches, so
|
|
6
|
+
* none of them needs a whole `Window`: `console` (the shared core's wrapper),
|
|
7
|
+
* `fetch`, `XMLHttpRequest`, `sendBeacon`, `history` + hash navigation, and the
|
|
8
|
+
* masked `click`/`input` listeners.
|
|
9
|
+
*
|
|
10
|
+
* Everything private is kept out **at the source**: `ui.input` records that an
|
|
11
|
+
* input changed, never its value; network crumbs carry method + URL + status only,
|
|
12
|
+
* **never** bodies; URLs are scrubbed as the crumb is built. The `beforeSend` choke
|
|
13
|
+
* point is the final gate over the whole report.
|
|
14
|
+
*
|
|
15
|
+
* The SDK stays dependency-free and must never throw into the host page, so every
|
|
16
|
+
* hook is wrapped: a capture failure is swallowed and the original behaviour (the
|
|
17
|
+
* real `console`, `fetch`, navigation) always runs.
|
|
18
|
+
*/
|
|
19
|
+
import { type BreadcrumbBuffer, type BreadcrumbLevel, type ConsoleLike, type Teardown } from "@fixback/sdk-core";
|
|
20
|
+
import { type InstrumentWindow } from "./window";
|
|
21
|
+
export { instrumentBeacon } from "./beacon";
|
|
22
|
+
export { instrumentConsole } from "./console";
|
|
23
|
+
export { instrumentFetch } from "./fetch";
|
|
24
|
+
export { instrumentNavigation } from "./navigation";
|
|
25
|
+
export { clickCrumb, inputCrumb, instrumentUiEvents } from "./ui";
|
|
26
|
+
export { instrumentXhr } from "./xhr";
|
|
27
|
+
export { type BeaconNavigator, type FetchFn, globalConsole, globalDocument, globalWindow, type HistoryLike, type InstrumentWindow, type XhrConstructor, type XhrInstance, } from "./window";
|
|
28
|
+
/** Options for {@link instrumentBreadcrumbs} and the individual installers. */
|
|
29
|
+
export interface InstrumentOptions {
|
|
30
|
+
/** The window to instrument. Defaults to the page's, when there is one. */
|
|
31
|
+
readonly win?: InstrumentWindow;
|
|
32
|
+
/** The document whose masked UI events are listened for. Defaults to the page's. */
|
|
33
|
+
readonly doc?: Document;
|
|
34
|
+
/** The console to wrap. Defaults to the page's. */
|
|
35
|
+
readonly consoleObj?: ConsoleLike;
|
|
36
|
+
readonly consoleLevels?: readonly BreadcrumbLevel[];
|
|
37
|
+
/** Skip URLs (e.g. the SDK's own ingest calls) so they never become crumbs. */
|
|
38
|
+
readonly ignoreUrl?: (url: string) => boolean;
|
|
39
|
+
readonly now?: () => number;
|
|
40
|
+
/**
|
|
41
|
+
* High-res monotonic clock (spec #122 §D) — used to measure a request's duration as
|
|
42
|
+
* a delta around the wrapped call. Defaults to `performance.now`; injectable for tests.
|
|
43
|
+
*/
|
|
44
|
+
readonly mono?: () => number;
|
|
45
|
+
/**
|
|
46
|
+
* Whether to instrument the **console** stream (spec #122 §L; ticket #138).
|
|
47
|
+
* Defaults to `true`; pass `false` and `console` is never wrapped, so no console
|
|
48
|
+
* entry is ever recorded — the per-project toggle stops the stream at its source.
|
|
49
|
+
*/
|
|
50
|
+
readonly captureConsole?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Whether to instrument the **network** stream — `fetch`, `XHR`, and `sendBeacon`
|
|
53
|
+
* (spec #122 §L; ticket #138). Defaults to `true`; pass `false` and none is
|
|
54
|
+
* wrapped, so no network entry is ever recorded. Navigation and masked UI events
|
|
55
|
+
* are unaffected.
|
|
56
|
+
*/
|
|
57
|
+
readonly captureNetwork?: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Install every capture hook onto a window/document and return a single teardown
|
|
61
|
+
* that removes them all. Each hook is independent and defensive: a failure in one
|
|
62
|
+
* never blocks the others, and none can throw into the host page.
|
|
63
|
+
*/
|
|
64
|
+
export declare function instrumentBreadcrumbs(buffer: BreadcrumbBuffer, options?: InstrumentOptions): Teardown;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The navigation hook (spec #122 §A): a `navigation` crumb on every SPA route
|
|
3
|
+
* change — `pushState`, `replaceState`, `popstate`, and `hashchange`.
|
|
4
|
+
*
|
|
5
|
+
* Both `history` methods are patched rather than listened for, because neither
|
|
6
|
+
* fires an event of its own; the original is always called first and its result
|
|
7
|
+
* returned, so the host page's routing is untouched.
|
|
8
|
+
*/
|
|
9
|
+
import { type BreadcrumbBuffer, type Teardown } from "@fixback/sdk-core";
|
|
10
|
+
import { type InstrumentWindow } from "./window";
|
|
11
|
+
/** Record a `navigation` crumb on `pushState`/`replaceState`/pop/hash changes. */
|
|
12
|
+
export declare function instrumentNavigation(buffer: BreadcrumbBuffer, win: InstrumentWindow, now: () => number): Teardown;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The masked **user-action** hook (spec #122 §A) and its two crumb builders.
|
|
3
|
+
*
|
|
4
|
+
* These are the browser-only crumbs: they need an `Element` to derive a masked CSS
|
|
5
|
+
* selector from, which is why they stay here rather than in the shared core with
|
|
6
|
+
* the rest of the builders. The masking is at the source and structural — a
|
|
7
|
+
* `ui.input` crumb records **that** a field changed and which one, and the shape
|
|
8
|
+
* has no slot for a value, so the typed text can never be recorded.
|
|
9
|
+
*
|
|
10
|
+
* Listeners are capture-phase and passive observers: they never call
|
|
11
|
+
* `preventDefault` or `stopPropagation`, and the SDK's own chrome (launcher,
|
|
12
|
+
* overlay, and anything inside their shadow roots) is skipped.
|
|
13
|
+
*/
|
|
14
|
+
import type { Breadcrumb, BreadcrumbBuffer, Teardown } from "@fixback/sdk-core";
|
|
15
|
+
/** A `ui.click` crumb: a masked target selector only — no text or value. */
|
|
16
|
+
export declare function clickCrumb(target: Element, timestamp: number): Breadcrumb;
|
|
17
|
+
/**
|
|
18
|
+
* A `ui.input` crumb: records **that** an input changed and which field, never
|
|
19
|
+
* the value typed into it. The `target` element's `.value` is never read.
|
|
20
|
+
*/
|
|
21
|
+
export declare function inputCrumb(target: Element, timestamp: number): Breadcrumb;
|
|
22
|
+
/** Listen (capture-phase) for clicks and input changes as masked crumbs. */
|
|
23
|
+
export declare function instrumentUiEvents(buffer: BreadcrumbBuffer, doc: Document, now: () => number): Teardown;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **structural browser surfaces** the trace instrumentation reaches into, and
|
|
3
|
+
* the resolvers that find them on the page.
|
|
4
|
+
*
|
|
5
|
+
* Everything the hooks touch is described as a narrow interface rather than taken
|
|
6
|
+
* from the DOM lib wholesale, so a test can build a plain object (`fakeWindow()`)
|
|
7
|
+
* instead of a whole `Window`. The shapes are deliberately written so the **real**
|
|
8
|
+
* `window` and `console` satisfy them by assignment — no `as unknown as` escape
|
|
9
|
+
* anywhere, in the SDK or in its tests.
|
|
10
|
+
*/
|
|
11
|
+
import type { ConsoleLike } from "@fixback/sdk-core";
|
|
12
|
+
/** The `fetch` signature the network hook wraps. */
|
|
13
|
+
export type FetchFn = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
14
|
+
/** The `history` slice the navigation hook patches. */
|
|
15
|
+
export interface HistoryLike {
|
|
16
|
+
pushState(data: unknown, unused: string, url?: string | URL | null): void;
|
|
17
|
+
replaceState(data: unknown, unused: string, url?: string | URL | null): void;
|
|
18
|
+
}
|
|
19
|
+
/** The XHR instance surface the network hook touches. */
|
|
20
|
+
export interface XhrInstance {
|
|
21
|
+
status: number;
|
|
22
|
+
statusText?: string;
|
|
23
|
+
open(method: string, url: string | URL, ...rest: unknown[]): void;
|
|
24
|
+
send(body?: unknown): void;
|
|
25
|
+
getResponseHeader?(name: string): string | null;
|
|
26
|
+
addEventListener(type: string, listener: () => void): void;
|
|
27
|
+
removeEventListener(type: string, listener: () => void): void;
|
|
28
|
+
}
|
|
29
|
+
/** The XHR constructor whose prototype the network hook patches. */
|
|
30
|
+
export interface XhrConstructor {
|
|
31
|
+
new (): XhrInstance;
|
|
32
|
+
prototype: XhrInstance;
|
|
33
|
+
}
|
|
34
|
+
/** The `navigator.sendBeacon` surface the beacon hook wraps. */
|
|
35
|
+
export interface BeaconNavigator {
|
|
36
|
+
sendBeacon?: (url: string | URL, data?: BodyInit | null) => boolean;
|
|
37
|
+
}
|
|
38
|
+
/** The structural window surface the instrumentation reaches into. */
|
|
39
|
+
export interface InstrumentWindow {
|
|
40
|
+
fetch?: FetchFn;
|
|
41
|
+
history?: HistoryLike;
|
|
42
|
+
location?: {
|
|
43
|
+
href: string;
|
|
44
|
+
};
|
|
45
|
+
XMLHttpRequest?: XhrConstructor;
|
|
46
|
+
navigator?: BeaconNavigator;
|
|
47
|
+
addEventListener(type: string, listener: (event: Event) => void, options?: boolean | AddEventListenerOptions): void;
|
|
48
|
+
removeEventListener(type: string, listener: (event: Event) => void, options?: boolean | EventListenerOptions): void;
|
|
49
|
+
}
|
|
50
|
+
/** An installed hook's detacher — nothing installed is the no-op below. */
|
|
51
|
+
export declare function noop(): void;
|
|
52
|
+
/**
|
|
53
|
+
* The page's `window`, when there is one (a worker or an SSR pass has none).
|
|
54
|
+
* `Window` satisfies {@link InstrumentWindow} structurally, so this is a plain
|
|
55
|
+
* assignment rather than a cast.
|
|
56
|
+
*/
|
|
57
|
+
export declare function globalWindow(): InstrumentWindow | undefined;
|
|
58
|
+
/** The page's `document`, when there is one. */
|
|
59
|
+
export declare function globalDocument(): Document | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* The page's `console`, when there is one. A real `Console` satisfies
|
|
62
|
+
* {@link ConsoleLike} (its optional-method shape is bivariant in its parameters,
|
|
63
|
+
* which is what makes `console.assert`'s overloads fit), so this too is an
|
|
64
|
+
* assignment, not a cast.
|
|
65
|
+
*/
|
|
66
|
+
export declare function globalConsole(): ConsoleLike | undefined;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `XMLHttpRequest` network hook (spec #122 §D, ticket #139).
|
|
3
|
+
*
|
|
4
|
+
* Patches the prototype's `open`/`send` to record a rich network crumb when a
|
|
5
|
+
* request settles: method, scrubbed URL, status + statusText, a
|
|
6
|
+
* `performance.now()` duration, request size (only when the body is trivially
|
|
7
|
+
* sized), response size (from the `content-length` header only), content type, and
|
|
8
|
+
* a failure outcome distinguished by which terminal event fired (`error` /
|
|
9
|
+
* `timeout` / `abort` / `load`). The `send` body argument is **never read for
|
|
10
|
+
* content** — only its trivially-known size — so a body can never reach the buffer.
|
|
11
|
+
*/
|
|
12
|
+
import { type BreadcrumbBuffer, type Teardown } from "@fixback/sdk-core";
|
|
13
|
+
import { type InstrumentWindow } from "./window";
|
|
14
|
+
/** Patch `XMLHttpRequest` to record a rich network crumb on settlement. See the module doc. */
|
|
15
|
+
export declare function instrumentXhr(buffer: BreadcrumbBuffer, win: InstrumentWindow, ignoreUrl: (url: string) => boolean, now: () => number, mono?: () => number): Teardown;
|
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.6.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",
|
|
@@ -35,15 +35,15 @@
|
|
|
35
35
|
"url": "git+https://github.com/wemuda/fixback.git",
|
|
36
36
|
"directory": "packages/sdk"
|
|
37
37
|
},
|
|
38
|
-
"homepage": "https://
|
|
39
|
-
"bugs": "https://
|
|
38
|
+
"homepage": "https://docs.fixback.dev/sdk/web",
|
|
39
|
+
"bugs": "https://docs.fixback.dev",
|
|
40
40
|
"publishConfig": {
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@rrweb/record": "^2.1.1",
|
|
45
45
|
"@rrweb/types": "^2.1.1",
|
|
46
|
-
"@fixback/sdk-core": "0.
|
|
46
|
+
"@fixback/sdk-core": "0.3.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"jsdom": "^30.0.1",
|
|
@@ -51,8 +51,11 @@
|
|
|
51
51
|
"vite": "^8.2.1",
|
|
52
52
|
"vitest": "^4.1.10"
|
|
53
53
|
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=20.12"
|
|
56
|
+
},
|
|
54
57
|
"scripts": {
|
|
55
|
-
"build": "vite build && tsc -p tsconfig.build.json",
|
|
58
|
+
"build": "vite build && tsc -p tsconfig.build.json && node ../../scripts/finalize-cjs.mjs dist",
|
|
56
59
|
"lint": "eslint .",
|
|
57
60
|
"typecheck": "tsc --noEmit",
|
|
58
61
|
"test": "vitest run"
|
package/dist/boot.d.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The ingest **boot** wire-contract, vendored.
|
|
3
|
-
*
|
|
4
|
-
* The SDK deliberately does not import `@fixback/shared` — that package is
|
|
5
|
-
* private and server-shaped (ticket #47). The boot request/response is small and
|
|
6
|
-
* stable, so the exact slice the SDK needs is copied here. Keep it in lock-step
|
|
7
|
-
* with the server: the request body accepted by `POST /api/ingest/boot`
|
|
8
|
-
* (`apps/api/src/ingest/ingest.controller.ts`) and the `BootAnswer` returned by
|
|
9
|
-
* `evaluateBoot` (`apps/api/src/ingest/reporter-identity.ts`).
|
|
10
|
-
*/
|
|
11
|
-
/** A Project's Gate — who may submit. Mirrors the server's `ProjectGate`. */
|
|
12
|
-
export type ProjectGate = "open" | "invited" | "internal";
|
|
13
|
-
/** The trust tier a Reporter holds. Mirrors the server's `ReporterTier`. */
|
|
14
|
-
export type ReporterTier = "public" | "invited" | "internal";
|
|
15
|
-
/**
|
|
16
|
-
* Optional identity evidence the SDK forwards to boot. None of it is a tier: the
|
|
17
|
-
* server re-derives trust from this evidence and never honours a self-declared
|
|
18
|
-
* tier, so the SDK does not send one.
|
|
19
|
-
*/
|
|
20
|
-
export interface IdentityInputs {
|
|
21
|
-
readonly signedIdentity?: string;
|
|
22
|
-
readonly reporterId?: string;
|
|
23
|
-
readonly anonymousId?: string;
|
|
24
|
-
}
|
|
25
|
-
/** The JSON body `POST /api/ingest/boot` accepts. */
|
|
26
|
-
export interface BootRequest extends IdentityInputs {
|
|
27
|
-
readonly key: string;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* The Project's effective console/network capture config, served on the boot answer
|
|
31
|
-
* (spec #122 §L; ticket #138). Each flag is the per-project master toggle ANDed with
|
|
32
|
-
* that stream's own toggle, so the SDK gates instrumentation on one boolean per
|
|
33
|
-
* stream. Optional on the wire: an older server that does not send it (or a
|
|
34
|
-
* malformed value) is treated as **capture on** — default-on, matching the server
|
|
35
|
-
* default — and `init` options override whatever is served.
|
|
36
|
-
*/
|
|
37
|
-
export interface CaptureConfig {
|
|
38
|
-
readonly console: boolean;
|
|
39
|
-
readonly network: boolean;
|
|
40
|
-
/**
|
|
41
|
-
* The Project's session-replay toggle (issue #189, ADR-0024). Optional on the
|
|
42
|
-
* wire: a server predating replay omits it, which — like the other streams —
|
|
43
|
-
* means **capture on**.
|
|
44
|
-
*/
|
|
45
|
-
readonly replay?: boolean;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* The boot answer: whether this origin is allowlisted, the Project's Gate, the
|
|
49
|
-
* caller's derived tier (`null` when a presented identity was refused), whether a
|
|
50
|
-
* submission would be accepted right now, and the Project's capture config. The
|
|
51
|
-
* launcher shows only when `canSubmit` is true.
|
|
52
|
-
*/
|
|
53
|
-
export interface BootAnswer {
|
|
54
|
-
readonly originAllowed: boolean;
|
|
55
|
-
readonly gate: ProjectGate;
|
|
56
|
-
readonly tier: ReporterTier | null;
|
|
57
|
-
readonly canSubmit: boolean;
|
|
58
|
-
/** The Project's console/network capture config; absent ⇒ default-on (#138). */
|
|
59
|
-
readonly capture?: CaptureConfig;
|
|
60
|
-
}
|
|
61
|
-
/** Join an API base URL with the boot path, tolerating a trailing slash. */
|
|
62
|
-
export declare function bootEndpoint(apiUrl: string): string;
|
|
63
|
-
/**
|
|
64
|
-
* Ask ingest whether a submission would be accepted for this key / origin / Gate.
|
|
65
|
-
* Resolves to the boot answer, or `null` when Fixback could not be reached, the
|
|
66
|
-
* key was refused, or the response was not a boot answer. It never throws: any
|
|
67
|
-
* non-answer is treated by the caller as "do not show the launcher", so a Fixback
|
|
68
|
-
* outage stays invisible to the host page (ticket #47: "fails quietly").
|
|
69
|
-
*
|
|
70
|
-
* The browser attaches the `Origin` header itself on this cross-origin request —
|
|
71
|
-
* the server reads it to decide `originAllowed` — so the SDK neither sets nor
|
|
72
|
-
* needs to set it. `fetchImpl` is injectable purely so the boot call is testable.
|
|
73
|
-
*/
|
|
74
|
-
export declare function requestBoot(apiUrl: string, request: BootRequest, fetchImpl?: typeof fetch): Promise<BootAnswer | null>;
|