@adriangalilea/utils 9.3.0 → 9.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/README.md CHANGED
@@ -77,6 +77,24 @@ Enable it only with a deletion and retention policy; no identity is inferred.
77
77
  `overlaps: [["a", "b"]]` declares unordered audience overlap, not an ordered funnel.
78
78
  Daily totals support counts and averages, not percentile latency or event ordering.
79
79
 
80
+ ## Visits
81
+
82
+ A site keeps its own visits: ui's `page-sensor` posts each page's lines (the utils contract, "Web pages") to the site's own route, and `@adriangalilea/utils/visits` puts each page in a visit and keeps the rows the views read (ui's `user-flows`). No other server, no cookie, nothing kept in the browser.
83
+
84
+ ```typescript
85
+ // app/api/events/route.ts
86
+ import { libsqlDriver } from '@adriangalilea/utils/metrics/libsql'
87
+ import { visitsRoute } from '@adriangalilea/utils/visits'
88
+
89
+ export const POST = visitsRoute({ db: libsqlDriver(client), site: 'untitled.garden', services: ['garden-web'] })
90
+
91
+ // a view
92
+ const visits = await readVisits(libsqlDriver(client), { site: 'untitled.garden', days: 14 })
93
+ // [{ id, started, places: ['studio', 'lore'], referrer: 'news.ycombinator.com', country: 'ES', … }]
94
+ ```
95
+
96
+ A visit is one caller's pages, each within 30 minutes of the one before, within one UTC day. The caller is a hash of its IP and user agent with a random salt of the day, deleted when the day is over: a visitor is recognisable within a day, never across two, and once the salt is gone by no one, the site's owner included. The IP and user agent are never stored; the country (two letters, from the platform's header) is. Pages and visits are kept 180 days, a page's other lines (requests, named events, errors) 30, a probe's (an automated browser) and the staff's 7, and both are left out of `readVisits` unless `flagged`. Refused, by line: a service the site did not name, a line outside a page's trace, a field named like a person (`ip`, `user_agent`, `email`, `user`, `name`, `phone`). A crawler's batch is dropped whole. Provision `VISITS_SCHEMA` once (the tables are prefixed `visits_`); the route updates and deletes, so its credential needs both. `readVisit(db, id)` returns one visit's pages and lines, for its trace.
97
+
80
98
  ## Installation
81
99
 
82
100
  ```bash
@@ -160,6 +178,30 @@ Node, Bun and Deno write every event through `console.error` (stdout stays data,
160
178
  | `LOG_TIME` | `setLogTime` | `1`, `0` | off. Puts a dim local `HH:MM:SS` before human lines; json always carries `time` |
161
179
  | `LOG_LEVEL`, `{SCOPE}_LOG_LEVEL` | `setLogLevel` | the levels | `info` |
162
180
  | `LOG_SERVICE` | `setLogService` | a name | the entry script's name without extension. A Worker or browser has none: set it, or the first json event panics |
181
+ | `LOG_SHIP` | `setLogShip` | an http(s) URL | unset. Every event that passes its threshold is also POSTed there (Shipping) |
182
+ | `LOG_SHIP_TOKEN` | `setLogShipToken` | a token | unset. Sent as `Authorization: Bearer` with each batch; a browser sends none, and `setLogShipToken` in one panics |
183
+
184
+ **Shipping.** With `LOG_SHIP` set, the logger also POSTs its events to that URL, so a process ships its stream with no code:
185
+
186
+ ```sh
187
+ LOG_SHIP=https://events.untitled.garden/ingest LOG_SHIP_TOKEN=… node server.js
188
+ ```
189
+
190
+ What ships is the stream's json line, byte for byte, whatever the console renders (human at a terminal included). A batch is lines joined by `\n`, `Content-Type: text/plain;charset=utf-8`, at most 64 KB and 50 events. It leaves 2s after its first event, at 50 events, when a span opened outside any local span ends (a root span, or a request joined from its caller: its whole trace leaves together), before a Node or Bun process exits on its own (`beforeExit`), and in a browser on `visibilitychange: hidden` and `pagehide`; whatever a hidden page sends goes through `sendBeacon`, the one request a browser finishes after the page is gone. `flushLog()` sends what waits and resolves when nothing is in flight: a Worker hands it to `ctx.waitUntil(flushLog())`, a serverless function to its runtime's `after`, and a process that calls `process.exit()` awaits it first (nothing else runs after `exit`). Shipping never throws and never blocks a write: a refused or lost batch is one `warnOnce` under scope `ship`, an event over 64 KB stays on the console and warns once. The wire and the collector's answers are the contract's, in [utils](https://github.com/adriangalilea/utils#shipping).
191
+
192
+ **A page's trace on a Next.js server** (`@adriangalilea/utils/log/next`, `next` an optional peer). A page that writes the stream (utils README, "Web pages") puts its `traceparent` on every same-origin request; these wrappers make the server's work a child of that request, in the page's trace, and hand `flushLog` to Next's `after` so a frozen instance holds nothing unsent. A request with no `traceparent` is a root span of its own.
193
+
194
+ ```typescript
195
+ import { traced, tracedAction } from '@adriangalilea/utils/log/next'
196
+
197
+ export const POST = traced('POST /api/vote', async (s, request) => {
198
+ await s.span('turso insert vote', () => db.execute(insert))
199
+ return Response.json({ ok: true })
200
+ })
201
+
202
+ // in a "use server" file
203
+ export const vote = tracedAction('vote', async (s, id: string) => { … })
204
+ ```
163
205
 
164
206
  **Reading the stream.** `LogEvent` is the wire shape as a type (a line, a span start, an ok end or a failed end, discriminated by `phase` and `status`), and `parseEvent(line)` reads one line back: an event, `null` for a line that is not one (pass it through), or a `Panic` for a JSON object with a `verb` key that breaks the shape.
165
207
 
@@ -318,6 +318,13 @@ export declare const accessControl: (opts: AccessControlOptions) => Plugin<Recor
318
318
  $clear: () => Promise<void>;
319
319
  };
320
320
  };
321
+ location: {
322
+ session: {
323
+ access?: AccessRecord;
324
+ } & LangSession & {
325
+ $clear: () => Promise<void>;
326
+ };
327
+ };
321
328
  channel_post: {
322
329
  session: {
323
330
  access?: AccessRecord;
@@ -500,13 +507,6 @@ export declare const accessControl: (opts: AccessControlOptions) => Plugin<Recor
500
507
  $clear: () => Promise<void>;
501
508
  };
502
509
  };
503
- location: {
504
- session: {
505
- access?: AccessRecord;
506
- } & LangSession & {
507
- $clear: () => Promise<void>;
508
- };
509
- };
510
510
  passport_data: {
511
511
  session: {
512
512
  access?: AccessRecord;
@@ -228,6 +228,11 @@ declare const buildLanguagePlugin: <Lang extends string>(args: {
228
228
  $clear: () => Promise<void>;
229
229
  };
230
230
  };
231
+ location: {
232
+ session: SessionLike & {
233
+ $clear: () => Promise<void>;
234
+ };
235
+ };
231
236
  channel_post: {
232
237
  session: SessionLike & {
233
238
  $clear: () => Promise<void>;
@@ -358,11 +363,6 @@ declare const buildLanguagePlugin: <Lang extends string>(args: {
358
363
  $clear: () => Promise<void>;
359
364
  };
360
365
  };
361
- location: {
362
- session: SessionLike & {
363
- $clear: () => Promise<void>;
364
- };
365
- };
366
366
  passport_data: {
367
367
  session: SessionLike & {
368
368
  $clear: () => Promise<void>;
package/dist/bot/llm.d.ts CHANGED
@@ -243,6 +243,11 @@ declare const buildHistoryPlugin: (args: {
243
243
  $clear: () => Promise<void>;
244
244
  };
245
245
  };
246
+ location: {
247
+ session: LLMSessionLike & {
248
+ $clear: () => Promise<void>;
249
+ };
250
+ };
246
251
  channel_post: {
247
252
  session: LLMSessionLike & {
248
253
  $clear: () => Promise<void>;
@@ -373,11 +378,6 @@ declare const buildHistoryPlugin: (args: {
373
378
  $clear: () => Promise<void>;
374
379
  };
375
380
  };
376
- location: {
377
- session: LLMSessionLike & {
378
- $clear: () => Promise<void>;
379
- };
380
- };
381
381
  passport_data: {
382
382
  session: LLMSessionLike & {
383
383
  $clear: () => Promise<void>;
@@ -118,6 +118,13 @@ declare const buildPlugin: (args: {
118
118
  $clear: () => Promise<void>;
119
119
  };
120
120
  };
121
+ location: {
122
+ session: {
123
+ pay?: import("./types.js").PaymentsSession;
124
+ } & import("../lang.js").LangSession & {
125
+ $clear: () => Promise<void>;
126
+ };
127
+ };
121
128
  channel_post: {
122
129
  session: {
123
130
  pay?: import("./types.js").PaymentsSession;
@@ -300,13 +307,6 @@ declare const buildPlugin: (args: {
300
307
  $clear: () => Promise<void>;
301
308
  };
302
309
  };
303
- location: {
304
- session: {
305
- pay?: import("./types.js").PaymentsSession;
306
- } & import("../lang.js").LangSession & {
307
- $clear: () => Promise<void>;
308
- };
309
- };
310
310
  passport_data: {
311
311
  session: {
312
312
  pay?: import("./types.js").PaymentsSession;
@@ -1,6 +1,6 @@
1
1
  import type { MetricsSchema, MetricsStore } from "./index.js";
2
2
  import { type ActiveActors, type Audience, type DailyMetric, type MetricsReport } from "./report.js";
3
- export type SqlValue = string | number;
3
+ export type SqlValue = string | number | null;
4
4
  export interface SqlStatement {
5
5
  sql: string;
6
6
  args: SqlValue[];
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/metrics/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC3E,OAAO,EACN,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,aAAa,EAElB,MAAM,aAAa,CAAC;AAErB,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AACvC,MAAM,WAAW,YAAY;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,QAAQ,EAAE,CAAC;CACjB;AAED,4FAA4F;AAC5F,MAAM,WAAW,aAAa;IAC7B,sCAAsC;IACtC,KAAK,CAAC,SAAS,EAAE,YAAY,GAAG,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;CAC3E;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAc,SAAQ,aAAa;IACnD,yFAAyF;IACzF,QAAQ,CAAC,UAAU,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACvD;AAED;;;GAGG;AACH,eAAO,MAAM,cAAc,uFAKjB,CAAC;AAEX,qEAAqE;AACrE,eAAO,MAAM,cAAc,88BAMjB,CAAC;AAsCX,gDAAgD;AAChD,wBAAgB,YAAY,CAC3B,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,MAAM,GACb,YAAY,CA0Dd;AAED,4FAA4F;AAC5F,wBAAsB,UAAU,CAC/B,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,MAAM,GACb,OAAO,CAAC,aAAa,CAAC,CA2BxB;AAED,uFAAuF;AACvF,wBAAsB,WAAW,CAChC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACnD,OAAO,CAAC,WAAW,EAAE,CAAC,CAmBxB;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CACjC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EACpD,MAAM,EAAE,aAAa,GACnB,OAAO,CAAC,QAAQ,CAAC,CAmDnB;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAC/B,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACnD,OAAO,CAAC,YAAY,EAAE,CAAC,CAgCzB;AAED,6FAA6F;AAC7F,wBAAsB,UAAU,CAC/B,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE;IACR,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,GAAG,CAAC,EAAE,IAAI,CAAC;CACX,GACC,OAAO,CAAC,aAAa,CAAC,CAkBxB"}
1
+ {"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/metrics/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC3E,OAAO,EACN,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,aAAa,EAElB,MAAM,aAAa,CAAC;AAErB,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;AAC9C,MAAM,WAAW,YAAY;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,QAAQ,EAAE,CAAC;CACjB;AAED,4FAA4F;AAC5F,MAAM,WAAW,aAAa;IAC7B,sCAAsC;IACtC,KAAK,CAAC,SAAS,EAAE,YAAY,GAAG,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;CAC3E;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAc,SAAQ,aAAa;IACnD,yFAAyF;IACzF,QAAQ,CAAC,UAAU,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACvD;AAED;;;GAGG;AACH,eAAO,MAAM,cAAc,uFAKjB,CAAC;AAEX,qEAAqE;AACrE,eAAO,MAAM,cAAc,88BAMjB,CAAC;AAsCX,gDAAgD;AAChD,wBAAgB,YAAY,CAC3B,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,MAAM,GACb,YAAY,CA0Dd;AAED,4FAA4F;AAC5F,wBAAsB,UAAU,CAC/B,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,MAAM,GACb,OAAO,CAAC,aAAa,CAAC,CA2BxB;AAED,uFAAuF;AACvF,wBAAsB,WAAW,CAChC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACnD,OAAO,CAAC,WAAW,EAAE,CAAC,CAmBxB;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CACjC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EACpD,MAAM,EAAE,aAAa,GACnB,OAAO,CAAC,QAAQ,CAAC,CAmDnB;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAC/B,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACnD,OAAO,CAAC,YAAY,EAAE,CAAC,CAgCzB;AAED,6FAA6F;AAC7F,wBAAsB,UAAU,CAC/B,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE;IACR,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,GAAG,CAAC,EAAE,IAAI,CAAC;CACX,GACC,OAAO,CAAC,aAAa,CAAC,CAkBxB"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * A Next.js server's half of a page's trace (utils README, "Web pages"). A
3
+ * request a page made carries the page's `traceparent` (log/web sets it on
4
+ * every same-origin fetch), so what the server does under these wrappers is
5
+ * a child of that request, in the page's trace:
6
+ *
7
+ * export const POST = traced("POST /api/vote", async (s, request) => {
8
+ * await s.span("turso insert vote", () => db.execute(...))
9
+ * return Response.json({ ok: true })
10
+ * })
11
+ *
12
+ * // in a "use server" file
13
+ * export const vote = tracedAction("vote", async (s, id: string) => { ... })
14
+ *
15
+ * A request with no traceparent is a root span of its own. Each wrapper hands
16
+ * flushLog to `after`, so a serverless instance frozen once its response is
17
+ * sent never holds unsent events (LOG_SHIP, set on the deployment, is where
18
+ * they go).
19
+ */
20
+ import { type Span } from "../universal/log.js";
21
+ /** A route handler whose work is a span, joined to the caller's trace. */
22
+ export declare function traced<A extends unknown[], R>(name: string, handler: (s: Span, request: Request, ...rest: A) => Promise<R>): (request: Request, ...rest: A) => Promise<R>;
23
+ /** A server action whose work is a span, joined to the trace of the page that called it. */
24
+ export declare function tracedAction<A extends unknown[], R>(name: string, action: (s: Span, ...args: A) => Promise<R>): (...args: A) => Promise<R>;
25
+ //# sourceMappingURL=log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../../src/next/log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,EAAiB,KAAK,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAE/D,0EAA0E;AAC1E,wBAAgB,MAAM,CAAC,CAAC,SAAS,OAAO,EAAE,EAAE,CAAC,EAC5C,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAC5D,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAO9C;AAED,4FAA4F;AAC5F,wBAAgB,YAAY,CAAC,CAAC,SAAS,OAAO,EAAE,EAAE,CAAC,EAClD,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GACzC,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAM5B"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * A Next.js server's half of a page's trace (utils README, "Web pages"). A
3
+ * request a page made carries the page's `traceparent` (log/web sets it on
4
+ * every same-origin fetch), so what the server does under these wrappers is
5
+ * a child of that request, in the page's trace:
6
+ *
7
+ * export const POST = traced("POST /api/vote", async (s, request) => {
8
+ * await s.span("turso insert vote", () => db.execute(...))
9
+ * return Response.json({ ok: true })
10
+ * })
11
+ *
12
+ * // in a "use server" file
13
+ * export const vote = tracedAction("vote", async (s, id: string) => { ... })
14
+ *
15
+ * A request with no traceparent is a root span of its own. Each wrapper hands
16
+ * flushLog to `after`, so a serverless instance frozen once its response is
17
+ * sent never holds unsent events (LOG_SHIP, set on the deployment, is where
18
+ * they go).
19
+ */
20
+ import { headers } from "next/headers";
21
+ import { after } from "next/server";
22
+ import { flushLog, log } from "../universal/log.js";
23
+ /** A route handler whose work is a span, joined to the caller's trace. */
24
+ export function traced(name, handler) {
25
+ return (request, ...rest) => {
26
+ after(flushLog);
27
+ return log
28
+ .join(request.headers.get("traceparent"))
29
+ .span(name, (s) => handler(s, request, ...rest));
30
+ };
31
+ }
32
+ /** A server action whose work is a span, joined to the trace of the page that called it. */
33
+ export function tracedAction(name, action) {
34
+ return async (...args) => {
35
+ after(flushLog);
36
+ const parent = (await headers()).get("traceparent");
37
+ return log.join(parent).span(name, (s) => action(s, ...args));
38
+ };
39
+ }
40
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/next/log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAa,MAAM,qBAAqB,CAAC;AAE/D,0EAA0E;AAC1E,MAAM,UAAU,MAAM,CACrB,IAAY,EACZ,OAA8D;IAE9D,OAAO,CAAC,OAAO,EAAE,GAAG,IAAI,EAAE,EAAE;QAC3B,KAAK,CAAC,QAAQ,CAAC,CAAC;QAChB,OAAO,GAAG;aACR,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;aACxC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC;IACnD,CAAC,CAAC;AACH,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,YAAY,CAC3B,IAAY,EACZ,MAA2C;IAE3C,OAAO,KAAK,EAAE,GAAG,IAAI,EAAE,EAAE;QACxB,KAAK,CAAC,QAAQ,CAAC,CAAC;QAChB,MAAM,MAAM,GAAG,CAAC,MAAM,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACpD,OAAO,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC;IAC/D,CAAC,CAAC;AACH,CAAC"}
@@ -76,6 +76,21 @@
76
76
  * log, stdout stays data, and cli/live's console patch keeps lines above
77
77
  * a live region. Browsers and Workers use the level's console method.
78
78
  *
79
+ * Shipping
80
+ * LOG_SHIP=<url> (setLogShip) also POSTs every event that passes its
81
+ * threshold there: the json line, whatever the console renders, in
82
+ * batches of lines joined by "\n" (text/plain;charset=utf-8, at most
83
+ * 64 KB and 50 events). A batch leaves 2s after its first event, at 50,
84
+ * when a span opened outside any local span ends (a request's trace
85
+ * leaves together), before a Node/Bun process exits (beforeExit), and in
86
+ * a browser on hidden or pagehide; whatever a hidden page sends goes
87
+ * through sendBeacon, which outlives the page. LOG_SHIP_TOKEN
88
+ * (setLogShipToken) is a bearer for servers; a browser sends none.
89
+ * flushLog() sends what waits and resolves when nothing is in flight:
90
+ * a Worker hands it to waitUntil, a process that calls process.exit()
91
+ * awaits it first. Shipping never throws and never blocks a write: a
92
+ * refused or lost batch is one warnOnce, scope ["ship"].
93
+ *
79
94
  * The wire shape is exported as LogEvent, and parseEvent(line) reads one back.
80
95
  */
81
96
  export type LogLevel = "silent" | "error" | "warn" | "info" | "debug" | "trace";
@@ -147,6 +162,16 @@ export declare function setLogFormat(format: LogFormat): void;
147
162
  export declare function setLogTime(on: boolean): void;
148
163
  /** The program's name in every event. LOG_SERVICE overrides it. */
149
164
  export declare function setLogService(name: string): void;
165
+ /** Where events are also POSTed, in batches. LOG_SHIP overrides it. */
166
+ export declare function setLogShip(url: string): void;
167
+ /** The bearer sent with each batch (servers only). LOG_SHIP_TOKEN overrides it. */
168
+ export declare function setLogShipToken(token: string): void;
169
+ /**
170
+ * Send what waits to LOG_SHIP and resolve when no batch is in flight. A
171
+ * Worker hands it to waitUntil; a process that calls process.exit() awaits it
172
+ * first. Never rejects: a lost batch is a warnOnce.
173
+ */
174
+ export declare function flushLog(): Promise<void>;
150
175
  export interface Logger {
151
176
  error(message: string, fields?: Fields): void;
152
177
  fail(message: string, fields?: Fields): void;
@@ -1 +1 @@
1
- {"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../../src/universal/log.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../../src/universal/log.ts"],"names":[],"mappings":"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"}
@@ -76,6 +76,21 @@
76
76
  * log, stdout stays data, and cli/live's console patch keeps lines above
77
77
  * a live region. Browsers and Workers use the level's console method.
78
78
  *
79
+ * Shipping
80
+ * LOG_SHIP=<url> (setLogShip) also POSTs every event that passes its
81
+ * threshold there: the json line, whatever the console renders, in
82
+ * batches of lines joined by "\n" (text/plain;charset=utf-8, at most
83
+ * 64 KB and 50 events). A batch leaves 2s after its first event, at 50,
84
+ * when a span opened outside any local span ends (a request's trace
85
+ * leaves together), before a Node/Bun process exits (beforeExit), and in
86
+ * a browser on hidden or pagehide; whatever a hidden page sends goes
87
+ * through sendBeacon, which outlives the page. LOG_SHIP_TOKEN
88
+ * (setLogShipToken) is a bearer for servers; a browser sends none.
89
+ * flushLog() sends what waits and resolves when nothing is in flight:
90
+ * a Worker hands it to waitUntil, a process that calls process.exit()
91
+ * awaits it first. Shipping never throws and never blocks a write: a
92
+ * refused or lost batch is one warnOnce, scope ["ship"].
93
+ *
79
94
  * The wire shape is exported as LogEvent, and parseEvent(line) reads one back.
80
95
  */
81
96
  import { assert, panic, SourcedError } from "../offensive.js";
@@ -468,6 +483,7 @@ function emit(d) {
468
483
  // Every knob is read on every event, so a bad value screams whichever rendering is on.
469
484
  const format = currentFormat();
470
485
  const clock = timeEnabled();
486
+ const target = shipTarget();
471
487
  const text = format === "json" ? json(d, now) : human(d, now, clock);
472
488
  // Node: everything to stderr through console.error, which cli/live patches
473
489
  // to keep lines above an active region. Elsewhere the level's method, so
@@ -476,6 +492,173 @@ function emit(d) {
476
492
  console.error(text);
477
493
  else
478
494
  console[CONSOLE_METHOD[VERBS[d.verb].level]](text);
495
+ // What ships is the stream's json line, whatever the console renders.
496
+ if (target !== undefined)
497
+ ship(target, format === "json" ? text : json(d, now));
498
+ }
499
+ // --- shipping ----------------------------------------------------------------
500
+ /** A batch's ceiling in UTF-8 bytes: navigator.sendBeacon's. */
501
+ const SHIP_MAX_BYTES = 64 * 1024;
502
+ const SHIP_MAX_EVENTS = 50;
503
+ const SHIP_AFTER_MS = 2000;
504
+ let shipSetting;
505
+ let shipTokenSetting;
506
+ function parseShip(raw, source) {
507
+ if (!URL.canParse(raw))
508
+ panic(`unknown ${source}:`, raw, "(want a URL)");
509
+ const url = new URL(raw);
510
+ if (url.protocol !== "https:" && url.protocol !== "http:")
511
+ panic(`unknown ${source}:`, raw, "(want an http(s) URL)");
512
+ return url.href;
513
+ }
514
+ /** Where events are also POSTed, in batches. LOG_SHIP overrides it. */
515
+ export function setLogShip(url) {
516
+ shipSetting = parseShip(url, "log ship");
517
+ }
518
+ /** The bearer sent with each batch (servers only). LOG_SHIP_TOKEN overrides it. */
519
+ export function setLogShipToken(token) {
520
+ assert(sink !== "browser", "a browser ships without a token: any token in a page is public");
521
+ if (typeof token !== "string" || token.length === 0)
522
+ panic("setLogShipToken wants a non-empty token, got:", token);
523
+ shipTokenSetting = token;
524
+ }
525
+ // LOG_SHIP is read live like every variable; the parse is kept per value.
526
+ let shipParsed;
527
+ function shipTarget() {
528
+ const fromEnv = env("LOG_SHIP");
529
+ const raw = fromEnv ?? shipSetting;
530
+ if (raw === undefined)
531
+ return undefined;
532
+ if (shipParsed?.raw !== raw)
533
+ shipParsed = {
534
+ raw,
535
+ href: fromEnv === undefined ? raw : parseShip(raw, "LOG_SHIP"),
536
+ };
537
+ return shipParsed.href;
538
+ }
539
+ const shipLog = () => log.scope("ship");
540
+ const utf8 = new TextEncoder();
541
+ let batch;
542
+ let batchTimer;
543
+ const inflight = new Set();
544
+ let exitHooked = false;
545
+ function ship(target, text) {
546
+ hookExit();
547
+ const bytes = utf8.encode(text).length;
548
+ if (bytes > SHIP_MAX_BYTES) {
549
+ shipLog().warnOnce("event too large to ship", {
550
+ bytes,
551
+ limit: SHIP_MAX_BYTES,
552
+ });
553
+ return;
554
+ }
555
+ if (batch !== undefined &&
556
+ (batch.target !== target || batch.bytes + 1 + bytes > SHIP_MAX_BYTES))
557
+ send();
558
+ if (batch === undefined) {
559
+ batch = { target, lines: [], bytes: -1 };
560
+ // A pending batch never keeps a process alive: beforeExit sends it.
561
+ const timer = setTimeout(send, SHIP_AFTER_MS);
562
+ timer.unref?.();
563
+ batchTimer = timer;
564
+ }
565
+ batch.lines.push(text);
566
+ batch.bytes += 1 + bytes;
567
+ if (batch.lines.length >= SHIP_MAX_EVENTS)
568
+ send();
569
+ }
570
+ /** Take the waiting batch, if any, and stop its timer. */
571
+ function takeBatch() {
572
+ if (batchTimer !== undefined)
573
+ clearTimeout(batchTimer);
574
+ batchTimer = undefined;
575
+ const taken = batch;
576
+ batch = undefined;
577
+ return taken;
578
+ }
579
+ function send() {
580
+ // A hidden page may be gone before a fetch answers: the beacon outlives it.
581
+ if (sink === "browser" &&
582
+ globalThis.document.visibilityState ===
583
+ "hidden") {
584
+ beacon();
585
+ return;
586
+ }
587
+ const b = takeBatch();
588
+ if (b === undefined)
589
+ return;
590
+ const p = post(b.target, b.lines.join("\n")).finally(() => inflight.delete(p));
591
+ inflight.add(p);
592
+ }
593
+ async function post(target, body) {
594
+ const token = sink === "browser"
595
+ ? undefined
596
+ : (env("LOG_SHIP_TOKEN") ?? shipTokenSetting);
597
+ const headers = {
598
+ "content-type": "text/plain;charset=utf-8",
599
+ };
600
+ if (token !== undefined)
601
+ headers.authorization = `Bearer ${token}`;
602
+ try {
603
+ const res = await fetch(target, {
604
+ method: "POST",
605
+ headers,
606
+ body,
607
+ keepalive: sink === "browser",
608
+ });
609
+ if (res.status === 204)
610
+ return;
611
+ shipLog().warnOnce("log batch refused", {
612
+ status: res.status,
613
+ answer: (await res.text()).slice(0, 1000),
614
+ });
615
+ }
616
+ catch (err) {
617
+ shipLog().warnOnce("log batch not delivered", { err });
618
+ }
619
+ }
620
+ // Hidden is the last moment a page reliably sends anything; sendBeacon is the
621
+ // one request the browser finishes after the page is gone.
622
+ function beacon() {
623
+ const b = takeBatch();
624
+ if (b === undefined)
625
+ return;
626
+ const page = globalThis;
627
+ if (!page.navigator.sendBeacon(b.target, b.lines.join("\n")))
628
+ shipLog().warnOnce("log batch refused by the browser", {
629
+ events: b.lines.length,
630
+ });
631
+ }
632
+ // Hooked on the first shipped event, so importing the logger has no effect.
633
+ function hookExit() {
634
+ if (exitHooked)
635
+ return;
636
+ exitHooked = true;
637
+ if (runtime.isNode || runtime.isBun) {
638
+ process.on("beforeExit", () => send());
639
+ return;
640
+ }
641
+ if (sink !== "browser")
642
+ return;
643
+ const page = globalThis;
644
+ page.document.addEventListener("visibilitychange", () => {
645
+ if (page.document.visibilityState === "hidden")
646
+ beacon();
647
+ });
648
+ page.addEventListener("pagehide", beacon);
649
+ }
650
+ /**
651
+ * Send what waits to LOG_SHIP and resolve when no batch is in flight. A
652
+ * Worker hands it to waitUntil; a process that calls process.exit() awaits it
653
+ * first. Never rejects: a lost batch is a warnOnce.
654
+ */
655
+ export async function flushLog() {
656
+ send();
657
+ while (inflight.size > 0) {
658
+ await Promise.all(inflight);
659
+ // A refused batch's warning is an event too: it ships next.
660
+ send();
661
+ }
479
662
  }
480
663
  const BOUND = 1024;
481
664
  const warned = new Set();
@@ -531,6 +714,52 @@ function open(ctx, name, initial) {
531
714
  parent,
532
715
  fields,
533
716
  });
717
+ function finish(err) {
718
+ const dur = performance.now() - began;
719
+ const failed = err !== undefined && err !== null;
720
+ const printed = err instanceof Error && caught.has(err) ? err : undefined;
721
+ // What spans inside this one printed stays printed for the spans around it.
722
+ for (const e of caught)
723
+ ctx.caught?.add(e);
724
+ if (!shown) {
725
+ // The end follows the decision made at begin: no end without its
726
+ // start. A failure is never lost to it: one line of the nearest
727
+ // shown span says the span failed.
728
+ if (!failed || !admits(ctx.scope, "error"))
729
+ return;
730
+ if (err instanceof Error)
731
+ ctx.caught?.add(err);
732
+ emit({
733
+ verb: "fail",
734
+ msg: name,
735
+ scope: ctx.scope,
736
+ depth: inner.depth,
737
+ trace: inner.owner === undefined ? undefined : trace,
738
+ span: inner.owner,
739
+ fields: { ...fields, ...added, err, dur_ms: round3(dur) },
740
+ printed,
741
+ });
742
+ return;
743
+ }
744
+ if (failed && err instanceof Error)
745
+ ctx.caught?.add(err);
746
+ const shows = failed ? { ...added, err } : added;
747
+ emit({
748
+ verb: failed ? "fail" : "success",
749
+ msg: name,
750
+ scope: ctx.scope,
751
+ depth: ctx.depth,
752
+ phase: "end",
753
+ trace,
754
+ span: id,
755
+ parent,
756
+ status: failed ? "error" : "ok",
757
+ dur,
758
+ fields: { ...fields, ...shows },
759
+ shows,
760
+ printed,
761
+ });
762
+ }
534
763
  // A hidden span is transparent: its lines and children attach to the nearest
535
764
  // shown span, a joined remote one included (it is shown in its own process's
536
765
  // stream). With none, its lines carry no ids, as lines outside any span.
@@ -565,50 +794,11 @@ function open(ctx, name, initial) {
565
794
  end(err) {
566
795
  assert(!ended, "span", name, "ended twice");
567
796
  ended = true;
568
- const dur = performance.now() - began;
569
- const failed = err !== undefined && err !== null;
570
- const printed = err instanceof Error && caught.has(err) ? err : undefined;
571
- // What spans inside this one printed stays printed for the spans around it.
572
- for (const e of caught)
573
- ctx.caught?.add(e);
574
- if (!shown) {
575
- // The end follows the decision made at begin: no end without its
576
- // start. A failure is never lost to it: one line of the nearest
577
- // shown span says the span failed.
578
- if (!failed || !admits(ctx.scope, "error"))
579
- return;
580
- if (err instanceof Error)
581
- ctx.caught?.add(err);
582
- emit({
583
- verb: "fail",
584
- msg: name,
585
- scope: ctx.scope,
586
- depth: inner.depth,
587
- trace: inner.owner === undefined ? undefined : trace,
588
- span: inner.owner,
589
- fields: { ...fields, ...added, err, dur_ms: round3(dur) },
590
- printed,
591
- });
592
- return;
593
- }
594
- if (failed && err instanceof Error)
595
- ctx.caught?.add(err);
596
- const shows = failed ? { ...added, err } : added;
597
- emit({
598
- verb: failed ? "fail" : "success",
599
- msg: name,
600
- scope: ctx.scope,
601
- depth: ctx.depth,
602
- phase: "end",
603
- trace,
604
- span: id,
605
- parent,
606
- status: failed ? "error" : "ok",
607
- dur,
608
- fields: { ...fields, ...shows },
609
- shows,
610
- printed,
611
- });
797
+ finish(err);
798
+ // A span opened outside any local span (a root, or a request joined
799
+ // from its caller) ends its trace here: what it wrote ships together.
800
+ if (ctx.caught === undefined)
801
+ send();
612
802
  },
613
803
  });
614
804
  }