@xeonr/telemetry-web 0.0.0-stage → 0.1.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.
Files changed (50) hide show
  1. package/README.md +168 -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,169 @@
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
+ (`telemetry.xeonr.io`). It replaces Sentry: uncaught errors, failed and
5
+ timed-out requests, web vitals and one span per RPC, as OTLP/HTTP JSON, with
6
+ an offline buffer so a console that cannot reach us still reports once it
7
+ can. No runtime dependencies; the browser entry is ~9 KiB min+gzip.
8
+
9
+ Every console that mounts `@xeonr/console-shell` ≥ 0.7 already has it — the
10
+ shell's `ConsoleProvider` starts it. Use this package directly on a page
11
+ without the shell, or for the extra pieces below (your own Connect clients,
12
+ the Vite plugin).
13
+
14
+ Design: fleet observability plan §10.2; wire contract:
15
+ `lantern/docs/WAVE4-client-ingest.md` §7 (both in platform-docs).
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pnpm add @xeonr/telemetry-web
21
+ ```
22
+
23
+ ## Start it
24
+
25
+ ```ts
26
+ import { init } from "@xeonr/telemetry-web";
27
+
28
+ init({
29
+ service: "functions-ui", // the Lantern client project; default <meta name="lantern-service">
30
+ product: "functions", // service.namespace
31
+ // release: default <meta name="lantern-release">, else "dev"
32
+ // endpoint: default https://telemetry.xeonr.io
33
+ // sampleRate: 0..1 for RPC spans and web vitals; errors and reachability always go
34
+ // enabled: default on, except on localhost/*.localhost/*.test and under
35
+ // <meta name="lantern-telemetry" content="off">
36
+ });
37
+ ```
38
+
39
+ Once per page: a second `init` returns the first client. Nothing the SDK does
40
+ ever throws into the page, and it never reports its own sends.
41
+
42
+ Authentication is the public-origin model: every request goes to
43
+ `<endpoint>/v1/logs?service=<service>` (or `/v1/traces`), `credentials: "omit"`,
44
+ as `text/plain` so no request needs a preflight. The ingest accepts it only
45
+ from an origin on the project's allow-list.
46
+
47
+ ## What it sends
48
+
49
+ | Record | When | Key attributes |
50
+ |---|---|---|
51
+ | `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` |
52
+ | `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` |
53
+ | `web_vital` (INFO, sampled) | LCP, INP, CLS (on hide), FCP, TTFB | `web_vital.name/value/rating` |
54
+ | `page_view`, `route_change` (INFO) | load; `trackRoute(path)` | `url.path`, `document_load.*` |
55
+ | CLIENT span (sampled) | every RPC / wrapped fetch | named by the procedure; `rpc.method`, `server.address`, `rpc.response.status_code` |
56
+
57
+ Every record carries `session.id` (per tab session); the resource carries
58
+ `service.instance.id` (per browser install), `telemetry.sdk.*` and `browser.*`.
59
+ Breadcrumbs are the last 20 route changes and request outcomes — procedure and
60
+ code, never a payload. Messages lose URL query strings, email addresses and
61
+ bearer-looking secrets before they leave; stack frames are sent untouched.
62
+
63
+ A browser cannot tell DNS from TLS from a refused connection: `fetch` rejects
64
+ with the same `TypeError` for all of them. Offline is `network`; a same-origin
65
+ failure is `network`; a cross-origin one is `cors` if a `no-cors` probe of the
66
+ same URL gets an answer, else `network`. `dns` and `tls` are never sent from a
67
+ browser.
68
+
69
+ Delivery: batches every 5 s or at 50 records. A send that gets no answer, a
70
+ 408, a 429 or a 5xx moves its records to `localStorage` (at most 200 records /
71
+ 256 KiB, oldest non-errors dropped first) and waits for `Retry-After` or a
72
+ backoff; the next page load replays them. On `pagehide` / hidden, whatever is
73
+ left goes by `navigator.sendBeacon`.
74
+
75
+ ## Errors
76
+
77
+ ```ts
78
+ import { captureException, traceIdFrom } from "@xeonr/telemetry-web";
79
+
80
+ try { await save(); } catch (err) {
81
+ captureException(err, { extra: { "app.form": "schedule" } }); // route and X-Trace-Id are filled in
82
+ showError(err, traceIdFrom(err)); // "trace 1a2b3c4d… [copy]"
83
+ }
84
+ ```
85
+
86
+ `traceIdFrom(errOrResponse)` reads `X-Trace-Id` from a ConnectError's
87
+ metadata, a fetch `Response`'s headers, or a `traceId` (and from their
88
+ `cause`). The kit's `TraceRef` renders it.
89
+
90
+ ### React
91
+
92
+ ```tsx
93
+ import { TelemetryErrorBoundary } from "@xeonr/telemetry-web/react";
94
+
95
+ <TelemetryErrorBoundary resetKeys={[pathname]} fallback={({ error, traceId, reset }) => <Oops traceId={traceId} onRetry={reset} />}>
96
+ <App />
97
+ </TelemetryErrorBoundary>
98
+ ```
99
+
100
+ ## Requests
101
+
102
+ ```ts
103
+ // ConnectRPC: first in the list, so it times what the others do.
104
+ import { telemetryInterceptor } from "@xeonr/telemetry-web/connect";
105
+ createConnectTransport({ baseUrl, interceptors: [telemetryInterceptor(), ...yours] });
106
+
107
+ // fetch
108
+ import { wrapFetch } from "@xeonr/telemetry-web";
109
+ const api = wrapFetch(); // wraps the global fetch; or wrapFetch(myFetch)
110
+ ```
111
+
112
+ A same-origin call carries a W3C `traceparent`, so the browser span parents the
113
+ server's trace at the edge. A cross-origin API gets none unless you say so —
114
+ its CORS policy has to allow the header first:
115
+
116
+ ```ts
117
+ init({ service: "x-ui", getTraceHeaders: (url, { traceparent }) => url.hostname === "api.x.xeonr.io" ? { traceparent } : undefined });
118
+ ```
119
+
120
+ ## Releases and source maps (Vite)
121
+
122
+ ```ts
123
+ // vite.config.ts
124
+ import { lanternTelemetry } from "@xeonr/telemetry-web/vite";
125
+
126
+ export default defineConfig({
127
+ plugins: [react(), lanternTelemetry({ service: "functions-ui" })],
128
+ });
129
+ ```
130
+
131
+ The plugin:
132
+
133
+ 1. writes `<meta name="lantern-service">` and `<meta name="lantern-release">`
134
+ into `index.html` — the release is `VERSION`, else `VITE_APP_VERSION`, else
135
+ `dev` — so the SDK needs no configuration for either;
136
+ 2. forces `build.sourcemap = "hidden"` (maps are written, no bundle links them);
137
+ 3. after the build, with `CI_JOB_TOKEN` (or `LANTERN_RELEASE_KEY`) set,
138
+ registers the release (`POST /v1/releases`) and uploads every
139
+ `dist/**/*.map` (gzipped) to `/v1/sourcemaps?service=&release=&path=` keyed
140
+ by the `.js` it belongs to. `LANTERN_INGEST_URL` moves the endpoint. A
141
+ failed upload warns; it never fails the build;
142
+ 4. deletes the `.map` files from `dist`, so they are never served.
143
+
144
+ The k8s pipeline already passes `CI_JOB_TOKEN` and `VERSION` to every image
145
+ build; declare them in the builder stage, before `vite build`:
146
+
147
+ ```dockerfile
148
+ FROM node:24-slim AS builder
149
+ WORKDIR /src
150
+ RUN corepack enable
151
+ COPY package.json pnpm-lock.yaml ./
152
+ RUN pnpm install --frozen-lockfile
153
+ COPY . .
154
+ ARG CI_JOB_TOKEN
155
+ ARG VERSION
156
+ RUN pnpm run build # vite build: meta tags, hidden maps, upload, maps deleted
157
+
158
+ FROM nginx:alpine
159
+ COPY --from=builder /src/dist /usr/share/nginx/html
160
+ ```
161
+
162
+ The `ARG`s sit after the install so a new version or token does not bust the
163
+ dependency layer, and they never reach the final stage.
164
+
165
+ ## Testing against it
166
+
167
+ `createClient(options, env)` builds a client against an injected environment
168
+ (fetch, storage, timers, events) instead of the page's, without touching the
169
+ 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://telemetry.xeonr.io";
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://telemetry.xeonr.io`. */
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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"}