highseat 0.3.1 → 0.5.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/server.js ADDED
@@ -0,0 +1,390 @@
1
+ import { findHostPlatform, findServerSite, isNextBuild } from "./env.js";
2
+ import { findValidSecret, getClientIpReader } from "./ingest.js";
3
+ //#region src/server.ts
4
+ const ENTRY = "highseat/server";
5
+ const MAX_BATCH_SIZE = 25;
6
+ const MAX_QUEUE_SIZE = 1e3;
7
+ const MAX_PROPERTY_LENGTH = 500;
8
+ const DEFAULT_FLUSH_INTERVAL_MS = 1e3;
9
+ const DEFAULT_MAX_RETRIES = 4;
10
+ const DEFAULT_TIMEOUT_MS = 1e4;
11
+ const RETRY_BASE_MS = 500;
12
+ const MAX_RETRY_DELAY_MS = 3e4;
13
+ function isRetryable(status) {
14
+ return status === 429 || status >= 500;
15
+ }
16
+ /** The wait `Retry-After` asks for, in seconds or as a date, or `undefined` when there is none. */
17
+ function parseRetryAfter(value) {
18
+ if (value === null) return;
19
+ const seconds = Number(value);
20
+ if (value.trim() !== "" && Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
21
+ const date = Date.parse(value);
22
+ return Number.isNaN(date) ? void 0 : Math.max(0, date - Date.now());
23
+ }
24
+ function getRetryDelay(attempt, retryAfter) {
25
+ const backoff = RETRY_BASE_MS * 2 ** attempt * (1 + Math.random() / 2);
26
+ return Math.min(retryAfter ?? backoff, MAX_RETRY_DELAY_MS);
27
+ }
28
+ const READ_HEADERS = [
29
+ "user-agent",
30
+ "host",
31
+ "x-forwarded-host",
32
+ "referer",
33
+ "x-real-ip",
34
+ "cf-connecting-ip",
35
+ "x-nf-client-connection-ip"
36
+ ];
37
+ function toHeaders(headers) {
38
+ if (headers instanceof Headers) return headers;
39
+ const copy = new Headers();
40
+ if (typeof headers.get === "function") {
41
+ const source = headers;
42
+ for (const name of READ_HEADERS) {
43
+ const value = source.get(name);
44
+ if (value !== null) copy.set(name, value);
45
+ }
46
+ return copy;
47
+ }
48
+ for (const [name, value] of Object.entries(headers)) if (value !== void 0) copy.set(name, Array.isArray(value) ? value.join(", ") : value);
49
+ return copy;
50
+ }
51
+ /** The host a request was sent to, without its port. */
52
+ function findRequestHost(request) {
53
+ return (request.headers.get("x-forwarded-host")?.split(",")[0]?.trim() ?? request.headers.get("host") ?? new URL(request.url).host).replace(/:\d+$/, "") || void 0;
54
+ }
55
+ /** The event's request, built from `headers` when it has none, so `clientIp` reads either. */
56
+ function findRequest({ request, headers }) {
57
+ if (request) return request;
58
+ if (!headers) return;
59
+ const copy = toHeaders(headers);
60
+ const host = copy.get("x-forwarded-host") ?? copy.get("host") ?? "localhost";
61
+ return new Request(`https://${host}/`, { headers: copy });
62
+ }
63
+ /** Properties as ingest stores them: strings, cut to its cap, with revenue on the event itself. */
64
+ function toWireProperties(properties) {
65
+ const { revenue, ...rest } = properties ?? {};
66
+ const values = {};
67
+ for (const [key, value] of Object.entries(rest)) {
68
+ const property = value;
69
+ if (property != null) values[key] = String(property).slice(0, MAX_PROPERTY_LENGTH);
70
+ }
71
+ return {
72
+ ...Object.keys(values).length ? { properties: values } : {},
73
+ ...revenue && typeof revenue === "object" ? { revenue } : {}
74
+ };
75
+ }
76
+ function warnOnError(error) {
77
+ console.warn(`${ENTRY}: ${error.message}`, error.events);
78
+ }
79
+ /** Whether a request ran inside a Cloudflare Worker, which gives every request a `cf` object. */
80
+ function isWorkerRequest(request) {
81
+ return "cf" in request && !!request.cf;
82
+ }
83
+ let hasWarnedMissing = false;
84
+ function createDisabledTracker(missing) {
85
+ if (!hasWarnedMissing && !isNextBuild()) {
86
+ hasWarnedMissing = true;
87
+ console.warn(`${ENTRY}: ${missing.join(" and ")} not set, so events are not sent.`);
88
+ }
89
+ return {
90
+ track: () => {},
91
+ flush: async () => {},
92
+ close: async () => {}
93
+ };
94
+ }
95
+ /**
96
+ * Creates a tracker for a site's backend. Credentials come from the options or from
97
+ * `HIGHSEAT_SITE` and `HIGHSEAT_SECRET`. Without them the tracker warns once and sends nothing,
98
+ * and a value that is not a site secret throws, since that is a copying mistake.
99
+ *
100
+ * ```ts
101
+ * const tracker = createTracker({ hostname: "example.com" })
102
+ * tracker.track("signup", { plan: "pro" }, { request, userId: user.id })
103
+ * ```
104
+ */
105
+ function createTracker(options = {}) {
106
+ const site = options.site ?? findServerSite();
107
+ const secret = findValidSecret(options.secret, ENTRY);
108
+ const missing = [...site ? [] : ["HIGHSEAT_SITE"], ...secret ? [] : ["HIGHSEAT_SECRET"]];
109
+ if (!site || !secret) return createDisabledTracker(missing);
110
+ const headers = {
111
+ "content-type": "application/json",
112
+ "x-site-id": site,
113
+ "x-hs-secret": secret
114
+ };
115
+ const endpoint = `${new URL(options.upstream ?? "https://cdn.highseat.com").origin}/api/event`;
116
+ const send = options.fetch ?? ((input, init) => fetch(input, init));
117
+ const onError = options.onError ?? warnOnError;
118
+ const flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL_MS;
119
+ const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
120
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
121
+ let queue = [];
122
+ let sending = [];
123
+ let timer;
124
+ let sender;
125
+ let closed = false;
126
+ let stopped = false;
127
+ const stopping = new AbortController();
128
+ let endPause;
129
+ let hasWarnedNoClientIp = false;
130
+ function report(error) {
131
+ try {
132
+ onError(error);
133
+ } catch {}
134
+ }
135
+ function listEvents(batch) {
136
+ return batch.map(({ event }) => ({
137
+ name: event.event_name,
138
+ eventId: event.event_id
139
+ }));
140
+ }
141
+ function readClientIp(request) {
142
+ const clientIp = options.clientIp ?? (isWorkerRequest(request) ? "cloudflare" : findHostPlatform());
143
+ if (!clientIp) {
144
+ if (!hasWarnedNoClientIp) {
145
+ hasWarnedNoClientIp = true;
146
+ console.warn(`${ENTRY}: no clientIp set, so events from a request carry no visitor IP and no country. Set clientIp, or pass ip with each event.`);
147
+ }
148
+ return;
149
+ }
150
+ return getClientIpReader(clientIp, ENTRY).read(request) || void 0;
151
+ }
152
+ function buildEvent(name, properties, eventOptions) {
153
+ const request = findRequest(eventOptions);
154
+ const isPageview = name === "pageview";
155
+ const userId = eventOptions.userId === void 0 ? "" : String(eventOptions.userId);
156
+ return {
157
+ event_name: name,
158
+ event_id: eventOptions.eventId ?? crypto.randomUUID(),
159
+ hostname: eventOptions.hostname ?? options.hostname ?? (request && findRequestHost(request)),
160
+ ...userId ? { user_id: userId } : {},
161
+ ip: eventOptions.ip ?? (request && readClientIp(request)),
162
+ user_agent: eventOptions.userAgent ?? request?.headers.get("user-agent") ?? void 0,
163
+ page_url: eventOptions.url ?? (isPageview && eventOptions.request ? request?.url : void 0),
164
+ referrer: eventOptions.referrer ?? (isPageview ? request?.headers.get("referer") ?? void 0 : void 0),
165
+ ...toWireProperties(properties)
166
+ };
167
+ }
168
+ function toErrorEvents(batch, entries) {
169
+ return entries.flatMap(({ index, reason }) => {
170
+ const queued = batch[index];
171
+ return queued ? [{
172
+ name: queued.event.event_name,
173
+ eventId: queued.event.event_id,
174
+ reason
175
+ }] : [];
176
+ });
177
+ }
178
+ /** Reports the events ingest refused or did not store, by their place in the batch. */
179
+ async function readAnswer(response, batch) {
180
+ let answer;
181
+ try {
182
+ answer = await response.json();
183
+ } catch {
184
+ return;
185
+ }
186
+ const { status } = response;
187
+ const rejected = toErrorEvents(batch, answer.rejections ?? []);
188
+ if (rejected.length) report({
189
+ type: "rejected",
190
+ message: "Highseat rejected events.",
191
+ status,
192
+ events: rejected
193
+ });
194
+ const dropped = toErrorEvents(batch, (answer.dropped ?? []).filter(({ reason }) => reason !== "duplicate"));
195
+ if (dropped.length) report({
196
+ type: "dropped",
197
+ message: "Highseat did not store events.",
198
+ status,
199
+ events: dropped
200
+ });
201
+ }
202
+ async function readRefusal(response) {
203
+ try {
204
+ const body = await response.json();
205
+ if (typeof body.error === "string") return body.error;
206
+ } catch {}
207
+ return `Highseat answered ${response.status}.`;
208
+ }
209
+ /** Waits before a retry, ended early when `close()` gives up. */
210
+ function pause(ms) {
211
+ return new Promise((resolve) => {
212
+ const wait = setTimeout(resolve, ms);
213
+ endPause = () => {
214
+ clearTimeout(wait);
215
+ resolve();
216
+ };
217
+ });
218
+ }
219
+ /** Sends one batch until it lands, fails for good, or `close()` gives up on it. */
220
+ async function sendBatch(batch) {
221
+ for (let attempt = 0; !stopped; attempt++) {
222
+ const now = Date.now();
223
+ const body = JSON.stringify(batch.map(({ event, at }) => ({
224
+ ...event,
225
+ age: Math.max(0, now - at)
226
+ })));
227
+ let response;
228
+ let failure = "Highseat is unreachable.";
229
+ try {
230
+ response = await send(endpoint, {
231
+ method: "POST",
232
+ headers,
233
+ body,
234
+ signal: AbortSignal.any([stopping.signal, AbortSignal.timeout(timeout)])
235
+ });
236
+ } catch (error) {
237
+ if (error instanceof Error && error.name === "TimeoutError") failure = `Highseat did not answer within ${timeout} ms.`;
238
+ }
239
+ if (stopped) return;
240
+ if (response?.ok) return readAnswer(response, batch);
241
+ if (response && !isRetryable(response.status) || attempt >= maxRetries) {
242
+ const message = response ? await readRefusal(response) : failure;
243
+ const status = response?.status;
244
+ return report({
245
+ type: "failed",
246
+ message,
247
+ status,
248
+ events: listEvents(batch)
249
+ });
250
+ }
251
+ await response?.body?.cancel().catch(() => {});
252
+ await pause(getRetryDelay(attempt, parseRetryAfter(response?.headers.get("retry-after") ?? null)));
253
+ }
254
+ }
255
+ function settle(batch) {
256
+ for (const { settle } of batch) settle?.();
257
+ }
258
+ async function runSender() {
259
+ while (queue.length && !stopped) {
260
+ const batch = queue.slice(0, MAX_BATCH_SIZE);
261
+ queue = queue.slice(MAX_BATCH_SIZE);
262
+ sending = batch;
263
+ try {
264
+ await sendBatch(batch);
265
+ } catch {}
266
+ sending = [];
267
+ settle(batch);
268
+ }
269
+ }
270
+ /** Starts the sender unless it runs, and resolves when it next stops. */
271
+ function startSender() {
272
+ if (timer !== void 0) {
273
+ clearTimeout(timer);
274
+ timer = void 0;
275
+ }
276
+ sender ??= runSender().finally(() => {
277
+ sender = void 0;
278
+ if (queue.length && !stopped) startSender();
279
+ });
280
+ return sender;
281
+ }
282
+ function schedule() {
283
+ if (timer !== void 0) return;
284
+ timer = setTimeout(() => {
285
+ timer = void 0;
286
+ startSender();
287
+ }, flushInterval);
288
+ if (typeof timer === "object") timer.unref?.();
289
+ }
290
+ async function flush() {
291
+ while ((queue.length || sending.length) && !stopped) await startSender();
292
+ }
293
+ /** Stops sending, and reports and settles every event not yet confirmed sent. */
294
+ function giveUp(deadline) {
295
+ stopped = true;
296
+ stopping.abort();
297
+ endPause?.();
298
+ if (timer !== void 0) clearTimeout(timer);
299
+ const unsent = [...sending, ...queue];
300
+ sending = [];
301
+ queue = [];
302
+ if (unsent.length) report({
303
+ type: "failed",
304
+ message: `close() stopped after ${deadline} ms with ${unsent.length} events not confirmed sent.`,
305
+ events: listEvents(unsent)
306
+ });
307
+ settle(unsent);
308
+ }
309
+ async function close({ timeout: deadline } = {}) {
310
+ closed = true;
311
+ const drained = flush();
312
+ if (deadline === void 0) return drained;
313
+ let wait;
314
+ const expired = new Promise((resolve) => {
315
+ wait = setTimeout(() => resolve(true), deadline);
316
+ });
317
+ const timedOut = await Promise.race([drained.then(() => false), expired]);
318
+ clearTimeout(wait);
319
+ if (timedOut) giveUp(deadline);
320
+ }
321
+ function track(name, ...args) {
322
+ try {
323
+ if (closed) return;
324
+ const [properties, eventOptions = {}] = args;
325
+ const at = eventOptions.timestamp?.getTime() ?? Date.now();
326
+ const queued = {
327
+ event: buildEvent(name, properties, eventOptions),
328
+ at
329
+ };
330
+ if (queue.length + sending.length >= MAX_QUEUE_SIZE) {
331
+ report({
332
+ type: "failed",
333
+ message: `More than ${MAX_QUEUE_SIZE} events are waiting to be sent, so this one was not kept.`,
334
+ events: listEvents([queued])
335
+ });
336
+ return;
337
+ }
338
+ queue.push(queued);
339
+ const waitUntil = eventOptions.waitUntil ?? options.waitUntil;
340
+ if (waitUntil) waitUntil(new Promise((resolve) => queued.settle = resolve));
341
+ if (waitUntil || queue.length >= MAX_BATCH_SIZE) startSender();
342
+ else schedule();
343
+ } catch (error) {
344
+ report({
345
+ type: "failed",
346
+ message: error instanceof Error ? error.message : String(error),
347
+ events: []
348
+ });
349
+ }
350
+ }
351
+ return {
352
+ track,
353
+ flush,
354
+ close
355
+ };
356
+ }
357
+ let defaultTracker;
358
+ function getDefaultTracker() {
359
+ if (defaultTracker) return defaultTracker;
360
+ try {
361
+ defaultTracker = createTracker();
362
+ } catch (error) {
363
+ console.warn(error instanceof Error ? error.message : error);
364
+ defaultTracker = createDisabledTracker([]);
365
+ }
366
+ return defaultTracker;
367
+ }
368
+ /**
369
+ * Records an event with the tracker made from `HIGHSEAT_SITE` and `HIGHSEAT_SECRET`. A known
370
+ * name checks its properties, as in the browser. Never throws.
371
+ *
372
+ * ```ts
373
+ * track("signup", { plan: "pro" }, { request, userId: user.id })
374
+ * ```
375
+ */
376
+ function track(name, ...args) {
377
+ getDefaultTracker().track(name, ...args);
378
+ }
379
+ /** Sends every event `track()` has queued. Await it before a short-lived script exits. */
380
+ function flush() {
381
+ return defaultTracker ? defaultTracker.flush() : Promise.resolve();
382
+ }
383
+ /** Sends what `track()` has queued and stops sending. Call it at shutdown. See `CloseOptions`. */
384
+ function close(options) {
385
+ return defaultTracker ? defaultTracker.close(options) : Promise.resolve();
386
+ }
387
+ //#endregion
388
+ export { close, createTracker, flush, track };
389
+
390
+ //# sourceMappingURL=server.js.map
package/server.js.map ADDED
@@ -0,0 +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":"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"}