@nekuda/webmcp-sdk 0.6.0-dev.17.1 → 0.7.0-dev.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +95 -8
- package/dist/telemetry.d.ts +71 -9
- package/dist/transport.d.ts +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.7.0 — 2026-09-21
|
|
4
|
+
|
|
5
|
+
One change, made twice: a page load costs the ingest edge one request instead of
|
|
6
|
+
four. The wire schema stays at `2` and every event still arrives — the two savings
|
|
7
|
+
are a header and an envelope, not a sample. **A minor, not a patch**, because the
|
|
8
|
+
second of them puts a request shape on the wire that no earlier backend parses: a
|
|
9
|
+
`batch` is quarantined as `unknown_event` by any transform older than the one that
|
|
10
|
+
ships with it. The events an install emits, and the fields on them, are byte for
|
|
11
|
+
byte 0.6.0's.
|
|
12
|
+
|
|
13
|
+
### Telemetry beacons without a preflight
|
|
4
14
|
|
|
5
15
|
- The telemetry beacon is sent as `text/plain` instead of `application/json`. The
|
|
6
16
|
body is unchanged; the header makes an anonymous beacon a CORS simple request, so
|
|
@@ -8,6 +18,19 @@
|
|
|
8
18
|
still carry `x-api-key` and still preflight. The edge has mapped `text/plain` to
|
|
9
19
|
the same template since ADR-0014, so no server change is needed.
|
|
10
20
|
|
|
21
|
+
### One beacon per page load
|
|
22
|
+
|
|
23
|
+
- `sdk_init` and `tool_registration` are held for 50 ms per destination (endpoint
|
|
24
|
+
plus key) and, when two or more are waiting, sent as one `batch` envelope
|
|
25
|
+
(`{"schema":2,"event":"batch","events":[…]}`); a lone event is sent as it was.
|
|
26
|
+
`tool_call` never waits, and drains the queue ahead of itself. `pagehide` and a
|
|
27
|
+
hidden tab flush at once, through the same `keepalive` fetch. The backend explodes
|
|
28
|
+
a batch into its events, so nothing downstream changes shape.
|
|
29
|
+
- Three bounds, each a fallback rather than a drop: at most 16 events per batch (the
|
|
30
|
+
count the backend accepts — the queue flushes at the cap and opens a new window),
|
|
31
|
+
at most 60 KB per request (an over-large batch goes as the per-event requests it
|
|
32
|
+
replaced), and a runtime that cannot schedule a timer sends immediately.
|
|
33
|
+
|
|
11
34
|
## 0.6.0 — 2026-09-18
|
|
12
35
|
|
|
13
36
|
Five additive changes, no breaking change. The wire schema stays at `2`. Four of them
|
package/dist/index.d.ts
CHANGED
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
* `tool_registration` once per `registerTools` call, carrying each tool's
|
|
54
54
|
* outcome; and `tool_call` once per settled invocation, as derived signals only —
|
|
55
55
|
* no raw `input`, `response`, or error message, unlike point 5. All three go to a
|
|
56
|
-
* separate `/v1/telemetry` endpoint, fire-and-forget
|
|
56
|
+
* separate `/v1/telemetry` endpoint, fire-and-forget; the two page-level ones may
|
|
57
|
+
* share one request as a `batch` envelope, `tool_call` never does. Arrival order
|
|
57
58
|
* is NOT guaranteed, so consumers join on `sessionId` and never on ordering. A
|
|
58
59
|
* page that configures point 5's `apiKey` has it sent here as `x-api-key` too,
|
|
59
60
|
* which only adds tenant attribution — an unkeyed page sends anonymously to the
|
package/dist/index.js
CHANGED
|
@@ -1105,7 +1105,7 @@ function shapeMetrics(inputSchema) {
|
|
|
1105
1105
|
|
|
1106
1106
|
// src/telemetry.ts
|
|
1107
1107
|
var SDK_NAME = "@nekuda/webmcp-sdk";
|
|
1108
|
-
var SDK_VERSION = "0.
|
|
1108
|
+
var SDK_VERSION = "0.7.0-dev.18.1";
|
|
1109
1109
|
var INSTALL_MODES = ["npm", "cdn_snippet"];
|
|
1110
1110
|
var SDK_INSTALL_MODE = INSTALL_MODES.find((mode) => mode === (typeof __WEBMCP_INSTALL_MODE__ === "string" ? __WEBMCP_INSTALL_MODE__ : "")) ?? "npm";
|
|
1111
1111
|
function parseSampleRate(raw) {
|
|
@@ -1113,7 +1113,7 @@ function parseSampleRate(raw) {
|
|
|
1113
1113
|
return Number.isFinite(rate) && rate > 0 && rate < 1 ? rate : 1;
|
|
1114
1114
|
}
|
|
1115
1115
|
var SDK_TELEMETRY_SAMPLE_RATE = parseSampleRate(typeof __WEBMCP_TELEMETRY_SAMPLE_RATE__ === "string" ? __WEBMCP_TELEMETRY_SAMPLE_RATE__ : undefined);
|
|
1116
|
-
var
|
|
1116
|
+
var PAGE_LEVEL_EVENTS = new Set(["sdk_init", "tool_registration"]);
|
|
1117
1117
|
function pageSampled(sessionId, rate = SDK_TELEMETRY_SAMPLE_RATE) {
|
|
1118
1118
|
if (rate >= 1)
|
|
1119
1119
|
return true;
|
|
@@ -1461,13 +1461,90 @@ function pruneByAllowlist(event, fields = TELEMETRY_FIELDS, toolFields = TELEMET
|
|
|
1461
1461
|
return pruned;
|
|
1462
1462
|
}
|
|
1463
1463
|
var defaultSinks2 = {
|
|
1464
|
-
sendTelemetry: (event) =>
|
|
1464
|
+
sendTelemetry: (event) => queueOrSend(event, telemetryEndpoint(), telemetryApiKey())
|
|
1465
1465
|
};
|
|
1466
|
+
var PAGE_BATCH_WINDOW_MS = 50;
|
|
1467
|
+
var PAGE_BATCH_MAX_BYTES = 60000;
|
|
1468
|
+
var PAGE_BATCH_EVENT = "batch";
|
|
1469
|
+
var PAGE_BATCH_EVENTS_KEY = "events";
|
|
1470
|
+
var PAGE_BATCH_MAX_EVENTS = 16;
|
|
1471
|
+
var pendingPageEvents = new Map;
|
|
1472
|
+
function queueOrSend(event, endpoint, apiKey) {
|
|
1473
|
+
const name = String(event.event);
|
|
1474
|
+
if (!PAGE_LEVEL_EVENTS.has(name)) {
|
|
1475
|
+
flushPageEventQueue();
|
|
1476
|
+
sendTelemetry(event, globalThis, endpoint, apiKey);
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
const key = `${endpoint ?? ""}
|
|
1480
|
+
${typeof apiKey === "string" ? apiKey : ""}`;
|
|
1481
|
+
const pending = pendingPageEvents.get(key);
|
|
1482
|
+
if (pending) {
|
|
1483
|
+
pending.events.push(event);
|
|
1484
|
+
if (pending.events.length >= PAGE_BATCH_MAX_EVENTS)
|
|
1485
|
+
flushPendingBatch(key);
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
const entry = { events: [event], endpoint, apiKey, cancel: () => {} };
|
|
1489
|
+
pendingPageEvents.set(key, entry);
|
|
1490
|
+
const cancel = tryAfterDelay(() => flushPendingBatch(key), PAGE_BATCH_WINDOW_MS);
|
|
1491
|
+
if (!cancel) {
|
|
1492
|
+
flushPendingBatch(key);
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
entry.cancel = cancel;
|
|
1496
|
+
}
|
|
1497
|
+
function flushPendingBatch(key) {
|
|
1498
|
+
const entry = pendingPageEvents.get(key);
|
|
1499
|
+
if (!entry)
|
|
1500
|
+
return;
|
|
1501
|
+
pendingPageEvents.delete(key);
|
|
1502
|
+
entry.cancel();
|
|
1503
|
+
const { events, endpoint, apiKey } = entry;
|
|
1504
|
+
const one = events[0];
|
|
1505
|
+
if (events.length === 1 && one) {
|
|
1506
|
+
sendTelemetry(one, globalThis, endpoint, apiKey);
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
const batch = {
|
|
1510
|
+
schema: TELEMETRY_SCHEMA_VERSION,
|
|
1511
|
+
event: PAGE_BATCH_EVENT,
|
|
1512
|
+
ts: new Date().toISOString(),
|
|
1513
|
+
sessionId: sessionId(),
|
|
1514
|
+
[PAGE_BATCH_EVENTS_KEY]: events
|
|
1515
|
+
};
|
|
1516
|
+
const json = safe(() => JSON.stringify(batch));
|
|
1517
|
+
const bytes = json === undefined ? Number.POSITIVE_INFINITY : utf8Length(json);
|
|
1518
|
+
if (bytes <= PAGE_BATCH_MAX_BYTES) {
|
|
1519
|
+
sendTelemetry(batch, globalThis, endpoint, apiKey);
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
for (const event of events)
|
|
1523
|
+
sendTelemetry(event, globalThis, endpoint, apiKey);
|
|
1524
|
+
}
|
|
1525
|
+
function utf8Length(text) {
|
|
1526
|
+
const encoded = safe(() => new TextEncoder().encode(text).length);
|
|
1527
|
+
return typeof encoded === "number" ? encoded : text.length * 3;
|
|
1528
|
+
}
|
|
1529
|
+
function flushPageEventQueue() {
|
|
1530
|
+
for (const key of Array.from(pendingPageEvents.keys()))
|
|
1531
|
+
flushPendingBatch(key);
|
|
1532
|
+
}
|
|
1533
|
+
safe(() => {
|
|
1534
|
+
const g = globalThis;
|
|
1535
|
+
if (typeof g.addEventListener !== "function")
|
|
1536
|
+
return;
|
|
1537
|
+
g.addEventListener("pagehide", flushPageEventQueue);
|
|
1538
|
+
g.addEventListener("visibilitychange", () => {
|
|
1539
|
+
if (g.document?.visibilityState === "hidden")
|
|
1540
|
+
flushPageEventQueue();
|
|
1541
|
+
});
|
|
1542
|
+
});
|
|
1466
1543
|
function batchTelemetrySinks(apiKey, endpoint) {
|
|
1467
1544
|
const own = batchApiKey(apiKey);
|
|
1468
1545
|
const ownEndpoint = telemetryEndpointFrom(endpoint);
|
|
1469
1546
|
return {
|
|
1470
|
-
sendTelemetry: (event) =>
|
|
1547
|
+
sendTelemetry: (event) => queueOrSend(event, ownEndpoint ?? telemetryEndpoint(), own ?? telemetryApiKey())
|
|
1471
1548
|
};
|
|
1472
1549
|
}
|
|
1473
1550
|
function batchApiKey(apiKey) {
|
|
@@ -1485,7 +1562,7 @@ function emitTelemetry(build, sinks = defaultSinks2, fields = TELEMETRY_FIELDS,
|
|
|
1485
1562
|
if (!telemetryEnabled())
|
|
1486
1563
|
return;
|
|
1487
1564
|
const event = { ...build() };
|
|
1488
|
-
if (
|
|
1565
|
+
if (PAGE_LEVEL_EVENTS.has(String(event.event)) && sampleRate < 1) {
|
|
1489
1566
|
if (!pageSampled(String(event.sessionId), sampleRate))
|
|
1490
1567
|
return;
|
|
1491
1568
|
event.sampleRate = sampleRate;
|
|
@@ -1543,11 +1620,18 @@ function deferInitEventForBatch(sinks) {
|
|
|
1543
1620
|
return;
|
|
1544
1621
|
afterDelay(() => flushInitEvent(sinks), 0);
|
|
1545
1622
|
}
|
|
1546
|
-
function
|
|
1623
|
+
function tryAfterDelay(run, ms) {
|
|
1547
1624
|
const schedule = safe(() => globalThis.setTimeout);
|
|
1548
1625
|
if (typeof schedule !== "function")
|
|
1549
|
-
return
|
|
1550
|
-
|
|
1626
|
+
return;
|
|
1627
|
+
let armed = false;
|
|
1628
|
+
const handle = safe(() => {
|
|
1629
|
+
const id = schedule.call(globalThis, () => run(), ms);
|
|
1630
|
+
armed = true;
|
|
1631
|
+
return id;
|
|
1632
|
+
});
|
|
1633
|
+
if (!armed)
|
|
1634
|
+
return;
|
|
1551
1635
|
return () => {
|
|
1552
1636
|
const clear = safe(() => globalThis.clearTimeout);
|
|
1553
1637
|
if (typeof clear !== "function")
|
|
@@ -1555,6 +1639,9 @@ function afterDelay(run, ms) {
|
|
|
1555
1639
|
safe(() => clear.call(globalThis, handle));
|
|
1556
1640
|
};
|
|
1557
1641
|
}
|
|
1642
|
+
function afterDelay(run, ms) {
|
|
1643
|
+
return tryAfterDelay(run, ms) ?? (() => {});
|
|
1644
|
+
}
|
|
1558
1645
|
var INIT_FALLBACK_MS = 1000;
|
|
1559
1646
|
cancelInitFallback = afterDelay(() => flushInitEvent(), INIT_FALLBACK_MS);
|
|
1560
1647
|
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
* on `registerTools`, with `globalThis.__WEBMCP_TELEMETRY__ = false`, or via
|
|
8
8
|
* Global Privacy Control.
|
|
9
9
|
*
|
|
10
|
-
* Event schemas, joined on the in-memory `sessionId`,
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* Event schemas, joined on the in-memory `sessionId`, fire-and-forget: `sdk_init`
|
|
11
|
+
* (once per page load, from the one-shot flush this module schedules at import
|
|
12
|
+
* time), `tool_registration` (once per `registerTools` call), and `tool_call`
|
|
13
|
+
* (once per settled invocation). Events go to the separate `/v1/telemetry`
|
|
14
|
+
* endpoint through `sendTelemetry` in `src/transport.ts`; the two page-level ones
|
|
15
|
+
* may share a request as a `batch` envelope (see {@link PAGE_BATCH_WINDOW_MS}).
|
|
15
16
|
*
|
|
16
17
|
* No browser storage is touched anywhere in this channel — no `visitorId`, no
|
|
17
18
|
* stored `sessionId` (the ePrivacy Art. 5(3) concern). Cross-visit stitching is
|
|
@@ -370,6 +371,55 @@ export declare function pruneByAllowlist(event: Record<string, unknown>, fields?
|
|
|
370
371
|
export interface TelemetrySinks {
|
|
371
372
|
sendTelemetry: (event: object) => void;
|
|
372
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* One beacon per page load, where the page allows it.
|
|
376
|
+
*
|
|
377
|
+
* `sdk_init` flushes one task after import and the first `tool_registration` settles a
|
|
378
|
+
* few milliseconds after `registerTools`, so on most pages the two page-level events
|
|
379
|
+
* leave within the same few milliseconds — as two requests. Each request is billed at
|
|
380
|
+
* the edge, and in 2026-09 page-level beacons were the whole of a seven-million-a-day
|
|
381
|
+
* request bill. Both events are therefore held for {@link PAGE_BATCH_WINDOW_MS} and
|
|
382
|
+
* sent as one `batch` envelope when two or more are waiting; a lone event is sent as
|
|
383
|
+
* itself, byte for byte what it was before. `tool_call` never waits: it is one per
|
|
384
|
+
* invocation, carries the call's own timing, and batching it would hold a signal a
|
|
385
|
+
* navigation could then lose.
|
|
386
|
+
*
|
|
387
|
+
* The queue is keyed on the destination — endpoint plus key — because a batch is one
|
|
388
|
+
* request with one `x-api-key`, and two tenants sharing an origin must never travel
|
|
389
|
+
* under each other's key (see {@link batchTelemetrySinks}). The init flush a batch
|
|
390
|
+
* defers (`deferInitEventForBatch`) uses that batch's sinks, so the common case lands
|
|
391
|
+
* in one queue. A `pagehide` or a hidden tab flushes everything at once, through the
|
|
392
|
+
* same `keepalive` fetch. A runtime that cannot schedule — no `setTimeout`, or one
|
|
393
|
+
* that throws — sends immediately: a queue that could never drain would be a queue
|
|
394
|
+
* that loses, and the events would sit until `pagehide` or forever.
|
|
395
|
+
*
|
|
396
|
+
* The transform explodes a `batch` into its events (internal/telemetrytransform); the
|
|
397
|
+
* server-written attribution is applied to every one, so a batch cannot say anything
|
|
398
|
+
* its events could not have said alone.
|
|
399
|
+
*/
|
|
400
|
+
export declare const PAGE_BATCH_WINDOW_MS = 50;
|
|
401
|
+
/** Under `fetch(keepalive)`'s 64 KB in-flight ceiling, with room for the wrapper. */
|
|
402
|
+
export declare const PAGE_BATCH_MAX_BYTES = 60000;
|
|
403
|
+
/**
|
|
404
|
+
* The batch envelope's `event` name and member-list key, and the member cap — the
|
|
405
|
+
* three spellings this file shares with the Go transform that explodes a batch
|
|
406
|
+
* (`internal/telemetrytransform`: `eventBatch`, `wireBatchEvents`, `MaxBatchEvents`).
|
|
407
|
+
* Nothing compiles the two together, so `analytics/telemetry_batch_contract_test.go`
|
|
408
|
+
* reads these three declarations out of this source and asserts they equal the Go
|
|
409
|
+
* constants. Exported for that gate, and used below rather than restated: a pin
|
|
410
|
+
* against a constant the envelope does not actually use is a tautology.
|
|
411
|
+
*
|
|
412
|
+
* The cap is the one the SERVER enforces. A seventeenth member does not make a
|
|
413
|
+
* larger batch, it makes an `unparseable_body` — one request in which every event is
|
|
414
|
+
* lost — so the queue flushes at the cap and starts a new batch instead. A page can
|
|
415
|
+
* reach it: `registerTools` may be called many times on an SPA entry, and each call
|
|
416
|
+
* emits its own `tool_registration`.
|
|
417
|
+
*/
|
|
418
|
+
export declare const PAGE_BATCH_EVENT = "batch";
|
|
419
|
+
export declare const PAGE_BATCH_EVENTS_KEY = "events";
|
|
420
|
+
export declare const PAGE_BATCH_MAX_EVENTS = 16;
|
|
421
|
+
/** Send every waiting page-level batch now — on `pagehide`, a hidden tab, or a test. */
|
|
422
|
+
export declare function flushPageEventQueue(): void;
|
|
373
423
|
/**
|
|
374
424
|
* The sink for the two events a *batch* owns. They carry that batch's tool identity —
|
|
375
425
|
* `stableKey`, `routeTemplate`, `errorSignature` — so they authenticate with that
|
|
@@ -506,10 +556,22 @@ export declare function flushInitEvent(sinks?: TelemetrySinks): void;
|
|
|
506
556
|
*/
|
|
507
557
|
export declare function deferInitEventForBatch(sinks: TelemetrySinks): void;
|
|
508
558
|
/**
|
|
509
|
-
* Run `run` after `ms`, returning a canceller
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
559
|
+
* Run `run` after `ms`, returning a canceller — or `undefined` when nothing was
|
|
560
|
+
* scheduled. Guarded end to end: a runtime with no timers (SSR) and a hostile
|
|
561
|
+
* `setTimeout` (an extension, a fake-timer harness) both mean the callback never
|
|
562
|
+
* runs, never a throw out of the import or out of `registerTools`.
|
|
563
|
+
*
|
|
564
|
+
* The `undefined` is the part {@link afterDelay} cannot express, and it matters to
|
|
565
|
+
* exactly one caller: the page-level batch holds events until its timer fires, so a
|
|
566
|
+
* `setTimeout` that exists and THROWS would leave them queued until `pagehide` — a
|
|
567
|
+
* silent loss on any page the user closes from a different tab. A caller that only
|
|
568
|
+
* defers work is happy with a no-op; a caller that defers DATA has to know.
|
|
569
|
+
*/
|
|
570
|
+
export declare function tryAfterDelay(run: () => void, ms: number): (() => void) | undefined;
|
|
571
|
+
/**
|
|
572
|
+
* {@link tryAfterDelay} for the callers that have nothing to do when the runtime
|
|
573
|
+
* cannot schedule: the canceller is safe to call when nothing was scheduled, and an
|
|
574
|
+
* unscheduled flush is simply one that never happens.
|
|
513
575
|
*/
|
|
514
576
|
export declare function afterDelay(run: () => void, ms: number): () => void;
|
|
515
577
|
/** How long a page that never registers gets to expose a real batch configuration. */
|
package/dist/transport.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ export interface CollectConfig {
|
|
|
56
56
|
* a no-`fetch` environment is a silent no-op.
|
|
57
57
|
*/
|
|
58
58
|
export declare function sendToCollect(event: TrackingEvent, config: CollectConfig, scope?: object): void;
|
|
59
|
+
/** CORS-safelisted, so an anonymous beacon needs no preflight — see sendTelemetry. */
|
|
60
|
+
export declare const TELEMETRY_CONTENT_TYPE = "text/plain";
|
|
59
61
|
/**
|
|
60
62
|
* POST a usage-telemetry event to the telemetry endpoint via `fetch(keepalive)`.
|
|
61
63
|
* The path is the same whether or not `apiKey` is given: the header only *adds*
|
|
@@ -90,8 +92,6 @@ export declare function sendToCollect(event: TrackingEvent, config: CollectConfi
|
|
|
90
92
|
* The event stays a bare `object` here so this module never has to import the
|
|
91
93
|
* assembled shape from `telemetry.ts` (which imports this function).
|
|
92
94
|
*/
|
|
93
|
-
/** CORS-safelisted, so an anonymous beacon needs no preflight — see sendTelemetry. */
|
|
94
|
-
export declare const TELEMETRY_CONTENT_TYPE = "text/plain";
|
|
95
95
|
export declare function sendTelemetry(event: object, scope?: object, endpoint?: string, apiKey?: unknown): void;
|
|
96
96
|
/** The `Logger.emit` slice we use — structural, no hard OTEL type coupling. */
|
|
97
97
|
interface LoggerLike {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nekuda/webmcp-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0-dev.18.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Phase-1 WebMCP SDK: a thin wrapper over document.modelContext that plugin-generated code targets — defineTool + register/unregister lifecycle. This package pins the plugin↔SDK seam; anonymous tool-call tracking (backend transport via apiKey, OTEL LogRecords via otel) is opt-in through registerTools and default-silent, while anonymous usage telemetry is a separate unauthenticated channel that is on by default (opt out with telemetry: false).",
|
|
6
6
|
"main": "./dist/index.js",
|