@xeonr/telemetry-web 0.0.0-stage → 0.1.1

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.
Files changed (50) hide show
  1. package/README.md +170 -2
  2. package/dist/classify.d.ts +48 -0
  3. package/dist/classify.d.ts.map +1 -0
  4. package/dist/classify.js +117 -0
  5. package/dist/classify.js.map +1 -0
  6. package/dist/client.d.ts +135 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +511 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/connect.d.ts +11 -0
  11. package/dist/connect.d.ts.map +1 -0
  12. package/dist/connect.js +44 -0
  13. package/dist/connect.js.map +1 -0
  14. package/dist/env.d.ts +68 -0
  15. package/dist/env.d.ts.map +1 -0
  16. package/dist/env.js +97 -0
  17. package/dist/env.js.map +1 -0
  18. package/dist/index.d.ts +34 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +89 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/otlp.d.ts +124 -0
  23. package/dist/otlp.d.ts.map +1 -0
  24. package/dist/otlp.js +105 -0
  25. package/dist/otlp.js.map +1 -0
  26. package/dist/queue.d.ts +92 -0
  27. package/dist/queue.d.ts.map +1 -0
  28. package/dist/queue.js +355 -0
  29. package/dist/queue.js.map +1 -0
  30. package/dist/react.d.ts +34 -0
  31. package/dist/react.d.ts.map +1 -0
  32. package/dist/react.js +46 -0
  33. package/dist/react.js.map +1 -0
  34. package/dist/stack.d.ts +23 -0
  35. package/dist/stack.d.ts.map +1 -0
  36. package/dist/stack.js +52 -0
  37. package/dist/stack.js.map +1 -0
  38. package/dist/trace.d.ts +10 -0
  39. package/dist/trace.d.ts.map +1 -0
  40. package/dist/trace.js +46 -0
  41. package/dist/trace.js.map +1 -0
  42. package/dist/vitals.d.ts +20 -0
  43. package/dist/vitals.d.ts.map +1 -0
  44. package/dist/vitals.js +151 -0
  45. package/dist/vitals.js.map +1 -0
  46. package/dist/vite.d.ts +41 -0
  47. package/dist/vite.d.ts.map +1 -0
  48. package/dist/vite.js +171 -0
  49. package/dist/vite.js.map +1 -0
  50. package/package.json +59 -3
