@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,471 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default-on usage telemetry for `@nekuda/webmcp-sdk`. Independent of the opt-in
|
|
3
|
+
* `tracking` channel (`src/tracking.ts`): it needs no `apiKey`, so it reports SDK
|
|
4
|
+
* adoption, browser mix, WebMCP availability, and tool-call reliability from every
|
|
5
|
+
* site the SDK runs on. A page that *does* configure one has it sent as
|
|
6
|
+
* `x-api-key`, which only adds tenant attribution. Opt out with `telemetry: false`
|
|
7
|
+
* on `registerTools`, with `globalThis.__WEBMCP_TELEMETRY__ = false`, or via
|
|
8
|
+
* Global Privacy Control.
|
|
9
|
+
*
|
|
10
|
+
* Event schemas, joined on the in-memory `sessionId`, no batching,
|
|
11
|
+
* fire-and-forget: `sdk_init` (once per page load, from the one-shot flush this
|
|
12
|
+
* module schedules at import time), `tool_registration` (once per `registerTools`
|
|
13
|
+
* call), and `tool_call` (once per settled invocation). Events go to the separate
|
|
14
|
+
* `/v1/telemetry` endpoint through `sendTelemetry` in `src/transport.ts`.
|
|
15
|
+
*
|
|
16
|
+
* No browser storage is touched anywhere in this channel — no `visitorId`, no
|
|
17
|
+
* stored `sessionId` (the ePrivacy Art. 5(3) concern). Cross-visit stitching is
|
|
18
|
+
* deliberately impossible here; the authenticated `tracking` channel keeps its own
|
|
19
|
+
* storage-backed identity, and this one never reads it.
|
|
20
|
+
*
|
|
21
|
+
* Every field an event may carry is gated by the baked-in allowlists in
|
|
22
|
+
* `src/telemetry-fields.ts` — one keyed by event path, one by per-tool field.
|
|
23
|
+
* Pruning happens on the way out, so flipping a field to `false` there is the
|
|
24
|
+
* single change needed to stop collecting it.
|
|
25
|
+
*/
|
|
26
|
+
import { type ToolIntent, type ToolSource } from "./define.js";
|
|
27
|
+
import type { ToolAnnotations } from "./spec.js";
|
|
28
|
+
import { type InstallMode, type RegisteredToolEntry, type RegistrationTrigger, type SdkInitEvent, type TelemetryEvent, type ToolCallEvent, type ToolCallOutcome, type ToolRegistrationEvent, type ToolRegistrationOutcome } from "./telemetry-events.js";
|
|
29
|
+
import { type TelemetryFieldMap, type TelemetryToolFieldMap } from "./telemetry-fields.js";
|
|
30
|
+
import { type TrackingOptions } from "./tracking.js";
|
|
31
|
+
/** Reported as `sdk.name` — the npm package identity. */
|
|
32
|
+
export declare const SDK_NAME = "@nekuda/webmcp-sdk";
|
|
33
|
+
/**
|
|
34
|
+
* Reported as `sdk.version`. The fallback is duplicated from `package.json`
|
|
35
|
+
* because a browser bundle cannot read it at runtime; a test pins the two
|
|
36
|
+
* together so a release that bumps one and not the other fails rather than
|
|
37
|
+
* mislabeling every event.
|
|
38
|
+
*/
|
|
39
|
+
export declare const SDK_VERSION: string;
|
|
40
|
+
/**
|
|
41
|
+
* Reported as `sdk.installMode`. Defaults to `"npm"` — that is how every published
|
|
42
|
+
* build reaches a page today, and a mistyped define must degrade to the truth for
|
|
43
|
+
* the overwhelming majority rather than emit a value no consumer can read.
|
|
44
|
+
*/
|
|
45
|
+
export declare const SDK_INSTALL_MODE: InstallMode;
|
|
46
|
+
/**
|
|
47
|
+
* Read a value that may not exist, collapsing both an absent property and a
|
|
48
|
+
* throwing getter to `undefined`. Every browser global this module touches is
|
|
49
|
+
* optional *and* attacker/extension-reachable, so reaching a property is as
|
|
50
|
+
* unsafe as calling a method — hence the thunk rather than a property lookup.
|
|
51
|
+
*
|
|
52
|
+
* Exported because the same hazard applies to the caller-supplied options object
|
|
53
|
+
* `register.ts` reads its channel configuration from.
|
|
54
|
+
*/
|
|
55
|
+
export declare function safe<T>(read: () => T): T | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Global Privacy Control: `navigator.globalPrivacyControl === true` is a legally
|
|
58
|
+
* recognized opt-out signal, so the default-on channel goes fully silent for it.
|
|
59
|
+
* Strictly `true` — a truthy `"1"`/`1` is not the spec'd signal and must not be
|
|
60
|
+
* read as consent *or* refusal. Absent or hostile `navigator` → not opted out.
|
|
61
|
+
*/
|
|
62
|
+
export declare function gpcOptedOut(scope?: object): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Whether the default-on telemetry channel may emit. Default-on means only three
|
|
65
|
+
* things silence it on a page: the explicit `telemetry: false` opt-out,
|
|
66
|
+
* {@link globalOptOut}, and GPC. When this is `false` nothing at all happens
|
|
67
|
+
* downstream — no `sdk_init`, no `tool_call`.
|
|
68
|
+
*
|
|
69
|
+
* Called twice per event, deliberately. `registerTools` asks once per call, with the
|
|
70
|
+
* batch's `option`, to decide whether that batch observes at all — a silent batch
|
|
71
|
+
* does zero work, not wasted work. {@link emitTelemetry} asks again without the
|
|
72
|
+
* option, at the moment an event would leave the page, because the two page-level
|
|
73
|
+
* levers can be flipped after that decision was made.
|
|
74
|
+
*
|
|
75
|
+
* A `document` is required on top of that, because this channel measures *browser*
|
|
76
|
+
* adoption and must never beacon from a non-browser runtime. The documented
|
|
77
|
+
* codegen shape calls `registerTools` at module scope, which also evaluates during
|
|
78
|
+
* SSR/prerender (Next, Remix), where an unauthenticated POST from the customer's
|
|
79
|
+
* own server is both unexpected egress and junk data: a server `fetch` sends no
|
|
80
|
+
* CORS `Origin`, the only site-attribution signal this channel has, and the event
|
|
81
|
+
* carries no client context, no page context, and `surface.available: false` for a
|
|
82
|
+
* runtime that was never going to have one. Every real browser has a `document`; an
|
|
83
|
+
* unsupported one is missing only `document.modelContext`, so the
|
|
84
|
+
* unsupported-browser adoption signal still reports.
|
|
85
|
+
*/
|
|
86
|
+
export declare function telemetryEnabled(option?: boolean, scope?: object): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* `client.browser` / `client.browserMajor` — the browser's own identity, the only
|
|
89
|
+
* environment fact these events carry. Both absent when UA-Client-Hints is.
|
|
90
|
+
*/
|
|
91
|
+
export interface BrowserBrand {
|
|
92
|
+
browser?: string;
|
|
93
|
+
browserMajor?: number;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A clustering key for the thrown value's message: the message with its
|
|
97
|
+
* high-cardinality substrings templated to `*`, so `no product for SKU-12345` and
|
|
98
|
+
* `no product for SKU-98765` land in one bucket. Complements
|
|
99
|
+
* {@link errorClassOf}, which is class-only and so cannot separate two failure
|
|
100
|
+
* modes that share a class — nearly every merchant failure is a bare `Error`.
|
|
101
|
+
*
|
|
102
|
+
* `undefined`, never an empty string, when the message is unreadable or empty, so
|
|
103
|
+
* the field is omitted rather than adding a meaningless key to every event. A
|
|
104
|
+
* nullish throw is one of those cases: `String(undefined)` would otherwise cluster
|
|
105
|
+
* as the literal signature `"undefined"`, describing an error nobody observed and
|
|
106
|
+
* mixing `throw undefined` in with the outcome that was recorded without one at all
|
|
107
|
+
* (`register.ts` carries no `error` for a rejection with `undefined`). Absent here
|
|
108
|
+
* matches {@link errorClassOf}, which is already `null` for the same value.
|
|
109
|
+
*
|
|
110
|
+
* Templating runs before the byte cap but after {@link MAX_INPUT_CHARS}. The prefix
|
|
111
|
+
* bound is what makes the cost of the scans safe: each rule scans from its own set of
|
|
112
|
+
* starting positions, and a thrown message is page-controlled, so an unbounded input
|
|
113
|
+
* turned a multi-megabyte one into a main-thread stall of hundreds of milliseconds on
|
|
114
|
+
* the error path. It cannot be left
|
|
115
|
+
* to {@link LONG_RUN} to collapse the run first: a run with no digits and few capitals
|
|
116
|
+
* reads as prose to `isOpaque` and survives whole. What the bound costs is the tail of
|
|
117
|
+
* a message whose middle collapses to nothing — two megabytes of digits between
|
|
118
|
+
* `order ` and ` not found` clusters as `order *` — the accepted trade, since the
|
|
119
|
+
* opening words are what make the key useful; see {@link MAX_INPUT_CHARS}.
|
|
120
|
+
*
|
|
121
|
+
* What the prefix bound does *not* do is license bounding a rule's own candidate. A
|
|
122
|
+
* rule that matches part of an oversized candidate — or, for {@link QUOTED_LITERAL},
|
|
123
|
+
* none of it — is a leak of exactly the value it exists to remove, so
|
|
124
|
+
* {@link QUOTED_LITERAL}, {@link EMAIL}, {@link URL_RUN}, and {@link PATH_RUN} each run
|
|
125
|
+
* to a delimiter instead — see each for why that costs nothing. Where the *cut* is what
|
|
126
|
+
* removes a rule's closing delimiter, {@link CUT_CANDIDATE} stands in as that
|
|
127
|
+
* delimiter, so the bound cannot leak by the back door what the rules no longer leak
|
|
128
|
+
* by the front.
|
|
129
|
+
*
|
|
130
|
+
* Templating before the *byte* cap is what makes a huge message cluster on its
|
|
131
|
+
* opening words instead of on whatever the byte cut happened to leave behind.
|
|
132
|
+
*
|
|
133
|
+
* The rules then run in a fixed order, and four adjacencies are load-bearing.
|
|
134
|
+
* {@link CUT_CANDIDATE} precedes {@link QUOTED_LITERAL} so the quote the cut left open
|
|
135
|
+
* cannot be read as an earlier literal's closing delimiter. {@link URL_RUN} precedes
|
|
136
|
+
* both {@link LONG_RUN} and {@link PATH_RUN} so an absolute URL is taken whole rather
|
|
137
|
+
* than shredded — {@link LONG_RUN}'s class includes `/`, so it would otherwise collapse
|
|
138
|
+
* an opaque `//host` run first and destroy the `://` anchor {@link URL_RUN} needs,
|
|
139
|
+
* leaving the path and query on the wire
|
|
140
|
+
* (`POST failed https:*.example.com/reset?user=johnsmith`); {@link PATH_RUN} would eat
|
|
141
|
+
* the `//` and leave `https:*`. {@link DIGIT_RUN} runs last so a partly-numeric token
|
|
142
|
+
* collapses to a single `*` rather than being shredded into a digit-free remnant —
|
|
143
|
+
* which is both a worse cluster key and a partial leak of the token.
|
|
144
|
+
*/
|
|
145
|
+
export declare function errorSignature(error: unknown): string | undefined;
|
|
146
|
+
/** What `sdk_init` needs from its caller: nothing but a scope to read. */
|
|
147
|
+
export interface InitEventParams {
|
|
148
|
+
/**
|
|
149
|
+
* Injectable global scope for every derived read (tests) — except `timeToInitMs`,
|
|
150
|
+
* which was read at module load and is not a read of this scope at all.
|
|
151
|
+
*/
|
|
152
|
+
scope?: object;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Assemble the `sdk_init` event: one per page load, reporting which build reached
|
|
156
|
+
* the page, what WebMCP surface it found, and what kind of page and client it
|
|
157
|
+
* landed in. It carries nothing about any `registerTools` call — that is
|
|
158
|
+
* `tool_registration`'s job, and this event fires even when no batch ever runs,
|
|
159
|
+
* which is the only way a broken integration is visible at all.
|
|
160
|
+
*
|
|
161
|
+
* The truncation ladder `emitTelemetry` runs on the way out never fires for this
|
|
162
|
+
* event: every string on it is capped at the read (`browser` at
|
|
163
|
+
* {@link MAX_BRAND_BYTES}, the route template, the spec version, a two-or-three-letter
|
|
164
|
+
* language subtag, a bounded `sessionId`), so the assembled event cannot approach the
|
|
165
|
+
* 64 KB transport bound in the first place.
|
|
166
|
+
*/
|
|
167
|
+
export declare function buildInitEvent(params?: InitEventParams): SdkInitEvent;
|
|
168
|
+
/**
|
|
169
|
+
* What `tool_registration` reads off a tool. Structurally satisfied by a defined
|
|
170
|
+
* tool; everything past the identity is optional because this builder must survive
|
|
171
|
+
* a caller that skipped `defineTool`.
|
|
172
|
+
*/
|
|
173
|
+
export interface RegisteredTelemetryTool {
|
|
174
|
+
name: string;
|
|
175
|
+
stableKey: string;
|
|
176
|
+
version?: string;
|
|
177
|
+
description?: string;
|
|
178
|
+
inputSchema?: unknown;
|
|
179
|
+
annotations?: ToolAnnotations;
|
|
180
|
+
source?: ToolSource;
|
|
181
|
+
intent?: ToolIntent;
|
|
182
|
+
}
|
|
183
|
+
/** One tool's registration, as the caller watched it settle. */
|
|
184
|
+
export interface RegisteredToolParams {
|
|
185
|
+
tool: RegisteredTelemetryTool;
|
|
186
|
+
outcome: ToolRegistrationOutcome;
|
|
187
|
+
/** What the surface rejected with; templated into `failureSignature` when `failed`. */
|
|
188
|
+
error?: unknown;
|
|
189
|
+
}
|
|
190
|
+
/** Where a batch sits in the page load, and what caused it. */
|
|
191
|
+
export interface RegistrationSequence {
|
|
192
|
+
/** 1-based position among the batches this page load reports. */
|
|
193
|
+
registrationIndex: number;
|
|
194
|
+
trigger: RegistrationTrigger;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Claim the next batch's position and trigger, advancing module state as it goes.
|
|
198
|
+
*
|
|
199
|
+
* Called at `registerTools` *call* time, not at settle time: the index is the order
|
|
200
|
+
* the calls were made in, and two overlapping batches would otherwise be numbered by
|
|
201
|
+
* whichever surface answered first. It follows that the route read here is the route
|
|
202
|
+
* the call happened on, which is what a `spa_navigation` trigger is about — the event
|
|
203
|
+
* itself derives `routeTemplate` again at emit time, and an SPA that navigates mid-
|
|
204
|
+
* registration is the one case where the two can differ.
|
|
205
|
+
*
|
|
206
|
+
* Only a batch that will actually emit may claim, so the reported sequence has no
|
|
207
|
+
* gaps: an index missing from the stream means a lost beacon, not an opt-out.
|
|
208
|
+
*/
|
|
209
|
+
export declare function nextRegistration(scope?: object): RegistrationSequence;
|
|
210
|
+
/** What `tool_registration` needs from `registerTools`; the rest is derived. */
|
|
211
|
+
export interface ToolRegistrationEventParams {
|
|
212
|
+
/** 1-based position of this batch within the page load. */
|
|
213
|
+
registrationIndex: number;
|
|
214
|
+
trigger: RegistrationTrigger;
|
|
215
|
+
/** Milliseconds from the call to the batch settling (or to the timeout). */
|
|
216
|
+
settleMs: number;
|
|
217
|
+
/** Every tool in the batch, in the order it was passed to `registerTools`. */
|
|
218
|
+
tools: readonly RegisteredToolParams[];
|
|
219
|
+
/** The batch's authenticated-channel options — reported as `config.*`. */
|
|
220
|
+
tracking?: TrackingOptions;
|
|
221
|
+
/** Injectable global scope for every derived read (tests). */
|
|
222
|
+
scope?: object;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Assemble the `tool_registration` event: one per `registerTools` call, carrying
|
|
226
|
+
* the per-tool outcomes v1 computed and threw away. A site where 3 of 8 tools fail
|
|
227
|
+
* on a duplicate name is the failure mode this event exists to make visible, and
|
|
228
|
+
* `tools[]` is the only place it is visible at all.
|
|
229
|
+
*
|
|
230
|
+
* `config.*` lives here rather than on `sdk_init` because it is per-batch: at
|
|
231
|
+
* `sdk_init` flush time no batch may have run, so there is nothing to report.
|
|
232
|
+
*
|
|
233
|
+
* The event is unbounded in size by design at this layer — a 500-tool batch is a real
|
|
234
|
+
* shape, and this is the one event of the three that can exceed the transport's 64 KB
|
|
235
|
+
* limit. Keeping it inside that limit is the `tools` rung of the truncation ladder in
|
|
236
|
+
* `src/tracking.ts`, which `emitTelemetry` runs after pruning: entries lose their shape
|
|
237
|
+
* detail first, and the array degrades to a bare count only if that was not enough.
|
|
238
|
+
*/
|
|
239
|
+
export declare function buildToolRegistrationEvent(params: ToolRegistrationEventParams): ToolRegistrationEvent & {
|
|
240
|
+
tools: RegisteredToolEntry[];
|
|
241
|
+
};
|
|
242
|
+
/** Where one invocation sits in the page load's sequence of tool calls. */
|
|
243
|
+
export interface ToolCallSequence {
|
|
244
|
+
/** Per-call random ID — deduplicates a retried beacon, joins nothing else. */
|
|
245
|
+
callId: string;
|
|
246
|
+
/** 1-based position among all tool calls in this page load. */
|
|
247
|
+
callIndex: number;
|
|
248
|
+
/** 1-based position among calls to *this* tool. */
|
|
249
|
+
toolCallIndex: number;
|
|
250
|
+
/** The previously called tool's `stableKey`; absent on the page's first call. */
|
|
251
|
+
precededBy?: string;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Claim this invocation's position in the page load, advancing module state as it
|
|
255
|
+
* goes. The counters are what turn isolated events into a *journey*: which tool an
|
|
256
|
+
* agent reached for first, how many times it retried the same one, and what it had
|
|
257
|
+
* just called when this one failed.
|
|
258
|
+
*
|
|
259
|
+
* Claimed before the handler runs, not at settle time, so the sequence is the order
|
|
260
|
+
* the agent made the calls in rather than the order they happened to finish in —
|
|
261
|
+
* the same reason {@link nextRegistration} claims at call time. `precededBy` may
|
|
262
|
+
* therefore be this tool's own key: a retry is a real sequence, not a gap.
|
|
263
|
+
*
|
|
264
|
+
* The `callId` is this channel's own; the authenticated channel mints a separate one
|
|
265
|
+
* per invocation, and the two are deliberately not joinable.
|
|
266
|
+
*
|
|
267
|
+
* `precededBy` is the one field here scoped to `tenantScope` rather than to the page
|
|
268
|
+
* load, because it is the only one carrying a *tenant-owned string*. Two tenants
|
|
269
|
+
* sharing an origin is the topology {@link batchTelemetrySinks} exists for, and module
|
|
270
|
+
* state that spans them would put tenant A's `stableKey` in an event authenticated with
|
|
271
|
+
* tenant B's key — A's tool names attributed to B's org, the same misattribution the
|
|
272
|
+
* per-batch sinks prevent one field over. Omitted rather than remembered per tenant: a
|
|
273
|
+
* `precededBy` that would have to name another tenant's call is a gap, and the field is
|
|
274
|
+
* already documented as absent on a page's first call. The counters stay page-scoped —
|
|
275
|
+
* `callIndex` is defined as a position in the page load and carries no tenant's string.
|
|
276
|
+
*/
|
|
277
|
+
export declare function nextCall(stableKey: string, tenantScope?: string): ToolCallSequence;
|
|
278
|
+
/** What `tool_call` reads off a tool — the two fields worth joining calls on. */
|
|
279
|
+
export interface CalledTelemetryTool {
|
|
280
|
+
stableKey: string;
|
|
281
|
+
/** Reported as a hash only; the schema itself never leaves the page. */
|
|
282
|
+
inputSchema?: unknown;
|
|
283
|
+
intent?: ToolIntent;
|
|
284
|
+
}
|
|
285
|
+
/** What `tool_call` reports about one settled invocation. */
|
|
286
|
+
export interface ToolCallEventParams extends ToolCallSequence {
|
|
287
|
+
tool: CalledTelemetryTool;
|
|
288
|
+
outcome: ToolCallOutcome;
|
|
289
|
+
/** Client-measured elapsed milliseconds. */
|
|
290
|
+
durationMs: number;
|
|
291
|
+
/** The normalized handler result (success path) — measured, never carried. */
|
|
292
|
+
response?: unknown;
|
|
293
|
+
/** The thrown value (error path) — reported as `errorClass` + `errorSignature`. */
|
|
294
|
+
error?: unknown;
|
|
295
|
+
/** Injectable global scope for every derived read (tests). */
|
|
296
|
+
scope?: object;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Assemble the `tool_call` event: one per settled invocation, derived signals only.
|
|
300
|
+
* The reliability question — did this tool work, how fast, and how did it fail — is
|
|
301
|
+
* answered by `outcome`, `durationMs`, the error *class* and *signature*, and the
|
|
302
|
+
* response *measurements*. The raw `input`, `response`, and error message v1 sent
|
|
303
|
+
* are gone: they carry merchant and visitor content, and none of them was needed to
|
|
304
|
+
* answer it.
|
|
305
|
+
*
|
|
306
|
+
* `outcome` alone decides the conditional halves, so neither a stray `error` on a
|
|
307
|
+
* successful call nor a stray `response` on a failed one can contradict it. No stack
|
|
308
|
+
* trace — unbounded, and it leaks code structure.
|
|
309
|
+
*
|
|
310
|
+
* The ladder never fires here either, for the same reason `buildInitEvent` never
|
|
311
|
+
* reaches it: every string is bounded at its source — `stableKey` at 1024 chars by `defineTool`,
|
|
312
|
+
* `errorClass` at {@link MAX_ERROR_CLASS_BYTES}, `errorSignature` at
|
|
313
|
+
* {@link MAX_ERROR_SIGNATURE_BYTES}, the route template in `telemetry-context.ts`,
|
|
314
|
+
* `intent` to one of its declared values by {@link toolEnum}, and
|
|
315
|
+
* `schemaHash`/`callId`/`sessionId` at fixed widths — so the assembled event
|
|
316
|
+
* cannot approach the 64 KB transport bound. That matters most inside `tool`, which
|
|
317
|
+
* no ladder rung can reach without discarding the identity this event joins on.
|
|
318
|
+
*/
|
|
319
|
+
export declare function buildToolCallEvent(params: ToolCallEventParams): ToolCallEvent;
|
|
320
|
+
/**
|
|
321
|
+
* Drop every field the allowlists map to `false`, keyed by the path the field
|
|
322
|
+
* occupies in the emitted event: a flat key (`sessionId`) is deleted from the
|
|
323
|
+
* event, a dotted key (`sdk.version`, `page.routeTemplate`) from its parent object,
|
|
324
|
+
* and a per-tool key from every `tools[]` entry and from `tool_call.tool`. A parent
|
|
325
|
+
* left empty by pruning is dropped too, so a disabled group leaves no
|
|
326
|
+
* `"response": {}` husk behind. Keys absent from the allowlists pass through
|
|
327
|
+
* untouched — they gate what they know about, they are not a whitelist of the
|
|
328
|
+
* event's shape.
|
|
329
|
+
*
|
|
330
|
+
* Copy-on-write: the input event and any nested object or tool entry it owns are
|
|
331
|
+
* never mutated, and untouched nested values are shared by reference.
|
|
332
|
+
*
|
|
333
|
+
* Event-level paths are at most two levels deep (that is the full extent of the
|
|
334
|
+
* event schemas), which is why this splits on the first dot rather than walking an
|
|
335
|
+
* arbitrary path; the third level, inside `tools[]`, is what `toolFields` is for.
|
|
336
|
+
*/
|
|
337
|
+
export declare function pruneByAllowlist(event: Record<string, unknown>, fields?: TelemetryFieldMap, toolFields?: TelemetryToolFieldMap): Record<string, unknown>;
|
|
338
|
+
/**
|
|
339
|
+
* The channel's single output. Injectable so orchestration can be tested without
|
|
340
|
+
* touching the network (the `TrackingSinks` pattern in `src/tracking.ts`).
|
|
341
|
+
*/
|
|
342
|
+
export interface TelemetrySinks {
|
|
343
|
+
sendTelemetry: (event: object) => void;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* The sink for the two events a *batch* owns. They carry that batch's tool identity —
|
|
347
|
+
* `stableKey`, `routeTemplate`, `errorSignature` — so they authenticate with that
|
|
348
|
+
* batch's own key rather than with module state. Two tenants sharing an origin is a
|
|
349
|
+
* topology `TrackingOptions.apiKey` designs for, and reading the last-captured key at
|
|
350
|
+
* emit time would send tenant A's stableKeys under tenant B's key, attributing A's
|
|
351
|
+
* tools to B's org.
|
|
352
|
+
*
|
|
353
|
+
* A keyless batch falls back to whatever key the page supplied, resolved at emit time
|
|
354
|
+
* for the reason {@link defaultSinks} resolves late: the batch has no tenant of its
|
|
355
|
+
* own, and the page it registered on does. `sdk_init` keeps the module key because it
|
|
356
|
+
* carries page context only — no batch, and so nothing to misattribute.
|
|
357
|
+
*
|
|
358
|
+
* That late fallback is why a `tool_call`, whose `precededBy` can name a *different*
|
|
359
|
+
* tenant's tool, passes {@link resolveTelemetryKey}'s answer through here rather than
|
|
360
|
+
* the raw batch option: a key resolved to a value pins it, closing the window in which
|
|
361
|
+
* the module key changes between the claim and the emit.
|
|
362
|
+
*/
|
|
363
|
+
export declare function batchTelemetrySinks(apiKey: unknown): TelemetrySinks;
|
|
364
|
+
/**
|
|
365
|
+
* The key a batch's beacons authenticate with, resolved now instead of at emit time:
|
|
366
|
+
* its own when it supplied one, otherwise the page's.
|
|
367
|
+
*
|
|
368
|
+
* Exists so a caller can resolve *once* and use the one answer for both halves of a
|
|
369
|
+
* beacon — the tenant it is claimed under ({@link telemetryTenantScope}) and the key it
|
|
370
|
+
* is sent with ({@link batchTelemetrySinks}). Both fall back to module state on a
|
|
371
|
+
* keyless batch, and `tool_call` consults them at two different moments: the scope
|
|
372
|
+
* before the handler runs, the key after it settles. A `registerTools` call landing in
|
|
373
|
+
* that window with a different `apiKey` made the two disagree, and the disagreement was
|
|
374
|
+
* exactly the misattribution the scope exists to prevent — tenant A's `stableKey`
|
|
375
|
+
* arriving via `precededBy` on a beacon authenticated as tenant C.
|
|
376
|
+
*
|
|
377
|
+
* Idempotent, so the resolved value can be handed straight back to either function: the
|
|
378
|
+
* result is `undefined` or an already-usable key, never a blank string.
|
|
379
|
+
*/
|
|
380
|
+
export declare function resolveTelemetryKey(apiKey: unknown): string | undefined;
|
|
381
|
+
/**
|
|
382
|
+
* The tenant a batch's beacons belong to, as an opaque token — which tenant, never
|
|
383
|
+
* which key. Resolved exactly as {@link batchTelemetrySinks} resolves the key it
|
|
384
|
+
* authenticates with, so the scope a `tool_call` is claimed under is the scope its
|
|
385
|
+
* beacon is sent under. Hashed for the reason `storageNamespace` is: this is only ever
|
|
386
|
+
* compared, so there is no call for holding the key itself in a second place.
|
|
387
|
+
*
|
|
388
|
+
* An unkeyed page is one scope (`""`), which is the single-tenant case and the common
|
|
389
|
+
* one; see {@link nextCall} for what the scope gates.
|
|
390
|
+
*/
|
|
391
|
+
export declare function telemetryTenantScope(apiKey: unknown): string;
|
|
392
|
+
/**
|
|
393
|
+
* Build one event, prune it by the allowlist, then hand it to the transport.
|
|
394
|
+
* Pruning happens **here**, on the way out, so it is the single choke point every
|
|
395
|
+
* event passes through — a field flipped to `false` in `telemetry-fields.ts`
|
|
396
|
+
* cannot leak through some builder that forgot to check.
|
|
397
|
+
*
|
|
398
|
+
* The 64 KB truncation ladder (`src/tracking.ts`) runs last, after pruning: a
|
|
399
|
+
* disabled field cannot be what pushes an event over the bound, and only
|
|
400
|
+
* `tool_registration` can approach it at all — every other string this channel emits
|
|
401
|
+
* is capped where it is read. An event still oversized after the ladder is dropped
|
|
402
|
+
* whole by `fetch(keepalive)`, which is why the ladder's `tools` rung degrades to a
|
|
403
|
+
* count rather than giving up.
|
|
404
|
+
*
|
|
405
|
+
* The page-level opt-outs are re-read here too, for the same reason pruning lives
|
|
406
|
+
* here: this is the only place all three events pass through. `registerTools`
|
|
407
|
+
* decides its batch's channel once, but a page can reach for a lever *after* that
|
|
408
|
+
* call — `__WEBMCP_TELEMETRY__` set from a consent callback, a `navigator` patched
|
|
409
|
+
* with GPC after load — and both are documented as silencing every event, not every
|
|
410
|
+
* event registered from then on. Trusting the latched decision instead would keep
|
|
411
|
+
* beaconing a page that asked for silence, which for GPC is a legally recognized
|
|
412
|
+
* opt-out. The cost is that a `callIndex`/`registrationIndex` claimed before the
|
|
413
|
+
* lever flipped leaves a gap; `docs/telemetry-schema.md` names that as one of the
|
|
414
|
+
* causes a gap can have.
|
|
415
|
+
*
|
|
416
|
+
* Never throws, which is why the event arrives as a thunk rather than a value: an
|
|
417
|
+
* argument is evaluated at the call site, *outside* this guard, so passing
|
|
418
|
+
* `buildToolCallEvent(...)` directly would leave assembly unprotected — and on the
|
|
419
|
+
* error path a builder throw would replace the tool's own error with a telemetry
|
|
420
|
+
* one, the worst possible failure for a channel that must stay an observer. Every
|
|
421
|
+
* global the builders read is guarded individually too; this is the backstop that
|
|
422
|
+
* keeps the guarantee true of assembly as a whole, not of each read in turn.
|
|
423
|
+
*/
|
|
424
|
+
export declare function emitTelemetry(build: () => TelemetryEvent, sinks?: TelemetrySinks, fields?: TelemetryFieldMap, toolFields?: TelemetryToolFieldMap): void;
|
|
425
|
+
/**
|
|
426
|
+
* Record the `apiKey` of a `registerTools` call so the deferred `sdk_init` flush —
|
|
427
|
+
* which runs with no batch in hand, and may run before any batch exists — can
|
|
428
|
+
* authenticate. Non-string, empty, and whitespace-only values are ignored, so a
|
|
429
|
+
* page that never keys the authenticated channel simply stays anonymous.
|
|
430
|
+
*
|
|
431
|
+
* A later keyless batch does **not** clear a key an earlier one supplied: the page
|
|
432
|
+
* belongs to that tenant either way, and dropping the attribution because the
|
|
433
|
+
* second `registerTools` call omitted it would lose the more specific fact. A later
|
|
434
|
+
* *differing* key does overwrite, and on a two-tenant page one of them will be wrong
|
|
435
|
+
* — which is why only `sdk_init` reads this. The batch-owned events, the ones
|
|
436
|
+
* carrying tool identity, go through {@link batchTelemetrySinks} instead.
|
|
437
|
+
*/
|
|
438
|
+
export declare function captureTelemetryApiKey(apiKey: unknown): void;
|
|
439
|
+
/** The key {@link captureTelemetryApiKey} last recorded, if any. */
|
|
440
|
+
export declare function telemetryApiKey(): string | undefined;
|
|
441
|
+
/**
|
|
442
|
+
* Cancel the deferred `sdk_init` below. Called by `registerTools({ telemetry: false })`,
|
|
443
|
+
* whose opt-out is page-level and not merely batch-level: the flush describes the
|
|
444
|
+
* *page load*, so one call asking for silence has to silence it even though the
|
|
445
|
+
* flush knows nothing about batches.
|
|
446
|
+
*
|
|
447
|
+
* Irreversible on purpose — a later batch that omits the option is not consent.
|
|
448
|
+
*/
|
|
449
|
+
export declare function cancelInitEvent(): void;
|
|
450
|
+
/**
|
|
451
|
+
* Emit `sdk_init` — at most once per module instance, ever. The flag is set before
|
|
452
|
+
* the opt-out is consulted, so a cancelled flush cannot fire later and a second
|
|
453
|
+
* attempt (a re-entrant timer, a test) is a no-op rather than a duplicate page-load
|
|
454
|
+
* event.
|
|
455
|
+
*
|
|
456
|
+
* Only `telemetry: false` is checked here; the rest of the cancellation set — the
|
|
457
|
+
* `__WEBMCP_TELEMETRY__` kill switch, GPC, and a runtime with no `document`
|
|
458
|
+
* (SSR/prerender) — is {@link emitTelemetry}'s to read, at the same moment it reads
|
|
459
|
+
* it for the other two events. That is *now* rather than at import either way, so a
|
|
460
|
+
* page that sets the switch inside a consent callback still wins the race.
|
|
461
|
+
*
|
|
462
|
+
* Exported for tests; the scheduled timer below is the one that runs it on a real page.
|
|
463
|
+
*/
|
|
464
|
+
export declare function flushInitEvent(sinks?: TelemetrySinks): void;
|
|
465
|
+
/**
|
|
466
|
+
* Run `run` after `ms`, returning a canceller. Guarded end to end: a runtime with no
|
|
467
|
+
* timers (SSR) and a hostile `setTimeout` (an extension, a fake-timer harness) both
|
|
468
|
+
* mean the callback never runs, never a throw out of the import or out of
|
|
469
|
+
* `registerTools`. The canceller is safe to call when nothing was scheduled.
|
|
470
|
+
*/
|
|
471
|
+
export declare function afterDelay(run: () => void, ms: number): () => void;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derived, non-personal metadata about a tool's `inputSchema`: a stable hash and
|
|
3
|
+
* a handful of shape metrics. Both feed the `tools[]` entries on the
|
|
4
|
+
* `tool_registration` telemetry event (and `tool.schemaHash` on `tool_call`), so
|
|
5
|
+
* schema drift and schema *shape* are measurable without the schema itself ever
|
|
6
|
+
* leaving the page — a schema carries merchant vocabulary and is not ours to send.
|
|
7
|
+
*
|
|
8
|
+
* `inputSchema` is caller-supplied and `defineTool` validates only that the top
|
|
9
|
+
* level is a plain object, so everything below is untrusted input: it can be
|
|
10
|
+
* cyclic, arbitrarily deep, arbitrarily wide, and studded with throwing getters.
|
|
11
|
+
* Every traversal here is therefore depth-capped, cycle-guarded, node-budgeted,
|
|
12
|
+
* and reads properties through a guard. Nothing in this module throws.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Deterministic, key-sorted JSON-ish encoding of any value. Not valid JSON for
|
|
16
|
+
* hostile input (trimmed branches become marker tokens) and never parsed — it
|
|
17
|
+
* exists only to be hashed. `depth` lets a caller start the depth budget partway
|
|
18
|
+
* down; it defaults to the top.
|
|
19
|
+
*/
|
|
20
|
+
export declare function canonicalize(value: unknown, depth?: number): string;
|
|
21
|
+
/**
|
|
22
|
+
* Stable 8-hex-char fingerprint of a tool's input schema. An absent — or
|
|
23
|
+
* non-object, which `defineTool` already rejects — schema is *omitted* rather
|
|
24
|
+
* than hashed, so "no schema" and "empty schema" stay distinguishable downstream.
|
|
25
|
+
*/
|
|
26
|
+
export declare function schemaHash(inputSchema?: unknown): string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Shape of a tool's parameters — enough to correlate tool design with agent
|
|
29
|
+
* success without carrying any parameter names or text. All counts are top-level
|
|
30
|
+
* parameters; `maxDepth` measures nesting (a flat object schema is `1`).
|
|
31
|
+
*/
|
|
32
|
+
export interface ToolShapeMetrics {
|
|
33
|
+
/** Keys in `properties`. */
|
|
34
|
+
paramCount: number;
|
|
35
|
+
/** String entries in `required`. */
|
|
36
|
+
requiredCount: number;
|
|
37
|
+
/** Params typed `string` with neither `enum` nor `format` — the ones an agent
|
|
38
|
+
* has to invent a value for, and the usual source of failed calls. */
|
|
39
|
+
freeTextParamCount: number;
|
|
40
|
+
/** Params constrained by an `enum`. */
|
|
41
|
+
enumParamCount: number;
|
|
42
|
+
/** Nesting depth of the parameter tree; `0` when there are no params. */
|
|
43
|
+
maxDepth: number;
|
|
44
|
+
/** Params carrying a non-empty `description`. */
|
|
45
|
+
describedParamCount: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Shape metrics for a tool's input schema, or `undefined` when there is no schema
|
|
49
|
+
* to measure — matching `schemaHash`, so an entry either carries both or neither.
|
|
50
|
+
* A present but unreadable schema reports zeros rather than disappearing.
|
|
51
|
+
*/
|
|
52
|
+
export declare function shapeMetrics(inputSchema?: unknown): ToolShapeMetrics | undefined;
|