highseat 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -0
- package/index.d.ts +21 -11
- package/index.js.map +1 -1
- package/mcp.d.ts +61 -0
- package/mcp.js +160 -0
- package/mcp.js.map +1 -0
- package/next.d.ts +1 -1
- package/package.json +9 -1
- package/server.d.ts +44 -4
- package/server.js.map +1 -1
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ npm install highseat
|
|
|
17
17
|
| `highseat/next` | `<Highseat />`, `useIdentify()`, `useBeforeSend()`, `createProxy()` and `withHighseat()` for Next.js | Next.js |
|
|
18
18
|
| `highseat/proxy` | `createProxy()`, a first-party proxy for any runtime with `fetch` | Nothing |
|
|
19
19
|
| `highseat/server` | `track()`, `flush()`, `close()` and `createTracker()`, for events sent from your backend | Nothing |
|
|
20
|
+
| `highseat/mcp` | `trackToolCalls()` and `mcpLink()`, for the tool calls AI assistants make to your MCP server | The MCP TypeScript SDK v2 |
|
|
20
21
|
|
|
21
22
|
Each entry point loads on its own and imports no framework it does not name, so `highseat/proxy` and `highseat/server` run in a Cloudflare Worker without React installed.
|
|
22
23
|
|
|
@@ -328,6 +329,48 @@ Each event takes `request` or `headers`, then `ip`, `userAgent`, `userId`, `host
|
|
|
328
329
|
|
|
329
330
|
Any language can send the same request. `POST https://cdn.highseat.com/api/event` with `x-site-id` and `x-hs-secret` headers and a JSON array of up to 25 events. Each event names `event_name`, `hostname` (or a `page_url`), and optionally `event_id`, `user_id`, `ip`, `user_agent`, `page_url`, `referrer`, `properties` (string values) and `revenue`. The answer lists each refused event in `rejections` and each event it did not store in `dropped`, by its index in the batch.
|
|
330
331
|
|
|
332
|
+
## MCP tool calls
|
|
333
|
+
|
|
334
|
+
`highseat/mcp` records each call an AI assistant (Claude, ChatGPT, Cursor and others) makes to a tool on your MCP server, built with the MCP TypeScript SDK v2. It sends through `highseat/server`, so set `HIGHSEAT_SITE` and `HIGHSEAT_SECRET` on the server as above. Call `trackToolCalls()` once, after creating the server and before registering any tool:
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
import { McpServer } from "@modelcontextprotocol/server"
|
|
338
|
+
import { mcpLink, trackToolCalls } from "highseat/mcp"
|
|
339
|
+
import { z } from "zod"
|
|
340
|
+
|
|
341
|
+
const server = new McpServer({ name: "Acme", version: "1.0.0" })
|
|
342
|
+
trackToolCalls(server, { hostname: "acme.com" })
|
|
343
|
+
|
|
344
|
+
server.registerTool(
|
|
345
|
+
"get_product",
|
|
346
|
+
{
|
|
347
|
+
description: "One product, by its slug.",
|
|
348
|
+
inputSchema: z.object({ slug: z.string() }),
|
|
349
|
+
annotations: { readOnlyHint: true },
|
|
350
|
+
},
|
|
351
|
+
async ({ slug }, context) => ({
|
|
352
|
+
content: [{ type: "text", text: mcpLink(`https://acme.com/products/${slug}`, context) }],
|
|
353
|
+
}),
|
|
354
|
+
)
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Each call sends one `mcp_tool_call` event with these properties:
|
|
358
|
+
|
|
359
|
+
| Property | |
|
|
360
|
+
| --- | --- |
|
|
361
|
+
| `tool` | The tool's name. |
|
|
362
|
+
| `server` | The `server` option, a label such as `public` or `admin` for a site with more than one MCP server. |
|
|
363
|
+
| `client` | The name the client declares in `clientInfo`, else the product name its User-Agent starts with, such as `Claude-User`, else `unknown`. |
|
|
364
|
+
| `client_version` | The version the client declares, when it declares one. |
|
|
365
|
+
| `client_source` | `client_info`, `user_agent` or `none`: how `client` was found. |
|
|
366
|
+
| `status` | `ok`, or the error code the client saw: the `code` of a thrown error, the `CODE:` an error result's text starts with, else `TOOL_ERROR` or `INTERNAL_SERVER_ERROR`. |
|
|
367
|
+
| `results` | How many items a read-only tool returned, when its result is a list, or an object holding exactly one list. |
|
|
368
|
+
| `duration_ms` | How long the call took, in whole milliseconds. |
|
|
369
|
+
|
|
370
|
+
It never sends the tool's arguments, its result, the caller's IP or its user agent, and it never reads ChatGPT's user or conversation ids. Assistant directories forbid collecting conversation data, and every call comes from the assistant's servers rather than from a person, so a tool call counts as an event, never as a visitor. Highseat accepts `mcp_tool_call` only from a server holding the site secret.
|
|
371
|
+
|
|
372
|
+
`mcpLink(url, context)` tags a link a tool returns with `utm_source` set to the client and `utm_medium=mcp`, so a visit it brings reads as that assistant in the AI channel. A link that already names a `utm_source` is returned as it is. Pass `waitUntil` to `trackToolCalls()` in a serverless function, and `tracker` to send with a tracker from `createTracker()`.
|
|
373
|
+
|
|
331
374
|
## First-party proxy
|
|
332
375
|
|
|
333
376
|
The proxy serves the tracker and forwards its events from your own domain, on the site's own path, so a filter list that blocks Highseat's host does not block your site. A `GET` on the path serves the script and a `POST` forwards events. The proxy sends the visitor's IP under your site secret, which a rewrite or a redirect rule cannot do: without it, every visitor would get your server's country.
|
package/index.d.ts
CHANGED
|
@@ -81,23 +81,33 @@ type CheckedProperties<Shape> = [keyof Shape] extends [never] ? Record<string, n
|
|
|
81
81
|
type RegisteredEvents = Register extends {
|
|
82
82
|
events: infer Registered;
|
|
83
83
|
} ? { [Name in keyof Registered]: CheckedProperties<Registered[Name]>; } : {};
|
|
84
|
-
type KnownEvents = Omit<
|
|
84
|
+
type KnownEvents<BuiltIns> = Omit<BuiltIns, keyof RegisteredEvents> & RegisteredEvents;
|
|
85
|
+
/**
|
|
86
|
+
* The event names a `track` with the given built-in events knows, built-in or registered.
|
|
87
|
+
* `highseat/server` passes its server-only events on top of {@link BuiltInEvents}.
|
|
88
|
+
*/
|
|
89
|
+
export type KnownEventName<BuiltIns> = Extract<keyof KnownEvents<BuiltIns>, string>;
|
|
85
90
|
/** Every event name `track` knows, built-in or registered. */
|
|
86
|
-
export type EventName =
|
|
91
|
+
export type EventName = KnownEventName<BuiltInEvents>;
|
|
87
92
|
/**
|
|
88
|
-
* The names `track` accepts: known names, and any other string
|
|
89
|
-
* `string & {}` keeps the known names in the editor's suggestions,
|
|
90
|
-
* union would absorb.
|
|
93
|
+
* The names a `track` with the given built-in events accepts: known names, and any other string
|
|
94
|
+
* unless the site is strict. `string & {}` keeps the known names in the editor's suggestions,
|
|
95
|
+
* which a plain `string` in the union would absorb.
|
|
91
96
|
*/
|
|
92
|
-
export type
|
|
97
|
+
export type TrackNameOf<BuiltIns> = Register extends {
|
|
93
98
|
strict: true;
|
|
94
|
-
} ?
|
|
99
|
+
} ? KnownEventName<BuiltIns> : KnownEventName<BuiltIns> | (string & {});
|
|
100
|
+
/** The names `track` accepts: known names, and any other string unless the site is strict. */
|
|
101
|
+
export type TrackName = TrackNameOf<BuiltInEvents>;
|
|
95
102
|
/**
|
|
96
|
-
* The arguments after an event's name
|
|
97
|
-
* when any of them is; any other name takes
|
|
98
|
-
* names is checked as a whole rather than
|
|
103
|
+
* The arguments after an event's name, for a `track` with the given built-in events. A known
|
|
104
|
+
* name takes its declared properties, required when any of them is; any other name takes
|
|
105
|
+
* {@link Properties}. Wrapped in tuples so a union of names is checked as a whole rather than
|
|
106
|
+
* name by name.
|
|
99
107
|
*/
|
|
100
|
-
export type
|
|
108
|
+
export type TrackArgsOf<BuiltIns, Name extends string> = [Name] extends [KnownEventName<BuiltIns>] ? {} extends KnownEvents<BuiltIns>[Name] ? [properties?: KnownEvents<BuiltIns>[Name]] : [properties: KnownEvents<BuiltIns>[Name]] : [properties?: Properties];
|
|
109
|
+
/** The arguments after an event's name: its properties. See {@link TrackArgsOf}. */
|
|
110
|
+
export type TrackArgs<Name extends string> = TrackArgsOf<BuiltInEvents, Name>;
|
|
101
111
|
/**
|
|
102
112
|
* A user id from your own system, as {@link identify} takes it. A string or a number identifies
|
|
103
113
|
* the visitor, `null` clears the id, and `undefined` or an empty string changes nothing. Pass your
|
package/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { isDevelopment } from \"./env\"\n\n/**\n * The browser client. Every call goes to the hosted tracker on `window.highseat`; before the\n * tracker has loaded, calls wait in a queue the tracker drains on startup. Nothing here throws:\n * on a server, with the tracker blocked, or with tracking off, a call does nothing.\n */\n\n/** A value an event property can hold. A `null` or `undefined` value is left out of the event. */\nexport type PropertyValue = string | number | boolean | null | undefined\n\n/**\n * Money attached to an event. `amount` is an integer in the currency's smallest unit, so\n * `{ amount: 4999, currency: \"USD\" }` is $49.99 and `{ amount: 5000, currency: \"JPY\" }` is ¥5000.\n */\nexport interface Revenue {\n /** An integer in minor units: 4999 for $49.99. An amount with decimals is rejected. */\n amount: number\n /** An ISO 4217 code, such as `USD` or `EUR`. */\n currency: string\n}\n\n/** The properties of an event with no declared shape: any string keys, plus optional revenue. */\nexport interface Properties {\n /** Money the event brought in. */\n revenue?: Revenue\n [key: string]: PropertyValue | Revenue\n}\n\n/**\n * The events Highseat knows by name. A site extends it, or overrides an entry, through\n * {@link Register}. Type-only: it adds nothing to the bundle.\n */\nexport interface BuiltInEvents {\n /** A visitor created an account. */\n signup: Properties\n /** A visitor signed in. */\n login: Properties\n /**\n * A sale. With a payment provider connected, leave `revenue` out: the provider's webhook\n * records the money, and revenue sent here as well would count the sale twice. Send `revenue`\n * only for a checkout no provider reports, and then with an `order_id`, so a provider sale\n * added later for the same order is not counted again.\n */\n purchase: Properties\n /** A paid subscription began. */\n subscription_started: Properties\n /** A free trial began. */\n trial_started: Properties\n /** A file download. The tracker records links to files on its own. */\n file_download: { url: string }\n /** A click on a link to another site. The tracker records these on its own. */\n outbound_link_click: { url: string }\n /** A visit to a missing page. Call it from a single-page app's not-found route. */\n \"404\": Properties\n}\n\n/**\n * Declares a site's own events. Augment it from any file the compiler sees:\n *\n * ```ts\n * declare module \"highseat\" {\n * interface Register {\n * events: {\n * signup: { plan: \"free\" | \"pro\" }\n * }\n * strict: true\n * }\n * }\n * ```\n *\n * `events` adds names, or replaces a built-in one's properties. `strict: true` makes `track`\n * accept only built-in and registered names, so a misspelt name is a type error.\n */\nexport interface Register {}\n\n/**\n * A registered event's properties as `track` accepts them. A shape with no properties takes none,\n * and a property whose type an event cannot carry becomes `never`, so a call passing it fails\n * instead of sending `\"[object Object]\"`. The shape is not rejected at registration, where a\n * failed check would silently drop every registered event at once.\n */\ntype CheckedProperties<Shape> = [keyof Shape] extends [never]\n ? Record<string, never>\n : { [Key in keyof Shape]: Shape[Key] extends PropertyValue | Revenue ? Shape[Key] : never }\n\ntype RegisteredEvents = Register extends { events: infer Registered }\n ? { [Name in keyof Registered]: CheckedProperties<Registered[Name]> }\n : {}\n\ntype KnownEvents = Omit<BuiltInEvents, keyof RegisteredEvents> & RegisteredEvents\n\n/** Every event name `track` knows, built-in or registered. */\nexport type EventName = Extract<keyof KnownEvents, string>\n\n/**\n * The names `track` accepts: known names, and any other string unless the site is strict.\n * `string & {}` keeps the known names in the editor's suggestions, which a plain `string` in the\n * union would absorb.\n */\nexport type TrackName = Register extends { strict: true } ? EventName : EventName | (string & {})\n\n/**\n * The arguments after an event's name. A known name takes its declared properties, required\n * when any of them is; any other name takes {@link Properties}. Wrapped in tuples so a union of\n * names is checked as a whole rather than name by name.\n */\nexport type TrackArgs<Name extends string> = [Name] extends [EventName]\n ? {} extends KnownEvents[Name]\n ? [properties?: KnownEvents[Name]]\n : [properties: KnownEvents[Name]]\n : [properties?: Properties]\n\n/**\n * A user id from your own system, as {@link identify} takes it. A string or a number identifies\n * the visitor, `null` clears the id, and `undefined` or an empty string changes nothing. Pass your\n * own database id, never an email or other personal data.\n */\nexport type UserId = string | number | null | undefined\n\n/**\n * The part of an event {@link BeforeSend} sees and may change. The site, the visitor's user id\n * and the attribution id are out of reach, so a hook cannot change who an event belongs to.\n */\nexport interface BeforeSendEvent {\n /** The event's name: `pageview`, `engagement`, or a name passed to `track`. Read-only. */\n readonly name: string\n /** The page's full URL. The page path is read off the URL you return. */\n url: string\n /** The page the visitor came from, or the page left on a client-side navigation. */\n referrer?: string\n /** The event's properties, each value a string. */\n properties?: Record<string, string>\n}\n\n/**\n * Runs in the browser on every event before the tracker sends it, page views from client-side\n * navigation included. Return the event, changed or not, or `null` to drop it. Any other falsy\n * answer drops it too, and so does a hook that throws: a hook that keeps a secret out of the\n * data must not leak it when it fails. The error still reaches the console.\n *\n * ```ts\n * function beforeSend(event: BeforeSendEvent) {\n * const url = new URL(event.url)\n * if (url.pathname.startsWith(\"/admin/\")) return null\n * url.searchParams.delete(\"email\")\n * return { ...event, url: url.href }\n * }\n * ```\n */\nexport type BeforeSend = (event: BeforeSendEvent) => BeforeSendEvent | null\n\n/** The tracker's API on `window.highseat`, or the queue that stands in for it until it loads. */\nexport interface HighseatGlobal {\n /** Records an event. */\n track?: (name: string, properties?: Properties) => void\n /** Sets or clears the visitor's user id. */\n identify?: (id: UserId) => void\n /** The id that joins a payment to this tab's visit. */\n getAttributionId?: () => string | undefined\n /** Calls made before the tracker loaded, drained in order when it starts. */\n q?: [string, unknown[]][]\n /** The site's {@link BeforeSend} hook, read on every event. */\n beforeSend?: BeforeSend\n}\n\n/** How long {@link getAttributionId} waits for the tracker. */\nexport interface AttributionOptions {\n /** Milliseconds to wait for the tracker to load. Defaults to 3000. */\n timeout?: number\n}\n\n/** How the tracker is loaded. Each option maps to an attribute on its script tag. */\nexport interface InjectOptions {\n /** The site id, `site_…`, from the site's Installation settings. */\n site: string\n /**\n * Where the script loads from: the site's proxy path (`/3f9a0c1e2b`) for first-party\n * delivery, or a full URL. Defaults to Highseat's own host. The tracker posts its events back\n * to the same place.\n */\n src?: string\n /** Records clicks on links to other sites as `outbound_link_click`. Defaults to `true`. */\n outbound?: boolean\n /** Records clicks on links to files as `file_download`. Defaults to `true`. */\n downloads?: boolean\n /** Counts a change of `#/route` as a page view, for an app that routes by hash. */\n hashRouting?: boolean\n /** Changes or drops each event before it leaves the browser. See {@link BeforeSend}. */\n beforeSend?: BeforeSend\n /**\n * Logs every `track()` and `identify()` call to the console, and warns once when no tracker\n * script is on the page or the script failed to load. Defaults to `true` when\n * `process.env.NODE_ENV` is `development`, so a production build logs nothing.\n */\n debug?: boolean\n}\n\ndeclare global {\n interface Window {\n highseat?: HighseatGlobal\n }\n}\n\n/** Where the tracker loads from when the site has no proxy path. */\nexport const SCRIPT_URL = \"https://cdn.highseat.com/script.js\"\n\n// The tracker marks its own API with this symbol. A site's own stub can define methods too, so\n// only the symbol proves the tracker is running.\nconst LIVE = Symbol.for(\"highseat\")\n\nconst SCRIPT_HOST = new URL(SCRIPT_URL).host\n// A site's proxy path: `/` and ten hex characters.\nconst PROXY_PATH = /^\\/[0-9a-f]{10}$/\n\nconst TRACKER_TIMEOUT = 3000\nconst TRACKER_POLL_INTERVAL = 50\n\nconst MISSING_SCRIPT_WARNING = [\n \"highseat: no tracker script on this page, so no event reaches Highseat.\",\n \"Render <Highseat />, call inject({ site }), or add the script tag from the site's Installation settings.\",\n].join(\" \")\nconst FAILED_SCRIPT_WARNING = [\n \"highseat: the tracker script failed to load, so no event reaches Highseat.\",\n \"An ad blocker may have stopped it: load the tracker from the site's proxy path instead.\",\n].join(\" \")\nconst SLOW_SCRIPT_WARNING = [\n `highseat: the tracker script has not loaded after ${TRACKER_TIMEOUT / 1000} seconds,`,\n \"so every call waits in the queue. Check the network panel for a blocked or failed request.\",\n].join(\" \")\n\n// The script tags `inject` added, and every tracker script whose request failed, such as one an\n// ad blocker stopped. A browser keeps no record of a failed script, so a failure is only known\n// to a listener attached before it happened.\nconst injectedScripts = new WeakSet<HTMLScriptElement>()\nconst failedScripts = new WeakSet<HTMLScriptElement>()\n\n// The `debug` option of the last `inject` call. Unset falls back to the build's environment.\nlet debugOption: boolean | undefined\n// The windows whose tracker is being watched, so a page warns about its script once.\nconst watchedWindows = new WeakSet<Window>()\n\nfunction findGlobal(): HighseatGlobal | undefined {\n if (typeof window === \"undefined\") return\n return (window.highseat ??= {})\n}\n\nfunction isLive(api: HighseatGlobal | undefined): api is HighseatGlobal {\n return !!api && (api as Record<symbol, unknown>)[LIVE] === true\n}\n\n// One function for every script, so attaching it on each poll adds no second listener.\nfunction markFailed(event: Event) {\n failedScripts.add(event.currentTarget as HTMLScriptElement)\n}\n\n/**\n * Whether a script tag loads the tracker, judged as the tracker judges itself. Fathom's snippet\n * carries a `data-site` too, so a tag is matched by a `site_` id, or by a `src` on a proxy path\n * or on Highseat's host: an older site's id is a UUID, which any tool could use.\n */\nfunction isTrackerScript(script: HTMLScriptElement) {\n const { site } = script.dataset\n if (injectedScripts.has(script) || site?.startsWith(\"site_\")) return true\n if (site === undefined) return false\n\n try {\n // A browser hands back `src` resolved; the base only matters to a relative one.\n const url = new URL(script.src, \"http://localhost\")\n return url.host === SCRIPT_HOST || PROXY_PATH.test(url.pathname)\n } catch {\n return false\n }\n}\n\n/** The page's tracker scripts, each one listened to from now on for a failed load. */\nfunction getTrackerScripts() {\n const scripts = Array.from(document.scripts).filter(isTrackerScript)\n for (const script of scripts) script.addEventListener(\"error\", markFailed)\n return scripts\n}\n\nfunction isDebug() {\n return debugOption ?? isDevelopment()\n}\n\n/**\n * Warns once per page when the tracker cannot arrive: no tracker script, or only failed ones. A\n * missing script is judged at the timeout, because `<Highseat />` adds its tag in an effect that\n * runs after the effects of the components inside it.\n */\nfunction watchTracker() {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return\n if (watchedWindows.has(window)) return\n watchedWindows.add(window)\n\n const page = window\n const deadline = Date.now() + TRACKER_TIMEOUT\n\n function check() {\n // A replaced global window, as a test sets up for each case, is another page.\n if (window !== page || isLive(page.highseat)) return\n\n const scripts = getTrackerScripts()\n\n if (scripts.length > 0 && scripts.every(script => failedScripts.has(script))) {\n console.warn(FAILED_SCRIPT_WARNING)\n return\n }\n if (Date.now() < deadline) {\n setTimeout(check, TRACKER_POLL_INTERVAL)\n return\n }\n console.warn(scripts.length === 0 ? MISSING_SCRIPT_WARNING : SLOW_SCRIPT_WARNING)\n }\n\n setTimeout(check, TRACKER_POLL_INTERVAL)\n}\n\n/**\n * Logs a call when debugging is on. The check waits a microtask, so a `debug` option that\n * `<Highseat />` passes in its effect applies to the calls its children make in theirs.\n */\nfunction logCall(method: \"track\" | \"identify\", args: unknown[], isQueued: boolean) {\n queueMicrotask(() => {\n if (!isDebug()) return\n\n const status = isQueued ? \"queued until the tracker loads\" : \"sent to the tracker\"\n const [value, properties] = args\n // `JSON.stringify` quotes a name or a string id, and returns `undefined` for an unset id.\n const message = `highseat: ${method} ${String(JSON.stringify(value))}, ${status}`\n\n if (properties === undefined) console.info(message)\n else console.info(message, properties)\n watchTracker()\n })\n}\n\nfunction call(method: \"track\" | \"identify\", args: unknown[]) {\n const api = findGlobal()\n if (!api) return\n\n const handler = api[method] as ((...values: unknown[]) => void) | undefined\n if (typeof handler !== \"function\") {\n api.q ??= []\n api.q.push([method, args])\n logCall(method, args, true)\n return\n }\n\n logCall(method, args, false)\n try {\n handler(...args)\n } catch {}\n}\n\n/**\n * Records an event. A known name autocompletes and checks its properties; any other name is a\n * custom event, unless {@link Register} sets `strict`. Names are stored exactly as written, so\n * `Purchase` and `purchase` are two events.\n */\nexport function track<Name extends TrackName>(name: Name, ...args: TrackArgs<Name>): void {\n const [properties] = args\n call(\"track\", properties === undefined ? [name] : [name, properties])\n}\n\n/**\n * Links the visitor to a user id from your own system, on this event and every later one, across\n * visits. `null` clears it at sign-out. An empty or `undefined` id is ignored, so\n * `identify(user?.id)` before a session loads does not sign the visitor out.\n */\nexport function identify(id: UserId): void {\n call(\"identify\", [id])\n}\n\n/**\n * The id that joins a payment to this tab's visit. Pass it to the checkout as metadata\n * (`attribution_id`), and the payment provider's webhook credits the sale to the visit's source.\n * Resolves to `undefined`, so a checkout that awaits it never hangs: at once when the page has\n * no tracker script or its script fails to load (an ad blocker stopping it), after the timeout\n * when the script is still loading, and on a server or with tracking off.\n */\nexport async function getAttributionId(\n options: AttributionOptions = {},\n): Promise<string | undefined> {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return\n\n const deadline = Date.now() + (options.timeout ?? TRACKER_TIMEOUT)\n let api = window.highseat\n\n while (!isLive(api)) {\n if (Date.now() >= deadline) return\n\n const scripts = getTrackerScripts()\n // No tracker script at all, or only failed ones: the tracker cannot arrive.\n if (scripts.every(script => failedScripts.has(script))) return\n\n await new Promise(resolve => setTimeout(resolve, TRACKER_POLL_INTERVAL))\n api = window.highseat\n }\n\n try {\n return api.getAttributionId?.()\n } catch {\n return\n }\n}\n\n/**\n * Sets the {@link BeforeSend} hook, or clears it with `undefined`. Works before the tracker has\n * loaded, which picks the hook up when it starts, and applies from the next event once it runs.\n * Does nothing on a server.\n */\nexport function setBeforeSend(beforeSend: BeforeSend | undefined): void {\n const api = findGlobal()\n if (api) api.beforeSend = beforeSend\n}\n\n/**\n * Adds the tracker's script tag to the page, once. A second call for the same site, such as a\n * component mounting again, finds the tag and adds nothing, but its `debug` option still applies.\n * Does nothing on a server.\n */\nexport function inject(options: InjectOptions): void {\n if (typeof document === \"undefined\") return\n\n const { site, src = SCRIPT_URL, outbound, downloads, hashRouting, beforeSend, debug } = options\n debugOption = debug\n // Set before the script tag is added, so the first page view already passes through it.\n if (beforeSend) setBeforeSend(beforeSend)\n if (isDebug()) watchTracker()\n if (Array.from(document.scripts).some(script => script.dataset.site === site)) return\n\n const script = document.createElement(\"script\")\n script.addEventListener(\"error\", markFailed)\n injectedScripts.add(script)\n script.src = src\n script.dataset.site = site\n if (outbound === false) script.dataset.outbound = \"false\"\n if (downloads === false) script.dataset.downloads = \"false\"\n if (hashRouting) script.dataset.hashRouting = \"\"\n document.head.append(script)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { isDevelopment } from \"./env\"\n\n/**\n * The browser client. Every call goes to the hosted tracker on `window.highseat`; before the\n * tracker has loaded, calls wait in a queue the tracker drains on startup. Nothing here throws:\n * on a server, with the tracker blocked, or with tracking off, a call does nothing.\n */\n\n/** A value an event property can hold. A `null` or `undefined` value is left out of the event. */\nexport type PropertyValue = string | number | boolean | null | undefined\n\n/**\n * Money attached to an event. `amount` is an integer in the currency's smallest unit, so\n * `{ amount: 4999, currency: \"USD\" }` is $49.99 and `{ amount: 5000, currency: \"JPY\" }` is ¥5000.\n */\nexport interface Revenue {\n /** An integer in minor units: 4999 for $49.99. An amount with decimals is rejected. */\n amount: number\n /** An ISO 4217 code, such as `USD` or `EUR`. */\n currency: string\n}\n\n/** The properties of an event with no declared shape: any string keys, plus optional revenue. */\nexport interface Properties {\n /** Money the event brought in. */\n revenue?: Revenue\n [key: string]: PropertyValue | Revenue\n}\n\n/**\n * The events Highseat knows by name. A site extends it, or overrides an entry, through\n * {@link Register}. Type-only: it adds nothing to the bundle.\n */\nexport interface BuiltInEvents {\n /** A visitor created an account. */\n signup: Properties\n /** A visitor signed in. */\n login: Properties\n /**\n * A sale. With a payment provider connected, leave `revenue` out: the provider's webhook\n * records the money, and revenue sent here as well would count the sale twice. Send `revenue`\n * only for a checkout no provider reports, and then with an `order_id`, so a provider sale\n * added later for the same order is not counted again.\n */\n purchase: Properties\n /** A paid subscription began. */\n subscription_started: Properties\n /** A free trial began. */\n trial_started: Properties\n /** A file download. The tracker records links to files on its own. */\n file_download: { url: string }\n /** A click on a link to another site. The tracker records these on its own. */\n outbound_link_click: { url: string }\n /** A visit to a missing page. Call it from a single-page app's not-found route. */\n \"404\": Properties\n}\n\n/**\n * Declares a site's own events. Augment it from any file the compiler sees:\n *\n * ```ts\n * declare module \"highseat\" {\n * interface Register {\n * events: {\n * signup: { plan: \"free\" | \"pro\" }\n * }\n * strict: true\n * }\n * }\n * ```\n *\n * `events` adds names, or replaces a built-in one's properties. `strict: true` makes `track`\n * accept only built-in and registered names, so a misspelt name is a type error.\n */\nexport interface Register {}\n\n/**\n * A registered event's properties as `track` accepts them. A shape with no properties takes none,\n * and a property whose type an event cannot carry becomes `never`, so a call passing it fails\n * instead of sending `\"[object Object]\"`. The shape is not rejected at registration, where a\n * failed check would silently drop every registered event at once.\n */\ntype CheckedProperties<Shape> = [keyof Shape] extends [never]\n ? Record<string, never>\n : { [Key in keyof Shape]: Shape[Key] extends PropertyValue | Revenue ? Shape[Key] : never }\n\ntype RegisteredEvents = Register extends { events: infer Registered }\n ? { [Name in keyof Registered]: CheckedProperties<Registered[Name]> }\n : {}\n\n// A registered event replaces a built-in one of the same name.\ntype KnownEvents<BuiltIns> = Omit<BuiltIns, keyof RegisteredEvents> & RegisteredEvents\n\n/**\n * The event names a `track` with the given built-in events knows, built-in or registered.\n * `highseat/server` passes its server-only events on top of {@link BuiltInEvents}.\n */\nexport type KnownEventName<BuiltIns> = Extract<keyof KnownEvents<BuiltIns>, string>\n\n/** Every event name `track` knows, built-in or registered. */\nexport type EventName = KnownEventName<BuiltInEvents>\n\n/**\n * The names a `track` with the given built-in events accepts: known names, and any other string\n * unless the site is strict. `string & {}` keeps the known names in the editor's suggestions,\n * which a plain `string` in the union would absorb.\n */\nexport type TrackNameOf<BuiltIns> = Register extends { strict: true }\n ? KnownEventName<BuiltIns>\n : KnownEventName<BuiltIns> | (string & {})\n\n/** The names `track` accepts: known names, and any other string unless the site is strict. */\nexport type TrackName = TrackNameOf<BuiltInEvents>\n\n/**\n * The arguments after an event's name, for a `track` with the given built-in events. A known\n * name takes its declared properties, required when any of them is; any other name takes\n * {@link Properties}. Wrapped in tuples so a union of names is checked as a whole rather than\n * name by name.\n */\nexport type TrackArgsOf<BuiltIns, Name extends string> = [Name] extends [KnownEventName<BuiltIns>]\n ? {} extends KnownEvents<BuiltIns>[Name]\n ? [properties?: KnownEvents<BuiltIns>[Name]]\n : [properties: KnownEvents<BuiltIns>[Name]]\n : [properties?: Properties]\n\n/** The arguments after an event's name: its properties. See {@link TrackArgsOf}. */\nexport type TrackArgs<Name extends string> = TrackArgsOf<BuiltInEvents, Name>\n\n/**\n * A user id from your own system, as {@link identify} takes it. A string or a number identifies\n * the visitor, `null` clears the id, and `undefined` or an empty string changes nothing. Pass your\n * own database id, never an email or other personal data.\n */\nexport type UserId = string | number | null | undefined\n\n/**\n * The part of an event {@link BeforeSend} sees and may change. The site, the visitor's user id\n * and the attribution id are out of reach, so a hook cannot change who an event belongs to.\n */\nexport interface BeforeSendEvent {\n /** The event's name: `pageview`, `engagement`, or a name passed to `track`. Read-only. */\n readonly name: string\n /** The page's full URL. The page path is read off the URL you return. */\n url: string\n /** The page the visitor came from, or the page left on a client-side navigation. */\n referrer?: string\n /** The event's properties, each value a string. */\n properties?: Record<string, string>\n}\n\n/**\n * Runs in the browser on every event before the tracker sends it, page views from client-side\n * navigation included. Return the event, changed or not, or `null` to drop it. Any other falsy\n * answer drops it too, and so does a hook that throws: a hook that keeps a secret out of the\n * data must not leak it when it fails. The error still reaches the console.\n *\n * ```ts\n * function beforeSend(event: BeforeSendEvent) {\n * const url = new URL(event.url)\n * if (url.pathname.startsWith(\"/admin/\")) return null\n * url.searchParams.delete(\"email\")\n * return { ...event, url: url.href }\n * }\n * ```\n */\nexport type BeforeSend = (event: BeforeSendEvent) => BeforeSendEvent | null\n\n/** The tracker's API on `window.highseat`, or the queue that stands in for it until it loads. */\nexport interface HighseatGlobal {\n /** Records an event. */\n track?: (name: string, properties?: Properties) => void\n /** Sets or clears the visitor's user id. */\n identify?: (id: UserId) => void\n /** The id that joins a payment to this tab's visit. */\n getAttributionId?: () => string | undefined\n /** Calls made before the tracker loaded, drained in order when it starts. */\n q?: [string, unknown[]][]\n /** The site's {@link BeforeSend} hook, read on every event. */\n beforeSend?: BeforeSend\n}\n\n/** How long {@link getAttributionId} waits for the tracker. */\nexport interface AttributionOptions {\n /** Milliseconds to wait for the tracker to load. Defaults to 3000. */\n timeout?: number\n}\n\n/** How the tracker is loaded. Each option maps to an attribute on its script tag. */\nexport interface InjectOptions {\n /** The site id, `site_…`, from the site's Installation settings. */\n site: string\n /**\n * Where the script loads from: the site's proxy path (`/3f9a0c1e2b`) for first-party\n * delivery, or a full URL. Defaults to Highseat's own host. The tracker posts its events back\n * to the same place.\n */\n src?: string\n /** Records clicks on links to other sites as `outbound_link_click`. Defaults to `true`. */\n outbound?: boolean\n /** Records clicks on links to files as `file_download`. Defaults to `true`. */\n downloads?: boolean\n /** Counts a change of `#/route` as a page view, for an app that routes by hash. */\n hashRouting?: boolean\n /** Changes or drops each event before it leaves the browser. See {@link BeforeSend}. */\n beforeSend?: BeforeSend\n /**\n * Logs every `track()` and `identify()` call to the console, and warns once when no tracker\n * script is on the page or the script failed to load. Defaults to `true` when\n * `process.env.NODE_ENV` is `development`, so a production build logs nothing.\n */\n debug?: boolean\n}\n\ndeclare global {\n interface Window {\n highseat?: HighseatGlobal\n }\n}\n\n/** Where the tracker loads from when the site has no proxy path. */\nexport const SCRIPT_URL = \"https://cdn.highseat.com/script.js\"\n\n// The tracker marks its own API with this symbol. A site's own stub can define methods too, so\n// only the symbol proves the tracker is running.\nconst LIVE = Symbol.for(\"highseat\")\n\nconst SCRIPT_HOST = new URL(SCRIPT_URL).host\n// A site's proxy path: `/` and ten hex characters.\nconst PROXY_PATH = /^\\/[0-9a-f]{10}$/\n\nconst TRACKER_TIMEOUT = 3000\nconst TRACKER_POLL_INTERVAL = 50\n\nconst MISSING_SCRIPT_WARNING = [\n \"highseat: no tracker script on this page, so no event reaches Highseat.\",\n \"Render <Highseat />, call inject({ site }), or add the script tag from the site's Installation settings.\",\n].join(\" \")\nconst FAILED_SCRIPT_WARNING = [\n \"highseat: the tracker script failed to load, so no event reaches Highseat.\",\n \"An ad blocker may have stopped it: load the tracker from the site's proxy path instead.\",\n].join(\" \")\nconst SLOW_SCRIPT_WARNING = [\n `highseat: the tracker script has not loaded after ${TRACKER_TIMEOUT / 1000} seconds,`,\n \"so every call waits in the queue. Check the network panel for a blocked or failed request.\",\n].join(\" \")\n\n// The script tags `inject` added, and every tracker script whose request failed, such as one an\n// ad blocker stopped. A browser keeps no record of a failed script, so a failure is only known\n// to a listener attached before it happened.\nconst injectedScripts = new WeakSet<HTMLScriptElement>()\nconst failedScripts = new WeakSet<HTMLScriptElement>()\n\n// The `debug` option of the last `inject` call. Unset falls back to the build's environment.\nlet debugOption: boolean | undefined\n// The windows whose tracker is being watched, so a page warns about its script once.\nconst watchedWindows = new WeakSet<Window>()\n\nfunction findGlobal(): HighseatGlobal | undefined {\n if (typeof window === \"undefined\") return\n return (window.highseat ??= {})\n}\n\nfunction isLive(api: HighseatGlobal | undefined): api is HighseatGlobal {\n return !!api && (api as Record<symbol, unknown>)[LIVE] === true\n}\n\n// One function for every script, so attaching it on each poll adds no second listener.\nfunction markFailed(event: Event) {\n failedScripts.add(event.currentTarget as HTMLScriptElement)\n}\n\n/**\n * Whether a script tag loads the tracker, judged as the tracker judges itself. Fathom's snippet\n * carries a `data-site` too, so a tag is matched by a `site_` id, or by a `src` on a proxy path\n * or on Highseat's host: an older site's id is a UUID, which any tool could use.\n */\nfunction isTrackerScript(script: HTMLScriptElement) {\n const { site } = script.dataset\n if (injectedScripts.has(script) || site?.startsWith(\"site_\")) return true\n if (site === undefined) return false\n\n try {\n // A browser hands back `src` resolved; the base only matters to a relative one.\n const url = new URL(script.src, \"http://localhost\")\n return url.host === SCRIPT_HOST || PROXY_PATH.test(url.pathname)\n } catch {\n return false\n }\n}\n\n/** The page's tracker scripts, each one listened to from now on for a failed load. */\nfunction getTrackerScripts() {\n const scripts = Array.from(document.scripts).filter(isTrackerScript)\n for (const script of scripts) script.addEventListener(\"error\", markFailed)\n return scripts\n}\n\nfunction isDebug() {\n return debugOption ?? isDevelopment()\n}\n\n/**\n * Warns once per page when the tracker cannot arrive: no tracker script, or only failed ones. A\n * missing script is judged at the timeout, because `<Highseat />` adds its tag in an effect that\n * runs after the effects of the components inside it.\n */\nfunction watchTracker() {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return\n if (watchedWindows.has(window)) return\n watchedWindows.add(window)\n\n const page = window\n const deadline = Date.now() + TRACKER_TIMEOUT\n\n function check() {\n // A replaced global window, as a test sets up for each case, is another page.\n if (window !== page || isLive(page.highseat)) return\n\n const scripts = getTrackerScripts()\n\n if (scripts.length > 0 && scripts.every(script => failedScripts.has(script))) {\n console.warn(FAILED_SCRIPT_WARNING)\n return\n }\n if (Date.now() < deadline) {\n setTimeout(check, TRACKER_POLL_INTERVAL)\n return\n }\n console.warn(scripts.length === 0 ? MISSING_SCRIPT_WARNING : SLOW_SCRIPT_WARNING)\n }\n\n setTimeout(check, TRACKER_POLL_INTERVAL)\n}\n\n/**\n * Logs a call when debugging is on. The check waits a microtask, so a `debug` option that\n * `<Highseat />` passes in its effect applies to the calls its children make in theirs.\n */\nfunction logCall(method: \"track\" | \"identify\", args: unknown[], isQueued: boolean) {\n queueMicrotask(() => {\n if (!isDebug()) return\n\n const status = isQueued ? \"queued until the tracker loads\" : \"sent to the tracker\"\n const [value, properties] = args\n // `JSON.stringify` quotes a name or a string id, and returns `undefined` for an unset id.\n const message = `highseat: ${method} ${String(JSON.stringify(value))}, ${status}`\n\n if (properties === undefined) console.info(message)\n else console.info(message, properties)\n watchTracker()\n })\n}\n\nfunction call(method: \"track\" | \"identify\", args: unknown[]) {\n const api = findGlobal()\n if (!api) return\n\n const handler = api[method] as ((...values: unknown[]) => void) | undefined\n if (typeof handler !== \"function\") {\n api.q ??= []\n api.q.push([method, args])\n logCall(method, args, true)\n return\n }\n\n logCall(method, args, false)\n try {\n handler(...args)\n } catch {}\n}\n\n/**\n * Records an event. A known name autocompletes and checks its properties; any other name is a\n * custom event, unless {@link Register} sets `strict`. Names are stored exactly as written, so\n * `Purchase` and `purchase` are two events.\n */\nexport function track<Name extends TrackName>(name: Name, ...args: TrackArgs<Name>): void {\n const [properties] = args\n call(\"track\", properties === undefined ? [name] : [name, properties])\n}\n\n/**\n * Links the visitor to a user id from your own system, on this event and every later one, across\n * visits. `null` clears it at sign-out. An empty or `undefined` id is ignored, so\n * `identify(user?.id)` before a session loads does not sign the visitor out.\n */\nexport function identify(id: UserId): void {\n call(\"identify\", [id])\n}\n\n/**\n * The id that joins a payment to this tab's visit. Pass it to the checkout as metadata\n * (`attribution_id`), and the payment provider's webhook credits the sale to the visit's source.\n * Resolves to `undefined`, so a checkout that awaits it never hangs: at once when the page has\n * no tracker script or its script fails to load (an ad blocker stopping it), after the timeout\n * when the script is still loading, and on a server or with tracking off.\n */\nexport async function getAttributionId(\n options: AttributionOptions = {},\n): Promise<string | undefined> {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return\n\n const deadline = Date.now() + (options.timeout ?? TRACKER_TIMEOUT)\n let api = window.highseat\n\n while (!isLive(api)) {\n if (Date.now() >= deadline) return\n\n const scripts = getTrackerScripts()\n // No tracker script at all, or only failed ones: the tracker cannot arrive.\n if (scripts.every(script => failedScripts.has(script))) return\n\n await new Promise(resolve => setTimeout(resolve, TRACKER_POLL_INTERVAL))\n api = window.highseat\n }\n\n try {\n return api.getAttributionId?.()\n } catch {\n return\n }\n}\n\n/**\n * Sets the {@link BeforeSend} hook, or clears it with `undefined`. Works before the tracker has\n * loaded, which picks the hook up when it starts, and applies from the next event once it runs.\n * Does nothing on a server.\n */\nexport function setBeforeSend(beforeSend: BeforeSend | undefined): void {\n const api = findGlobal()\n if (api) api.beforeSend = beforeSend\n}\n\n/**\n * Adds the tracker's script tag to the page, once. A second call for the same site, such as a\n * component mounting again, finds the tag and adds nothing, but its `debug` option still applies.\n * Does nothing on a server.\n */\nexport function inject(options: InjectOptions): void {\n if (typeof document === \"undefined\") return\n\n const { site, src = SCRIPT_URL, outbound, downloads, hashRouting, beforeSend, debug } = options\n debugOption = debug\n // Set before the script tag is added, so the first page view already passes through it.\n if (beforeSend) setBeforeSend(beforeSend)\n if (isDebug()) watchTracker()\n if (Array.from(document.scripts).some(script => script.dataset.site === site)) return\n\n const script = document.createElement(\"script\")\n script.addEventListener(\"error\", markFailed)\n injectedScripts.add(script)\n script.src = src\n script.dataset.site = site\n if (outbound === false) script.dataset.outbound = \"false\"\n if (downloads === false) script.dataset.downloads = \"false\"\n if (hashRouting) script.dataset.hashRouting = \"\"\n document.head.append(script)\n}\n"],"mappings":"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"}
|
package/mcp.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ServerBuiltInEvents, ServerEventOptions, WaitUntil } from "./server.js";
|
|
2
|
+
import { McpServer, ServerContext } from "@modelcontextprotocol/server";
|
|
3
|
+
//#region src/mcp.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* MCP tool calls, recorded from your own MCP server built on the MCP TypeScript SDK v2: one
|
|
6
|
+
* `mcp_tool_call` event per call, with the tool, the client, the outcome and the time it took.
|
|
7
|
+
* The tool's arguments, the caller's IP and its user agent are never sent: assistant directories
|
|
8
|
+
* forbid collecting conversation data, and the caller is the assistant's server, not a person.
|
|
9
|
+
*/
|
|
10
|
+
type ToolCall = ServerBuiltInEvents["mcp_tool_call"];
|
|
11
|
+
/** What tool calls are sent with: a tracker from `createTracker()`, or any `track()` like its. */
|
|
12
|
+
export interface ToolCallTracker {
|
|
13
|
+
track: (name: "mcp_tool_call", properties: ToolCall, options?: ServerEventOptions) => void;
|
|
14
|
+
}
|
|
15
|
+
export interface TrackToolCallsOptions {
|
|
16
|
+
/** The tracker events are sent with. Defaults to the one `track()` from `highseat/server` uses. */
|
|
17
|
+
tracker?: ToolCallTracker;
|
|
18
|
+
/** A label for this MCP server, such as `public` or `admin`, when the site runs more than one. */
|
|
19
|
+
server?: string;
|
|
20
|
+
/** The site hostname every call is filed under. Defaults to the tracker's `hostname`. */
|
|
21
|
+
hostname?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Keeps a serverless function alive until each event is sent: `after` from `next/server`, or
|
|
24
|
+
* `ctx.waitUntil` in a Cloudflare Worker, bound to its object.
|
|
25
|
+
*/
|
|
26
|
+
waitUntil?: WaitUntil;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Records every tool call on an MCP server built with the MCP TypeScript SDK v2. Call it once,
|
|
30
|
+
* right after creating the server and before registering any tool: it wraps `registerTool`, so a
|
|
31
|
+
* tool registered earlier would go unrecorded, and it throws when the server already has tools.
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* const server = new McpServer({ name: "Acme", version: "1.0.0" })
|
|
35
|
+
* trackToolCalls(server, { server: "public" })
|
|
36
|
+
* server.registerTool("search", config, handler)
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* Each call sends `mcp_tool_call` with the tool, the client, the status, the time it took and,
|
|
40
|
+
* for a read-only tool whose result is a list, how many items it returned. The tool's arguments,
|
|
41
|
+
* the caller's IP and its user agent are never sent. A call still waiting for input from the
|
|
42
|
+
* client is recorded once it ends. Failures to send never reach the tool: they go to the
|
|
43
|
+
* tracker's `onError`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function trackToolCalls(server: McpServer, options?: TrackToolCallsOptions): void;
|
|
46
|
+
/**
|
|
47
|
+
* Tags a link a tool returns, so the visit it brings reads as the assistant that showed it:
|
|
48
|
+
* `utm_source` names the client and `utm_medium` is `mcp`, which Highseat reads as the AI
|
|
49
|
+
* channel. A link that already has a `utm_source` is returned as it is. Pass the tool handler's
|
|
50
|
+
* context.
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* server.registerTool("get_product", config, async ({ slug }, context) => ({
|
|
54
|
+
* content: [{ type: "text", text: mcpLink(`https://acme.com/products/${slug}`, context) }],
|
|
55
|
+
* }))
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare function mcpLink(url: string, context: ServerContext): string;
|
|
59
|
+
//#endregion
|
|
60
|
+
export {};
|
|
61
|
+
//# sourceMappingURL=mcp.d.ts.map
|
package/mcp.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { track } from "./server.js";
|
|
2
|
+
//#region src/mcp.ts
|
|
3
|
+
const CLIENT_INFO_KEY = "io.modelcontextprotocol/clientInfo";
|
|
4
|
+
const ERROR_CODE = /^[A-Z][A-Z0-9_]{1,63}$/;
|
|
5
|
+
const ERROR_CODE_PREFIX = /^([A-Z][A-Z0-9_]{1,63}):/;
|
|
6
|
+
const MAX_PARSED_LENGTH = 262144;
|
|
7
|
+
const wrapped = /* @__PURE__ */ new WeakSet();
|
|
8
|
+
const callClients = /* @__PURE__ */ new WeakMap();
|
|
9
|
+
function isRecord(value) {
|
|
10
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
function toClient(info) {
|
|
13
|
+
if (!info?.name) return;
|
|
14
|
+
return {
|
|
15
|
+
client: info.name,
|
|
16
|
+
...info.version ? { client_version: info.version } : {},
|
|
17
|
+
client_source: "client_info"
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The client behind a call. A client of the 2026-07-28 protocol names itself on every request. A
|
|
22
|
+
* 2025-era client names itself only in its `initialize` handshake, which a stateful server
|
|
23
|
+
* remembers and a stateless one never sees again, so its User-Agent's product name stands in.
|
|
24
|
+
*/
|
|
25
|
+
function readClient(context, server) {
|
|
26
|
+
const envelope = context.mcpReq.envelope;
|
|
27
|
+
const declared = isRecord(envelope) ? envelope[CLIENT_INFO_KEY] : void 0;
|
|
28
|
+
const client = toClient(isRecord(declared) ? declared : void 0) ?? toClient(server?.server.getClientVersion());
|
|
29
|
+
if (client) return client;
|
|
30
|
+
const product = context.http?.req?.headers.get("user-agent")?.split(/[\s/]/)[0];
|
|
31
|
+
return product ? {
|
|
32
|
+
client: product,
|
|
33
|
+
client_source: "user_agent"
|
|
34
|
+
} : {
|
|
35
|
+
client: "unknown",
|
|
36
|
+
client_source: "none"
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* How many items a result holds: the length of a list, or of the one list a result object holds,
|
|
41
|
+
* such as `data` in a page of rows. A result with no list, or with several, has no count.
|
|
42
|
+
*/
|
|
43
|
+
function countItems(data) {
|
|
44
|
+
if (Array.isArray(data)) return data.length;
|
|
45
|
+
if (!isRecord(data)) return;
|
|
46
|
+
const lists = Object.values(data).filter(Array.isArray);
|
|
47
|
+
return lists.length === 1 ? lists[0]?.length : void 0;
|
|
48
|
+
}
|
|
49
|
+
/** The structured data a result carries, or the JSON of its one text block. */
|
|
50
|
+
function readData(result) {
|
|
51
|
+
if (result.structuredContent !== void 0) return result.structuredContent;
|
|
52
|
+
const [block, ...rest] = result.content;
|
|
53
|
+
if (rest.length > 0 || block?.type !== "text" || block.text.length > MAX_PARSED_LENGTH) return;
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(block.text);
|
|
56
|
+
} catch {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** The error code a failed result starts its text with, such as `NOT_FOUND: …`. */
|
|
61
|
+
function readErrorCode(result) {
|
|
62
|
+
const block = result.content.find((content) => content.type === "text");
|
|
63
|
+
return block?.type === "text" ? block.text.match(ERROR_CODE_PREFIX)?.[1] : void 0;
|
|
64
|
+
}
|
|
65
|
+
function readOutcome(result, isReadOnly) {
|
|
66
|
+
if (result.isError) return { status: readErrorCode(result) ?? "TOOL_ERROR" };
|
|
67
|
+
const results = isReadOnly ? countItems(readData(result)) : void 0;
|
|
68
|
+
return {
|
|
69
|
+
status: "ok",
|
|
70
|
+
...results === void 0 ? {} : { results }
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** The code a thrown error carries, such as an `ORPCError`'s, or a generic one. */
|
|
74
|
+
function readThrownCode(error) {
|
|
75
|
+
const code = isRecord(error) ? error.code : void 0;
|
|
76
|
+
return typeof code === "string" && ERROR_CODE.test(code) ? code : "INTERNAL_SERVER_ERROR";
|
|
77
|
+
}
|
|
78
|
+
function isCallToolResult(value) {
|
|
79
|
+
return isRecord(value) && Array.isArray(value.content);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Records every tool call on an MCP server built with the MCP TypeScript SDK v2. Call it once,
|
|
83
|
+
* right after creating the server and before registering any tool: it wraps `registerTool`, so a
|
|
84
|
+
* tool registered earlier would go unrecorded, and it throws when the server already has tools.
|
|
85
|
+
*
|
|
86
|
+
* ```ts
|
|
87
|
+
* const server = new McpServer({ name: "Acme", version: "1.0.0" })
|
|
88
|
+
* trackToolCalls(server, { server: "public" })
|
|
89
|
+
* server.registerTool("search", config, handler)
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* Each call sends `mcp_tool_call` with the tool, the client, the status, the time it took and,
|
|
93
|
+
* for a read-only tool whose result is a list, how many items it returned. The tool's arguments,
|
|
94
|
+
* the caller's IP and its user agent are never sent. A call still waiting for input from the
|
|
95
|
+
* client is recorded once it ends. Failures to send never reach the tool: they go to the
|
|
96
|
+
* tracker's `onError`.
|
|
97
|
+
*/
|
|
98
|
+
function trackToolCalls(server, options = {}) {
|
|
99
|
+
if (wrapped.has(server)) return;
|
|
100
|
+
const registered = server._registeredTools;
|
|
101
|
+
if (registered && Object.keys(registered).length > 0) throw new Error("highseat/mcp: call trackToolCalls() before registering any tool, or those tools go unrecorded.");
|
|
102
|
+
wrapped.add(server);
|
|
103
|
+
const { tracker = { track }, server: label, hostname, waitUntil } = options;
|
|
104
|
+
const register = server.registerTool.bind(server);
|
|
105
|
+
function send(tool, client, outcome, startedAt) {
|
|
106
|
+
tracker.track("mcp_tool_call", {
|
|
107
|
+
tool,
|
|
108
|
+
...label ? { server: label } : {},
|
|
109
|
+
...client,
|
|
110
|
+
...outcome,
|
|
111
|
+
duration_ms: Math.round(performance.now() - startedAt)
|
|
112
|
+
}, {
|
|
113
|
+
...hostname ? { hostname } : {},
|
|
114
|
+
...waitUntil ? { waitUntil } : {}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function registerTracked(name, config, handler) {
|
|
118
|
+
const isReadOnly = config.annotations?.readOnlyHint === true;
|
|
119
|
+
return register(name, config, async (...args) => {
|
|
120
|
+
const context = args.at(-1);
|
|
121
|
+
const client = readClient(context, server);
|
|
122
|
+
callClients.set(context, client);
|
|
123
|
+
const startedAt = performance.now();
|
|
124
|
+
let result;
|
|
125
|
+
try {
|
|
126
|
+
result = await handler(...args);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
send(name, client, { status: readThrownCode(error) }, startedAt);
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
if (isCallToolResult(result)) send(name, client, readOutcome(result, isReadOnly), startedAt);
|
|
132
|
+
return result;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
server.registerTool = registerTracked;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Tags a link a tool returns, so the visit it brings reads as the assistant that showed it:
|
|
139
|
+
* `utm_source` names the client and `utm_medium` is `mcp`, which Highseat reads as the AI
|
|
140
|
+
* channel. A link that already has a `utm_source` is returned as it is. Pass the tool handler's
|
|
141
|
+
* context.
|
|
142
|
+
*
|
|
143
|
+
* ```ts
|
|
144
|
+
* server.registerTool("get_product", config, async ({ slug }, context) => ({
|
|
145
|
+
* content: [{ type: "text", text: mcpLink(`https://acme.com/products/${slug}`, context) }],
|
|
146
|
+
* }))
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
function mcpLink(url, context) {
|
|
150
|
+
const link = new URL(url);
|
|
151
|
+
if (link.searchParams.has("utm_source")) return url;
|
|
152
|
+
const { client, client_source } = callClients.get(context) ?? readClient(context);
|
|
153
|
+
link.searchParams.set("utm_source", client_source === "none" ? "mcp" : client);
|
|
154
|
+
link.searchParams.set("utm_medium", "mcp");
|
|
155
|
+
return link.href;
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
export { mcpLink, trackToolCalls };
|
|
159
|
+
|
|
160
|
+
//# sourceMappingURL=mcp.js.map
|
package/mcp.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp.js","names":[],"sources":["../src/mcp.ts"],"sourcesContent":["import type {\n CallToolResult,\n Implementation,\n McpServer,\n ServerContext,\n ToolAnnotations,\n} from \"@modelcontextprotocol/server\"\nimport { type ServerBuiltInEvents, type ServerEventOptions, track, type WaitUntil } from \"./server\"\n\n/**\n * MCP tool calls, recorded from your own MCP server built on the MCP TypeScript SDK v2: one\n * `mcp_tool_call` event per call, with the tool, the client, the outcome and the time it took.\n * The tool's arguments, the caller's IP and its user agent are never sent: assistant directories\n * forbid collecting conversation data, and the caller is the assistant's server, not a person.\n */\n\ntype ToolCall = ServerBuiltInEvents[\"mcp_tool_call\"]\n\n/** What the event records about the client behind a call. */\ntype ToolClient = Pick<ToolCall, \"client\" | \"client_version\" | \"client_source\">\n\n/** What tool calls are sent with: a tracker from `createTracker()`, or any `track()` like its. */\nexport interface ToolCallTracker {\n track: (name: \"mcp_tool_call\", properties: ToolCall, options?: ServerEventOptions) => void\n}\n\nexport interface TrackToolCallsOptions {\n /** The tracker events are sent with. Defaults to the one `track()` from `highseat/server` uses. */\n tracker?: ToolCallTracker\n /** A label for this MCP server, such as `public` or `admin`, when the site runs more than one. */\n server?: string\n /** The site hostname every call is filed under. Defaults to the tracker's `hostname`. */\n hostname?: string\n /**\n * Keeps a serverless function alive until each event is sent: `after` from `next/server`, or\n * `ctx.waitUntil` in a Cloudflare Worker, bound to its object.\n */\n waitUntil?: WaitUntil\n}\n\n/** The parts of a tool's registration the wrapper reads. */\ninterface ToolConfig {\n annotations?: ToolAnnotations\n}\n\ntype ToolHandler = (...args: unknown[]) => unknown\n\ntype RegisterTool = (name: string, config: ToolConfig, handler: ToolHandler) => unknown\n\n/** How a call ended, as the event records it. */\ninterface ToolOutcome {\n status: string\n results?: number\n}\n\n// The `_meta` key a client of the 2026-07-28 protocol names itself under on every request. The\n// SDK exports it as `CLIENT_INFO_META_KEY`; it is written out so this module loads no SDK code.\nconst CLIENT_INFO_KEY = \"io.modelcontextprotocol/clientInfo\"\n\n// An error code as the agent sees it, such as `NOT_FOUND` or `RATE_LIMITED`.\nconst ERROR_CODE = /^[A-Z][A-Z0-9_]{1,63}$/\nconst ERROR_CODE_PREFIX = /^([A-Z][A-Z0-9_]{1,63}):/\n\n// A text result is parsed for its count only below this length, so a large page is never parsed\n// a second time.\nconst MAX_PARSED_LENGTH = 256 * 1024\n\nconst wrapped = new WeakSet<McpServer>()\n\n// The client each call was recorded under, so `mcpLink()` tags a link with the same name.\nconst callClients = new WeakMap<ServerContext, ToolClient>()\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === \"object\" && !Array.isArray(value)\n}\n\nfunction toClient(info: Implementation | undefined): ToolClient | undefined {\n if (!info?.name) return\n return {\n client: info.name,\n ...(info.version ? { client_version: info.version } : {}),\n client_source: \"client_info\",\n }\n}\n\n/**\n * The client behind a call. A client of the 2026-07-28 protocol names itself on every request. A\n * 2025-era client names itself only in its `initialize` handshake, which a stateful server\n * remembers and a stateless one never sees again, so its User-Agent's product name stands in.\n */\nfunction readClient(context: ServerContext, server?: McpServer): ToolClient {\n const envelope: unknown = context.mcpReq.envelope\n const declared = isRecord(envelope) ? envelope[CLIENT_INFO_KEY] : undefined\n\n const client =\n toClient(isRecord(declared) ? (declared as Implementation) : undefined) ??\n toClient(server?.server.getClientVersion())\n if (client) return client\n\n const product = context.http?.req?.headers.get(\"user-agent\")?.split(/[\\s/]/)[0]\n return product\n ? { client: product, client_source: \"user_agent\" }\n : { client: \"unknown\", client_source: \"none\" }\n}\n\n/**\n * How many items a result holds: the length of a list, or of the one list a result object holds,\n * such as `data` in a page of rows. A result with no list, or with several, has no count.\n */\nfunction countItems(data: unknown): number | undefined {\n if (Array.isArray(data)) return data.length\n if (!isRecord(data)) return\n\n const lists = Object.values(data).filter(Array.isArray)\n return lists.length === 1 ? lists[0]?.length : undefined\n}\n\n/** The structured data a result carries, or the JSON of its one text block. */\nfunction readData(result: CallToolResult): unknown {\n if (result.structuredContent !== undefined) return result.structuredContent\n\n const [block, ...rest] = result.content\n if (rest.length > 0 || block?.type !== \"text\" || block.text.length > MAX_PARSED_LENGTH) return\n\n try {\n return JSON.parse(block.text) as unknown\n } catch {\n return\n }\n}\n\n/** The error code a failed result starts its text with, such as `NOT_FOUND: …`. */\nfunction readErrorCode(result: CallToolResult) {\n const block = result.content.find(content => content.type === \"text\")\n return block?.type === \"text\" ? block.text.match(ERROR_CODE_PREFIX)?.[1] : undefined\n}\n\nfunction readOutcome(result: CallToolResult, isReadOnly: boolean): ToolOutcome {\n if (result.isError) return { status: readErrorCode(result) ?? \"TOOL_ERROR\" }\n\n const results = isReadOnly ? countItems(readData(result)) : undefined\n return { status: \"ok\", ...(results === undefined ? {} : { results }) }\n}\n\n/** The code a thrown error carries, such as an `ORPCError`'s, or a generic one. */\nfunction readThrownCode(error: unknown) {\n const code = isRecord(error) ? error.code : undefined\n return typeof code === \"string\" && ERROR_CODE.test(code) ? code : \"INTERNAL_SERVER_ERROR\"\n}\n\nfunction isCallToolResult(value: unknown): value is CallToolResult {\n return isRecord(value) && Array.isArray(value.content)\n}\n\n/**\n * Records every tool call on an MCP server built with the MCP TypeScript SDK v2. Call it once,\n * right after creating the server and before registering any tool: it wraps `registerTool`, so a\n * tool registered earlier would go unrecorded, and it throws when the server already has tools.\n *\n * ```ts\n * const server = new McpServer({ name: \"Acme\", version: \"1.0.0\" })\n * trackToolCalls(server, { server: \"public\" })\n * server.registerTool(\"search\", config, handler)\n * ```\n *\n * Each call sends `mcp_tool_call` with the tool, the client, the status, the time it took and,\n * for a read-only tool whose result is a list, how many items it returned. The tool's arguments,\n * the caller's IP and its user agent are never sent. A call still waiting for input from the\n * client is recorded once it ends. Failures to send never reach the tool: they go to the\n * tracker's `onError`.\n */\nexport function trackToolCalls(server: McpServer, options: TrackToolCallsOptions = {}): void {\n if (wrapped.has(server)) return\n\n // The SDK keeps its tools in a private record. Reading it is the only way to tell that some\n // were registered before this call, and a version without it skips the check.\n const registered = (server as unknown as { _registeredTools?: Record<string, unknown> })\n ._registeredTools\n if (registered && Object.keys(registered).length > 0) {\n throw new Error(\n \"highseat/mcp: call trackToolCalls() before registering any tool, or those tools go unrecorded.\",\n )\n }\n\n wrapped.add(server)\n\n const { tracker = { track }, server: label, hostname, waitUntil } = options\n const register = server.registerTool.bind(server) as RegisterTool\n\n function send(tool: string, client: ToolClient, outcome: ToolOutcome, startedAt: number) {\n tracker.track(\n \"mcp_tool_call\",\n {\n tool,\n ...(label ? { server: label } : {}),\n ...client,\n ...outcome,\n duration_ms: Math.round(performance.now() - startedAt),\n },\n { ...(hostname ? { hostname } : {}), ...(waitUntil ? { waitUntil } : {}) },\n )\n }\n\n function registerTracked(name: string, config: ToolConfig, handler: ToolHandler) {\n const isReadOnly = config.annotations?.readOnlyHint === true\n\n return register(name, config, async (...args: unknown[]) => {\n // The context is the handler's last argument: a tool with an input schema gets its\n // arguments first.\n const context = args.at(-1) as ServerContext\n const client = readClient(context, server)\n callClients.set(context, client)\n const startedAt = performance.now()\n\n let result: unknown\n try {\n result = await handler(...args)\n } catch (error) {\n send(name, client, { status: readThrownCode(error) }, startedAt)\n throw error\n }\n\n if (isCallToolResult(result)) send(name, client, readOutcome(result, isReadOnly), startedAt)\n return result\n })\n }\n\n server.registerTool = registerTracked as McpServer[\"registerTool\"]\n}\n\n/**\n * Tags a link a tool returns, so the visit it brings reads as the assistant that showed it:\n * `utm_source` names the client and `utm_medium` is `mcp`, which Highseat reads as the AI\n * channel. A link that already has a `utm_source` is returned as it is. Pass the tool handler's\n * context.\n *\n * ```ts\n * server.registerTool(\"get_product\", config, async ({ slug }, context) => ({\n * content: [{ type: \"text\", text: mcpLink(`https://acme.com/products/${slug}`, context) }],\n * }))\n * ```\n */\nexport function mcpLink(url: string, context: ServerContext): string {\n const link = new URL(url)\n if (link.searchParams.has(\"utm_source\")) return url\n\n const { client, client_source } = callClients.get(context) ?? readClient(context)\n link.searchParams.set(\"utm_source\", client_source === \"none\" ? \"mcp\" : client)\n link.searchParams.set(\"utm_medium\", \"mcp\")\n return link.href\n}\n"],"mappings":"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"}
|
package/next.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { HighseatProps as HighseatProps$1, useBeforeSend, useIdentify } from "./react.js";
|
|
2
1
|
import { ClientIp, ClientIpPreset } from "./ingest.js";
|
|
2
|
+
import { HighseatProps as HighseatProps$1, useBeforeSend, useIdentify } from "./react.js";
|
|
3
3
|
import { ProxyHandler, ProxyOptions, createProxy } from "./proxy.js";
|
|
4
4
|
import { ReactElement } from "react";
|
|
5
5
|
//#region src/next.d.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "highseat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Highseat analytics for JavaScript: typed tracking in the browser and on the server, a first-party proxy and framework components.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"analytics",
|
|
@@ -42,16 +42,24 @@
|
|
|
42
42
|
"./server": {
|
|
43
43
|
"types": "./server.d.ts",
|
|
44
44
|
"default": "./server.js"
|
|
45
|
+
},
|
|
46
|
+
"./mcp": {
|
|
47
|
+
"types": "./mcp.d.ts",
|
|
48
|
+
"default": "./mcp.js"
|
|
45
49
|
}
|
|
46
50
|
},
|
|
47
51
|
"publishConfig": {
|
|
48
52
|
"access": "public"
|
|
49
53
|
},
|
|
50
54
|
"peerDependencies": {
|
|
55
|
+
"@modelcontextprotocol/server": ">=2",
|
|
51
56
|
"next": ">=14",
|
|
52
57
|
"react": ">=18"
|
|
53
58
|
},
|
|
54
59
|
"peerDependenciesMeta": {
|
|
60
|
+
"@modelcontextprotocol/server": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
55
63
|
"next": {
|
|
56
64
|
"optional": true
|
|
57
65
|
},
|
package/server.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BuiltInEvents, TrackArgsOf, TrackNameOf } from "./index.js";
|
|
2
2
|
import { ClientIp, ClientIpPreset } from "./ingest.js";
|
|
3
3
|
//#region src/server.d.ts
|
|
4
4
|
/**
|
|
@@ -118,14 +118,54 @@ export interface TrackerOptions {
|
|
|
118
118
|
/** Milliseconds one request may take. Defaults to 10000. */
|
|
119
119
|
timeout?: number;
|
|
120
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* The events Highseat knows by name that only a server sends, on top of the browser's built-in
|
|
123
|
+
* events. Highseat refuses them from a browser. A site overrides an entry through `Register`.
|
|
124
|
+
*/
|
|
125
|
+
export interface ServerBuiltInEvents {
|
|
126
|
+
/**
|
|
127
|
+
* One call to a tool on the site's MCP server. `highseat/mcp` sends it for every call. Never
|
|
128
|
+
* put the tool's arguments in it: assistant directories forbid recording conversation data.
|
|
129
|
+
*/
|
|
130
|
+
mcp_tool_call: {
|
|
131
|
+
/** The tool's name. */
|
|
132
|
+
tool: string;
|
|
133
|
+
/** A label for the MCP server, such as `public` or `admin`, when the site runs more than one. */
|
|
134
|
+
server?: string;
|
|
135
|
+
/**
|
|
136
|
+
* The client's name as it declares it in `clientInfo`, else the product name its User-Agent
|
|
137
|
+
* starts with (`Claude-User`), else `unknown`.
|
|
138
|
+
*/
|
|
139
|
+
client: string;
|
|
140
|
+
/** The version the client declares in `clientInfo`, when it declares one. */
|
|
141
|
+
client_version?: string;
|
|
142
|
+
/**
|
|
143
|
+
* How `client` was found: declared in `clientInfo`, read off the User-Agent, or neither. A
|
|
144
|
+
* declared name is the one to trust.
|
|
145
|
+
*/
|
|
146
|
+
client_source: "client_info" | "user_agent" | "none";
|
|
147
|
+
/** `ok`, or the error code the client saw, such as `NOT_FOUND`. */
|
|
148
|
+
status: "ok" | (string & {});
|
|
149
|
+
/**
|
|
150
|
+
* How many items the call returned, for a read-only tool whose result is a list. Left out
|
|
151
|
+
* for any other tool, so an empty list counts as an empty result and nothing else does.
|
|
152
|
+
*/
|
|
153
|
+
results?: number;
|
|
154
|
+
/** How long the call took, in whole milliseconds. */
|
|
155
|
+
duration_ms: number;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
type ServerEvents = BuiltInEvents & ServerBuiltInEvents;
|
|
159
|
+
/** The names the server's `track()` accepts: the browser's, plus {@link ServerBuiltInEvents}. */
|
|
160
|
+
export type ServerTrackName = TrackNameOf<ServerEvents>;
|
|
121
161
|
/**
|
|
122
162
|
* The arguments after an event's name: its properties, typed by `Register` as for the browser's
|
|
123
163
|
* `track()`, then the options that say who it belongs to.
|
|
124
164
|
*/
|
|
125
|
-
export type ServerTrackArgs<Name extends string> = [...
|
|
165
|
+
export type ServerTrackArgs<Name extends string> = [...TrackArgsOf<ServerEvents, Name>, options?: ServerEventOptions];
|
|
126
166
|
export interface Tracker {
|
|
127
167
|
/** Records an event, queued and sent in the background. Returns at once and never throws. */
|
|
128
|
-
track: <Name extends
|
|
168
|
+
track: <Name extends ServerTrackName>(name: Name, ...args: ServerTrackArgs<Name>) => void;
|
|
129
169
|
/** Sends every queued event, and resolves once each has landed or failed. Never rejects. */
|
|
130
170
|
flush: () => Promise<void>;
|
|
131
171
|
/**
|
|
@@ -161,7 +201,7 @@ export declare function createTracker(options?: TrackerOptions): Tracker;
|
|
|
161
201
|
* track("signup", { plan: "pro" }, { request, userId: user.id })
|
|
162
202
|
* ```
|
|
163
203
|
*/
|
|
164
|
-
export declare function track<Name extends
|
|
204
|
+
export declare function track<Name extends ServerTrackName>(name: Name, ...args: ServerTrackArgs<Name>): void;
|
|
165
205
|
/** Sends every event `track()` has queued. Await it before a short-lived script exits. */
|
|
166
206
|
export declare function flush(): Promise<void>;
|
|
167
207
|
/** Sends what `track()` has queued and stops sending. Call it at shutdown. See `CloseOptions`. */
|
package/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["import { findHostPlatform, findServerSite, isNextBuild } from \"./env\"\nimport type { PropertyValue, Revenue, TrackArgs, TrackName } from \"./index\"\nimport { type ClientIp, DEFAULT_UPSTREAM, findValidSecret, getClientIpReader } from \"./ingest\"\n\nexport type { ClientIp, ClientIpPreset } from \"./ingest\"\n\n/**\n * Server-side events, sent from your own backend with the site secret: a server action, a route\n * handler, a webhook handler or a job. Events are batched and sent in the background, retried\n * when Highseat is busy or unreachable, and nothing here throws: a failure goes to `onError`.\n */\n\n/**\n * Request headers in any shape a server hands them over: a `Headers` object, what Next.js's\n * `headers()` returns, or Node's `request.headers` record.\n */\nexport type HeadersLike =\n | { get: (name: string) => string | null }\n | Record<string, string | string[] | undefined>\n\n/**\n * Keeps a serverless function alive until a promise settles: `waitUntil` from\n * `@vercel/functions`, or `ctx.waitUntil` in a Cloudflare Worker and `context.waitUntil` in a\n * Netlify function, bound to their object.\n */\nexport type WaitUntil = (promise: Promise<unknown>) => void\n\n/** Who an event belongs to and where it happened, given with each `track()` call. */\nexport interface ServerEventOptions {\n /**\n * The request being handled. The visitor's IP (see `clientIp`), their user agent and the\n * hostname are read from it, and a `pageview` takes its URL and referrer too.\n */\n request?: Request\n /** The request's headers, when there is no `Request`: `await headers()` in a server action. */\n headers?: HeadersLike\n /** The visitor's IP. Wins over the one read from `request` or `headers`. */\n ip?: string\n /** The visitor's user agent. Wins over the one read from `request` or `headers`. */\n userAgent?: string\n /**\n * A user id from your own system. An event with no visitor (no IP and no user agent, as from\n * a job) counts as this user, and without one as a visitor of its own.\n */\n userId?: string | number\n /** A hostname of the site. Defaults to the tracker's `hostname`, then the request's host. */\n hostname?: string\n /** The page's URL. A `pageview` defaults to the request's URL. */\n url?: string\n /** The page the visitor came from. A `pageview` defaults to the request's `Referer`. */\n referrer?: string\n /**\n * The event's id, any string up to 255 characters. Defaults to a new UUID. Pass an id your\n * system already holds (a job id, a webhook event id), and a retry of the same event within\n * three days is stored once.\n */\n eventId?: string\n /** When the event happened, up to a day ago. Defaults to the moment `track()` is called. */\n timestamp?: Date\n /**\n * Sends this event at once and keeps the function alive until it lands. Wins over the\n * tracker's `waitUntil`.\n */\n waitUntil?: WaitUntil\n}\n\n/** An event Highseat did not store, as `onError` reports it. */\nexport interface TrackErrorEvent {\n /** The event's name. */\n name: string\n /** The event's id: the one passed to `track()`, or the UUID it was given. */\n eventId: string\n /** Why Highseat refused or dropped it, such as `revenue.amount_not_integer` or `bot`. */\n reason?: string\n}\n\n/** A failure `track()` never throws, handed to `onError`. */\nexport interface TrackError {\n /**\n * `rejected`: refused as invalid, for good. `dropped`: accepted but not stored, for a `reason`\n * of `foreign_hostname`, `bot` or `excluded` (a recognised retry is never reported). `failed`:\n * never landed, after every retry or at `close()`'s deadline, or refused whole (a wrong secret).\n */\n type: \"rejected\" | \"dropped\" | \"failed\"\n /** What happened, in a sentence. */\n message: string\n /** The HTTP status Highseat answered, when it answered. */\n status?: number\n /** The events concerned. */\n events: TrackErrorEvent[]\n}\n\nexport interface TrackerOptions {\n /** The site id, `site_…`. Defaults to `HIGHSEAT_SITE`, then `NEXT_PUBLIC_HIGHSEAT_SITE`. */\n site?: string\n /**\n * The site secret, `hs_secret_…`, from the site's Installation settings. Defaults to\n * `HIGHSEAT_SECRET`. Keep it on the server: it lets a caller name any visitor.\n */\n secret?: string\n /** The hostname every event is filed under unless it names its own. */\n hostname?: string\n /**\n * Where the visitor's IP is read from a request: `vercel`, `cloudflare`, `netlify`, `nginx`\n * or a function, as for `createProxy()`. Defaults to `cloudflare` in a Worker, `vercel` on\n * Vercel and `netlify` on Netlify. Anywhere else, set it, or pass `ip` with each event.\n */\n clientIp?: ClientIp\n /** The ingest origin. Defaults to `https://cdn.highseat.com`. */\n upstream?: string\n /** The `fetch` events are sent with. Defaults to the global one. */\n fetch?: typeof fetch\n /** Receives every failure. Defaults to a `console.warn`. */\n onError?: (error: TrackError) => void\n /**\n * Sends every event at once and keeps the function alive until it lands. Pass it in a\n * serverless function, which may stop as soon as it answers.\n */\n waitUntil?: WaitUntil\n /** Milliseconds an event waits for others to share its request. Defaults to 1000. */\n flushInterval?: number\n /**\n * Retries of a batch Highseat answered 429 or 5xx, or never answered. Defaults to 4. Batches\n * go one at a time, so a retry's wait holds back every event behind it.\n */\n maxRetries?: number\n /** Milliseconds one request may take. Defaults to 10000. */\n timeout?: number\n}\n\n/**\n * The arguments after an event's name: its properties, typed by `Register` as for the browser's\n * `track()`, then the options that say who it belongs to.\n */\nexport type ServerTrackArgs<Name extends string> = [\n ...TrackArgs<Name>,\n options?: ServerEventOptions,\n]\n\nexport interface Tracker {\n /** Records an event, queued and sent in the background. Returns at once and never throws. */\n track: <Name extends TrackName>(name: Name, ...args: ServerTrackArgs<Name>) => void\n /** Sends every queued event, and resolves once each has landed or failed. Never rejects. */\n flush: () => Promise<void>\n /**\n * Sends what is queued and stops: a later `track()` sends nothing. Call it at shutdown, with a\n * `timeout` shorter than the shutdown's grace period.\n */\n close: (options?: CloseOptions) => Promise<void>\n}\n\nexport interface CloseOptions {\n /**\n * Milliseconds to wait for the queue to drain. Past it, sending stops, the events still\n * unsent go to `onError` as `failed`, and `close()` resolves. Without one, `close()` waits for\n * every retry, which during an outage can take minutes.\n */\n timeout?: number\n}\n\n/** One event as ingest takes it, before its age is known. */\ninterface WireEvent {\n event_name: string\n event_id: string\n hostname?: string\n user_id?: string\n ip?: string\n user_agent?: string\n page_url?: string\n referrer?: string\n properties?: Record<string, string>\n revenue?: Revenue\n}\n\ninterface QueuedEvent {\n event: WireEvent\n /** When it happened, which its `age` is measured from on every attempt. */\n at: number\n /** Resolves the promise handed to `waitUntil`, once the event has landed or failed. */\n settle?: () => void\n}\n\ninterface BatchEntry {\n index: number\n reason: string\n}\n\ninterface IngestAnswer {\n rejections?: BatchEntry[]\n dropped?: BatchEntry[]\n}\n\nconst ENTRY = \"highseat/server\"\n\n// Ingest's limit per request.\nconst MAX_BATCH_SIZE = 25\n\n// Events held while Highseat is unreachable, the batch being sent included. A newer one past\n// this is reported and not kept, so an outage cannot grow the process without bound.\nconst MAX_QUEUE_SIZE = 1000\n\n// Ingest cuts a property value to this, and the tracker does too.\nconst MAX_PROPERTY_LENGTH = 500\n\nconst DEFAULT_FLUSH_INTERVAL_MS = 1000\nconst DEFAULT_MAX_RETRIES = 4\nconst DEFAULT_TIMEOUT_MS = 10_000\n\n// The first retry's wait, doubled on each one after it, and the longest any wait lasts,\n// `Retry-After` included.\nconst RETRY_BASE_MS = 500\nconst MAX_RETRY_DELAY_MS = 30_000\n\nfunction isRetryable(status: number) {\n return status === 429 || status >= 500\n}\n\n/** The wait `Retry-After` asks for, in seconds or as a date, or `undefined` when there is none. */\nfunction parseRetryAfter(value: string | null): number | undefined {\n if (value === null) return\n const seconds = Number(value)\n if (value.trim() !== \"\" && Number.isFinite(seconds)) return Math.max(0, seconds * 1000)\n const date = Date.parse(value)\n return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now())\n}\n\nfunction getRetryDelay(attempt: number, retryAfter?: number) {\n const backoff = RETRY_BASE_MS * 2 ** attempt * (1 + Math.random() / 2)\n return Math.min(retryAfter ?? backoff, MAX_RETRY_DELAY_MS)\n}\n\n// The headers read off an object that offers only `get`: the ones the presets and the event's\n// own fields read.\nconst READ_HEADERS = [\n \"user-agent\",\n \"host\",\n \"x-forwarded-host\",\n \"referer\",\n \"x-real-ip\",\n \"cf-connecting-ip\",\n \"x-nf-client-connection-ip\",\n]\n\nfunction toHeaders(headers: HeadersLike): Headers {\n if (headers instanceof Headers) return headers\n\n const copy = new Headers()\n if (typeof headers.get === \"function\") {\n const source = headers as { get: (name: string) => string | null }\n for (const name of READ_HEADERS) {\n const value = source.get(name)\n if (value !== null) copy.set(name, value)\n }\n return copy\n }\n\n for (const [name, value] of Object.entries(headers)) {\n if (value !== undefined) copy.set(name, Array.isArray(value) ? value.join(\", \") : value)\n }\n return copy\n}\n\n/** The host a request was sent to, without its port. */\nfunction findRequestHost(request: Request) {\n const forwarded = request.headers.get(\"x-forwarded-host\")?.split(\",\")[0]?.trim()\n const host = forwarded ?? request.headers.get(\"host\") ?? new URL(request.url).host\n return host.replace(/:\\d+$/, \"\") || undefined\n}\n\n/** The event's request, built from `headers` when it has none, so `clientIp` reads either. */\nfunction findRequest({ request, headers }: ServerEventOptions): Request | undefined {\n if (request) return request\n if (!headers) return\n\n const copy = toHeaders(headers)\n const host = copy.get(\"x-forwarded-host\") ?? copy.get(\"host\") ?? \"localhost\"\n return new Request(`https://${host}/`, { headers: copy })\n}\n\n/** Properties as ingest stores them: strings, cut to its cap, with revenue on the event itself. */\nfunction toWireProperties(properties: Record<string, PropertyValue | Revenue> | undefined) {\n const { revenue, ...rest } = properties ?? {}\n const values: Record<string, string> = {}\n for (const [key, value] of Object.entries(rest)) {\n // `revenue` is out of `rest`, so every value left is a property value.\n const property = value as PropertyValue\n if (property != null) values[key] = String(property).slice(0, MAX_PROPERTY_LENGTH)\n }\n return {\n ...(Object.keys(values).length ? { properties: values } : {}),\n ...(revenue && typeof revenue === \"object\" ? { revenue } : {}),\n }\n}\n\nfunction warnOnError(error: TrackError) {\n console.warn(`${ENTRY}: ${error.message}`, error.events)\n}\n\n/** Whether a request ran inside a Cloudflare Worker, which gives every request a `cf` object. */\nfunction isWorkerRequest(request: Request) {\n return \"cf\" in request && !!(request as { cf?: unknown }).cf\n}\n\n// A missing variable is reported once per process, as `createProxy()` does.\nlet hasWarnedMissing = false\n\nfunction createDisabledTracker(missing: string[]): Tracker {\n if (!hasWarnedMissing && !isNextBuild()) {\n hasWarnedMissing = true\n console.warn(`${ENTRY}: ${missing.join(\" and \")} not set, so events are not sent.`)\n }\n\n return {\n track: () => {},\n flush: async () => {},\n close: async () => {},\n }\n}\n\n/**\n * Creates a tracker for a site's backend. Credentials come from the options or from\n * `HIGHSEAT_SITE` and `HIGHSEAT_SECRET`. Without them the tracker warns once and sends nothing,\n * and a value that is not a site secret throws, since that is a copying mistake.\n *\n * ```ts\n * const tracker = createTracker({ hostname: \"example.com\" })\n * tracker.track(\"signup\", { plan: \"pro\" }, { request, userId: user.id })\n * ```\n */\nexport function createTracker(options: TrackerOptions = {}): Tracker {\n const site = options.site ?? findServerSite()\n const secret = findValidSecret(options.secret, ENTRY)\n const missing = [...(site ? [] : [\"HIGHSEAT_SITE\"]), ...(secret ? [] : [\"HIGHSEAT_SECRET\"])]\n if (!site || !secret) return createDisabledTracker(missing)\n\n const headers = { \"content-type\": \"application/json\", \"x-site-id\": site, \"x-hs-secret\": secret }\n\n const endpoint = `${new URL(options.upstream ?? DEFAULT_UPSTREAM).origin}/api/event`\n const send =\n options.fetch ?? ((input: RequestInfo | URL, init?: RequestInit) => fetch(input, init))\n const onError = options.onError ?? warnOnError\n const flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL_MS\n const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES\n const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS\n\n // The events waiting for the sender and the batch it holds, which both count toward the cap.\n let queue: QueuedEvent[] = []\n let sending: QueuedEvent[] = []\n let timer: ReturnType<typeof setTimeout> | undefined\n // The one sender loop, while it runs. Batches go one at a time, so a 429's `Retry-After`\n // pauses the whole tracker rather than one batch among many retrying at once.\n let sender: Promise<void> | undefined\n let closed = false\n // Set when `close()` reaches its deadline: the sender stops, and what is left is reported.\n let stopped = false\n const stopping = new AbortController()\n let endPause: (() => void) | undefined\n let hasWarnedNoClientIp = false\n\n function report(error: TrackError) {\n try {\n onError(error)\n } catch {}\n }\n\n function listEvents(batch: QueuedEvent[]): TrackErrorEvent[] {\n return batch.map(({ event }) => ({ name: event.event_name, eventId: event.event_id }))\n }\n\n function readClientIp(request: Request): string | undefined {\n const clientIp =\n options.clientIp ?? (isWorkerRequest(request) ? \"cloudflare\" : findHostPlatform())\n\n if (!clientIp) {\n if (!hasWarnedNoClientIp) {\n hasWarnedNoClientIp = true\n console.warn(\n `${ENTRY}: no clientIp set, so events from a request carry no visitor IP and no country. Set clientIp, or pass ip with each event.`,\n )\n }\n return\n }\n\n return getClientIpReader(clientIp, ENTRY).read(request) || undefined\n }\n\n function buildEvent(name: string, properties: unknown, eventOptions: ServerEventOptions) {\n const request = findRequest(eventOptions)\n const isPageview = name === \"pageview\"\n const userId = eventOptions.userId === undefined ? \"\" : String(eventOptions.userId)\n const event: WireEvent = {\n event_name: name,\n event_id: eventOptions.eventId ?? crypto.randomUUID(),\n hostname: eventOptions.hostname ?? options.hostname ?? (request && findRequestHost(request)),\n ...(userId ? { user_id: userId } : {}),\n ip: eventOptions.ip ?? (request && readClientIp(request)),\n user_agent: eventOptions.userAgent ?? request?.headers.get(\"user-agent\") ?? undefined,\n page_url: eventOptions.url ?? (isPageview && eventOptions.request ? request?.url : undefined),\n referrer:\n eventOptions.referrer ??\n (isPageview ? (request?.headers.get(\"referer\") ?? undefined) : undefined),\n ...toWireProperties(properties as Record<string, PropertyValue | Revenue> | undefined),\n }\n return event\n }\n\n function toErrorEvents(batch: QueuedEvent[], entries: BatchEntry[]): TrackErrorEvent[] {\n return entries.flatMap(({ index, reason }) => {\n const queued = batch[index]\n return queued\n ? [{ name: queued.event.event_name, eventId: queued.event.event_id, reason }]\n : []\n })\n }\n\n /** Reports the events ingest refused or did not store, by their place in the batch. */\n async function readAnswer(response: Response, batch: QueuedEvent[]) {\n let answer: IngestAnswer\n try {\n answer = (await response.json()) as IngestAnswer\n } catch {\n return\n }\n\n const { status } = response\n const rejected = toErrorEvents(batch, answer.rejections ?? [])\n if (rejected.length) {\n report({ type: \"rejected\", message: \"Highseat rejected events.\", status, events: rejected })\n }\n\n const dropped = toErrorEvents(\n batch,\n (answer.dropped ?? []).filter(({ reason }) => reason !== \"duplicate\"),\n )\n if (dropped.length) {\n report({\n type: \"dropped\",\n message: \"Highseat did not store events.\",\n status,\n events: dropped,\n })\n }\n }\n\n async function readRefusal(response: Response) {\n try {\n const body = (await response.json()) as { error?: unknown }\n if (typeof body.error === \"string\") return body.error\n } catch {}\n return `Highseat answered ${response.status}.`\n }\n\n /** Waits before a retry, ended early when `close()` gives up. */\n function pause(ms: number) {\n return new Promise<void>(resolve => {\n const wait = setTimeout(resolve, ms)\n endPause = () => {\n clearTimeout(wait)\n resolve()\n }\n })\n }\n\n /** Sends one batch until it lands, fails for good, or `close()` gives up on it. */\n async function sendBatch(batch: QueuedEvent[]) {\n for (let attempt = 0; !stopped; attempt++) {\n const now = Date.now()\n // The age is measured again on each attempt, so a retried event keeps its own moment.\n const body = JSON.stringify(\n batch.map(({ event, at }) => ({ ...event, age: Math.max(0, now - at) })),\n )\n let response: Response | undefined\n let failure = \"Highseat is unreachable.\"\n\n try {\n response = await send(endpoint, {\n method: \"POST\",\n headers,\n body,\n signal: AbortSignal.any([stopping.signal, AbortSignal.timeout(timeout)]),\n })\n } catch (error) {\n if (error instanceof Error && error.name === \"TimeoutError\") {\n failure = `Highseat did not answer within ${timeout} ms.`\n }\n }\n\n // `close()` reports the batch it gave up on.\n if (stopped) return\n if (response?.ok) return readAnswer(response, batch)\n\n if ((response && !isRetryable(response.status)) || attempt >= maxRetries) {\n const message = response ? await readRefusal(response) : failure\n const status = response?.status\n return report({ type: \"failed\", message, status, events: listEvents(batch) })\n }\n\n // Released, so a runtime that pools connections can reuse this one.\n await response?.body?.cancel().catch(() => {})\n await pause(\n getRetryDelay(attempt, parseRetryAfter(response?.headers.get(\"retry-after\") ?? null)),\n )\n }\n }\n\n function settle(batch: QueuedEvent[]) {\n for (const { settle } of batch) settle?.()\n }\n\n async function runSender() {\n while (queue.length && !stopped) {\n const batch = queue.slice(0, MAX_BATCH_SIZE)\n queue = queue.slice(MAX_BATCH_SIZE)\n sending = batch\n try {\n await sendBatch(batch)\n } catch {}\n sending = []\n settle(batch)\n }\n }\n\n /** Starts the sender unless it runs, and resolves when it next stops. */\n function startSender(): Promise<void> {\n if (timer !== undefined) {\n clearTimeout(timer)\n timer = undefined\n }\n\n sender ??= runSender().finally(() => {\n sender = undefined\n // An event queued in the tick the loop ended in found it still set, so it runs again.\n if (queue.length && !stopped) void startSender()\n })\n return sender\n }\n\n function schedule() {\n if (timer !== undefined) return\n timer = setTimeout(() => {\n timer = undefined\n void startSender()\n }, flushInterval)\n // A queued event must not keep a script or a test runner alive by itself.\n if (typeof timer === \"object\") timer.unref?.()\n }\n\n async function flush() {\n while ((queue.length || sending.length) && !stopped) await startSender()\n }\n\n /** Stops sending, and reports and settles every event not yet confirmed sent. */\n function giveUp(deadline: number) {\n stopped = true\n stopping.abort()\n endPause?.()\n if (timer !== undefined) clearTimeout(timer)\n\n const unsent = [...sending, ...queue]\n sending = []\n queue = []\n if (unsent.length) {\n report({\n type: \"failed\",\n message: `close() stopped after ${deadline} ms with ${unsent.length} events not confirmed sent.`,\n events: listEvents(unsent),\n })\n }\n settle(unsent)\n }\n\n async function close({ timeout: deadline }: CloseOptions = {}) {\n closed = true\n const drained = flush()\n if (deadline === undefined) return drained\n\n let wait: ReturnType<typeof setTimeout> | undefined\n const expired = new Promise<boolean>(resolve => {\n wait = setTimeout(() => resolve(true), deadline)\n })\n const timedOut = await Promise.race([drained.then(() => false), expired])\n clearTimeout(wait)\n if (timedOut) giveUp(deadline)\n }\n\n function track(name: string, ...args: unknown[]) {\n try {\n if (closed) return\n const [properties, eventOptions = {}] = args as [unknown, ServerEventOptions | undefined]\n const at = eventOptions.timestamp?.getTime() ?? Date.now()\n const queued: QueuedEvent = { event: buildEvent(name, properties, eventOptions), at }\n\n if (queue.length + sending.length >= MAX_QUEUE_SIZE) {\n report({\n type: \"failed\",\n message: `More than ${MAX_QUEUE_SIZE} events are waiting to be sent, so this one was not kept.`,\n events: listEvents([queued]),\n })\n return\n }\n\n queue.push(queued)\n const waitUntil = eventOptions.waitUntil ?? options.waitUntil\n if (waitUntil) waitUntil(new Promise<void>(resolve => (queued.settle = resolve)))\n\n if (waitUntil || queue.length >= MAX_BATCH_SIZE) {\n void startSender()\n } else {\n schedule()\n }\n } catch (error) {\n report({\n type: \"failed\",\n message: error instanceof Error ? error.message : String(error),\n events: [],\n })\n }\n }\n\n return { track, flush, close }\n}\n\n// The tracker the module-level functions share, made from the environment on first use.\nlet defaultTracker: Tracker | undefined\n\nfunction getDefaultTracker(): Tracker {\n if (defaultTracker) return defaultTracker\n try {\n defaultTracker = createTracker()\n } catch (error) {\n console.warn(error instanceof Error ? error.message : error)\n defaultTracker = createDisabledTracker([])\n }\n return defaultTracker\n}\n\n/**\n * Records an event with the tracker made from `HIGHSEAT_SITE` and `HIGHSEAT_SECRET`. A known\n * name checks its properties, as in the browser. Never throws.\n *\n * ```ts\n * track(\"signup\", { plan: \"pro\" }, { request, userId: user.id })\n * ```\n */\nexport function track<Name extends TrackName>(name: Name, ...args: ServerTrackArgs<Name>): void {\n getDefaultTracker().track(name, ...args)\n}\n\n/** Sends every event `track()` has queued. Await it before a short-lived script exits. */\nexport function flush(): Promise<void> {\n return defaultTracker ? defaultTracker.flush() : Promise.resolve()\n}\n\n/** Sends what `track()` has queued and stops sending. Call it at shutdown. See `CloseOptions`. */\nexport function close(options?: CloseOptions): Promise<void> {\n return defaultTracker ? defaultTracker.close(options) : Promise.resolve()\n}\n"],"mappings":";;;AAgMA,MAAM,QAAQ;AAGd,MAAM,iBAAiB;AAIvB,MAAM,iBAAiB;AAGvB,MAAM,sBAAsB;AAE5B,MAAM,4BAA4B;AAClC,MAAM,sBAAsB;AAC5B,MAAM,qBAAqB;AAI3B,MAAM,gBAAgB;AACtB,MAAM,qBAAqB;AAE3B,SAAS,YAAY,QAAgB;CACnC,OAAO,WAAW,OAAO,UAAU;AACrC;;AAGA,SAAS,gBAAgB,OAA0C;CACjE,IAAI,UAAU,MAAM;CACpB,MAAM,UAAU,OAAO,KAAK;CAC5B,IAAI,MAAM,KAAK,MAAM,MAAM,OAAO,SAAS,OAAO,GAAG,OAAO,KAAK,IAAI,GAAG,UAAU,GAAI;CACtF,MAAM,OAAO,KAAK,MAAM,KAAK;CAC7B,OAAO,OAAO,MAAM,IAAI,IAAI,KAAA,IAAY,KAAK,IAAI,GAAG,OAAO,KAAK,IAAI,CAAC;AACvE;AAEA,SAAS,cAAc,SAAiB,YAAqB;CAC3D,MAAM,UAAU,gBAAgB,KAAK,WAAW,IAAI,KAAK,OAAO,IAAI;CACpE,OAAO,KAAK,IAAI,cAAc,SAAS,kBAAkB;AAC3D;AAIA,MAAM,eAAe;CACnB;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,SAAS,UAAU,SAA+B;CAChD,IAAI,mBAAmB,SAAS,OAAO;CAEvC,MAAM,OAAO,IAAI,QAAQ;CACzB,IAAI,OAAO,QAAQ,QAAQ,YAAY;EACrC,MAAM,SAAS;EACf,KAAK,MAAM,QAAQ,cAAc;GAC/B,MAAM,QAAQ,OAAO,IAAI,IAAI;GAC7B,IAAI,UAAU,MAAM,KAAK,IAAI,MAAM,KAAK;EAC1C;EACA,OAAO;CACT;CAEA,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,GAChD,IAAI,UAAU,KAAA,GAAW,KAAK,IAAI,MAAM,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK;CAEzF,OAAO;AACT;;AAGA,SAAS,gBAAgB,SAAkB;CAGzC,QAFkB,QAAQ,QAAQ,IAAI,kBAAkB,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK,KACrD,QAAQ,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,QAAQ,GAAG,CAAC,CAAC,KAAA,CAClE,QAAQ,SAAS,EAAE,KAAK,KAAA;AACtC;;AAGA,SAAS,YAAY,EAAE,SAAS,WAAoD;CAClF,IAAI,SAAS,OAAO;CACpB,IAAI,CAAC,SAAS;CAEd,MAAM,OAAO,UAAU,OAAO;CAC9B,MAAM,OAAO,KAAK,IAAI,kBAAkB,KAAK,KAAK,IAAI,MAAM,KAAK;CACjE,OAAO,IAAI,QAAQ,WAAW,KAAK,IAAI,EAAE,SAAS,KAAK,CAAC;AAC1D;;AAGA,SAAS,iBAAiB,YAAiE;CACzF,MAAM,EAAE,SAAS,GAAG,SAAS,cAAc,CAAC;CAC5C,MAAM,SAAiC,CAAC;CACxC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,IAAI,GAAG;EAE/C,MAAM,WAAW;EACjB,IAAI,YAAY,MAAM,OAAO,OAAO,OAAO,QAAQ,CAAC,CAAC,MAAM,GAAG,mBAAmB;CACnF;CACA,OAAO;EACL,GAAI,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,EAAE,YAAY,OAAO,IAAI,CAAC;EAC3D,GAAI,WAAW,OAAO,YAAY,WAAW,EAAE,QAAQ,IAAI,CAAC;CAC9D;AACF;AAEA,SAAS,YAAY,OAAmB;CACtC,QAAQ,KAAK,GAAG,MAAM,IAAI,MAAM,WAAW,MAAM,MAAM;AACzD;;AAGA,SAAS,gBAAgB,SAAkB;CACzC,OAAO,QAAQ,WAAW,CAAC,CAAE,QAA6B;AAC5D;AAGA,IAAI,mBAAmB;AAEvB,SAAS,sBAAsB,SAA4B;CACzD,IAAI,CAAC,oBAAoB,CAAC,YAAY,GAAG;EACvC,mBAAmB;EACnB,QAAQ,KAAK,GAAG,MAAM,IAAI,QAAQ,KAAK,OAAO,EAAE,kCAAkC;CACpF;CAEA,OAAO;EACL,aAAa,CAAC;EACd,OAAO,YAAY,CAAC;EACpB,OAAO,YAAY,CAAC;CACtB;AACF;;;;;;;;;;;AAYA,SAAgB,cAAc,UAA0B,CAAC,GAAY;CACnE,MAAM,OAAO,QAAQ,QAAQ,eAAe;CAC5C,MAAM,SAAS,gBAAgB,QAAQ,QAAQ,KAAK;CACpD,MAAM,UAAU,CAAC,GAAI,OAAO,CAAC,IAAI,CAAC,eAAe,GAAI,GAAI,SAAS,CAAC,IAAI,CAAC,iBAAiB,CAAE;CAC3F,IAAI,CAAC,QAAQ,CAAC,QAAQ,OAAO,sBAAsB,OAAO;CAE1D,MAAM,UAAU;EAAE,gBAAgB;EAAoB,aAAa;EAAM,eAAe;CAAO;CAE/F,MAAM,WAAW,GAAG,IAAI,IAAI,QAAQ,YAAA,0BAA4B,CAAC,CAAC,OAAO;CACzE,MAAM,OACJ,QAAQ,WAAW,OAA0B,SAAuB,MAAM,OAAO,IAAI;CACvF,MAAM,UAAU,QAAQ,WAAW;CACnC,MAAM,gBAAgB,QAAQ,iBAAiB;CAC/C,MAAM,aAAa,QAAQ,cAAc;CACzC,MAAM,UAAU,QAAQ,WAAW;CAGnC,IAAI,QAAuB,CAAC;CAC5B,IAAI,UAAyB,CAAC;CAC9B,IAAI;CAGJ,IAAI;CACJ,IAAI,SAAS;CAEb,IAAI,UAAU;CACd,MAAM,WAAW,IAAI,gBAAgB;CACrC,IAAI;CACJ,IAAI,sBAAsB;CAE1B,SAAS,OAAO,OAAmB;EACjC,IAAI;GACF,QAAQ,KAAK;EACf,QAAQ,CAAC;CACX;CAEA,SAAS,WAAW,OAAyC;EAC3D,OAAO,MAAM,KAAK,EAAE,aAAa;GAAE,MAAM,MAAM;GAAY,SAAS,MAAM;EAAS,EAAE;CACvF;CAEA,SAAS,aAAa,SAAsC;EAC1D,MAAM,WACJ,QAAQ,aAAa,gBAAgB,OAAO,IAAI,eAAe,iBAAiB;EAElF,IAAI,CAAC,UAAU;GACb,IAAI,CAAC,qBAAqB;IACxB,sBAAsB;IACtB,QAAQ,KACN,GAAG,MAAM,0HACX;GACF;GACA;EACF;EAEA,OAAO,kBAAkB,UAAU,KAAK,CAAC,CAAC,KAAK,OAAO,KAAK,KAAA;CAC7D;CAEA,SAAS,WAAW,MAAc,YAAqB,cAAkC;EACvF,MAAM,UAAU,YAAY,YAAY;EACxC,MAAM,aAAa,SAAS;EAC5B,MAAM,SAAS,aAAa,WAAW,KAAA,IAAY,KAAK,OAAO,aAAa,MAAM;EAclF,OAAO;GAZL,YAAY;GACZ,UAAU,aAAa,WAAW,OAAO,WAAW;GACpD,UAAU,aAAa,YAAY,QAAQ,aAAa,WAAW,gBAAgB,OAAO;GAC1F,GAAI,SAAS,EAAE,SAAS,OAAO,IAAI,CAAC;GACpC,IAAI,aAAa,OAAO,WAAW,aAAa,OAAO;GACvD,YAAY,aAAa,aAAa,SAAS,QAAQ,IAAI,YAAY,KAAK,KAAA;GAC5E,UAAU,aAAa,QAAQ,cAAc,aAAa,UAAU,SAAS,MAAM,KAAA;GACnF,UACE,aAAa,aACZ,aAAc,SAAS,QAAQ,IAAI,SAAS,KAAK,KAAA,IAAa,KAAA;GACjE,GAAG,iBAAiB,UAAiE;EAE5E;CACb;CAEA,SAAS,cAAc,OAAsB,SAA0C;EACrF,OAAO,QAAQ,SAAS,EAAE,OAAO,aAAa;GAC5C,MAAM,SAAS,MAAM;GACrB,OAAO,SACH,CAAC;IAAE,MAAM,OAAO,MAAM;IAAY,SAAS,OAAO,MAAM;IAAU;GAAO,CAAC,IAC1E,CAAC;EACP,CAAC;CACH;;CAGA,eAAe,WAAW,UAAoB,OAAsB;EAClE,IAAI;EACJ,IAAI;GACF,SAAU,MAAM,SAAS,KAAK;EAChC,QAAQ;GACN;EACF;EAEA,MAAM,EAAE,WAAW;EACnB,MAAM,WAAW,cAAc,OAAO,OAAO,cAAc,CAAC,CAAC;EAC7D,IAAI,SAAS,QACX,OAAO;GAAE,MAAM;GAAY,SAAS;GAA6B;GAAQ,QAAQ;EAAS,CAAC;EAG7F,MAAM,UAAU,cACd,QACC,OAAO,WAAW,CAAC,EAAA,CAAG,QAAQ,EAAE,aAAa,WAAW,WAAW,CACtE;EACA,IAAI,QAAQ,QACV,OAAO;GACL,MAAM;GACN,SAAS;GACT;GACA,QAAQ;EACV,CAAC;CAEL;CAEA,eAAe,YAAY,UAAoB;EAC7C,IAAI;GACF,MAAM,OAAQ,MAAM,SAAS,KAAK;GAClC,IAAI,OAAO,KAAK,UAAU,UAAU,OAAO,KAAK;EAClD,QAAQ,CAAC;EACT,OAAO,qBAAqB,SAAS,OAAO;CAC9C;;CAGA,SAAS,MAAM,IAAY;EACzB,OAAO,IAAI,SAAc,YAAW;GAClC,MAAM,OAAO,WAAW,SAAS,EAAE;GACnC,iBAAiB;IACf,aAAa,IAAI;IACjB,QAAQ;GACV;EACF,CAAC;CACH;;CAGA,eAAe,UAAU,OAAsB;EAC7C,KAAK,IAAI,UAAU,GAAG,CAAC,SAAS,WAAW;GACzC,MAAM,MAAM,KAAK,IAAI;GAErB,MAAM,OAAO,KAAK,UAChB,MAAM,KAAK,EAAE,OAAO,UAAU;IAAE,GAAG;IAAO,KAAK,KAAK,IAAI,GAAG,MAAM,EAAE;GAAE,EAAE,CACzE;GACA,IAAI;GACJ,IAAI,UAAU;GAEd,IAAI;IACF,WAAW,MAAM,KAAK,UAAU;KAC9B,QAAQ;KACR;KACA;KACA,QAAQ,YAAY,IAAI,CAAC,SAAS,QAAQ,YAAY,QAAQ,OAAO,CAAC,CAAC;IACzE,CAAC;GACH,SAAS,OAAO;IACd,IAAI,iBAAiB,SAAS,MAAM,SAAS,gBAC3C,UAAU,kCAAkC,QAAQ;GAExD;GAGA,IAAI,SAAS;GACb,IAAI,UAAU,IAAI,OAAO,WAAW,UAAU,KAAK;GAEnD,IAAK,YAAY,CAAC,YAAY,SAAS,MAAM,KAAM,WAAW,YAAY;IACxE,MAAM,UAAU,WAAW,MAAM,YAAY,QAAQ,IAAI;IACzD,MAAM,SAAS,UAAU;IACzB,OAAO,OAAO;KAAE,MAAM;KAAU;KAAS;KAAQ,QAAQ,WAAW,KAAK;IAAE,CAAC;GAC9E;GAGA,MAAM,UAAU,MAAM,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC;GAC7C,MAAM,MACJ,cAAc,SAAS,gBAAgB,UAAU,QAAQ,IAAI,aAAa,KAAK,IAAI,CAAC,CACtF;EACF;CACF;CAEA,SAAS,OAAO,OAAsB;EACpC,KAAK,MAAM,EAAE,YAAY,OAAO,SAAS;CAC3C;CAEA,eAAe,YAAY;EACzB,OAAO,MAAM,UAAU,CAAC,SAAS;GAC/B,MAAM,QAAQ,MAAM,MAAM,GAAG,cAAc;GAC3C,QAAQ,MAAM,MAAM,cAAc;GAClC,UAAU;GACV,IAAI;IACF,MAAM,UAAU,KAAK;GACvB,QAAQ,CAAC;GACT,UAAU,CAAC;GACX,OAAO,KAAK;EACd;CACF;;CAGA,SAAS,cAA6B;EACpC,IAAI,UAAU,KAAA,GAAW;GACvB,aAAa,KAAK;GAClB,QAAQ,KAAA;EACV;EAEA,WAAW,UAAU,CAAC,CAAC,cAAc;GACnC,SAAS,KAAA;GAET,IAAI,MAAM,UAAU,CAAC,SAAS,YAAiB;EACjD,CAAC;EACD,OAAO;CACT;CAEA,SAAS,WAAW;EAClB,IAAI,UAAU,KAAA,GAAW;EACzB,QAAQ,iBAAiB;GACvB,QAAQ,KAAA;GACR,YAAiB;EACnB,GAAG,aAAa;EAEhB,IAAI,OAAO,UAAU,UAAU,MAAM,QAAQ;CAC/C;CAEA,eAAe,QAAQ;EACrB,QAAQ,MAAM,UAAU,QAAQ,WAAW,CAAC,SAAS,MAAM,YAAY;CACzE;;CAGA,SAAS,OAAO,UAAkB;EAChC,UAAU;EACV,SAAS,MAAM;EACf,WAAW;EACX,IAAI,UAAU,KAAA,GAAW,aAAa,KAAK;EAE3C,MAAM,SAAS,CAAC,GAAG,SAAS,GAAG,KAAK;EACpC,UAAU,CAAC;EACX,QAAQ,CAAC;EACT,IAAI,OAAO,QACT,OAAO;GACL,MAAM;GACN,SAAS,yBAAyB,SAAS,WAAW,OAAO,OAAO;GACpE,QAAQ,WAAW,MAAM;EAC3B,CAAC;EAEH,OAAO,MAAM;CACf;CAEA,eAAe,MAAM,EAAE,SAAS,aAA2B,CAAC,GAAG;EAC7D,SAAS;EACT,MAAM,UAAU,MAAM;EACtB,IAAI,aAAa,KAAA,GAAW,OAAO;EAEnC,IAAI;EACJ,MAAM,UAAU,IAAI,SAAiB,YAAW;GAC9C,OAAO,iBAAiB,QAAQ,IAAI,GAAG,QAAQ;EACjD,CAAC;EACD,MAAM,WAAW,MAAM,QAAQ,KAAK,CAAC,QAAQ,WAAW,KAAK,GAAG,OAAO,CAAC;EACxE,aAAa,IAAI;EACjB,IAAI,UAAU,OAAO,QAAQ;CAC/B;CAEA,SAAS,MAAM,MAAc,GAAG,MAAiB;EAC/C,IAAI;GACF,IAAI,QAAQ;GACZ,MAAM,CAAC,YAAY,eAAe,CAAC,KAAK;GACxC,MAAM,KAAK,aAAa,WAAW,QAAQ,KAAK,KAAK,IAAI;GACzD,MAAM,SAAsB;IAAE,OAAO,WAAW,MAAM,YAAY,YAAY;IAAG;GAAG;GAEpF,IAAI,MAAM,SAAS,QAAQ,UAAU,gBAAgB;IACnD,OAAO;KACL,MAAM;KACN,SAAS,aAAa,eAAe;KACrC,QAAQ,WAAW,CAAC,MAAM,CAAC;IAC7B,CAAC;IACD;GACF;GAEA,MAAM,KAAK,MAAM;GACjB,MAAM,YAAY,aAAa,aAAa,QAAQ;GACpD,IAAI,WAAW,UAAU,IAAI,SAAc,YAAY,OAAO,SAAS,OAAQ,CAAC;GAEhF,IAAI,aAAa,MAAM,UAAU,gBAC/B,YAAiB;QAEjB,SAAS;EAEb,SAAS,OAAO;GACd,OAAO;IACL,MAAM;IACN,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;IAC9D,QAAQ,CAAC;GACX,CAAC;EACH;CACF;CAEA,OAAO;EAAE;EAAO;EAAO;CAAM;AAC/B;AAGA,IAAI;AAEJ,SAAS,oBAA6B;CACpC,IAAI,gBAAgB,OAAO;CAC3B,IAAI;EACF,iBAAiB,cAAc;CACjC,SAAS,OAAO;EACd,QAAQ,KAAK,iBAAiB,QAAQ,MAAM,UAAU,KAAK;EAC3D,iBAAiB,sBAAsB,CAAC,CAAC;CAC3C;CACA,OAAO;AACT;;;;;;;;;AAUA,SAAgB,MAA8B,MAAY,GAAG,MAAmC;CAC9F,kBAAkB,CAAC,CAAC,MAAM,MAAM,GAAG,IAAI;AACzC;;AAGA,SAAgB,QAAuB;CACrC,OAAO,iBAAiB,eAAe,MAAM,IAAI,QAAQ,QAAQ;AACnE;;AAGA,SAAgB,MAAM,SAAuC;CAC3D,OAAO,iBAAiB,eAAe,MAAM,OAAO,IAAI,QAAQ,QAAQ;AAC1E"}
|
|
1
|
+
{"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["import { findHostPlatform, findServerSite, isNextBuild } from \"./env\"\nimport type { BuiltInEvents, PropertyValue, Revenue, TrackArgsOf, TrackNameOf } from \"./index\"\nimport { type ClientIp, DEFAULT_UPSTREAM, findValidSecret, getClientIpReader } from \"./ingest\"\n\nexport type { ClientIp, ClientIpPreset } from \"./ingest\"\n\n/**\n * Server-side events, sent from your own backend with the site secret: a server action, a route\n * handler, a webhook handler or a job. Events are batched and sent in the background, retried\n * when Highseat is busy or unreachable, and nothing here throws: a failure goes to `onError`.\n */\n\n/**\n * Request headers in any shape a server hands them over: a `Headers` object, what Next.js's\n * `headers()` returns, or Node's `request.headers` record.\n */\nexport type HeadersLike =\n | { get: (name: string) => string | null }\n | Record<string, string | string[] | undefined>\n\n/**\n * Keeps a serverless function alive until a promise settles: `waitUntil` from\n * `@vercel/functions`, or `ctx.waitUntil` in a Cloudflare Worker and `context.waitUntil` in a\n * Netlify function, bound to their object.\n */\nexport type WaitUntil = (promise: Promise<unknown>) => void\n\n/** Who an event belongs to and where it happened, given with each `track()` call. */\nexport interface ServerEventOptions {\n /**\n * The request being handled. The visitor's IP (see `clientIp`), their user agent and the\n * hostname are read from it, and a `pageview` takes its URL and referrer too.\n */\n request?: Request\n /** The request's headers, when there is no `Request`: `await headers()` in a server action. */\n headers?: HeadersLike\n /** The visitor's IP. Wins over the one read from `request` or `headers`. */\n ip?: string\n /** The visitor's user agent. Wins over the one read from `request` or `headers`. */\n userAgent?: string\n /**\n * A user id from your own system. An event with no visitor (no IP and no user agent, as from\n * a job) counts as this user, and without one as a visitor of its own.\n */\n userId?: string | number\n /** A hostname of the site. Defaults to the tracker's `hostname`, then the request's host. */\n hostname?: string\n /** The page's URL. A `pageview` defaults to the request's URL. */\n url?: string\n /** The page the visitor came from. A `pageview` defaults to the request's `Referer`. */\n referrer?: string\n /**\n * The event's id, any string up to 255 characters. Defaults to a new UUID. Pass an id your\n * system already holds (a job id, a webhook event id), and a retry of the same event within\n * three days is stored once.\n */\n eventId?: string\n /** When the event happened, up to a day ago. Defaults to the moment `track()` is called. */\n timestamp?: Date\n /**\n * Sends this event at once and keeps the function alive until it lands. Wins over the\n * tracker's `waitUntil`.\n */\n waitUntil?: WaitUntil\n}\n\n/** An event Highseat did not store, as `onError` reports it. */\nexport interface TrackErrorEvent {\n /** The event's name. */\n name: string\n /** The event's id: the one passed to `track()`, or the UUID it was given. */\n eventId: string\n /** Why Highseat refused or dropped it, such as `revenue.amount_not_integer` or `bot`. */\n reason?: string\n}\n\n/** A failure `track()` never throws, handed to `onError`. */\nexport interface TrackError {\n /**\n * `rejected`: refused as invalid, for good. `dropped`: accepted but not stored, for a `reason`\n * of `foreign_hostname`, `bot` or `excluded` (a recognised retry is never reported). `failed`:\n * never landed, after every retry or at `close()`'s deadline, or refused whole (a wrong secret).\n */\n type: \"rejected\" | \"dropped\" | \"failed\"\n /** What happened, in a sentence. */\n message: string\n /** The HTTP status Highseat answered, when it answered. */\n status?: number\n /** The events concerned. */\n events: TrackErrorEvent[]\n}\n\nexport interface TrackerOptions {\n /** The site id, `site_…`. Defaults to `HIGHSEAT_SITE`, then `NEXT_PUBLIC_HIGHSEAT_SITE`. */\n site?: string\n /**\n * The site secret, `hs_secret_…`, from the site's Installation settings. Defaults to\n * `HIGHSEAT_SECRET`. Keep it on the server: it lets a caller name any visitor.\n */\n secret?: string\n /** The hostname every event is filed under unless it names its own. */\n hostname?: string\n /**\n * Where the visitor's IP is read from a request: `vercel`, `cloudflare`, `netlify`, `nginx`\n * or a function, as for `createProxy()`. Defaults to `cloudflare` in a Worker, `vercel` on\n * Vercel and `netlify` on Netlify. Anywhere else, set it, or pass `ip` with each event.\n */\n clientIp?: ClientIp\n /** The ingest origin. Defaults to `https://cdn.highseat.com`. */\n upstream?: string\n /** The `fetch` events are sent with. Defaults to the global one. */\n fetch?: typeof fetch\n /** Receives every failure. Defaults to a `console.warn`. */\n onError?: (error: TrackError) => void\n /**\n * Sends every event at once and keeps the function alive until it lands. Pass it in a\n * serverless function, which may stop as soon as it answers.\n */\n waitUntil?: WaitUntil\n /** Milliseconds an event waits for others to share its request. Defaults to 1000. */\n flushInterval?: number\n /**\n * Retries of a batch Highseat answered 429 or 5xx, or never answered. Defaults to 4. Batches\n * go one at a time, so a retry's wait holds back every event behind it.\n */\n maxRetries?: number\n /** Milliseconds one request may take. Defaults to 10000. */\n timeout?: number\n}\n\n/**\n * The events Highseat knows by name that only a server sends, on top of the browser's built-in\n * events. Highseat refuses them from a browser. A site overrides an entry through `Register`.\n */\nexport interface ServerBuiltInEvents {\n /**\n * One call to a tool on the site's MCP server. `highseat/mcp` sends it for every call. Never\n * put the tool's arguments in it: assistant directories forbid recording conversation data.\n */\n mcp_tool_call: {\n /** The tool's name. */\n tool: string\n /** A label for the MCP server, such as `public` or `admin`, when the site runs more than one. */\n server?: string\n /**\n * The client's name as it declares it in `clientInfo`, else the product name its User-Agent\n * starts with (`Claude-User`), else `unknown`.\n */\n client: string\n /** The version the client declares in `clientInfo`, when it declares one. */\n client_version?: string\n /**\n * How `client` was found: declared in `clientInfo`, read off the User-Agent, or neither. A\n * declared name is the one to trust.\n */\n client_source: \"client_info\" | \"user_agent\" | \"none\"\n /** `ok`, or the error code the client saw, such as `NOT_FOUND`. */\n status: \"ok\" | (string & {})\n /**\n * How many items the call returned, for a read-only tool whose result is a list. Left out\n * for any other tool, so an empty list counts as an empty result and nothing else does.\n */\n results?: number\n /** How long the call took, in whole milliseconds. */\n duration_ms: number\n }\n}\n\ntype ServerEvents = BuiltInEvents & ServerBuiltInEvents\n\n/** The names the server's `track()` accepts: the browser's, plus {@link ServerBuiltInEvents}. */\nexport type ServerTrackName = TrackNameOf<ServerEvents>\n\n/**\n * The arguments after an event's name: its properties, typed by `Register` as for the browser's\n * `track()`, then the options that say who it belongs to.\n */\nexport type ServerTrackArgs<Name extends string> = [\n ...TrackArgsOf<ServerEvents, Name>,\n options?: ServerEventOptions,\n]\n\nexport interface Tracker {\n /** Records an event, queued and sent in the background. Returns at once and never throws. */\n track: <Name extends ServerTrackName>(name: Name, ...args: ServerTrackArgs<Name>) => void\n /** Sends every queued event, and resolves once each has landed or failed. Never rejects. */\n flush: () => Promise<void>\n /**\n * Sends what is queued and stops: a later `track()` sends nothing. Call it at shutdown, with a\n * `timeout` shorter than the shutdown's grace period.\n */\n close: (options?: CloseOptions) => Promise<void>\n}\n\nexport interface CloseOptions {\n /**\n * Milliseconds to wait for the queue to drain. Past it, sending stops, the events still\n * unsent go to `onError` as `failed`, and `close()` resolves. Without one, `close()` waits for\n * every retry, which during an outage can take minutes.\n */\n timeout?: number\n}\n\n/** One event as ingest takes it, before its age is known. */\ninterface WireEvent {\n event_name: string\n event_id: string\n hostname?: string\n user_id?: string\n ip?: string\n user_agent?: string\n page_url?: string\n referrer?: string\n properties?: Record<string, string>\n revenue?: Revenue\n}\n\ninterface QueuedEvent {\n event: WireEvent\n /** When it happened, which its `age` is measured from on every attempt. */\n at: number\n /** Resolves the promise handed to `waitUntil`, once the event has landed or failed. */\n settle?: () => void\n}\n\ninterface BatchEntry {\n index: number\n reason: string\n}\n\ninterface IngestAnswer {\n rejections?: BatchEntry[]\n dropped?: BatchEntry[]\n}\n\nconst ENTRY = \"highseat/server\"\n\n// Ingest's limit per request.\nconst MAX_BATCH_SIZE = 25\n\n// Events held while Highseat is unreachable, the batch being sent included. A newer one past\n// this is reported and not kept, so an outage cannot grow the process without bound.\nconst MAX_QUEUE_SIZE = 1000\n\n// Ingest cuts a property value to this, and the tracker does too.\nconst MAX_PROPERTY_LENGTH = 500\n\nconst DEFAULT_FLUSH_INTERVAL_MS = 1000\nconst DEFAULT_MAX_RETRIES = 4\nconst DEFAULT_TIMEOUT_MS = 10_000\n\n// The first retry's wait, doubled on each one after it, and the longest any wait lasts,\n// `Retry-After` included.\nconst RETRY_BASE_MS = 500\nconst MAX_RETRY_DELAY_MS = 30_000\n\nfunction isRetryable(status: number) {\n return status === 429 || status >= 500\n}\n\n/** The wait `Retry-After` asks for, in seconds or as a date, or `undefined` when there is none. */\nfunction parseRetryAfter(value: string | null): number | undefined {\n if (value === null) return\n const seconds = Number(value)\n if (value.trim() !== \"\" && Number.isFinite(seconds)) return Math.max(0, seconds * 1000)\n const date = Date.parse(value)\n return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now())\n}\n\nfunction getRetryDelay(attempt: number, retryAfter?: number) {\n const backoff = RETRY_BASE_MS * 2 ** attempt * (1 + Math.random() / 2)\n return Math.min(retryAfter ?? backoff, MAX_RETRY_DELAY_MS)\n}\n\n// The headers read off an object that offers only `get`: the ones the presets and the event's\n// own fields read.\nconst READ_HEADERS = [\n \"user-agent\",\n \"host\",\n \"x-forwarded-host\",\n \"referer\",\n \"x-real-ip\",\n \"cf-connecting-ip\",\n \"x-nf-client-connection-ip\",\n]\n\nfunction toHeaders(headers: HeadersLike): Headers {\n if (headers instanceof Headers) return headers\n\n const copy = new Headers()\n if (typeof headers.get === \"function\") {\n const source = headers as { get: (name: string) => string | null }\n for (const name of READ_HEADERS) {\n const value = source.get(name)\n if (value !== null) copy.set(name, value)\n }\n return copy\n }\n\n for (const [name, value] of Object.entries(headers)) {\n if (value !== undefined) copy.set(name, Array.isArray(value) ? value.join(\", \") : value)\n }\n return copy\n}\n\n/** The host a request was sent to, without its port. */\nfunction findRequestHost(request: Request) {\n const forwarded = request.headers.get(\"x-forwarded-host\")?.split(\",\")[0]?.trim()\n const host = forwarded ?? request.headers.get(\"host\") ?? new URL(request.url).host\n return host.replace(/:\\d+$/, \"\") || undefined\n}\n\n/** The event's request, built from `headers` when it has none, so `clientIp` reads either. */\nfunction findRequest({ request, headers }: ServerEventOptions): Request | undefined {\n if (request) return request\n if (!headers) return\n\n const copy = toHeaders(headers)\n const host = copy.get(\"x-forwarded-host\") ?? copy.get(\"host\") ?? \"localhost\"\n return new Request(`https://${host}/`, { headers: copy })\n}\n\n/** Properties as ingest stores them: strings, cut to its cap, with revenue on the event itself. */\nfunction toWireProperties(properties: Record<string, PropertyValue | Revenue> | undefined) {\n const { revenue, ...rest } = properties ?? {}\n const values: Record<string, string> = {}\n for (const [key, value] of Object.entries(rest)) {\n // `revenue` is out of `rest`, so every value left is a property value.\n const property = value as PropertyValue\n if (property != null) values[key] = String(property).slice(0, MAX_PROPERTY_LENGTH)\n }\n return {\n ...(Object.keys(values).length ? { properties: values } : {}),\n ...(revenue && typeof revenue === \"object\" ? { revenue } : {}),\n }\n}\n\nfunction warnOnError(error: TrackError) {\n console.warn(`${ENTRY}: ${error.message}`, error.events)\n}\n\n/** Whether a request ran inside a Cloudflare Worker, which gives every request a `cf` object. */\nfunction isWorkerRequest(request: Request) {\n return \"cf\" in request && !!(request as { cf?: unknown }).cf\n}\n\n// A missing variable is reported once per process, as `createProxy()` does.\nlet hasWarnedMissing = false\n\nfunction createDisabledTracker(missing: string[]): Tracker {\n if (!hasWarnedMissing && !isNextBuild()) {\n hasWarnedMissing = true\n console.warn(`${ENTRY}: ${missing.join(\" and \")} not set, so events are not sent.`)\n }\n\n return {\n track: () => {},\n flush: async () => {},\n close: async () => {},\n }\n}\n\n/**\n * Creates a tracker for a site's backend. Credentials come from the options or from\n * `HIGHSEAT_SITE` and `HIGHSEAT_SECRET`. Without them the tracker warns once and sends nothing,\n * and a value that is not a site secret throws, since that is a copying mistake.\n *\n * ```ts\n * const tracker = createTracker({ hostname: \"example.com\" })\n * tracker.track(\"signup\", { plan: \"pro\" }, { request, userId: user.id })\n * ```\n */\nexport function createTracker(options: TrackerOptions = {}): Tracker {\n const site = options.site ?? findServerSite()\n const secret = findValidSecret(options.secret, ENTRY)\n const missing = [...(site ? [] : [\"HIGHSEAT_SITE\"]), ...(secret ? [] : [\"HIGHSEAT_SECRET\"])]\n if (!site || !secret) return createDisabledTracker(missing)\n\n const headers = { \"content-type\": \"application/json\", \"x-site-id\": site, \"x-hs-secret\": secret }\n\n const endpoint = `${new URL(options.upstream ?? DEFAULT_UPSTREAM).origin}/api/event`\n const send =\n options.fetch ?? ((input: RequestInfo | URL, init?: RequestInit) => fetch(input, init))\n const onError = options.onError ?? warnOnError\n const flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL_MS\n const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES\n const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS\n\n // The events waiting for the sender and the batch it holds, which both count toward the cap.\n let queue: QueuedEvent[] = []\n let sending: QueuedEvent[] = []\n let timer: ReturnType<typeof setTimeout> | undefined\n // The one sender loop, while it runs. Batches go one at a time, so a 429's `Retry-After`\n // pauses the whole tracker rather than one batch among many retrying at once.\n let sender: Promise<void> | undefined\n let closed = false\n // Set when `close()` reaches its deadline: the sender stops, and what is left is reported.\n let stopped = false\n const stopping = new AbortController()\n let endPause: (() => void) | undefined\n let hasWarnedNoClientIp = false\n\n function report(error: TrackError) {\n try {\n onError(error)\n } catch {}\n }\n\n function listEvents(batch: QueuedEvent[]): TrackErrorEvent[] {\n return batch.map(({ event }) => ({ name: event.event_name, eventId: event.event_id }))\n }\n\n function readClientIp(request: Request): string | undefined {\n const clientIp =\n options.clientIp ?? (isWorkerRequest(request) ? \"cloudflare\" : findHostPlatform())\n\n if (!clientIp) {\n if (!hasWarnedNoClientIp) {\n hasWarnedNoClientIp = true\n console.warn(\n `${ENTRY}: no clientIp set, so events from a request carry no visitor IP and no country. Set clientIp, or pass ip with each event.`,\n )\n }\n return\n }\n\n return getClientIpReader(clientIp, ENTRY).read(request) || undefined\n }\n\n function buildEvent(name: string, properties: unknown, eventOptions: ServerEventOptions) {\n const request = findRequest(eventOptions)\n const isPageview = name === \"pageview\"\n const userId = eventOptions.userId === undefined ? \"\" : String(eventOptions.userId)\n const event: WireEvent = {\n event_name: name,\n event_id: eventOptions.eventId ?? crypto.randomUUID(),\n hostname: eventOptions.hostname ?? options.hostname ?? (request && findRequestHost(request)),\n ...(userId ? { user_id: userId } : {}),\n ip: eventOptions.ip ?? (request && readClientIp(request)),\n user_agent: eventOptions.userAgent ?? request?.headers.get(\"user-agent\") ?? undefined,\n page_url: eventOptions.url ?? (isPageview && eventOptions.request ? request?.url : undefined),\n referrer:\n eventOptions.referrer ??\n (isPageview ? (request?.headers.get(\"referer\") ?? undefined) : undefined),\n ...toWireProperties(properties as Record<string, PropertyValue | Revenue> | undefined),\n }\n return event\n }\n\n function toErrorEvents(batch: QueuedEvent[], entries: BatchEntry[]): TrackErrorEvent[] {\n return entries.flatMap(({ index, reason }) => {\n const queued = batch[index]\n return queued\n ? [{ name: queued.event.event_name, eventId: queued.event.event_id, reason }]\n : []\n })\n }\n\n /** Reports the events ingest refused or did not store, by their place in the batch. */\n async function readAnswer(response: Response, batch: QueuedEvent[]) {\n let answer: IngestAnswer\n try {\n answer = (await response.json()) as IngestAnswer\n } catch {\n return\n }\n\n const { status } = response\n const rejected = toErrorEvents(batch, answer.rejections ?? [])\n if (rejected.length) {\n report({ type: \"rejected\", message: \"Highseat rejected events.\", status, events: rejected })\n }\n\n const dropped = toErrorEvents(\n batch,\n (answer.dropped ?? []).filter(({ reason }) => reason !== \"duplicate\"),\n )\n if (dropped.length) {\n report({\n type: \"dropped\",\n message: \"Highseat did not store events.\",\n status,\n events: dropped,\n })\n }\n }\n\n async function readRefusal(response: Response) {\n try {\n const body = (await response.json()) as { error?: unknown }\n if (typeof body.error === \"string\") return body.error\n } catch {}\n return `Highseat answered ${response.status}.`\n }\n\n /** Waits before a retry, ended early when `close()` gives up. */\n function pause(ms: number) {\n return new Promise<void>(resolve => {\n const wait = setTimeout(resolve, ms)\n endPause = () => {\n clearTimeout(wait)\n resolve()\n }\n })\n }\n\n /** Sends one batch until it lands, fails for good, or `close()` gives up on it. */\n async function sendBatch(batch: QueuedEvent[]) {\n for (let attempt = 0; !stopped; attempt++) {\n const now = Date.now()\n // The age is measured again on each attempt, so a retried event keeps its own moment.\n const body = JSON.stringify(\n batch.map(({ event, at }) => ({ ...event, age: Math.max(0, now - at) })),\n )\n let response: Response | undefined\n let failure = \"Highseat is unreachable.\"\n\n try {\n response = await send(endpoint, {\n method: \"POST\",\n headers,\n body,\n signal: AbortSignal.any([stopping.signal, AbortSignal.timeout(timeout)]),\n })\n } catch (error) {\n if (error instanceof Error && error.name === \"TimeoutError\") {\n failure = `Highseat did not answer within ${timeout} ms.`\n }\n }\n\n // `close()` reports the batch it gave up on.\n if (stopped) return\n if (response?.ok) return readAnswer(response, batch)\n\n if ((response && !isRetryable(response.status)) || attempt >= maxRetries) {\n const message = response ? await readRefusal(response) : failure\n const status = response?.status\n return report({ type: \"failed\", message, status, events: listEvents(batch) })\n }\n\n // Released, so a runtime that pools connections can reuse this one.\n await response?.body?.cancel().catch(() => {})\n await pause(\n getRetryDelay(attempt, parseRetryAfter(response?.headers.get(\"retry-after\") ?? null)),\n )\n }\n }\n\n function settle(batch: QueuedEvent[]) {\n for (const { settle } of batch) settle?.()\n }\n\n async function runSender() {\n while (queue.length && !stopped) {\n const batch = queue.slice(0, MAX_BATCH_SIZE)\n queue = queue.slice(MAX_BATCH_SIZE)\n sending = batch\n try {\n await sendBatch(batch)\n } catch {}\n sending = []\n settle(batch)\n }\n }\n\n /** Starts the sender unless it runs, and resolves when it next stops. */\n function startSender(): Promise<void> {\n if (timer !== undefined) {\n clearTimeout(timer)\n timer = undefined\n }\n\n sender ??= runSender().finally(() => {\n sender = undefined\n // An event queued in the tick the loop ended in found it still set, so it runs again.\n if (queue.length && !stopped) void startSender()\n })\n return sender\n }\n\n function schedule() {\n if (timer !== undefined) return\n timer = setTimeout(() => {\n timer = undefined\n void startSender()\n }, flushInterval)\n // A queued event must not keep a script or a test runner alive by itself.\n if (typeof timer === \"object\") timer.unref?.()\n }\n\n async function flush() {\n while ((queue.length || sending.length) && !stopped) await startSender()\n }\n\n /** Stops sending, and reports and settles every event not yet confirmed sent. */\n function giveUp(deadline: number) {\n stopped = true\n stopping.abort()\n endPause?.()\n if (timer !== undefined) clearTimeout(timer)\n\n const unsent = [...sending, ...queue]\n sending = []\n queue = []\n if (unsent.length) {\n report({\n type: \"failed\",\n message: `close() stopped after ${deadline} ms with ${unsent.length} events not confirmed sent.`,\n events: listEvents(unsent),\n })\n }\n settle(unsent)\n }\n\n async function close({ timeout: deadline }: CloseOptions = {}) {\n closed = true\n const drained = flush()\n if (deadline === undefined) return drained\n\n let wait: ReturnType<typeof setTimeout> | undefined\n const expired = new Promise<boolean>(resolve => {\n wait = setTimeout(() => resolve(true), deadline)\n })\n const timedOut = await Promise.race([drained.then(() => false), expired])\n clearTimeout(wait)\n if (timedOut) giveUp(deadline)\n }\n\n function track(name: string, ...args: unknown[]) {\n try {\n if (closed) return\n const [properties, eventOptions = {}] = args as [unknown, ServerEventOptions | undefined]\n const at = eventOptions.timestamp?.getTime() ?? Date.now()\n const queued: QueuedEvent = { event: buildEvent(name, properties, eventOptions), at }\n\n if (queue.length + sending.length >= MAX_QUEUE_SIZE) {\n report({\n type: \"failed\",\n message: `More than ${MAX_QUEUE_SIZE} events are waiting to be sent, so this one was not kept.`,\n events: listEvents([queued]),\n })\n return\n }\n\n queue.push(queued)\n const waitUntil = eventOptions.waitUntil ?? options.waitUntil\n if (waitUntil) waitUntil(new Promise<void>(resolve => (queued.settle = resolve)))\n\n if (waitUntil || queue.length >= MAX_BATCH_SIZE) {\n void startSender()\n } else {\n schedule()\n }\n } catch (error) {\n report({\n type: \"failed\",\n message: error instanceof Error ? error.message : String(error),\n events: [],\n })\n }\n }\n\n return { track, flush, close }\n}\n\n// The tracker the module-level functions share, made from the environment on first use.\nlet defaultTracker: Tracker | undefined\n\nfunction getDefaultTracker(): Tracker {\n if (defaultTracker) return defaultTracker\n try {\n defaultTracker = createTracker()\n } catch (error) {\n console.warn(error instanceof Error ? error.message : error)\n defaultTracker = createDisabledTracker([])\n }\n return defaultTracker\n}\n\n/**\n * Records an event with the tracker made from `HIGHSEAT_SITE` and `HIGHSEAT_SECRET`. A known\n * name checks its properties, as in the browser. Never throws.\n *\n * ```ts\n * track(\"signup\", { plan: \"pro\" }, { request, userId: user.id })\n * ```\n */\nexport function track<Name extends ServerTrackName>(\n name: Name,\n ...args: ServerTrackArgs<Name>\n): void {\n getDefaultTracker().track(name, ...args)\n}\n\n/** Sends every event `track()` has queued. Await it before a short-lived script exits. */\nexport function flush(): Promise<void> {\n return defaultTracker ? defaultTracker.flush() : Promise.resolve()\n}\n\n/** Sends what `track()` has queued and stops sending. Call it at shutdown. See `CloseOptions`. */\nexport function close(options?: CloseOptions): Promise<void> {\n return defaultTracker ? defaultTracker.close(options) : Promise.resolve()\n}\n"],"mappings":"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"}
|