@nekuda/webmcp-sdk 0.4.0-dev.7.3
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/CHANGELOG.md +244 -0
- package/dist/define.d.ts +79 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +1607 -0
- package/dist/register.d.ts +80 -0
- package/dist/spec.d.ts +56 -0
- package/dist/telemetry-context.d.ts +164 -0
- package/dist/telemetry-events.d.ts +224 -0
- package/dist/telemetry-fields.d.ts +119 -0
- package/dist/telemetry.d.ts +471 -0
- package/dist/tool-metrics.d.ts +52 -0
- package/dist/tracking.d.ts +208 -0
- package/dist/transport.d.ts +94 -0
- package/package.json +45 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { AnyWebMCPTool } from "./define.js";
|
|
2
|
+
import { type ModelContextLike } from "./spec.js";
|
|
3
|
+
import { type TrackingOptions } from "./tracking.js";
|
|
4
|
+
/**
|
|
5
|
+
* Per-tool outcome of a `registerTools` call:
|
|
6
|
+
* - `registered` — live on the page's WebMCP surface
|
|
7
|
+
* - `unsupported` — this browser has no WebMCP surface; nothing was registered
|
|
8
|
+
* - `aborted` — the registration's lifetime ended before/while registering
|
|
9
|
+
* - `failed` — the native surface rejected (e.g. duplicate name)
|
|
10
|
+
*/
|
|
11
|
+
export type ToolRegistrationState = "registered" | "unsupported" | "aborted" | "failed";
|
|
12
|
+
export interface ToolRegistrationResult {
|
|
13
|
+
stableKey: string;
|
|
14
|
+
name: string;
|
|
15
|
+
state: ToolRegistrationState;
|
|
16
|
+
error?: unknown;
|
|
17
|
+
}
|
|
18
|
+
export interface RegisterToolsOptions {
|
|
19
|
+
/**
|
|
20
|
+
* External lifetime for the registration (e.g. a component's unmount signal).
|
|
21
|
+
* Aborting it unregisters every tool in this batch, same as `unregister()`.
|
|
22
|
+
*/
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
/** Injectable surface for tests; defaults to the page's `document.modelContext`. */
|
|
25
|
+
modelContext?: ModelContextLike;
|
|
26
|
+
/**
|
|
27
|
+
* Configures anonymous tool-call tracking for this batch. Default-silent: with
|
|
28
|
+
* neither `apiKey` nor `otel` set — including an empty `tracking: {}` — nothing
|
|
29
|
+
* is built at all, so omitting it means zero tracking and no behavior change.
|
|
30
|
+
* Once an output is enabled, each tool invocation emits a `tool_call_request`
|
|
31
|
+
* event before its handler runs and a `tool_call_response` event after.
|
|
32
|
+
* `disabled: true` is this channel's consent gate; it does not narrow the
|
|
33
|
+
* default-on telemetry channel below, which resolves no identity and touches no
|
|
34
|
+
* storage. An `apiKey` set here is also sent as `x-api-key` on that channel's
|
|
35
|
+
* beacons, which only attributes them to this tenant.
|
|
36
|
+
*/
|
|
37
|
+
tracking?: TrackingOptions;
|
|
38
|
+
/**
|
|
39
|
+
* Opt out of the default-on usage telemetry (`src/telemetry.ts`): `telemetry: false`
|
|
40
|
+
* means no `tool_registration` and no `tool_call` for this batch, and it cancels the
|
|
41
|
+
* page's one `sdk_init` outright — that event describes the page load, so it is
|
|
42
|
+
* cancelled page-wide and irreversibly. Unlike `tracking` this channel needs no
|
|
43
|
+
* `apiKey`.
|
|
44
|
+
*
|
|
45
|
+
* The other two events are per-batch, so the option is read per call: a *later*
|
|
46
|
+
* `registerTools` that omits it reports its own batch and calls. Cancelling
|
|
47
|
+
* `sdk_init` also requires winning the race with its deferred flush — a batch
|
|
48
|
+
* registered at module scope does, one registered from a mount effect or a consent
|
|
49
|
+
* callback may not. A site that wants the whole page silent regardless of where its
|
|
50
|
+
* `registerTools` calls come from (generated code, a CDN snippet) should use a
|
|
51
|
+
* page-level lever instead: `globalThis.__WEBMCP_TELEMETRY__ = false` — strictly
|
|
52
|
+
* `false`, so a truthy `"0"` does not opt out — or Global Privacy Control
|
|
53
|
+
* (`navigator.globalPrivacyControl === true`), both of which are read at emit time
|
|
54
|
+
* and so cover every event. With no `document` in scope the channel is silent
|
|
55
|
+
* anyway, so SSR never beacons.
|
|
56
|
+
*/
|
|
57
|
+
telemetry?: boolean;
|
|
58
|
+
}
|
|
59
|
+
export interface ToolRegistration {
|
|
60
|
+
/**
|
|
61
|
+
* Settles when every tool's native registration settles. Never rejects — per-tool
|
|
62
|
+
* outcomes (including failures) are reported in the results, so one bad tool
|
|
63
|
+
* cannot mask the rest.
|
|
64
|
+
*/
|
|
65
|
+
ready: Promise<ToolRegistrationResult[]>;
|
|
66
|
+
/** Unregister every tool in this batch. Idempotent. */
|
|
67
|
+
unregister(): void;
|
|
68
|
+
/** The signal carrying this registration's lifetime (aborted once unregistered). */
|
|
69
|
+
signal: AbortSignal;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Register a batch of `defineTool` tools on the page's WebMCP surface.
|
|
73
|
+
*
|
|
74
|
+
* Lifecycle: registration starts immediately; the tools stay live until
|
|
75
|
+
* `unregister()` is called or the external `options.signal` aborts (both funnel
|
|
76
|
+
* into one internal `AbortController`, the spec's only unregistration mechanism).
|
|
77
|
+
* On browsers without a WebMCP surface this is a graceful no-op — generated code
|
|
78
|
+
* can run unconditionally on every page.
|
|
79
|
+
*/
|
|
80
|
+
export declare function registerTools(tools: readonly AnyWebMCPTool[], options?: RegisterToolsOptions): ToolRegistration;
|
package/dist/spec.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pinned WebMCP spec surface — the ONLY module that knows the raw browser API.
|
|
3
|
+
*
|
|
4
|
+
* Pinned against the WebMCP editor's draft as of 2026-07
|
|
5
|
+
* (https://webmachinelearning.github.io/webmcp/):
|
|
6
|
+
*
|
|
7
|
+
* - `document.modelContext` is the surface (Chrome 150+). `navigator.modelContext`
|
|
8
|
+
* is deprecated but is the only surface on Chrome 149, so resolution reads
|
|
9
|
+
* `document` first and falls back — `??` picks exactly one object, so a browser
|
|
10
|
+
* exposing both never double-registers.
|
|
11
|
+
* - `registerTool(tool, { signal })` returns a promise that settles when the
|
|
12
|
+
* registration completes; it rejects on a duplicate name, an invalid tool, an
|
|
13
|
+
* inactive document, or an abort.
|
|
14
|
+
* - Unregistration happens ONLY by aborting the `AbortSignal` passed at
|
|
15
|
+
* registration. `unregisterTool()` and `provideContext()` are dead APIs and are
|
|
16
|
+
* never referenced.
|
|
17
|
+
* - Tool names: 1–128 chars of [A-Za-z0-9_\-.].
|
|
18
|
+
* - `annotations.readOnlyHint` / `annotations.untrustedContentHint`.
|
|
19
|
+
*
|
|
20
|
+
* The draft breaks monthly; absorbing that churn is this module's job (R23). All
|
|
21
|
+
* other modules — and all plugin-generated code — target `ModelContextLike`, never
|
|
22
|
+
* the raw API.
|
|
23
|
+
*/
|
|
24
|
+
export interface ToolAnnotations {
|
|
25
|
+
/** The tool does not modify page or user state. */
|
|
26
|
+
readOnlyHint?: boolean;
|
|
27
|
+
/** The tool's output can contain untrusted (user or third-party) data. */
|
|
28
|
+
untrustedContentHint?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** The tool shape `registerTool` accepts (`ModelContextTool` in the draft IDL). */
|
|
31
|
+
export interface SpecTool {
|
|
32
|
+
name: string;
|
|
33
|
+
title?: string;
|
|
34
|
+
description: string;
|
|
35
|
+
inputSchema?: Record<string, unknown>;
|
|
36
|
+
annotations?: ToolAnnotations;
|
|
37
|
+
execute(input: Record<string, unknown>): Promise<unknown>;
|
|
38
|
+
}
|
|
39
|
+
export interface RegisterToolOptions {
|
|
40
|
+
/** Aborting unregisters the tool — the spec's only unregistration mechanism. */
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The slice of the native surface the SDK uses, typed to the draft contract:
|
|
45
|
+
* `registerTool` returns a promise. Pre-draft surfaces that return nothing are a
|
|
46
|
+
* runtime concern only — the SDK normalizes via `Promise.resolve` at the call
|
|
47
|
+
* site, never by widening this type.
|
|
48
|
+
*/
|
|
49
|
+
export interface ModelContextLike {
|
|
50
|
+
registerTool(tool: SpecTool, options?: RegisterToolOptions): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the page's WebMCP surface; `undefined` on non-supporting browsers.
|
|
54
|
+
* Injectable global scope for tests.
|
|
55
|
+
*/
|
|
56
|
+
export declare function resolveModelContext(g?: object): ModelContextLike | undefined;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derived page and client context for the telemetry events (`src/telemetry.ts`):
|
|
3
|
+
* a route *template* instead of a URL, a referrer *class* instead of a referrer,
|
|
4
|
+
* a form factor instead of a screen size, a language subtag instead of a locale.
|
|
5
|
+
* Every function here exists so the event can carry the signal without carrying
|
|
6
|
+
* the identifying string it was derived from — no `url`, `referrer`, `title`,
|
|
7
|
+
* `screen`, or full UA string ever leaves the page. It also holds what the events
|
|
8
|
+
* report about the page's WebMCP surface, the navigation clock, and the in-memory
|
|
9
|
+
* session ID that joins them.
|
|
10
|
+
*
|
|
11
|
+
* Two properties hold throughout. First, nothing throws: every browser global is
|
|
12
|
+
* optional in a non-DOM runtime and page-script- and extension-writable in a real
|
|
13
|
+
* one, so each read goes through a guard and an unreadable value simply means the
|
|
14
|
+
* field is **omitted** — never sent as `null`. Second, every input is bounded:
|
|
15
|
+
* a pathname, referrer, or UA string is caller-controlled, and the truncation
|
|
16
|
+
* ladder in `src/tracking.ts` does not reach a derived field, so the cap has to
|
|
17
|
+
* happen here.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Prefix of a page-controlled string any derived field will even look at: a
|
|
21
|
+
* pathname, a referrer, an error message. All come from values page script can set
|
|
22
|
+
* to a megabyte-long one, and a regex scan over that is main-thread cost on the
|
|
23
|
+
* tool-call path. Real URLs and messages live well under 2 KB.
|
|
24
|
+
*
|
|
25
|
+
* What slicing first costs: templating only ever shortens and never moves a
|
|
26
|
+
* character earlier, so the *first* bytes of a derived value are unaffected — this
|
|
27
|
+
* prefix is an order of magnitude above every output cap ({@link MAX_ROUTE_BYTES},
|
|
28
|
+
* `MAX_ERROR_SIGNATURE_BYTES`). What it does drop is the tail of an input whose
|
|
29
|
+
* middle would have collapsed to nothing: a message that is two megabytes of digits
|
|
30
|
+
* between `order ` and ` not found` clusters as `order *`, without the trailing
|
|
31
|
+
* words. That is the accepted trade — the alternative is scanning a page-controlled
|
|
32
|
+
* megabyte on the tool-call path.
|
|
33
|
+
*/
|
|
34
|
+
export declare const MAX_INPUT_CHARS = 4096;
|
|
35
|
+
/**
|
|
36
|
+
* Whether a run reads as an opaque blob rather than as words. Shared with
|
|
37
|
+
* `errorSignature` in `src/telemetry.ts`, which needs the same distinction on a
|
|
38
|
+
* message's long runs; it lives here because this module is the leaf of the two.
|
|
39
|
+
*
|
|
40
|
+
* Counted by index rather than by code point so the share is measured against the
|
|
41
|
+
* same unit as `length` — a route segment can hold any Unicode, and only ASCII
|
|
42
|
+
* capitals are evidence of an encoding.
|
|
43
|
+
*/
|
|
44
|
+
export declare function uppercaseHeavy(run: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* A `location.pathname` reduced to a low-cardinality route template
|
|
47
|
+
* (`/products/12345` → `/products/:id`). Query and fragment are dropped whole —
|
|
48
|
+
* they carry search terms, session tokens, and ad parameters. A non-string
|
|
49
|
+
* pathname is omitted; any string yields a rooted template, so the root path is
|
|
50
|
+
* `"/"`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function routeTemplate(pathname?: unknown): string | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Non-empty segments in a pathname, emitted alongside {@link routeTemplate}. It
|
|
55
|
+
* is the check on the templating: a route reported as `/` with a segment count of
|
|
56
|
+
* 4, or a count above {@link MAX_ROUTE_SEGMENTS}, says the template lost
|
|
57
|
+
* something — which is invisible from the template alone.
|
|
58
|
+
*/
|
|
59
|
+
export declare function segmentCount(pathname?: unknown): number | undefined;
|
|
60
|
+
/** How the visitor arrived, as a class rather than a referrer URL. */
|
|
61
|
+
export type ReferrerClass = "internal" | "ai_assistant" | "search" | "social" | "direct" | "other";
|
|
62
|
+
/**
|
|
63
|
+
* Classify `document.referrer` against the page's own origin. An empty referrer
|
|
64
|
+
* is `direct` — that is what a typed URL, a bookmark, and a stripped
|
|
65
|
+
* cross-origin referrer all look like. An unparseable one is `other`, since it
|
|
66
|
+
* was *something*; conflating it with `direct` would inflate the direct bucket.
|
|
67
|
+
*/
|
|
68
|
+
export declare function referrerClass(referrer?: unknown, origin?: unknown): ReferrerClass;
|
|
69
|
+
/** Device class, at the only granularity that stays non-identifying. */
|
|
70
|
+
export type FormFactor = "mobile" | "desktop";
|
|
71
|
+
/**
|
|
72
|
+
* Mobile vs desktop, from the narrowest signal that answers it: UA-Client-Hints
|
|
73
|
+
* `mobile` is a boolean the browser computes and survives UA reduction, so it
|
|
74
|
+
* wins; a coarse pointer with no hover is the CSS-level answer for browsers
|
|
75
|
+
* without UA-CH; screen width is the last resort, and the only one a desktop
|
|
76
|
+
* window resize can fool. Omitted when none of the three is readable.
|
|
77
|
+
*/
|
|
78
|
+
export declare function formFactor(scope?: object): FormFactor | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* The primary language subtag only (`en`, not `en-US`). The region half is a
|
|
81
|
+
* coarse location signal and adds nothing to the "what language do these tools
|
|
82
|
+
* get called in" question. An unrecognized tag (`x-private`, `""`) is omitted.
|
|
83
|
+
*/
|
|
84
|
+
export declare function languageSubtag(scope?: object): string | undefined;
|
|
85
|
+
/** What is driving the page, as far as the page can tell. */
|
|
86
|
+
export type AgentRuntime = "chatgpt" | "claude" | "perplexity" | "headless" | "browser" | "unknown";
|
|
87
|
+
/**
|
|
88
|
+
* Best-effort runtime identification. Deliberately weak, and known to be: real
|
|
89
|
+
* AI crawlers fetch server-side and never run this code, so the browser almost
|
|
90
|
+
* always sees `browser` or `headless`. The edge is authoritative — it reads the
|
|
91
|
+
* beacon's own `User-Agent` header, which no body field may carry. The one signal
|
|
92
|
+
* only the client has is `navigator.webdriver`, folded into `headless` here.
|
|
93
|
+
*
|
|
94
|
+
* `unknown` rather than omitted when there is no UA at all: "we looked and could
|
|
95
|
+
* not tell" is a different fact from "this field was not collected".
|
|
96
|
+
*/
|
|
97
|
+
export declare function agentRuntime(scope?: object): AgentRuntime;
|
|
98
|
+
/** Whether the SDK is running in the top-level document or inside a frame. */
|
|
99
|
+
export type FrameContext = "top" | "iframe";
|
|
100
|
+
/**
|
|
101
|
+
* Top document or embedded frame. Reading `top` from a cross-origin frame can
|
|
102
|
+
* throw, and the throw *is* the answer: only an embedded document can be denied
|
|
103
|
+
* access to its own top. Omitted when there is no `self` to compare against,
|
|
104
|
+
* which means no browser at all.
|
|
105
|
+
*/
|
|
106
|
+
export declare function frameContext(scope?: object): FrameContext | undefined;
|
|
107
|
+
/** `document.visibilityState`, restricted to the values the spec defines. */
|
|
108
|
+
export type PageVisibility = "visible" | "hidden" | "prerender";
|
|
109
|
+
/**
|
|
110
|
+
* Whether the page was in the foreground when the event was assembled — a
|
|
111
|
+
* prerendered or background page is where "the SDK loaded but nothing happened"
|
|
112
|
+
* comes from. An unrecognized state is omitted rather than coerced.
|
|
113
|
+
*/
|
|
114
|
+
export declare function visibility(scope?: object): PageVisibility | undefined;
|
|
115
|
+
/** Where the page's WebMCP surface came from. */
|
|
116
|
+
export type SurfaceProvenance = "native" | "polyfill" | "extension" | "none";
|
|
117
|
+
/** Which global carries the surface — the spec's own, or its deprecated predecessor. */
|
|
118
|
+
export type SurfaceGlobal = "document.modelContext" | "navigator.modelContext";
|
|
119
|
+
/** What `sdk_init` reports about the WebMCP surface it found (or didn't). */
|
|
120
|
+
export interface SurfaceInfo {
|
|
121
|
+
available: boolean;
|
|
122
|
+
provenance: SurfaceProvenance;
|
|
123
|
+
/** Absent when no surface was found. */
|
|
124
|
+
global?: SurfaceGlobal;
|
|
125
|
+
/** Absent unless the surface declares one. */
|
|
126
|
+
specVersion?: string;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The WebMCP surface as `registerTools` sees it. Availability comes from
|
|
130
|
+
* {@link resolveModelContext} itself rather than a second lookup, so what this
|
|
131
|
+
* event reports can never disagree with what registration actually used — the
|
|
132
|
+
* `document`-first, `navigator`-fallback precedence lives in exactly one place
|
|
133
|
+
* (`src/spec.ts`), and a browser exposing both is reported as the one that wins.
|
|
134
|
+
*
|
|
135
|
+
* A surface that is present but not the native API is worth separating: a
|
|
136
|
+
* polyfill's failure modes are its own, and an extension-injected surface means
|
|
137
|
+
* tools registered on a browser that has no WebMCP of its own.
|
|
138
|
+
*/
|
|
139
|
+
export declare function surfaceInfo(scope?: object): SurfaceInfo;
|
|
140
|
+
/**
|
|
141
|
+
* Milliseconds from navigation start to now, the basis of `timeToInitMs` and
|
|
142
|
+
* `timeSinceInitMs`. `performance.now()` is already relative to
|
|
143
|
+
* `performance.timeOrigin` and monotonic, so it is the primary reading;
|
|
144
|
+
* `Date.now() - timeOrigin` is the fallback for a surface that exposes the origin
|
|
145
|
+
* but not a usable clock. `undefined` when neither is usable — a shimmed `now()`
|
|
146
|
+
* returning `NaN` or a negative value must not become a nonsense duration on the
|
|
147
|
+
* event.
|
|
148
|
+
*/
|
|
149
|
+
export declare function timeSinceNavigation(scope?: object): number | undefined;
|
|
150
|
+
/**
|
|
151
|
+
* The init mark itself, reported as `sdk_init.timeToInitMs`. Deliberately not
|
|
152
|
+
* scope-injectable: reading it fresh when the deferred flush assembles the event
|
|
153
|
+
* would measure the flush hop instead of module load, and the three events would
|
|
154
|
+
* no longer share one baseline.
|
|
155
|
+
*/
|
|
156
|
+
export declare function initAt(): number | undefined;
|
|
157
|
+
/**
|
|
158
|
+
* Milliseconds from SDK init to now. Both readings are navigation-relative, so
|
|
159
|
+
* their difference is the page time elapsed between them. Clamped at `0`: a
|
|
160
|
+
* shimmed clock that runs backwards must not report a negative age.
|
|
161
|
+
*/
|
|
162
|
+
export declare function timeSinceInit(scope?: object): number | undefined;
|
|
163
|
+
/** The in-memory session ID for this module instance; see {@link SESSION_ID}. */
|
|
164
|
+
export declare function sessionId(): string;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three telemetry events as explicit types — the wire contract `src/telemetry.ts`
|
|
3
|
+
* assembles and `/v1/telemetry` ingests. They live in their own leaf module so the
|
|
4
|
+
* shape can be read and diffed without the builders around it, and so the builders,
|
|
5
|
+
* the allowlist (`src/telemetry-fields.ts`), and `docs/telemetry-schema.md` all
|
|
6
|
+
* answer to one declaration.
|
|
7
|
+
*
|
|
8
|
+
* Two rules hold across every interface here:
|
|
9
|
+
*
|
|
10
|
+
* - **A field that can be absent is `?`, never `| null`.** Every builder omits an
|
|
11
|
+
* underivable field rather than sending a null, so a `null` on the wire is a bug
|
|
12
|
+
* and not a value. The one exception is `tool_call.errorClass`: "we looked and
|
|
13
|
+
* there was no error" is a different fact from "this event does not carry that
|
|
14
|
+
* field", and only the success path can state the former.
|
|
15
|
+
* - **No index signatures.** The v1 event type was open (`[key: string]: unknown`),
|
|
16
|
+
* which let a field reach the wire with nothing declaring it. Exact interfaces are
|
|
17
|
+
* what let the pinning tests and the allowlist disagree with a builder.
|
|
18
|
+
*
|
|
19
|
+
* Type-only imports throughout: this module contributes one constant and no runtime
|
|
20
|
+
* behavior, so nothing here pulls another module into a consumer's bundle.
|
|
21
|
+
*/
|
|
22
|
+
import type { ToolIntent, ToolSource } from "./define.js";
|
|
23
|
+
import type { ToolRegistrationState } from "./register.js";
|
|
24
|
+
import type { ToolAnnotations } from "./spec.js";
|
|
25
|
+
import type { AgentRuntime, FormFactor, FrameContext, PageVisibility, ReferrerClass, SurfaceInfo } from "./telemetry-context.js";
|
|
26
|
+
import type { ToolShapeMetrics } from "./tool-metrics.js";
|
|
27
|
+
/**
|
|
28
|
+
* Wire-format version carried by every event as `schema`. `2` is a clean break from
|
|
29
|
+
* v1 — different event names, different field set, no storage-backed identity — and
|
|
30
|
+
* nothing is deployed against v1, so no compatibility shim exists.
|
|
31
|
+
*/
|
|
32
|
+
export declare const TELEMETRY_SCHEMA_VERSION = 2;
|
|
33
|
+
/** The discriminant: which of the three events this is. */
|
|
34
|
+
export type TelemetryEventName = "sdk_init" | "tool_registration" | "tool_call";
|
|
35
|
+
/**
|
|
36
|
+
* What every event carries. `sessionId` is in-memory only (`sessionId()` in
|
|
37
|
+
* `src/telemetry-context.ts`) and is the sole join key between the three: arrival
|
|
38
|
+
* order is not guaranteed, so nothing downstream may assume `sdk_init` lands first.
|
|
39
|
+
*/
|
|
40
|
+
export interface TelemetryEnvelope {
|
|
41
|
+
schema: typeof TELEMETRY_SCHEMA_VERSION;
|
|
42
|
+
event: TelemetryEventName;
|
|
43
|
+
/** Event time as an ISO-8601 string. */
|
|
44
|
+
ts: string;
|
|
45
|
+
sessionId: string;
|
|
46
|
+
}
|
|
47
|
+
/** How the SDK reached the page — a build-time fact, not an observed one. */
|
|
48
|
+
export type InstallMode = "npm" | "cdn_snippet";
|
|
49
|
+
/** `sdk.*` — which build of which package emitted the event. */
|
|
50
|
+
export interface SdkInfo {
|
|
51
|
+
name: string;
|
|
52
|
+
version: string;
|
|
53
|
+
installMode: InstallMode;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* `page.*` — the page as derived signals rather than strings. `referrerClass` is
|
|
57
|
+
* always present because an absent referrer is itself a class (`direct`); the rest
|
|
58
|
+
* are omitted when the global they come from is missing or unreadable.
|
|
59
|
+
*/
|
|
60
|
+
export interface PageContext {
|
|
61
|
+
routeTemplate?: string;
|
|
62
|
+
segmentCount?: number;
|
|
63
|
+
referrerClass: ReferrerClass;
|
|
64
|
+
frameContext?: FrameContext;
|
|
65
|
+
visibility?: PageVisibility;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* `client.*` — who is driving the page. `agentRuntime` is always present because
|
|
69
|
+
* "we looked and could not tell" is reported as `unknown` rather than by omission;
|
|
70
|
+
* see the accuracy caveat on `agentRuntime` in `src/telemetry-context.ts`.
|
|
71
|
+
*/
|
|
72
|
+
export interface ClientContext {
|
|
73
|
+
agentRuntime: AgentRuntime;
|
|
74
|
+
browser?: string;
|
|
75
|
+
browserMajor?: number;
|
|
76
|
+
formFactor?: FormFactor;
|
|
77
|
+
language?: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* `sdk_init` — once per page load, at import time via a deferred flush. It fires
|
|
81
|
+
* even when nobody ever calls `registerTools`: a site that loads the SDK and never
|
|
82
|
+
* registers is a broken integration, and this event is the only way to see it.
|
|
83
|
+
*/
|
|
84
|
+
export interface SdkInitEvent extends TelemetryEnvelope {
|
|
85
|
+
event: "sdk_init";
|
|
86
|
+
sdk: SdkInfo;
|
|
87
|
+
surface: SurfaceInfo;
|
|
88
|
+
page: PageContext;
|
|
89
|
+
client: ClientContext;
|
|
90
|
+
/** Milliseconds from navigation start; absent when the clock is unusable. */
|
|
91
|
+
timeToInitMs?: number;
|
|
92
|
+
}
|
|
93
|
+
/** What caused this `registerTools` call, inferred from module state. */
|
|
94
|
+
export type RegistrationTrigger = "initial" | "spa_navigation" | "re_register";
|
|
95
|
+
/**
|
|
96
|
+
* How one tool's registration settled. The registration states plus `pending`,
|
|
97
|
+
* which only telemetry can report: the batch emits on a timeout rather than wait
|
|
98
|
+
* forever on a surface whose `registerTool` never settles. Derived from
|
|
99
|
+
* {@link ToolRegistrationState} so the two can never drift apart.
|
|
100
|
+
*/
|
|
101
|
+
export type ToolRegistrationOutcome = ToolRegistrationState | "pending";
|
|
102
|
+
/**
|
|
103
|
+
* `config.*` — what the authenticated `tracking` channel is actually doing on this
|
|
104
|
+
* page, moved here from `sdk_init` because it is per-batch information and at
|
|
105
|
+
* `sdk_init` flush time no batch may have run.
|
|
106
|
+
*/
|
|
107
|
+
export interface TrackingConfigInfo {
|
|
108
|
+
trackingEnabled: boolean;
|
|
109
|
+
otelEnabled: boolean;
|
|
110
|
+
customEndpoint: boolean;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* One tool in a `tool_registration` batch. The shape metrics are inherited as
|
|
114
|
+
* optional because they and `schemaHash` are derived from `inputSchema` together:
|
|
115
|
+
* an entry carries both or neither, matching `shapeMetrics`/`schemaHash`.
|
|
116
|
+
*/
|
|
117
|
+
export interface RegisteredToolEntry extends Partial<ToolShapeMetrics> {
|
|
118
|
+
name: string;
|
|
119
|
+
stableKey: string;
|
|
120
|
+
version?: string;
|
|
121
|
+
schemaHash?: string;
|
|
122
|
+
source?: ToolSource;
|
|
123
|
+
intent?: ToolIntent;
|
|
124
|
+
outcome: ToolRegistrationOutcome;
|
|
125
|
+
/** Templated failure message; absent unless `outcome` is `failed`. */
|
|
126
|
+
failureSignature?: string;
|
|
127
|
+
descriptionLength: number;
|
|
128
|
+
/** Absent when the tool declares none — an empty object would say the same thing
|
|
129
|
+
* at the cost of a key on every entry. */
|
|
130
|
+
annotations?: ToolAnnotations;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* What an entry degrades to on the truncation ladder's first `tools` rung (schema
|
|
134
|
+
* §"Size bound and truncation order", step 1): identity and outcome, the two things
|
|
135
|
+
* the event exists for. Declared rather than left implicit because the ladder runs
|
|
136
|
+
* *after* assembly, so a consumer typing against {@link ToolRegistrationEvent} would
|
|
137
|
+
* otherwise read `descriptionLength` as always-present on a beacon that has none.
|
|
138
|
+
*/
|
|
139
|
+
export type StrippedToolEntry = Pick<RegisteredToolEntry, "name" | "stableKey" | "outcome" | "schemaHash" | "source" | "intent">;
|
|
140
|
+
/**
|
|
141
|
+
* What `tools` degrades to on the ladder's second rung (step 2) when stripping the
|
|
142
|
+
* entries still leaves the batch over the 64 KB bound: the count, which answers "did
|
|
143
|
+
* this site register tools" where an event dropped whole answers nothing.
|
|
144
|
+
*/
|
|
145
|
+
export interface TruncatedTools {
|
|
146
|
+
__truncated: true;
|
|
147
|
+
/** Serialized bytes the entries occupied; `0` when they could not be serialized. */
|
|
148
|
+
originalBytes: number;
|
|
149
|
+
/** The `tools` array's own length, so it survives unserializable entries. */
|
|
150
|
+
toolCount: number;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* `tool_registration` — once per `registerTools` call, carrying the per-tool
|
|
154
|
+
* outcomes v1 computed and threw away. A site where 3 of 8 tools fail on a
|
|
155
|
+
* duplicate name is the failure mode this event exists to make visible.
|
|
156
|
+
*/
|
|
157
|
+
export interface ToolRegistrationEvent extends TelemetryEnvelope {
|
|
158
|
+
event: "tool_registration";
|
|
159
|
+
/** 1-based position of this batch within the page load. */
|
|
160
|
+
registrationIndex: number;
|
|
161
|
+
trigger: RegistrationTrigger;
|
|
162
|
+
routeTemplate?: string;
|
|
163
|
+
timeSinceInitMs?: number;
|
|
164
|
+
/** Milliseconds from the call to the batch settling (or to the timeout). */
|
|
165
|
+
settleMs: number;
|
|
166
|
+
config: TrackingConfigInfo;
|
|
167
|
+
/**
|
|
168
|
+
* One entry per tool in the batch — unless the batch could not be sent whole, in
|
|
169
|
+
* which case the truncation ladder degrades this field in two ordered steps. Narrow
|
|
170
|
+
* with `Array.isArray` before reading entries.
|
|
171
|
+
*/
|
|
172
|
+
tools: RegisteredToolEntry[] | StrippedToolEntry[] | TruncatedTools;
|
|
173
|
+
}
|
|
174
|
+
/** How a tool invocation settled. */
|
|
175
|
+
export type ToolCallOutcome = "success" | "error";
|
|
176
|
+
/** `tool.*` on `tool_call` — identity and the two fields worth joining calls on. */
|
|
177
|
+
export interface ToolCallToolInfo {
|
|
178
|
+
stableKey: string;
|
|
179
|
+
schemaHash?: string;
|
|
180
|
+
intent?: ToolIntent;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* `response.*` — the handler's result as measurements, never as content. Absent
|
|
184
|
+
* when there is no result to measure (the error path, or a handler returning
|
|
185
|
+
* nothing).
|
|
186
|
+
*/
|
|
187
|
+
export interface ToolCallResponseMetrics {
|
|
188
|
+
bytes: number;
|
|
189
|
+
contentBlocks: number;
|
|
190
|
+
/** The normalized result's own `isError` flag — a handler-reported failure that
|
|
191
|
+
* did not throw, so `outcome` alone cannot see it. */
|
|
192
|
+
isError: boolean;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* `tool_call` — once per settled invocation, derived signals only. No raw `input`,
|
|
196
|
+
* `response`, or error message: the reliability question is answered by `outcome`,
|
|
197
|
+
* `durationMs`, and the error *class* and *signature*, none of which carry merchant
|
|
198
|
+
* or visitor content.
|
|
199
|
+
*/
|
|
200
|
+
export interface ToolCallEvent extends TelemetryEnvelope {
|
|
201
|
+
event: "tool_call";
|
|
202
|
+
/** Per-call random ID — deduplicates a retried beacon, joins nothing else. */
|
|
203
|
+
callId: string;
|
|
204
|
+
/** 1-based position among all tool calls in this page load. */
|
|
205
|
+
callIndex: number;
|
|
206
|
+
/** 1-based position among calls to *this* tool. */
|
|
207
|
+
toolCallIndex: number;
|
|
208
|
+
/** `stableKey` of the previously called tool; absent on the first call. */
|
|
209
|
+
precededBy?: string;
|
|
210
|
+
routeTemplate?: string;
|
|
211
|
+
agentRuntime: AgentRuntime;
|
|
212
|
+
timeSinceInitMs?: number;
|
|
213
|
+
tool: ToolCallToolInfo;
|
|
214
|
+
outcome: ToolCallOutcome;
|
|
215
|
+
/** Client-measured elapsed milliseconds. */
|
|
216
|
+
durationMs: number;
|
|
217
|
+
response?: ToolCallResponseMetrics;
|
|
218
|
+
/** Explicitly `null` on the success path — see the module doc. */
|
|
219
|
+
errorClass: string | null;
|
|
220
|
+
/** Templated error message; absent unless `outcome` is `error`. */
|
|
221
|
+
errorSignature?: string;
|
|
222
|
+
}
|
|
223
|
+
/** Any event the channel emits, discriminated on `event`. */
|
|
224
|
+
export type TelemetryEvent = SdkInitEvent | ToolRegistrationEvent | ToolCallEvent;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field allowlist for the default-on usage-telemetry channel
|
|
3
|
+
* (`src/telemetry.ts`). Every field an event may carry is listed here, and nothing
|
|
4
|
+
* is emitted unless its entry is `true`. `pruneByAllowlist` applies both maps on
|
|
5
|
+
* the way out, so flipping an entry to `false` and releasing is the single change
|
|
6
|
+
* needed to stop collecting a field — no builder can leak it.
|
|
7
|
+
*
|
|
8
|
+
* **Two maps, because one event field is an array.** `tool_registration.tools` is a
|
|
9
|
+
* list of per-tool entries: a dotted path cannot address the third entry's
|
|
10
|
+
* `schemaHash`, and per-index paths would mean a different allowlist per batch size.
|
|
11
|
+
* {@link TELEMETRY_TOOL_FIELDS} is keyed by the per-entry field name and gates it
|
|
12
|
+
* across every entry at once. The same map gates `tool_call.tool`, whose keys are a
|
|
13
|
+
* subset of an entry's — one decision covers both places a tool is described. The
|
|
14
|
+
* two namespaces are independent on purpose: `outcome` is the *call's* outcome at
|
|
15
|
+
* the event level and the *tool's registration* outcome inside an entry.
|
|
16
|
+
*
|
|
17
|
+
* `tools` and `tool` also appear in {@link TELEMETRY_FIELDS} as whole-key gates, so
|
|
18
|
+
* dropping either collection outright does not mean disabling every per-entry field.
|
|
19
|
+
*
|
|
20
|
+
* Event-level keys are the paths as they appear on the wire, at most two levels
|
|
21
|
+
* deep — the full extent of the event schemas. `routeTemplate` and `timeSinceInitMs`
|
|
22
|
+
* are listed once each and gate both events that carry them; `page.routeTemplate` and
|
|
23
|
+
* `client.agentRuntime` are separate keys because `sdk_init` nests them.
|
|
24
|
+
*
|
|
25
|
+
* **What is missing is missing by design.** The v1 fields that carried PII or
|
|
26
|
+
* unbounded cardinality (`fullUserAgent`, `screen`, `timezone`, `platform`, `url`,
|
|
27
|
+
* `path`, `referrer`, `title`, raw `input`/`response`/`error`, storage-backed
|
|
28
|
+
* `visitorId`/`sessionId`) are not listed as `false` — they are gone, replaced by
|
|
29
|
+
* derived signals, so no allowlist entry can bring them back.
|
|
30
|
+
*
|
|
31
|
+
* The envelope (`schema`, `event`, `ts`, `sessionId`) is listed like everything
|
|
32
|
+
* else, but it is what the backend routes, versions, and joins on: turning one of
|
|
33
|
+
* those off would emit unroutable payloads, so treat them as fixed in practice.
|
|
34
|
+
*
|
|
35
|
+
* This module is pure data — no imports, no side effects at import time.
|
|
36
|
+
*/
|
|
37
|
+
/** Event-level fields, keyed by their dotted path on the wire. */
|
|
38
|
+
export declare const TELEMETRY_FIELDS: {
|
|
39
|
+
readonly schema: true;
|
|
40
|
+
readonly event: true;
|
|
41
|
+
readonly ts: true;
|
|
42
|
+
readonly sessionId: true;
|
|
43
|
+
readonly "sdk.name": true;
|
|
44
|
+
readonly "sdk.version": true;
|
|
45
|
+
readonly "sdk.installMode": true;
|
|
46
|
+
readonly "surface.available": true;
|
|
47
|
+
readonly "surface.provenance": true;
|
|
48
|
+
readonly "surface.global": true;
|
|
49
|
+
readonly "surface.specVersion": true;
|
|
50
|
+
readonly "page.routeTemplate": true;
|
|
51
|
+
readonly "page.segmentCount": true;
|
|
52
|
+
readonly "page.referrerClass": true;
|
|
53
|
+
readonly "page.frameContext": true;
|
|
54
|
+
readonly "page.visibility": true;
|
|
55
|
+
readonly "client.agentRuntime": true;
|
|
56
|
+
readonly "client.browser": true;
|
|
57
|
+
readonly "client.browserMajor": true;
|
|
58
|
+
readonly "client.formFactor": true;
|
|
59
|
+
readonly "client.language": true;
|
|
60
|
+
readonly timeToInitMs: true;
|
|
61
|
+
readonly registrationIndex: true;
|
|
62
|
+
readonly trigger: true;
|
|
63
|
+
readonly settleMs: true;
|
|
64
|
+
readonly "config.trackingEnabled": true;
|
|
65
|
+
readonly "config.otelEnabled": true;
|
|
66
|
+
readonly "config.customEndpoint": true;
|
|
67
|
+
readonly tools: true;
|
|
68
|
+
readonly callId: true;
|
|
69
|
+
readonly callIndex: true;
|
|
70
|
+
readonly toolCallIndex: true;
|
|
71
|
+
readonly precededBy: true;
|
|
72
|
+
readonly tool: true;
|
|
73
|
+
readonly outcome: true;
|
|
74
|
+
readonly durationMs: true;
|
|
75
|
+
readonly "response.bytes": true;
|
|
76
|
+
readonly "response.contentBlocks": true;
|
|
77
|
+
readonly "response.isError": true;
|
|
78
|
+
readonly errorClass: true;
|
|
79
|
+
readonly errorSignature: true;
|
|
80
|
+
readonly routeTemplate: true;
|
|
81
|
+
readonly timeSinceInitMs: true;
|
|
82
|
+
readonly agentRuntime: true;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Per-entry fields inside `tool_registration.tools[]` and on `tool_call.tool`.
|
|
86
|
+
* `annotations` gates the whole sub-object: its two booleans are one declaration by
|
|
87
|
+
* the tool author, and splitting them would be a third level of allowlist for no
|
|
88
|
+
* decision anyone needs to make separately.
|
|
89
|
+
*/
|
|
90
|
+
export declare const TELEMETRY_TOOL_FIELDS: {
|
|
91
|
+
readonly name: true;
|
|
92
|
+
readonly stableKey: true;
|
|
93
|
+
readonly version: true;
|
|
94
|
+
readonly schemaHash: true;
|
|
95
|
+
readonly source: true;
|
|
96
|
+
readonly intent: true;
|
|
97
|
+
readonly outcome: true;
|
|
98
|
+
readonly failureSignature: true;
|
|
99
|
+
readonly descriptionLength: true;
|
|
100
|
+
readonly annotations: true;
|
|
101
|
+
readonly paramCount: true;
|
|
102
|
+
readonly requiredCount: true;
|
|
103
|
+
readonly freeTextParamCount: true;
|
|
104
|
+
readonly enumParamCount: true;
|
|
105
|
+
readonly maxDepth: true;
|
|
106
|
+
readonly describedParamCount: true;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Every event-level field name, as a dotted event path. Derived from the map above
|
|
110
|
+
* so the field list exists in exactly one place — adding a field is one line, and
|
|
111
|
+
* it cannot be half-added.
|
|
112
|
+
*/
|
|
113
|
+
export type TelemetryField = keyof typeof TELEMETRY_FIELDS;
|
|
114
|
+
/** Field name → collected. A `false` entry is pruned from every event. */
|
|
115
|
+
export type TelemetryFieldMap = Readonly<Record<TelemetryField, boolean>>;
|
|
116
|
+
/** Every per-entry field name, derived from {@link TELEMETRY_TOOL_FIELDS}. */
|
|
117
|
+
export type TelemetryToolField = keyof typeof TELEMETRY_TOOL_FIELDS;
|
|
118
|
+
/** Per-entry field name → collected. A `false` entry is pruned from every entry. */
|
|
119
|
+
export type TelemetryToolFieldMap = Readonly<Record<TelemetryToolField, boolean>>;
|