package/README.md CHANGED
@@ -1,3 +1,171 @@
1
- # Temporary Holding Version
1
+ # @xeonr/telemetry-web
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Browser telemetry for Xeonr consoles, sent to Lantern's client ingest
4
+ (`https://in.prod.wtf`; 0.1.0 defaulted to `telemetry.xeonr.io`, which still
5
+ works as an alias — the name moved because `telemetry.` hosts are on browser
6
+ block lists and home DNS filters). It replaces Sentry: uncaught errors, failed and
7
+ timed-out requests, web vitals and one span per RPC, as OTLP/HTTP JSON, with
8
+ an offline buffer so a console that cannot reach us still reports once it
9
+ can. No runtime dependencies; the browser entry is ~9 KiB min+gzip.
10
+
11
+ Every console that mounts `@xeonr/console-shell` ≥ 0.7 already has it — the
12
+ shell's `ConsoleProvider` starts it. Use this package directly on a page
13
+ without the shell, or for the extra pieces below (your own Connect clients,
14
+ the Vite plugin).
15
+
16
+ Design: fleet observability plan §10.2; wire contract:
17
+ `lantern/docs/WAVE4-client-ingest.md` §7 (both in platform-docs).
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pnpm add @xeonr/telemetry-web
23
+ ```
24
+
25
+ ## Start it
26
+
27
+ ```ts
28
+ import { init } from "@xeonr/telemetry-web";
29
+
30
+ init({
31
+ service: "functions-ui", // the Lantern client project; default <meta name="lantern-service">
32
+ product: "functions", // service.namespace
33
+ // release: default <meta name="lantern-release">, else "dev"
34
+ // endpoint: default https://in.prod.wtf
35
+ // sampleRate: 0..1 for RPC spans and web vitals; errors and reachability always go
36
+ // enabled: default on, except on localhost/*.localhost/*.test and under
37
+ // <meta name="lantern-telemetry" content="off">
38
+ });
39
+ ```
40
+
41
+ Once per page: a second `init` returns the first client. Nothing the SDK does
42
+ ever throws into the page, and it never reports its own sends.
43
+
44
+ Authentication is the public-origin model: every request goes to
45
+ `<endpoint>/v1/logs?service=<service>` (or `/v1/traces`), `credentials: "omit"`,
46
+ as `text/plain` so no request needs a preflight. The ingest accepts it only
47
+ from an origin on the project's allow-list.
48
+
49
+ ## What it sends
50
+
51
+ | Record | When | Key attributes |
52
+ |---|---|---|
53
+ | `exception` (ERROR) | `window` error, unhandled rejection, the React boundary, `captureException`, a kit error toast | `exception.type/message/stacktrace` (raw stack — Lantern symbolicates), `exception.mechanism`, `code.filepath/lineno/column`, `url.path`, `http.response.header.x-trace-id`, `app.breadcrumbs` |
54
+ | `client.request.failed` (WARN) | a request through `wrapFetch` or the interceptor that got no answer, timed out, or failed server-side | `server.address`, `rpc.method`, `error.type` = `timeout\|http_5xx\|cors\|network\|aborted`, `client.online`, `client.since_last_success_ms` |
55
+ | `web_vital` (INFO, sampled) | LCP, INP, CLS (on hide), FCP, TTFB | `web_vital.name/value/rating` |
56
+ | `page_view`, `route_change` (INFO) | load; `trackRoute(path)` | `url.path`, `document_load.*` |
57
+ | CLIENT span (sampled) | every RPC / wrapped fetch | named by the procedure; `rpc.method`, `server.address`, `rpc.response.status_code` |
58
+
59
+ Every record carries `session.id` (per tab session); the resource carries
60
+ `service.instance.id` (per browser install), `telemetry.sdk.*` and `browser.*`.
61
+ Breadcrumbs are the last 20 route changes and request outcomes — procedure and
62
+ code, never a payload. Messages lose URL query strings, email addresses and
63
+ bearer-looking secrets before they leave; stack frames are sent untouched.
64
+
65
+ A browser cannot tell DNS from TLS from a refused connection: `fetch` rejects
66
+ with the same `TypeError` for all of them. Offline is `network`; a same-origin
67
+ failure is `network`; a cross-origin one is `cors` if a `no-cors` probe of the
68
+ same URL gets an answer, else `network`. `dns` and `tls` are never sent from a
69
+ browser.
70
+
71
+ Delivery: batches every 5 s or at 50 records. A send that gets no answer, a
72
+ 408, a 429 or a 5xx moves its records to `localStorage` (at most 200 records /
73
+ 256 KiB, oldest non-errors dropped first) and waits for `Retry-After` or a
74
+ backoff; the next page load replays them. On `pagehide` / hidden, whatever is
75
+ left goes by `navigator.sendBeacon`.
76
+
77
+ ## Errors
78
+
79
+ ```ts
80
+ import { captureException, traceIdFrom } from "@xeonr/telemetry-web";
81
+
82
+ try { await save(); } catch (err) {
83
+ captureException(err, { extra: { "app.form": "schedule" } }); // route and X-Trace-Id are filled in
84
+ showError(err, traceIdFrom(err)); // "trace 1a2b3c4d… [copy]"
85
+ }
86
+ ```
87
+
88
+ `traceIdFrom(errOrResponse)` reads `X-Trace-Id` from a ConnectError's
89
+ metadata, a fetch `Response`'s headers, or a `traceId` (and from their
90
+ `cause`). The kit's `TraceRef` renders it.
91
+
92
+ ### React
93
+
94
+ ```tsx
95
+ import { TelemetryErrorBoundary } from "@xeonr/telemetry-web/react";
96
+
97
+ <TelemetryErrorBoundary resetKeys={[pathname]} fallback={({ error, traceId, reset }) => <Oops traceId={traceId} onRetry={reset} />}>
98
+ <App />
99
+ </TelemetryErrorBoundary>
100
+ ```
101
+
102
+ ## Requests
103
+
104
+ ```ts
105
+ // ConnectRPC: first in the list, so it times what the others do.
106
+ import { telemetryInterceptor } from "@xeonr/telemetry-web/connect";
107
+ createConnectTransport({ baseUrl, interceptors: [telemetryInterceptor(), ...yours] });
108
+
109
+ // fetch
110
+ import { wrapFetch } from "@xeonr/telemetry-web";
111
+ const api = wrapFetch(); // wraps the global fetch; or wrapFetch(myFetch)
112
+ ```
113
+
114
+ A same-origin call carries a W3C `traceparent`, so the browser span parents the
115
+ server's trace at the edge. A cross-origin API gets none unless you say so —
116
+ its CORS policy has to allow the header first:
117
+
118
+ ```ts
119
+ init({ service: "x-ui", getTraceHeaders: (url, { traceparent }) => url.hostname === "api.x.xeonr.io" ? { traceparent } : undefined });
120
+ ```
121
+
122
+ ## Releases and source maps (Vite)
123
+
124
+ ```ts
125
+ // vite.config.ts
126
+ import { lanternTelemetry } from "@xeonr/telemetry-web/vite";
127
+
128
+ export default defineConfig({
129
+ plugins: [react(), lanternTelemetry({ service: "functions-ui" })],
130
+ });
131
+ ```
132
+
133
+ The plugin:
134
+
135
+ 1. writes `<meta name="lantern-service">` and `<meta name="lantern-release">`
136
+ into `index.html` — the release is `VERSION`, else `VITE_APP_VERSION`, else
137
+ `dev` — so the SDK needs no configuration for either;
138
+ 2. forces `build.sourcemap = "hidden"` (maps are written, no bundle links them);
139
+ 3. after the build, with `CI_JOB_TOKEN` (or `LANTERN_RELEASE_KEY`) set,
140
+ registers the release (`POST /v1/releases`) and uploads every
141
+ `dist/**/*.map` (gzipped) to `/v1/sourcemaps?service=&release=&path=` keyed
142
+ by the `.js` it belongs to. `LANTERN_INGEST_URL` moves the endpoint. A
143
+ failed upload warns; it never fails the build;
144
+ 4. deletes the `.map` files from `dist`, so they are never served.
145
+
146
+ The k8s pipeline already passes `CI_JOB_TOKEN` and `VERSION` to every image
147
+ build; declare them in the builder stage, before `vite build`:
148
+
149
+ ```dockerfile
150
+ FROM node:24-slim AS builder
151
+ WORKDIR /src
152
+ RUN corepack enable
153
+ COPY package.json pnpm-lock.yaml ./
154
+ RUN pnpm install --frozen-lockfile
155
+ COPY . .
156
+ ARG CI_JOB_TOKEN
157
+ ARG VERSION
158
+ RUN pnpm run build # vite build: meta tags, hidden maps, upload, maps deleted
159
+
160
+ FROM nginx:alpine
161
+ COPY --from=builder /src/dist /usr/share/nginx/html
162
+ ```
163
+
164
+ The `ARG`s sit after the install so a new version or token does not bust the
165
+ dependency layer, and they never reach the final stage.
166
+
167
+ ## Testing against it
168
+
169
+ `createClient(options, env)` builds a client against an injected environment
170
+ (fetch, storage, timers, events) instead of the page's, without touching the
171
+ page-wide singleton.
@@ -0,0 +1,48 @@
1
+ export type ErrorClass = "dns" | "tls" | "timeout" | "http_5xx" | "cors" | "network" | "aborted";
2
+ /** Connect's numeric codes, so this package carries no runtime import of @connectrpc/connect. */
3
+ export declare const CODE: {
4
+ readonly Canceled: 1;
5
+ readonly Unknown: 2;
6
+ readonly DeadlineExceeded: 4;
7
+ readonly ResourceExhausted: 8;
8
+ readonly Internal: 13;
9
+ readonly Unavailable: 14;
10
+ readonly DataLoss: 15;
11
+ };
12
+ /** Connect's code as its wire name ("permission_denied"); "unknown" for anything out of range. */
13
+ export declare function codeName(code: number): string;
14
+ export interface ConnectErrorLike {
15
+ name: string;
16
+ code: number;
17
+ message: string;
18
+ rawMessage?: string;
19
+ metadata?: {
20
+ get(k: string): string | null;
21
+ };
22
+ cause?: unknown;
23
+ }
24
+ export declare function isConnectError(e: unknown): e is ConnectErrorLike;
25
+ /** fetch's "could not get an answer at all": a TypeError (or, in old Safari, a bare "Load failed"). */
26
+ export declare function isNetworkTypeError(e: unknown): boolean;
27
+ export interface ClassifyInput {
28
+ /** What fetch rejected with, or what the Connect call threw. */
29
+ error?: unknown;
30
+ /** The HTTP status, when there was an answer. */
31
+ status?: number | undefined;
32
+ online: boolean;
33
+ sameOrigin: boolean;
34
+ }
35
+ /**
36
+ * The class of a failure, or "maybe-cors" when only a probe can decide,
37
+ * or null when this was not a reachability failure at all (a 4xx, a
38
+ * Connect PermissionDenied — the server answered, and answered no).
39
+ */
40
+ export declare function classify(i: ClassifyInput): ErrorClass | "maybe-cors" | null;
41
+ /**
42
+ * Settles "maybe-cors": a `no-cors` request to the same URL resolves (with an
43
+ * opaque response) when the server answered, and rejects when nothing did.
44
+ * Uses the SDK's own unwrapped fetch, so it is never itself reported. Bounded
45
+ * by a short timeout; an undecidable probe says "network".
46
+ */
47
+ export declare function probeCors(fetchImpl: typeof fetch | undefined, url: string, timeoutMs?: number): Promise<ErrorClass>;
48
+ //# sourceMappingURL=classify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../src/classify.ts"],"names":[],"mappings":"AAkBA,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,KAAK,GAAG,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC;AAEjG,iGAAiG;AACjG,eAAO,MAAM,IAAI;;;;;;;;CAQP,CAAC;AAQX,kGAAkG;AAClG,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE;QAAE,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAC7C,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,gBAAgB,CAOhE;AAMD,uGAAuG;AACvG,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAEtD;AAED,MAAM,WAAW,aAAa;IAC5B,gEAAgE;IAChE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,aAAa,GAAG,UAAU,GAAG,YAAY,GAAG,IAAI,CA+B3E;AAED;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,SAAS,EAAE,OAAO,KAAK,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,SAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAcvH"}
@@ -0,0 +1,117 @@
1
+ // Why a request failed, from what a browser is willing to say — which is not
2
+ // much. fetch rejects with the same opaque TypeError for a DNS failure, a TLS
3
+ // failure, a refused connection and a CORS refusal, by design. What can be
4
+ // told apart:
5
+ //
6
+ // offline (navigator.onLine false) → network
7
+ // AbortSignal.timeout / Connect deadline → timeout
8
+ // an abort the caller asked for → aborted
9
+ // an answer with status ≥ 500 → http_5xx
10
+ // an opaque TypeError, same-origin → network (CORS cannot apply)
11
+ // an opaque TypeError, cross-origin → cors if a no-cors probe of the
12
+ // same URL gets an answer (the
13
+ // server is up, the browser hid
14
+ // it), otherwise network
15
+ //
16
+ // `dns` and `tls` are in the vocabulary for clients that can tell (device
17
+ // apps); a browser never can, so this module never returns them.
18
+ /** Connect's numeric codes, so this package carries no runtime import of @connectrpc/connect. */
19
+ export const CODE = {
20
+ Canceled: 1,
21
+ Unknown: 2,
22
+ DeadlineExceeded: 4,
23
+ ResourceExhausted: 8,
24
+ Internal: 13,
25
+ Unavailable: 14,
26
+ DataLoss: 15,
27
+ };
28
+ const CODE_NAMES = [
29
+ "ok", "canceled", "unknown", "invalid_argument", "deadline_exceeded", "not_found", "already_exists",
30
+ "permission_denied", "resource_exhausted", "failed_precondition", "aborted", "out_of_range",
31
+ "unimplemented", "internal", "unavailable", "data_loss", "unauthenticated",
32
+ ];
33
+ /** Connect's code as its wire name ("permission_denied"); "unknown" for anything out of range. */
34
+ export function codeName(code) {
35
+ return CODE_NAMES[code] ?? "unknown";
36
+ }
37
+ export function isConnectError(e) {
38
+ return (typeof e === "object" &&
39
+ e !== null &&
40
+ e.name === "ConnectError" &&
41
+ typeof e.code === "number");
42
+ }
43
+ function errName(e) {
44
+ return typeof e === "object" && e !== null && typeof e.name === "string" ? e.name : undefined;
45
+ }
46
+ /** fetch's "could not get an answer at all": a TypeError (or, in old Safari, a bare "Load failed"). */
47
+ export function isNetworkTypeError(e) {
48
+ return errName(e) === "TypeError" || (e instanceof Error && /load failed|failed to fetch|networkerror/i.test(e.message));
49
+ }
50
+ /**
51
+ * The class of a failure, or "maybe-cors" when only a probe can decide,
52
+ * or null when this was not a reachability failure at all (a 4xx, a
53
+ * Connect PermissionDenied — the server answered, and answered no).
54
+ */
55
+ export function classify(i) {
56
+ if (i.status !== undefined && i.status >= 500)
57
+ return "http_5xx";
58
+ const e = i.error;
59
+ if (e === undefined || e === null)
60
+ return null;
61
+ const name = errName(e);
62
+ if (name === "TimeoutError")
63
+ return "timeout";
64
+ if (name === "AbortError")
65
+ return "aborted";
66
+ if (isConnectError(e)) {
67
+ switch (e.code) {
68
+ case CODE.DeadlineExceeded:
69
+ return "timeout";
70
+ case CODE.Canceled:
71
+ // connect-web maps AbortSignal.timeout() to Canceled as well; the cause says which.
72
+ return /timed out|timeout/i.test(e.message) ? "timeout" : "aborted";
73
+ case CODE.Unknown:
74
+ // A fetch that never got an answer arrives as Unknown with the TypeError as its cause.
75
+ if (isNetworkTypeError(e.cause))
76
+ return !i.online ? "network" : i.sameOrigin ? "network" : "maybe-cors";
77
+ return "http_5xx";
78
+ case CODE.Internal:
79
+ case CODE.Unavailable:
80
+ case CODE.DataLoss:
81
+ return "http_5xx";
82
+ default:
83
+ return null;
84
+ }
85
+ }
86
+ if (isNetworkTypeError(e)) {
87
+ if (!i.online)
88
+ return "network";
89
+ return i.sameOrigin ? "network" : "maybe-cors";
90
+ }
91
+ return null;
92
+ }
93
+ /**
94
+ * Settles "maybe-cors": a `no-cors` request to the same URL resolves (with an
95
+ * opaque response) when the server answered, and rejects when nothing did.
96
+ * Uses the SDK's own unwrapped fetch, so it is never itself reported. Bounded
97
+ * by a short timeout; an undecidable probe says "network".
98
+ */
99
+ export async function probeCors(fetchImpl, url, timeoutMs = 3000) {
100
+ if (!fetchImpl)
101
+ return "network";
102
+ try {
103
+ const ctrl = typeof AbortController === "function" ? new AbortController() : undefined;
104
+ const t = setTimeout(() => ctrl?.abort(), timeoutMs);
105
+ try {
106
+ await fetchImpl(url, { method: "HEAD", mode: "no-cors", credentials: "omit", cache: "no-store", ...(ctrl ? { signal: ctrl.signal } : {}) });
107
+ return "cors";
108
+ }
109
+ finally {
110
+ clearTimeout(t);
111
+ }
112
+ }
113
+ catch {
114
+ return "network";
115
+ }
116
+ }
117
+ //# sourceMappingURL=classify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.js","sourceRoot":"","sources":["../src/classify.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,135 @@
1
+ import { type Env } from "./env.ts";
2
+ import { type Attrs, type SeverityName } from "./otlp.ts";
3
+ export declare const SDK_NAME = "@xeonr/telemetry-web";
4
+ export declare const SDK_VERSION = "0.1.0";
5
+ export declare const DEFAULT_ENDPOINT = "https://in.prod.wtf";
6
+ /** The record attribute carrying breadcrumbs (a JSON string). */
7
+ export declare const BREADCRUMBS_KEY = "app.breadcrumbs";
8
+ export interface TelemetryOptions {
9
+ /**
10
+ * The client project's service name in Lantern, `<product>-ui`. Default:
11
+ * `<meta name="lantern-service">` (the Vite plugin writes it). With neither,
12
+ * the client is disabled: there is nowhere to send to.
13
+ */
14
+ service?: string | undefined;
15
+ /** The ingest. Default `https://in.prod.wtf`. */
16
+ endpoint?: string | undefined;
17
+ /** service.version. Default `<meta name="lantern-release">`, else "dev". */
18
+ release?: string | undefined;
19
+ /** deployment.environment. The ingest stamps the project's own; this is a hint. */
20
+ environment?: string | undefined;
21
+ /** The console's product key (`service.namespace`). */
22
+ product?: string | undefined;
23
+ /**
24
+ * The propagation headers for an outgoing request, or undefined for none.
25
+ * Default: `{ traceparent }` for a same-origin request only — a
26
+ * cross-origin API has to allow the header in its CORS policy first, and a
27
+ * request it refuses is worse than an unparented trace.
28
+ */
29
+ getTraceHeaders?: ((url: URL, ctx: {
30
+ traceparent: string;
31
+ sameOrigin: boolean;
32
+ }) => Record<string, string> | undefined) | undefined;
33
+ /** Share of RPC spans, page loads' web vitals that are sent, 0..1. Errors and reachability are always sent. Default 1. */
34
+ sampleRate?: number | undefined;
35
+ /**
36
+ * Default: on, except on a local development host (localhost, 127.0.0.1,
37
+ * [::1], *.localhost, *.test) and on a page with
38
+ * `<meta name="lantern-telemetry" content="off">`. An explicit value wins.
39
+ */
40
+ enabled?: boolean | undefined;
41
+ /** Window error/unhandledrejection handlers. Default true. */
42
+ captureGlobalErrors?: boolean | undefined;
43
+ /** Web vitals and the page_view record. Default true. */
44
+ capturePerformance?: boolean | undefined;
45
+ /** Advanced: flush cadence and buffer bounds. */
46
+ flushIntervalMs?: number | undefined;
47
+ maxBatch?: number | undefined;
48
+ maxStored?: number | undefined;
49
+ maxStoredBytes?: number | undefined;
50
+ }
51
+ export interface CaptureContext {
52
+ /** The route (url.path) — default the last trackRoute, else location.pathname. Query strings are stripped. */
53
+ route?: string | undefined;
54
+ /** The X-Trace-Id of the failed call, when there was one. Default: read from the error itself. */
55
+ traceId?: string | undefined;
56
+ /** More record attributes. Keys the ingest has not declared are dropped there. */
57
+ extra?: Attrs | undefined;
58
+ /** How it was caught: onerror, unhandledrejection, react, toast, manual (default). */
59
+ mechanism?: string | undefined;
60
+ /** Default ERROR. A handled, expected refusal (a toast for a 4xx) can go as WARN. */
61
+ severity?: Extract<SeverityName, "ERROR" | "WARN"> | undefined;
62
+ /** React's component stack, from an error boundary. */
63
+ componentStack?: string | undefined;
64
+ }
65
+ export type Breadcrumb = {
66
+ type: "route";
67
+ path: string;
68
+ } | {
69
+ type: "rpc";
70
+ procedure: string;
71
+ code: string;
72
+ } | {
73
+ type: "fetch";
74
+ method: string;
75
+ host: string;
76
+ status: string;
77
+ };
78
+ export interface RequestStart {
79
+ /** The full request URL. */
80
+ url: string;
81
+ /** Connect procedure (`pkg.Service/Method`), for an RPC. */
82
+ procedure?: string | undefined;
83
+ /** HTTP method. Default POST for an RPC, GET otherwise. */
84
+ method?: string | undefined;
85
+ }
86
+ export interface RequestEnd {
87
+ /** HTTP status, when there was an answer (fetch). */
88
+ status?: number | undefined;
89
+ /** What the call threw. */
90
+ error?: unknown;
91
+ /** The response's X-Trace-Id, when there was one. */
92
+ traceId?: string | null | undefined;
93
+ }
94
+ export interface RequestHandle {
95
+ /** Headers to add to the request (traceparent, when propagating). */
96
+ readonly headers: Record<string, string>;
97
+ end(r: RequestEnd): void;
98
+ }
99
+ export interface TelemetryClient {
100
+ readonly enabled: boolean;
101
+ readonly service: string;
102
+ readonly release: string;
103
+ readonly endpoint: string;
104
+ /** service.instance.id: one per browser install (localStorage). */
105
+ readonly instanceId: string;
106
+ /** session.id: one per tab session (sessionStorage). */
107
+ readonly sessionId: string;
108
+ captureException(err: unknown, ctx?: CaptureContext): void;
109
+ /** A route change: a breadcrumb, a route_change record, and the url.path of later records. */
110
+ trackRoute(path: string): void;
111
+ /** A record of your own: `event.name` = name. */
112
+ event(name: string, attrs?: Attrs, opts?: {
113
+ severity?: SeverityName | undefined;
114
+ body?: string | undefined;
115
+ }): void;
116
+ breadcrumb(b: Breadcrumb): void;
117
+ /** Attributes added to every later record (`xeonr.subject`, say). undefined removes one. */
118
+ setAttributes(attrs: Attrs): void;
119
+ /** Instrument one request: the fetch wrapper and the Connect interceptor are built on this. */
120
+ beginRequest(r: RequestStart): RequestHandle;
121
+ /** Whether a URL is the ingest's own (never instrumented: no recursion). */
122
+ isOwnUrl(url: string): boolean;
123
+ flush(): Promise<void>;
124
+ shutdown(): void;
125
+ }
126
+ /** A client that does nothing, for a disabled or unconfigured page. Same shape, so callers never null-check. */
127
+ export declare function noopClient(service?: string, release?: string, endpoint?: string): TelemetryClient;
128
+ /** Whether this page is a local development server, where telemetry is off by default. */
129
+ export declare function isLocalDevHost(hostname: string | undefined): boolean;
130
+ /**
131
+ * Build a client against an explicit environment. `init` is this over the
132
+ * real browser, plus the page-wide singleton; tests call it directly.
133
+ */
134
+ export declare function createClient(opts?: TelemetryOptions, env?: Env): TelemetryClient;
135
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}