@nekuda/webmcp-sdk 0.6.0 → 0.7.0-dev.19.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 +46 -9
- package/dist/index.d.ts +2 -1
- package/dist/index.js +98 -10
- package/dist/telemetry.d.ts +77 -12
- package/dist/transport.d.ts +12 -0
- package/package.json +8 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
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
|
|
14
|
+
|
|
15
|
+
- The telemetry beacon is sent as `text/plain` instead of `application/json`. The
|
|
16
|
+
body is unchanged; the header makes an anonymous beacon a CORS simple request, so
|
|
17
|
+
the browser stops sending an `OPTIONS` preflight before every event. Keyed beacons
|
|
18
|
+
still carry `x-api-key` and still preflight. The edge has mapped `text/plain` to
|
|
19
|
+
the same template since ADR-0014, so no server change is needed.
|
|
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
|
+
|
|
3
34
|
## 0.6.0 — 2026-09-18
|
|
4
35
|
|
|
5
36
|
Five additive changes, no breaking change. The wire schema stays at `2`. Four of them
|
|
@@ -9,20 +40,26 @@ silent when unset, so **an npm consumer who sets none of them sends exactly the
|
|
|
9
40
|
registers under a batch that is already posting to the collect edge; it is invisible to
|
|
10
41
|
enumeration and to `JSON` and never reaches the wire, so it changes no bytes either.
|
|
11
42
|
|
|
12
|
-
|
|
13
|
-
`bun build --define`,
|
|
14
|
-
|
|
15
|
-
|
|
43
|
+
Beside them is a **mechanism nothing switches on**: page-level telemetry sampling exists
|
|
44
|
+
as a `bun build --define`, and no build this release ships passes a rate. **Every
|
|
45
|
+
install, npm and CDN snippet alike, is unsampled** and sends every page-level event,
|
|
46
|
+
exactly as 0.5.0 did.
|
|
16
47
|
|
|
17
|
-
### Page-level telemetry sampling —
|
|
48
|
+
### Page-level telemetry sampling — available, off everywhere
|
|
18
49
|
|
|
19
|
-
- A build may sample the two page-level telemetry events (`sdk_init`,
|
|
50
|
+
- A build **may** sample the two page-level telemetry events (`sdk_init`,
|
|
20
51
|
`tool_registration`) with `--define '__WEBMCP_TELEMETRY_SAMPLE_RATE__="0.1"'`. The
|
|
21
52
|
decision is one coin per page load from the `sessionId`, so a page's events are kept
|
|
22
53
|
or dropped together, and every kept event carries `sampleRate` for weighting.
|
|
23
|
-
`tool_call` is never sampled. Unset, or anything outside (0, 1), sends
|
|
24
|
-
|
|
25
|
-
|
|
54
|
+
`tool_call` is never sampled. Unset, `1`, or anything outside (0, 1), sends
|
|
55
|
+
everything — the previous behaviour byte for byte.
|
|
56
|
+
- **Nothing opts in.** Neither publish lane passes the define, and the CDN snippet
|
|
57
|
+
build (`infra/deploy-snippet.sh`) defines the rate as `1`. These two events carry
|
|
58
|
+
the connects, registered-tool counts, refreshes and unique user agents the product
|
|
59
|
+
counts active users from, so they ship complete; the beacon volume gets a fast-path
|
|
60
|
+
ingest queue rather than a tenth of the observations. Turning sampling on for one
|
|
61
|
+
deploy is `SNIPPET_TELEMETRY_SAMPLE_RATE=<rate>` in front of that script, and the
|
|
62
|
+
kept events then say what rate they were kept at.
|
|
26
63
|
|
|
27
64
|
### `tracking.builtWith`
|
|
28
65
|
|
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
|
@@ -136,7 +136,7 @@ function resolveModelContext(scope = globalThis) {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
// src/transport.ts
|
|
139
|
-
var INGEST_BASE = "https://ingest.agentlane.
|
|
139
|
+
var INGEST_BASE = "https://ingest.agentlane.dev";
|
|
140
140
|
var DEFAULT_COLLECT_ENDPOINT = `${INGEST_BASE}/v1/collect`;
|
|
141
141
|
var DEFAULT_TELEMETRY_ENDPOINT = `${INGEST_BASE}/v1/telemetry`;
|
|
142
142
|
function tryFetch(scope, url, headers, json, onResponse) {
|
|
@@ -179,13 +179,14 @@ function sendToCollect(event, config, scope = globalThis) {
|
|
|
179
179
|
tryFetch(scope, url, headers, json);
|
|
180
180
|
} catch {}
|
|
181
181
|
}
|
|
182
|
+
var TELEMETRY_CONTENT_TYPE = "text/plain";
|
|
182
183
|
function sendTelemetry(event, scope = globalThis, endpoint, apiKey) {
|
|
183
184
|
try {
|
|
184
185
|
const json = JSON.stringify(event);
|
|
185
186
|
if (json === undefined)
|
|
186
187
|
return;
|
|
187
188
|
const url = endpoint || DEFAULT_TELEMETRY_ENDPOINT;
|
|
188
|
-
const headers = { "content-type":
|
|
189
|
+
const headers = { "content-type": TELEMETRY_CONTENT_TYPE };
|
|
189
190
|
const authenticated = typeof apiKey === "string" && apiKey.trim().length > 0;
|
|
190
191
|
if (authenticated)
|
|
191
192
|
headers["x-api-key"] = apiKey;
|
|
@@ -1104,7 +1105,7 @@ function shapeMetrics(inputSchema) {
|
|
|
1104
1105
|
|
|
1105
1106
|
// src/telemetry.ts
|
|
1106
1107
|
var SDK_NAME = "@nekuda/webmcp-sdk";
|
|
1107
|
-
var SDK_VERSION = "0.
|
|
1108
|
+
var SDK_VERSION = "0.7.0-dev.19.1";
|
|
1108
1109
|
var INSTALL_MODES = ["npm", "cdn_snippet"];
|
|
1109
1110
|
var SDK_INSTALL_MODE = INSTALL_MODES.find((mode) => mode === (typeof __WEBMCP_INSTALL_MODE__ === "string" ? __WEBMCP_INSTALL_MODE__ : "")) ?? "npm";
|
|
1110
1111
|
function parseSampleRate(raw) {
|
|
@@ -1112,7 +1113,7 @@ function parseSampleRate(raw) {
|
|
|
1112
1113
|
return Number.isFinite(rate) && rate > 0 && rate < 1 ? rate : 1;
|
|
1113
1114
|
}
|
|
1114
1115
|
var SDK_TELEMETRY_SAMPLE_RATE = parseSampleRate(typeof __WEBMCP_TELEMETRY_SAMPLE_RATE__ === "string" ? __WEBMCP_TELEMETRY_SAMPLE_RATE__ : undefined);
|
|
1115
|
-
var
|
|
1116
|
+
var PAGE_LEVEL_EVENTS = new Set(["sdk_init", "tool_registration"]);
|
|
1116
1117
|
function pageSampled(sessionId, rate = SDK_TELEMETRY_SAMPLE_RATE) {
|
|
1117
1118
|
if (rate >= 1)
|
|
1118
1119
|
return true;
|
|
@@ -1460,13 +1461,90 @@ function pruneByAllowlist(event, fields = TELEMETRY_FIELDS, toolFields = TELEMET
|
|
|
1460
1461
|
return pruned;
|
|
1461
1462
|
}
|
|
1462
1463
|
var defaultSinks2 = {
|
|
1463
|
-
sendTelemetry: (event) =>
|
|
1464
|
+
sendTelemetry: (event) => queueOrSend(event, telemetryEndpoint(), telemetryApiKey())
|
|
1464
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
|
+
});
|
|
1465
1543
|
function batchTelemetrySinks(apiKey, endpoint) {
|
|
1466
1544
|
const own = batchApiKey(apiKey);
|
|
1467
1545
|
const ownEndpoint = telemetryEndpointFrom(endpoint);
|
|
1468
1546
|
return {
|
|
1469
|
-
sendTelemetry: (event) =>
|
|
1547
|
+
sendTelemetry: (event) => queueOrSend(event, ownEndpoint ?? telemetryEndpoint(), own ?? telemetryApiKey())
|
|
1470
1548
|
};
|
|
1471
1549
|
}
|
|
1472
1550
|
function batchApiKey(apiKey) {
|
|
@@ -1484,7 +1562,7 @@ function emitTelemetry(build, sinks = defaultSinks2, fields = TELEMETRY_FIELDS,
|
|
|
1484
1562
|
if (!telemetryEnabled())
|
|
1485
1563
|
return;
|
|
1486
1564
|
const event = { ...build() };
|
|
1487
|
-
if (
|
|
1565
|
+
if (PAGE_LEVEL_EVENTS.has(String(event.event)) && sampleRate < 1) {
|
|
1488
1566
|
if (!pageSampled(String(event.sessionId), sampleRate))
|
|
1489
1567
|
return;
|
|
1490
1568
|
event.sampleRate = sampleRate;
|
|
@@ -1542,11 +1620,18 @@ function deferInitEventForBatch(sinks) {
|
|
|
1542
1620
|
return;
|
|
1543
1621
|
afterDelay(() => flushInitEvent(sinks), 0);
|
|
1544
1622
|
}
|
|
1545
|
-
function
|
|
1623
|
+
function tryAfterDelay(run, ms) {
|
|
1546
1624
|
const schedule = safe(() => globalThis.setTimeout);
|
|
1547
1625
|
if (typeof schedule !== "function")
|
|
1548
|
-
return
|
|
1549
|
-
|
|
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;
|
|
1550
1635
|
return () => {
|
|
1551
1636
|
const clear = safe(() => globalThis.clearTimeout);
|
|
1552
1637
|
if (typeof clear !== "function")
|
|
@@ -1554,6 +1639,9 @@ function afterDelay(run, ms) {
|
|
|
1554
1639
|
safe(() => clear.call(globalThis, handle));
|
|
1555
1640
|
};
|
|
1556
1641
|
}
|
|
1642
|
+
function afterDelay(run, ms) {
|
|
1643
|
+
return tryAfterDelay(run, ms) ?? (() => {});
|
|
1644
|
+
}
|
|
1557
1645
|
var INIT_FALLBACK_MS = 1000;
|
|
1558
1646
|
cancelInitFallback = afterDelay(() => flushInitEvent(), INIT_FALLBACK_MS);
|
|
1559
1647
|
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -7,16 +7,20 @@
|
|
|
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
|
-
* stored `sessionId` (the ePrivacy Art. 5(3) concern).
|
|
18
|
-
*
|
|
19
|
-
* storage-backed identity, and this one never reads it.
|
|
18
|
+
* stored `sessionId` (the ePrivacy Art. 5(3) concern). Nothing this SDK sends can
|
|
19
|
+
* stitch two visits together; the authenticated `tracking` channel keeps its own
|
|
20
|
+
* storage-backed identity, and this one never reads it. The ingest edge derives a
|
|
21
|
+
* bounded identifier of its own from the request it receives (a daily-salted hash,
|
|
22
|
+
* see *Privacy posture* in `docs/telemetry-schema.md`), which is not something the
|
|
23
|
+
* client participates in or can observe.
|
|
20
24
|
*
|
|
21
25
|
* Every field an event may carry is gated by the baked-in allowlists in
|
|
22
26
|
* `src/telemetry-fields.ts` — one keyed by event path, one by per-tool field.
|
|
@@ -370,6 +374,55 @@ export declare function pruneByAllowlist(event: Record<string, unknown>, fields?
|
|
|
370
374
|
export interface TelemetrySinks {
|
|
371
375
|
sendTelemetry: (event: object) => void;
|
|
372
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* One beacon per page load, where the page allows it.
|
|
379
|
+
*
|
|
380
|
+
* `sdk_init` flushes one task after import and the first `tool_registration` settles a
|
|
381
|
+
* few milliseconds after `registerTools`, so on most pages the two page-level events
|
|
382
|
+
* leave within the same few milliseconds — as two requests. Each request is billed at
|
|
383
|
+
* the edge, and in 2026-09 page-level beacons were the whole of a seven-million-a-day
|
|
384
|
+
* request bill. Both events are therefore held for {@link PAGE_BATCH_WINDOW_MS} and
|
|
385
|
+
* sent as one `batch` envelope when two or more are waiting; a lone event is sent as
|
|
386
|
+
* itself, byte for byte what it was before. `tool_call` never waits: it is one per
|
|
387
|
+
* invocation, carries the call's own timing, and batching it would hold a signal a
|
|
388
|
+
* navigation could then lose.
|
|
389
|
+
*
|
|
390
|
+
* The queue is keyed on the destination — endpoint plus key — because a batch is one
|
|
391
|
+
* request with one `x-api-key`, and two tenants sharing an origin must never travel
|
|
392
|
+
* under each other's key (see {@link batchTelemetrySinks}). The init flush a batch
|
|
393
|
+
* defers (`deferInitEventForBatch`) uses that batch's sinks, so the common case lands
|
|
394
|
+
* in one queue. A `pagehide` or a hidden tab flushes everything at once, through the
|
|
395
|
+
* same `keepalive` fetch. A runtime that cannot schedule — no `setTimeout`, or one
|
|
396
|
+
* that throws — sends immediately: a queue that could never drain would be a queue
|
|
397
|
+
* that loses, and the events would sit until `pagehide` or forever.
|
|
398
|
+
*
|
|
399
|
+
* The transform explodes a `batch` into its events (internal/telemetrytransform); the
|
|
400
|
+
* server-written attribution is applied to every one, so a batch cannot say anything
|
|
401
|
+
* its events could not have said alone.
|
|
402
|
+
*/
|
|
403
|
+
export declare const PAGE_BATCH_WINDOW_MS = 50;
|
|
404
|
+
/** Under `fetch(keepalive)`'s 64 KB in-flight ceiling, with room for the wrapper. */
|
|
405
|
+
export declare const PAGE_BATCH_MAX_BYTES = 60000;
|
|
406
|
+
/**
|
|
407
|
+
* The batch envelope's `event` name and member-list key, and the member cap — the
|
|
408
|
+
* three spellings this file shares with the Go transform that explodes a batch
|
|
409
|
+
* (`internal/telemetrytransform`: `eventBatch`, `wireBatchEvents`, `MaxBatchEvents`).
|
|
410
|
+
* Nothing compiles the two together, so `analytics/telemetry_batch_contract_test.go`
|
|
411
|
+
* reads these three declarations out of this source and asserts they equal the Go
|
|
412
|
+
* constants. Exported for that gate, and used below rather than restated: a pin
|
|
413
|
+
* against a constant the envelope does not actually use is a tautology.
|
|
414
|
+
*
|
|
415
|
+
* The cap is the one the SERVER enforces. A seventeenth member does not make a
|
|
416
|
+
* larger batch, it makes an `unparseable_body` — one request in which every event is
|
|
417
|
+
* lost — so the queue flushes at the cap and starts a new batch instead. A page can
|
|
418
|
+
* reach it: `registerTools` may be called many times on an SPA entry, and each call
|
|
419
|
+
* emits its own `tool_registration`.
|
|
420
|
+
*/
|
|
421
|
+
export declare const PAGE_BATCH_EVENT = "batch";
|
|
422
|
+
export declare const PAGE_BATCH_EVENTS_KEY = "events";
|
|
423
|
+
export declare const PAGE_BATCH_MAX_EVENTS = 16;
|
|
424
|
+
/** Send every waiting page-level batch now — on `pagehide`, a hidden tab, or a test. */
|
|
425
|
+
export declare function flushPageEventQueue(): void;
|
|
373
426
|
/**
|
|
374
427
|
* The sink for the two events a *batch* owns. They carry that batch's tool identity —
|
|
375
428
|
* `stableKey`, `routeTemplate`, `errorSignature` — so they authenticate with that
|
|
@@ -506,10 +559,22 @@ export declare function flushInitEvent(sinks?: TelemetrySinks): void;
|
|
|
506
559
|
*/
|
|
507
560
|
export declare function deferInitEventForBatch(sinks: TelemetrySinks): void;
|
|
508
561
|
/**
|
|
509
|
-
* Run `run` after `ms`, returning a canceller
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
562
|
+
* Run `run` after `ms`, returning a canceller — or `undefined` when nothing was
|
|
563
|
+
* scheduled. Guarded end to end: a runtime with no timers (SSR) and a hostile
|
|
564
|
+
* `setTimeout` (an extension, a fake-timer harness) both mean the callback never
|
|
565
|
+
* runs, never a throw out of the import or out of `registerTools`.
|
|
566
|
+
*
|
|
567
|
+
* The `undefined` is the part {@link afterDelay} cannot express, and it matters to
|
|
568
|
+
* exactly one caller: the page-level batch holds events until its timer fires, so a
|
|
569
|
+
* `setTimeout` that exists and THROWS would leave them queued until `pagehide` — a
|
|
570
|
+
* silent loss on any page the user closes from a different tab. A caller that only
|
|
571
|
+
* defers work is happy with a no-op; a caller that defers DATA has to know.
|
|
572
|
+
*/
|
|
573
|
+
export declare function tryAfterDelay(run: () => void, ms: number): (() => void) | undefined;
|
|
574
|
+
/**
|
|
575
|
+
* {@link tryAfterDelay} for the callers that have nothing to do when the runtime
|
|
576
|
+
* cannot schedule: the canceller is safe to call when nothing was scheduled, and an
|
|
577
|
+
* unscheduled flush is simply one that never happens.
|
|
513
578
|
*/
|
|
514
579
|
export declare function afterDelay(run: () => void, ms: number): () => void;
|
|
515
580
|
/** 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*
|
|
@@ -70,6 +72,16 @@ export declare function sendToCollect(event: TrackingEvent, config: CollectConfi
|
|
|
70
72
|
* wire. Note that adding the header makes the request non-simple under CORS, so
|
|
71
73
|
* an authenticated beacon costs a preflight the anonymous one does not.
|
|
72
74
|
*
|
|
75
|
+
* The body is sent as `text/plain`, not `application/json`, and that is a cost
|
|
76
|
+
* decision, not a formatting one: `text/plain` is a CORS-safelisted content type,
|
|
77
|
+
* so an anonymous beacon is a "simple request" and the browser sends it with no
|
|
78
|
+
* OPTIONS preflight. With `application/json` every page load paid two requests
|
|
79
|
+
* per event at the edge; at 2026-09 volume that preflight was roughly half of the
|
|
80
|
+
* seven million requests a day the collect API billed. The edge maps `text/plain`
|
|
81
|
+
* to the same template as JSON (ingestion-edge/telemetry.tf), so nothing changes
|
|
82
|
+
* on the wire but the header. The keyed beacon keeps `x-api-key`, which forces a
|
|
83
|
+
* preflight regardless of content type; that path is a small fraction of traffic.
|
|
84
|
+
*
|
|
73
85
|
* A `401` on this path means the key that WAS sent resolved to nothing; the beacon
|
|
74
86
|
* itself was still recorded, anonymously. That is reported through
|
|
75
87
|
* {@link warnKeyRejected} and nothing else — no throw, no retry, no second send.
|
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.19.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",
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
"default": "./dist/index.js"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
-
"files": [
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"CHANGELOG.md"
|
|
17
|
+
],
|
|
15
18
|
"publishConfig": {
|
|
16
19
|
"access": "public"
|
|
17
20
|
},
|
|
@@ -36,5 +39,7 @@
|
|
|
36
39
|
"optional": true
|
|
37
40
|
}
|
|
38
41
|
},
|
|
39
|
-
"trustedDependencies": [
|
|
42
|
+
"trustedDependencies": [
|
|
43
|
+
"@biomejs/biome"
|
|
44
|
+
]
|
|
40
45
|
}
|