@fixback/sdk-core 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -9
- package/{src/annotation.ts → dist/annotation.d.ts} +36 -47
- package/dist/anonymous-id.d.ts +23 -0
- package/dist/anonymous-id.js +39 -0
- package/dist/anonymous-id.js.map +1 -0
- package/dist/auto-capture.d.ts +129 -0
- package/dist/auto-capture.js +210 -0
- package/dist/auto-capture.js.map +1 -0
- package/{src/backoff.ts → dist/backoff.d.ts} +17 -52
- package/dist/boot.d.ts +148 -0
- package/dist/boot.js +113 -0
- package/dist/boot.js.map +1 -0
- package/dist/breadcrumb.d.ts +133 -0
- package/dist/connect.d.ts +110 -0
- package/dist/connect.js +147 -0
- package/dist/connect.js.map +1 -0
- package/dist/env.d.ts +37 -0
- package/dist/env.js +83 -0
- package/dist/env.js.map +1 -0
- package/dist/fingerprint.d.ts +39 -0
- package/dist/fingerprint.js +10 -15
- package/dist/fingerprint.js.map +1 -1
- package/dist/http.d.ts +51 -0
- package/dist/http.js +42 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +115 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1057 -0
- package/dist/index.mjs.map +7 -0
- package/dist/options.d.ts +82 -0
- package/dist/options.js +22 -0
- package/dist/options.js.map +1 -0
- package/dist/release.d.ts +19 -0
- package/dist/release.js +36 -0
- package/dist/release.js.map +1 -0
- package/dist/scrub.d.ts +62 -0
- package/dist/stack.d.ts +51 -0
- package/dist/stack.js +97 -0
- package/dist/stack.js.map +1 -0
- package/dist/trace/buffer.d.ts +121 -0
- package/dist/trace/buffer.js +230 -0
- package/dist/trace/buffer.js.map +1 -0
- package/dist/trace/console-args.d.ts +60 -0
- package/dist/trace/console-args.js +189 -0
- package/dist/trace/console-args.js.map +1 -0
- package/dist/trace/console.d.ts +42 -0
- package/dist/trace/console.js +71 -0
- package/dist/trace/console.js.map +1 -0
- package/dist/trace/crumbs.d.ts +88 -0
- package/dist/trace/crumbs.js +164 -0
- package/dist/trace/crumbs.js.map +1 -0
- package/dist/trace/source.d.ts +30 -0
- package/dist/trace/source.js +59 -0
- package/dist/trace/source.js.map +1 -0
- package/dist/version.d.ts +13 -0
- package/dist/version.js +17 -0
- package/dist/version.js.map +1 -0
- package/dist/wire.d.ts +101 -0
- package/package.json +12 -8
- package/src/backoff.test.ts +0 -94
- package/src/breadcrumb.ts +0 -169
- package/src/fingerprint.test.ts +0 -96
- package/src/fingerprint.ts +0 -112
- package/src/index.ts +0 -63
- package/src/scrub.test.ts +0 -215
- package/src/scrub.ts +0 -226
- package/src/wire.ts +0 -116
|
@@ -12,13 +12,10 @@
|
|
|
12
12
|
* The clock is injectable so the window is unit-tested deterministically, never on
|
|
13
13
|
* wall time — mirroring the server limiter's `Clock`.
|
|
14
14
|
*/
|
|
15
|
-
|
|
16
15
|
/** A source of the current time in epoch milliseconds — injectable for tests. */
|
|
17
16
|
export type Clock = () => number;
|
|
18
|
-
|
|
19
17
|
/** The hold window applied when a `429` carries no usable `Retry-After` (spec §E). */
|
|
20
|
-
export const DEFAULT_RETRY_AFTER_SECONDS = 60;
|
|
21
|
-
|
|
18
|
+
export declare const DEFAULT_RETRY_AFTER_SECONDS = 60;
|
|
22
19
|
/**
|
|
23
20
|
* Parse a `Retry-After` header into whole seconds to hold for. Handles both HTTP
|
|
24
21
|
* forms — a delta-seconds integer and an HTTP-date (measured from `now`, rounded up
|
|
@@ -26,58 +23,26 @@ export const DEFAULT_RETRY_AFTER_SECONDS = 60;
|
|
|
26
23
|
* the header is absent, blank, or unparseable. Ingest sends the delta-seconds form;
|
|
27
24
|
* the date form is handled for spec-completeness.
|
|
28
25
|
*/
|
|
29
|
-
export function parseRetryAfter(
|
|
30
|
-
header: string | null | undefined,
|
|
31
|
-
now: number,
|
|
32
|
-
): number {
|
|
33
|
-
if (header == null) return DEFAULT_RETRY_AFTER_SECONDS;
|
|
34
|
-
const value = header.trim();
|
|
35
|
-
if (value === "") return DEFAULT_RETRY_AFTER_SECONDS;
|
|
36
|
-
|
|
37
|
-
if (/^\d+$/.test(value)) {
|
|
38
|
-
return Number(value);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const when = Date.parse(value);
|
|
42
|
-
if (!Number.isNaN(when)) {
|
|
43
|
-
return Math.max(0, Math.ceil((when - now) / 1000));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return DEFAULT_RETRY_AFTER_SECONDS;
|
|
47
|
-
}
|
|
48
|
-
|
|
26
|
+
export declare function parseRetryAfter(header: string | null | undefined, now: number): number;
|
|
49
27
|
/**
|
|
50
28
|
* A single pause window for automatic (`source: error`) reports. `hold` opens (or
|
|
51
29
|
* extends) it from a `429`'s `Retry-After`; `isPaused` reports whether it is still
|
|
52
30
|
* open. The default instance in the SDK's transport is shared across a page's
|
|
53
31
|
* reports so the hold persists across successive automatic submissions.
|
|
54
32
|
*/
|
|
55
|
-
export class AutoReportBackoff {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Open (or extend) the window from a `429`'s `Retry-After` value, returning the
|
|
73
|
-
* seconds it will hold for. The window only ever grows — a shorter later hold
|
|
74
|
-
* never clips a longer one already in effect.
|
|
75
|
-
*/
|
|
76
|
-
hold(retryAfterHeader: string | null | undefined): number {
|
|
77
|
-
const now = this.now();
|
|
78
|
-
const seconds = parseRetryAfter(retryAfterHeader, now);
|
|
79
|
-
const until = now + seconds * 1000;
|
|
80
|
-
if (until > this.pausedUntil) this.pausedUntil = until;
|
|
81
|
-
return seconds;
|
|
82
|
-
}
|
|
33
|
+
export declare class AutoReportBackoff {
|
|
34
|
+
private readonly now;
|
|
35
|
+
/** Epoch ms until which automatic reports are held; `0` when clear. */
|
|
36
|
+
private pausedUntil;
|
|
37
|
+
constructor(now?: Clock);
|
|
38
|
+
/** Is the automatic-report pause window currently open? */
|
|
39
|
+
isPaused(): boolean;
|
|
40
|
+
/** Whole seconds remaining in the pause window (`0` when clear). */
|
|
41
|
+
retryAfterSeconds(): number;
|
|
42
|
+
/**
|
|
43
|
+
* Open (or extend) the window from a `429`'s `Retry-After` value, returning the
|
|
44
|
+
* seconds it will hold for. The window only ever grows — a shorter later hold
|
|
45
|
+
* never clips a longer one already in effect.
|
|
46
|
+
*/
|
|
47
|
+
hold(retryAfterHeader: string | null | undefined): number;
|
|
83
48
|
}
|
package/dist/boot.d.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ingest **boot + feedback wire contract** — the one copy every capture SDK
|
|
3
|
+
* speaks (ADR-0028).
|
|
4
|
+
*
|
|
5
|
+
* The SDKs deliberately do not import `@fixback/shared` — that package is private
|
|
6
|
+
* and server-shaped (ticket #47) — so the exact slice they need lives here, in the
|
|
7
|
+
* shared core, rather than being vendored once per SDK. Keep it in lock-step with
|
|
8
|
+
* the server: the request body accepted by `POST /api/ingest/boot`
|
|
9
|
+
* (`apps/api/src/ingest/ingest.controller.ts`) and the `BootAnswer` returned by
|
|
10
|
+
* `evaluateBoot` (`apps/api/src/ingest/reporter-identity.ts`).
|
|
11
|
+
*
|
|
12
|
+
* One runtime difference is carried as an option, not a fork: a browser attaches
|
|
13
|
+
* the `Origin` header itself, while a native HTTP stack attaches none — so React
|
|
14
|
+
* Native passes its configured `origin` and the server reads it to decide
|
|
15
|
+
* `originAllowed` exactly as it does for a browser request (spec 0004 §A).
|
|
16
|
+
*/
|
|
17
|
+
import { type FetchLike } from "./http";
|
|
18
|
+
/** A Project's Gate — who may submit. Mirrors the server's `ProjectGate`. */
|
|
19
|
+
export type ProjectGate = "open" | "invited" | "internal";
|
|
20
|
+
/** The trust tier a Reporter holds. Mirrors the server's `ReporterTier`. */
|
|
21
|
+
export type ReporterTier = "public" | "invited" | "internal";
|
|
22
|
+
/**
|
|
23
|
+
* Optional identity evidence an SDK forwards to boot. None of it is a tier: the
|
|
24
|
+
* server re-derives trust from this evidence and never honours a self-declared
|
|
25
|
+
* tier, so no SDK sends one.
|
|
26
|
+
*/
|
|
27
|
+
export interface IdentityInputs {
|
|
28
|
+
/** A Host identity token minted by the customer's server (the escape hatch). */
|
|
29
|
+
readonly hostIdentity?: string;
|
|
30
|
+
/**
|
|
31
|
+
* The **Reporter session** token the SDK holds after a Connect (ADR-0032). The
|
|
32
|
+
* server checks it against the live session row and re-derives the Account's tier;
|
|
33
|
+
* a revoked, expired, or forged token is treated as absent (the anonymous branch),
|
|
34
|
+
* never an error. Replaces the removed device-local `reporterId` branch.
|
|
35
|
+
*/
|
|
36
|
+
readonly reporterSession?: string;
|
|
37
|
+
/** The anonymous first-party identifier for a Public Reporter on an Open Project. */
|
|
38
|
+
readonly anonymousId?: string;
|
|
39
|
+
}
|
|
40
|
+
/** The JSON body `POST /api/ingest/boot` accepts. */
|
|
41
|
+
export interface BootRequest extends IdentityInputs {
|
|
42
|
+
readonly key: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The signed-in Account a valid Reporter session resolves to, as the boot answer and
|
|
46
|
+
* the Connect exchange report it for the identity chip (ADR-0032). Name and email
|
|
47
|
+
* come from the `user`, never from anything the client set. `null`/absent means the
|
|
48
|
+
* caller is anonymous.
|
|
49
|
+
*/
|
|
50
|
+
export interface ConnectedAccount {
|
|
51
|
+
readonly name: string;
|
|
52
|
+
readonly email: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The Project's effective console/network capture config, served on the boot answer
|
|
56
|
+
* (spec #122 §L; ticket #138). Each flag is the per-project master toggle ANDed with
|
|
57
|
+
* that stream's own toggle, so an SDK gates instrumentation on one boolean per
|
|
58
|
+
* stream. Optional on the wire: an older server that does not send it (or a
|
|
59
|
+
* malformed value) is treated as **capture on** — default-on, matching the server
|
|
60
|
+
* default — and init options override whatever is served.
|
|
61
|
+
*/
|
|
62
|
+
export interface CaptureConfig {
|
|
63
|
+
readonly console: boolean;
|
|
64
|
+
readonly network: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* The Project's session-replay toggle (issue #189, ADR-0024) — browser only.
|
|
67
|
+
* Optional on the wire: a server predating replay omits it, which — like the
|
|
68
|
+
* other streams — means **capture on**.
|
|
69
|
+
*/
|
|
70
|
+
readonly replay?: boolean;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The boot answer: whether this origin is allowlisted, the Project's Gate, the
|
|
74
|
+
* caller's derived tier (`null` when a presented identity was refused), whether a
|
|
75
|
+
* submission would be accepted right now, and the Project's capture config. An SDK
|
|
76
|
+
* shows its launcher (or arms its gesture) only when `canSubmit` is true.
|
|
77
|
+
*/
|
|
78
|
+
export interface BootAnswer {
|
|
79
|
+
readonly originAllowed: boolean;
|
|
80
|
+
readonly gate: ProjectGate;
|
|
81
|
+
readonly tier: ReporterTier | null;
|
|
82
|
+
readonly canSubmit: boolean;
|
|
83
|
+
/** The Project's console/network capture config; absent ⇒ default-on (#138). */
|
|
84
|
+
readonly capture?: CaptureConfig;
|
|
85
|
+
/**
|
|
86
|
+
* Where the SDK opens **Connect** (ADR-0032) — the platform's connect page, built
|
|
87
|
+
* by the server from its configured dashboard origin and carried on every boot so
|
|
88
|
+
* the SDK does not need to know the Fixback dashboard origin itself. Absent from an
|
|
89
|
+
* older server; the SDK then cannot offer Connect.
|
|
90
|
+
*/
|
|
91
|
+
readonly connectUrl?: string;
|
|
92
|
+
/**
|
|
93
|
+
* The signed-in Account when a valid Reporter session was presented, else `null`.
|
|
94
|
+
* Lets the identity chip show the person's name on a page reload, when the SDK holds
|
|
95
|
+
* only the stored token.
|
|
96
|
+
*/
|
|
97
|
+
readonly account?: ConnectedAccount | null;
|
|
98
|
+
/**
|
|
99
|
+
* A **refreshed** Reporter session token when the presented one was valid (30-day
|
|
100
|
+
* sliding lifetime; ADR-0032). The SDK swaps its stored token for this on every
|
|
101
|
+
* boot, so an actively-used session never lapses. Absent when no valid session was
|
|
102
|
+
* presented.
|
|
103
|
+
*/
|
|
104
|
+
readonly reporterSession?: string;
|
|
105
|
+
}
|
|
106
|
+
/** Join an API base URL with the boot path, tolerating a trailing slash. */
|
|
107
|
+
export declare function bootEndpoint(apiUrl: string): string;
|
|
108
|
+
/** Join an API base URL with the feedback path, tolerating a trailing slash. */
|
|
109
|
+
export declare function feedbackEndpoint(apiUrl: string): string;
|
|
110
|
+
/** Narrow an unknown JSON body to a {@link BootAnswer} before an SDK trusts it. */
|
|
111
|
+
export declare function isBootAnswer(value: unknown): value is BootAnswer;
|
|
112
|
+
/** Drop `undefined` identity fields so a payload carries only what was given. */
|
|
113
|
+
export declare function compactIdentity(identity: IdentityInputs | undefined): IdentityInputs;
|
|
114
|
+
/**
|
|
115
|
+
* Does `url` address one of the SDK's **own** API endpoints? Every request a capture
|
|
116
|
+
* SDK issues lives under two namespaces beneath the API origin: `/api/ingest/*` —
|
|
117
|
+
* boot and feedback — and `/api/connect/*` — the Connect code exchange and sign-out
|
|
118
|
+
* (ADR-0032).
|
|
119
|
+
*
|
|
120
|
+
* The trace's network capture ignores **only** these, never the whole origin. That
|
|
121
|
+
* distinction matters when the instrumented app and the Fixback API share an origin — a
|
|
122
|
+
* self-hosted deployment, or Fixback running its own SDK on its own dashboard — where
|
|
123
|
+
* ignoring the origin wholesale would swallow all of the app's own requests and leave the
|
|
124
|
+
* Network tab empty. When the app and the API sit on different origins (the common case)
|
|
125
|
+
* this matches nothing extra, since the app's origin is never `apiUrl`. `url` is the raw
|
|
126
|
+
* request URL — absolute for every call an SDK makes — so a prefix test over the
|
|
127
|
+
* normalised origin is exact.
|
|
128
|
+
*/
|
|
129
|
+
export declare function isFixbackApiRequest(apiUrl: string, url: string): boolean;
|
|
130
|
+
/** Injectable collaborators for {@link requestBoot}. */
|
|
131
|
+
export interface BootDeps {
|
|
132
|
+
/** The `fetch` to call. Defaults to the runtime's global. */
|
|
133
|
+
readonly fetch?: FetchLike;
|
|
134
|
+
/**
|
|
135
|
+
* The origin to declare explicitly, for a runtime that attaches no `Origin`
|
|
136
|
+
* header of its own (React Native). Omit in a browser — the user agent sets it,
|
|
137
|
+
* and a page may not override it.
|
|
138
|
+
*/
|
|
139
|
+
readonly origin?: string;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Ask ingest whether a submission would be accepted for this key / origin / Gate.
|
|
143
|
+
* Resolves to the boot answer, or `null` when Fixback could not be reached, the key
|
|
144
|
+
* was refused, or the response was not a boot answer. It never throws: any
|
|
145
|
+
* non-answer is treated by the caller as "stay dormant", so a Fixback outage stays
|
|
146
|
+
* invisible to the host app (ticket #47: "fails quietly").
|
|
147
|
+
*/
|
|
148
|
+
export declare function requestBoot(apiUrl: string, request: BootRequest, deps?: BootDeps): Promise<BootAnswer | null>;
|
package/dist/boot.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The ingest **boot + feedback wire contract** — the one copy every capture SDK
|
|
4
|
+
* speaks (ADR-0028).
|
|
5
|
+
*
|
|
6
|
+
* The SDKs deliberately do not import `@fixback/shared` — that package is private
|
|
7
|
+
* and server-shaped (ticket #47) — so the exact slice they need lives here, in the
|
|
8
|
+
* shared core, rather than being vendored once per SDK. Keep it in lock-step with
|
|
9
|
+
* the server: the request body accepted by `POST /api/ingest/boot`
|
|
10
|
+
* (`apps/api/src/ingest/ingest.controller.ts`) and the `BootAnswer` returned by
|
|
11
|
+
* `evaluateBoot` (`apps/api/src/ingest/reporter-identity.ts`).
|
|
12
|
+
*
|
|
13
|
+
* One runtime difference is carried as an option, not a fork: a browser attaches
|
|
14
|
+
* the `Origin` header itself, while a native HTTP stack attaches none — so React
|
|
15
|
+
* Native passes its configured `origin` and the server reads it to decide
|
|
16
|
+
* `originAllowed` exactly as it does for a browser request (spec 0004 §A).
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.bootEndpoint = bootEndpoint;
|
|
20
|
+
exports.feedbackEndpoint = feedbackEndpoint;
|
|
21
|
+
exports.isBootAnswer = isBootAnswer;
|
|
22
|
+
exports.compactIdentity = compactIdentity;
|
|
23
|
+
exports.isFixbackApiRequest = isFixbackApiRequest;
|
|
24
|
+
exports.requestBoot = requestBoot;
|
|
25
|
+
const http_1 = require("./http");
|
|
26
|
+
/** Join an API base URL with the boot path, tolerating a trailing slash. */
|
|
27
|
+
function bootEndpoint(apiUrl) {
|
|
28
|
+
return `${apiUrl.replace(/\/+$/, "")}/api/ingest/boot`;
|
|
29
|
+
}
|
|
30
|
+
/** Join an API base URL with the feedback path, tolerating a trailing slash. */
|
|
31
|
+
function feedbackEndpoint(apiUrl) {
|
|
32
|
+
return `${apiUrl.replace(/\/+$/, "")}/api/ingest/feedback`;
|
|
33
|
+
}
|
|
34
|
+
/** Narrow an unknown JSON body to a {@link BootAnswer} before an SDK trusts it. */
|
|
35
|
+
function isBootAnswer(value) {
|
|
36
|
+
if (typeof value !== "object" || value === null)
|
|
37
|
+
return false;
|
|
38
|
+
const v = value;
|
|
39
|
+
return (typeof v.originAllowed === "boolean" &&
|
|
40
|
+
typeof v.canSubmit === "boolean" &&
|
|
41
|
+
typeof v.gate === "string" &&
|
|
42
|
+
(v.tier === null || typeof v.tier === "string"));
|
|
43
|
+
}
|
|
44
|
+
/** Drop `undefined` identity fields so a payload carries only what was given. */
|
|
45
|
+
function compactIdentity(identity) {
|
|
46
|
+
if (!identity)
|
|
47
|
+
return {};
|
|
48
|
+
const out = {};
|
|
49
|
+
if (identity.hostIdentity)
|
|
50
|
+
out.hostIdentity = identity.hostIdentity;
|
|
51
|
+
if (identity.reporterSession)
|
|
52
|
+
out.reporterSession = identity.reporterSession;
|
|
53
|
+
if (identity.anonymousId)
|
|
54
|
+
out.anonymousId = identity.anonymousId;
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Does `url` address one of the SDK's **own** API endpoints? Every request a capture
|
|
59
|
+
* SDK issues lives under two namespaces beneath the API origin: `/api/ingest/*` —
|
|
60
|
+
* boot and feedback — and `/api/connect/*` — the Connect code exchange and sign-out
|
|
61
|
+
* (ADR-0032).
|
|
62
|
+
*
|
|
63
|
+
* The trace's network capture ignores **only** these, never the whole origin. That
|
|
64
|
+
* distinction matters when the instrumented app and the Fixback API share an origin — a
|
|
65
|
+
* self-hosted deployment, or Fixback running its own SDK on its own dashboard — where
|
|
66
|
+
* ignoring the origin wholesale would swallow all of the app's own requests and leave the
|
|
67
|
+
* Network tab empty. When the app and the API sit on different origins (the common case)
|
|
68
|
+
* this matches nothing extra, since the app's origin is never `apiUrl`. `url` is the raw
|
|
69
|
+
* request URL — absolute for every call an SDK makes — so a prefix test over the
|
|
70
|
+
* normalised origin is exact.
|
|
71
|
+
*/
|
|
72
|
+
function isFixbackApiRequest(apiUrl, url) {
|
|
73
|
+
const apiBase = apiUrl.replace(/\/+$/, "");
|
|
74
|
+
return (url.startsWith(`${apiBase}/api/ingest/`) ||
|
|
75
|
+
url.startsWith(`${apiBase}/api/connect/`));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Ask ingest whether a submission would be accepted for this key / origin / Gate.
|
|
79
|
+
* Resolves to the boot answer, or `null` when Fixback could not be reached, the key
|
|
80
|
+
* was refused, or the response was not a boot answer. It never throws: any
|
|
81
|
+
* non-answer is treated by the caller as "stay dormant", so a Fixback outage stays
|
|
82
|
+
* invisible to the host app (ticket #47: "fails quietly").
|
|
83
|
+
*/
|
|
84
|
+
async function requestBoot(apiUrl, request, deps = {}) {
|
|
85
|
+
const doFetch = deps.fetch ?? (0, http_1.resolveFetch)();
|
|
86
|
+
if (!doFetch)
|
|
87
|
+
return null;
|
|
88
|
+
let response;
|
|
89
|
+
try {
|
|
90
|
+
response = await doFetch(bootEndpoint(apiUrl), {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: {
|
|
93
|
+
"content-type": "application/json",
|
|
94
|
+
...(deps.origin ? { origin: deps.origin } : {}),
|
|
95
|
+
},
|
|
96
|
+
body: JSON.stringify(request),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null; // network error / Fixback unreachable
|
|
101
|
+
}
|
|
102
|
+
if (!response.ok)
|
|
103
|
+
return null; // 401 unknown key, or any other refusal
|
|
104
|
+
let body;
|
|
105
|
+
try {
|
|
106
|
+
body = await response.json?.();
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return isBootAnswer(body) ? body : null;
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=boot.js.map
|
package/dist/boot.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boot.js","sourceRoot":"","sources":["../src/boot.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The trace **breadcrumb wire types** (spec 0003 §C, spec #122) — the shared,
|
|
3
|
+
* runtime-agnostic shapes for one entry of a captured trace stream (ADR-0028).
|
|
4
|
+
*
|
|
5
|
+
* A breadcrumb records recent activity — console, navigation, network metadata,
|
|
6
|
+
* masked user actions, and the failing error — that rides on a report as
|
|
7
|
+
* Evidence. This module carries only the value types; the SDK's ring buffer and
|
|
8
|
+
* DOM/Node instrumentation that produce them live in the consuming SDK.
|
|
9
|
+
* Everything private is kept out **at the source** by that instrumentation:
|
|
10
|
+
* `ui.input` records that an input changed, never its value; network crumbs carry
|
|
11
|
+
* method + URL + status only, never bodies.
|
|
12
|
+
*/
|
|
13
|
+
/** Console-style severity a `console` crumb records. */
|
|
14
|
+
export type BreadcrumbLevel = "log" | "info" | "warn" | "error" | "assert" | "debug";
|
|
15
|
+
/** The kind of activity a crumb records. */
|
|
16
|
+
export type BreadcrumbCategory = "console" | "navigation" | "fetch" | "xhr" | "beacon" | "ui.click" | "ui.input" | "error";
|
|
17
|
+
/**
|
|
18
|
+
* Which browser API issued a captured network request (spec #122 §D, ticket #139).
|
|
19
|
+
* Every network crumb's `category` is one of these too, so a stream router and the
|
|
20
|
+
* read model agree on what is a network entry.
|
|
21
|
+
*/
|
|
22
|
+
export type NetworkApi = "fetch" | "xhr" | "beacon";
|
|
23
|
+
/**
|
|
24
|
+
* A network request's failure classification (spec #122 §D, ticket #139). `ok` and
|
|
25
|
+
* the HTTP status classes come from a settled response; `network-error` / `timeout`
|
|
26
|
+
* / `aborted` from how a request failed; `opaque-cors` from a cross-origin response
|
|
27
|
+
* whose status is unreadable. The Network tab flags every non-`ok` outcome.
|
|
28
|
+
*/
|
|
29
|
+
export type NetworkOutcome = "ok" | "http-4xx" | "http-5xx" | "network-error" | "timeout" | "aborted" | "opaque-cors";
|
|
30
|
+
/**
|
|
31
|
+
* A crumb's structured detail. Deliberately narrow: there is **no** field for a
|
|
32
|
+
* request/response body or an input value, so those can never be recorded.
|
|
33
|
+
*/
|
|
34
|
+
export interface BreadcrumbData {
|
|
35
|
+
readonly url?: string;
|
|
36
|
+
readonly method?: string;
|
|
37
|
+
readonly status?: number;
|
|
38
|
+
/** A masked CSS selector for a `ui.*` target — never its text or value. */
|
|
39
|
+
readonly target?: string;
|
|
40
|
+
readonly from?: string;
|
|
41
|
+
readonly to?: string;
|
|
42
|
+
readonly errorType?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The type tag on a structured console argument (spec #122 §C, decision D7). A
|
|
46
|
+
* console call's arguments are preserved **type-tagged** rather than flattened to a
|
|
47
|
+
* string, so an object/array argument can be inspected in the Console tab rather
|
|
48
|
+
* than read as `[object Object]`: `string`/`number`/`bool`/`null` carry the value
|
|
49
|
+
* directly, `json` a depth-/byte-capped JSON-safe clone, and `error` an Error's
|
|
50
|
+
* `{ name, message, stack }`.
|
|
51
|
+
*/
|
|
52
|
+
export type ConsoleArgType = "string" | "number" | "bool" | "null" | "json" | "error";
|
|
53
|
+
/**
|
|
54
|
+
* One structured console argument (spec #122 §C): a type tag plus a JSON-safe value.
|
|
55
|
+
* `v` is always serializable — a `json` arg is depth-, breadth-, and string-capped at
|
|
56
|
+
* assembly, and exotic values (bigint, symbol, function, circular refs) are rendered
|
|
57
|
+
* to safe text — so a console crumb can never carry an unserializable or unbounded
|
|
58
|
+
* value onto the wire.
|
|
59
|
+
*/
|
|
60
|
+
export interface ConsoleArg {
|
|
61
|
+
readonly t: ConsoleArgType;
|
|
62
|
+
readonly v: unknown;
|
|
63
|
+
}
|
|
64
|
+
/** A `file:line` source location (spec #122 §C) — where a console call was made. */
|
|
65
|
+
export interface SourceLocation {
|
|
66
|
+
readonly file: string;
|
|
67
|
+
readonly line: number;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* One entry in a trace stream. Alongside its semantic fields every entry carries a
|
|
71
|
+
* stable {@link id} and a high-res monotonic {@link mono} timestamp — both stamped
|
|
72
|
+
* by the buffer on `add` — so entries from the three independent streams order and
|
|
73
|
+
* cross-link exactly (spec #122 §B, decision D9). `timestamp` stays epoch ms.
|
|
74
|
+
*/
|
|
75
|
+
export interface Breadcrumb {
|
|
76
|
+
readonly category: BreadcrumbCategory;
|
|
77
|
+
readonly message?: string;
|
|
78
|
+
readonly level?: BreadcrumbLevel;
|
|
79
|
+
/** Epoch milliseconds when the crumb was recorded. */
|
|
80
|
+
readonly timestamp: number;
|
|
81
|
+
/** A stable id, unique within the buffer — assigned on `add` when not already set. */
|
|
82
|
+
readonly id?: string;
|
|
83
|
+
/** A high-res monotonic timestamp (`performance.now()`) — assigned on `add`. */
|
|
84
|
+
readonly mono?: number;
|
|
85
|
+
readonly data?: BreadcrumbData;
|
|
86
|
+
/**
|
|
87
|
+
* For a `console` crumb (spec #122 §C): the call's arguments preserved as
|
|
88
|
+
* structured, type-tagged values (see {@link ConsoleArg}), so the Console tab can
|
|
89
|
+
* render each argument expandably instead of a flattened string. `message` stays
|
|
90
|
+
* the one-line preview. Absent on non-console crumbs.
|
|
91
|
+
*/
|
|
92
|
+
readonly args?: readonly ConsoleArg[];
|
|
93
|
+
/**
|
|
94
|
+
* For a `console` crumb (spec #122 §C): the `file:line` the call was made from,
|
|
95
|
+
* parsed best-effort from the call stack. Captured only for the levels that keep a
|
|
96
|
+
* source — `warn`/`error`/`assert` — and only when the stack yields a usable
|
|
97
|
+
* location; a chatty app's `log`/`info`/`debug` omit it. Absent on non-console crumbs.
|
|
98
|
+
*/
|
|
99
|
+
readonly source?: SourceLocation;
|
|
100
|
+
/**
|
|
101
|
+
* For an auto-captured `error` crumb only (spec #122 §F): the ids of the entries
|
|
102
|
+
* immediately preceding the throw — a causal pointer into the same trace, so a
|
|
103
|
+
* machine-filed crash names its lead-up. Absent on every other crumb.
|
|
104
|
+
*/
|
|
105
|
+
readonly causedBy?: readonly string[];
|
|
106
|
+
/**
|
|
107
|
+
* For a network crumb (`fetch` / `xhr` / `beacon`) — spec #122 §D, ticket #139.
|
|
108
|
+
* The rich request metadata the Network tab renders, lifted to the top level (the
|
|
109
|
+
* console-enrichment precedent) so the read model shapes each into a
|
|
110
|
+
* `NetworkTraceEntry`. **No field carries a request/response body or an arbitrary
|
|
111
|
+
* header** — `respSize` derives from the `content-length` response header only and
|
|
112
|
+
* `contentType` from `content-type`; nothing else is read. Absent on every other crumb.
|
|
113
|
+
*/
|
|
114
|
+
readonly api?: NetworkApi;
|
|
115
|
+
/** The request method (network crumb) — e.g. `GET`, `POST`. */
|
|
116
|
+
readonly method?: string;
|
|
117
|
+
/** The scrubbed request URL (network crumb) — query dropped, path PII redacted. */
|
|
118
|
+
readonly url?: string;
|
|
119
|
+
/** The final HTTP status (network crumb), when one was known; absent on a network error/beacon. */
|
|
120
|
+
readonly status?: number;
|
|
121
|
+
/** The HTTP status text (network crumb), when the response carried one. */
|
|
122
|
+
readonly statusText?: string;
|
|
123
|
+
/** Wall-clock duration of the request in ms (network crumb) — a `performance.now()` delta. */
|
|
124
|
+
readonly durationMs?: number;
|
|
125
|
+
/** Request body size in bytes (network crumb) — only when trivially known (string/Blob/ArrayBuffer), never by reading a stream. */
|
|
126
|
+
readonly reqSize?: number;
|
|
127
|
+
/** Response body size in bytes (network crumb) — from the `content-length` response header only. */
|
|
128
|
+
readonly respSize?: number;
|
|
129
|
+
/** Response content type (network crumb) — the `content-type` header's media type. */
|
|
130
|
+
readonly contentType?: string;
|
|
131
|
+
/** The request's failure classification (network crumb) — see {@link NetworkOutcome}. */
|
|
132
|
+
readonly outcome?: NetworkOutcome;
|
|
133
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **Connect** wire contract (ADR-0032, CONTEXT.md "Connect") — the one copy every
|
|
3
|
+
* capture SDK speaks, kept in the shared core beside the boot contract for the same
|
|
4
|
+
* reason (`@fixback/shared` is server-shaped and private, ticket #47).
|
|
5
|
+
*
|
|
6
|
+
* Connect is the round trip that binds an Account to a site: the SDK opens the
|
|
7
|
+
* platform's connect page ({@link BootAnswer.connectUrl}), the person signs in, the
|
|
8
|
+
* page hands back a **one-time code**, and the SDK exchanges it here for a **Reporter
|
|
9
|
+
* session** — a compact token it stores per origin and forwards on every boot. Keep
|
|
10
|
+
* this in lock-step with the server: `POST /api/connect/exchange` and
|
|
11
|
+
* `POST /api/connect/signout` (`apps/api/src/connect/*`).
|
|
12
|
+
*/
|
|
13
|
+
import type { ConnectedAccount, ReporterTier } from "./boot";
|
|
14
|
+
import { type FetchLike } from "./http";
|
|
15
|
+
/**
|
|
16
|
+
* The `postMessage` a connect page in a popup sends back to the opener. `source`
|
|
17
|
+
* pins it to Fixback so an unrelated message on the window is ignored; `code` is the
|
|
18
|
+
* one-time Connect code to exchange. Read with {@link parseConnectMessage}.
|
|
19
|
+
*/
|
|
20
|
+
export declare const CONNECT_MESSAGE_SOURCE = "fixback-connect";
|
|
21
|
+
/**
|
|
22
|
+
* The query parameter a Fixback link — and the connect page's **redirect fallback** —
|
|
23
|
+
* carries the one-time code back on, when a popup could not be used (ADR-0032,
|
|
24
|
+
* "arriving with the `fixback` query parameter"). The SDK detects it on boot, exchanges
|
|
25
|
+
* it, and strips it from the URL.
|
|
26
|
+
*/
|
|
27
|
+
export declare const CONNECT_RETURN_PARAM = "fixback";
|
|
28
|
+
/** The shape of the popup-to-opener message; `code` is the one-time Connect code. */
|
|
29
|
+
export interface ConnectMessage {
|
|
30
|
+
readonly source: typeof CONNECT_MESSAGE_SOURCE;
|
|
31
|
+
readonly code: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The identity the SDK reports through `Fixback.identity()` and renders in the chip:
|
|
35
|
+
* anonymous, or a connected Account with its live tier. The tier is the server's, from
|
|
36
|
+
* the last boot — never self-declared.
|
|
37
|
+
*/
|
|
38
|
+
export type ReporterIdentity = {
|
|
39
|
+
readonly status: "anonymous";
|
|
40
|
+
} | {
|
|
41
|
+
readonly status: "connected";
|
|
42
|
+
readonly name: string;
|
|
43
|
+
readonly email: string;
|
|
44
|
+
readonly tier: ReporterTier | null;
|
|
45
|
+
};
|
|
46
|
+
/** The JSON body `POST /api/connect/exchange` accepts. */
|
|
47
|
+
export interface ConnectExchangeRequest {
|
|
48
|
+
/** The Project's publishable key — the exchange must be for the code's Project. */
|
|
49
|
+
readonly key: string;
|
|
50
|
+
/** The one-time Connect code from the popup message or the return query parameter. */
|
|
51
|
+
readonly code: string;
|
|
52
|
+
/**
|
|
53
|
+
* The browser's current anonymous id, if any — so the server can link the existing
|
|
54
|
+
* anonymous Reporter to the Account (one Reporter, prior Feedback attributed).
|
|
55
|
+
*/
|
|
56
|
+
readonly anonymousId?: string;
|
|
57
|
+
/** The Platform the Connect happened on; defaults to `browser` server-side. */
|
|
58
|
+
readonly platform?: "browser" | "expo";
|
|
59
|
+
}
|
|
60
|
+
/** The JSON `POST /api/connect/exchange` returns on success. */
|
|
61
|
+
export interface ConnectExchangeResponse {
|
|
62
|
+
/** The Reporter session token to store per origin and forward on every boot. */
|
|
63
|
+
readonly reporterSession: string;
|
|
64
|
+
/** The signed-in Account, for the identity chip. */
|
|
65
|
+
readonly account: ConnectedAccount;
|
|
66
|
+
}
|
|
67
|
+
/** Join an API base URL with the Connect exchange path, tolerating a trailing slash. */
|
|
68
|
+
export declare function connectExchangeEndpoint(apiUrl: string): string;
|
|
69
|
+
/** Join an API base URL with the Connect sign-out path, tolerating a trailing slash. */
|
|
70
|
+
export declare function connectSignOutEndpoint(apiUrl: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* Build the connect-page URL the SDK opens (popup or redirect). `connectUrl` is the
|
|
73
|
+
* base the boot answer carried; `key` identifies the Project and `returnUrl` is where
|
|
74
|
+
* the person is sent back — validated server-side against the Project's allowed
|
|
75
|
+
* origins, so a hostile page cannot redirect the code anywhere else.
|
|
76
|
+
*/
|
|
77
|
+
export declare function buildConnectUrl(connectUrl: string, params: {
|
|
78
|
+
readonly key: string;
|
|
79
|
+
readonly returnUrl: string;
|
|
80
|
+
}): string;
|
|
81
|
+
/**
|
|
82
|
+
* The `localStorage` key a browser SDK stores its Reporter session token under,
|
|
83
|
+
* **scoped by publishable key** so two Fixback Projects on one origin keep separate
|
|
84
|
+
* sessions (ADR-0032). The storage binding is per-runtime; the key format is shared.
|
|
85
|
+
*/
|
|
86
|
+
export declare function reporterSessionStorageKey(publishableKey: string): string;
|
|
87
|
+
/**
|
|
88
|
+
* Narrow an unknown `message.data` to the Connect code it carries, or `null`. Guards
|
|
89
|
+
* `source` so an unrelated `postMessage` — the web is noisy — is ignored.
|
|
90
|
+
*/
|
|
91
|
+
export declare function parseConnectMessage(data: unknown): string | null;
|
|
92
|
+
/** Narrow an unknown JSON body to a {@link ConnectExchangeResponse}. */
|
|
93
|
+
export declare function isConnectExchangeResponse(value: unknown): value is ConnectExchangeResponse;
|
|
94
|
+
/** Injectable collaborators for the Connect fetch helpers. */
|
|
95
|
+
export interface ConnectDeps {
|
|
96
|
+
readonly fetch?: FetchLike;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Exchange a one-time code for a Reporter session. Resolves to the response, or `null`
|
|
100
|
+
* when Fixback was unreachable, the code was refused, or the body was not a valid
|
|
101
|
+
* response — the caller stays anonymous rather than surfacing an error into the host
|
|
102
|
+
* page (ticket #47: "fails quietly").
|
|
103
|
+
*/
|
|
104
|
+
export declare function exchangeConnectCode(apiUrl: string, request: ConnectExchangeRequest, deps?: ConnectDeps): Promise<ConnectExchangeResponse | null>;
|
|
105
|
+
/**
|
|
106
|
+
* Revoke a Reporter session server-side ("sign out"). Best-effort: a network failure
|
|
107
|
+
* still lets the SDK clear its stored token locally, and a revoked-anyway token is
|
|
108
|
+
* refused at the next boot regardless.
|
|
109
|
+
*/
|
|
110
|
+
export declare function revokeReporterSession(apiUrl: string, token: string, deps?: ConnectDeps): Promise<void>;
|