effect-inspect 0.1.0 → 0.2.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.
Files changed (35) hide show
  1. package/README.md +73 -4
  2. package/app/dist/client/assets/{index-smV05cfr.js → index-T-bCzOWw.js} +2 -2
  3. package/app/dist/client/assets/routes-C2qO8k2W.js +5 -0
  4. package/app/dist/server/assets/_tanstack-start-manifest_v-B2BMiICr.js +20 -0
  5. package/app/dist/server/assets/{router-CN98Ramo.js → router-dMcw-pHq.js} +1 -1
  6. package/app/dist/server/assets/{routes-eZ4XqxE9.js → routes-BmnrEVoN.js} +203 -157
  7. package/app/dist/server/server.js +2 -2
  8. package/dist/cli/QueryCommands.d.ts +127 -0
  9. package/dist/cli/QueryCommands.js +768 -0
  10. package/dist/cli.d.ts +1 -2
  11. package/dist/cli.js +150 -8
  12. package/dist/client/Client.d.ts +59 -5
  13. package/dist/client/Client.js +196 -16
  14. package/dist/collector/QueryApi.d.ts +29 -0
  15. package/dist/collector/QueryApi.js +91 -0
  16. package/dist/collector/Server.d.ts +1 -1
  17. package/dist/collector/Server.js +15 -8
  18. package/dist/collector/Store.d.ts +56 -16
  19. package/dist/collector/Store.js +37 -11
  20. package/dist/protocol/Codec.d.ts +10 -0
  21. package/dist/protocol/Schema.d.ts +118 -1
  22. package/dist/protocol/Schema.js +53 -1
  23. package/dist/query/Client.d.ts +30 -0
  24. package/dist/query/Client.js +74 -0
  25. package/dist/query/Query.d.ts +599 -0
  26. package/dist/query/Query.js +876 -0
  27. package/dist/trace/Timing.d.ts +18 -0
  28. package/dist/trace/Timing.js +57 -0
  29. package/dist/trace/TraceFile.d.ts +54 -0
  30. package/dist/trace/TraceFile.js +86 -0
  31. package/dist/trace/TraceStore.d.ts +202 -0
  32. package/dist/trace/TraceStore.js +328 -0
  33. package/package.json +3 -1
  34. package/app/dist/client/assets/routes-TKgeFdSW.js +0 -5
  35. package/app/dist/server/assets/_tanstack-start-manifest_v-Co953HeC.js +0 -20
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The collector's read-only query API over HTTP, on the collector's port.
3
+ *
4
+ * - `POST /api/v1/query` — body: a `Query.QueryRequest` JSON object. Reply:
5
+ * a `Query.QueryResponse` JSON object. The body is authoritative; the
6
+ * status mirrors it (200 ok, 400 InvalidRequest, 404 SessionNotFound or
7
+ * SpanNotFound, 409 SessionConflict, 413 ResponseTooLarge). Every JSON
8
+ * reply, errors included, is held to `Query.limits.responseBytes`.
9
+ * - `GET /api/v1/export?sessionId=ID` — the session's frozen snapshot as
10
+ * `.eitrace` text with its loss counters in the header (200), or a
11
+ * `QueryFailure` JSON body (400/404). A conflicted session still exports,
12
+ * so the evidence is kept; queries against the file refuse it the same way.
13
+ * Export is a lossless artifact transfer, so it is not held to the JSON
14
+ * response bound: its size is the retained session (up to capacity).
15
+ *
16
+ * Each request answers from one atomic snapshot of one session. Two requests
17
+ * against an active session see different snapshots; compare
18
+ * `completeness.messagesObserved` to tell whether data changed between pages.
19
+ */
20
+ import { Clock, Effect, Result } from 'effect';
21
+ import { HttpServerRequest, HttpServerResponse } from 'effect/unstable/http';
22
+ import * as Query from '../query/Query.js';
23
+ import { Store } from './Store.js';
24
+ /** Path prefix of the query API. */
25
+ export const apiPath = '/api/v1/';
26
+ const statusOf = (response) => {
27
+ if (response.ok)
28
+ return 200;
29
+ switch (response.error._tag) {
30
+ case 'InvalidRequest':
31
+ return 400;
32
+ case 'SessionNotFound':
33
+ case 'SpanNotFound':
34
+ return 404;
35
+ case 'SessionConflict':
36
+ return 409;
37
+ case 'ResponseTooLarge':
38
+ return 413;
39
+ default:
40
+ return 500;
41
+ }
42
+ };
43
+ /** Every JSON reply goes through the response byte bound, errors included. */
44
+ const reply = (unbounded) => {
45
+ const response = Query.limitResponse(unbounded);
46
+ return HttpServerResponse.jsonUnsafe(response, { status: statusOf(response) });
47
+ };
48
+ /** Answers one decoded request from the store. */
49
+ export const answer = (input) => Effect.gen(function* () {
50
+ const decoded = Query.decodeRequest(input);
51
+ if (Result.isFailure(decoded))
52
+ return decoded.failure;
53
+ const request = decoded.success;
54
+ const store = yield* Store;
55
+ if (request.op === 'sessions') {
56
+ return Query.listSessions({ kind: 'live', file: null }, yield* store.sessions, request);
57
+ }
58
+ if (request.sessionId === undefined)
59
+ return Query.sessionRequired(request.op);
60
+ const snapshot = yield* store.snapshot(request.sessionId);
61
+ if (snapshot === undefined)
62
+ return Query.liveSessionNotFound(request.op, request.sessionId);
63
+ // ponytail: rebuilds the trace model per request (~O(retained messages)),
64
+ // synchronously, which can delay ingest on this event loop meanwhile.
65
+ // Cache by `messagesObserved` if repeated queries on huge sessions show up.
66
+ return Query.run(Query.fromSnapshot(snapshot, yield* Clock.currentTimeMillis), request);
67
+ });
68
+ /** Handles a request under {@link apiPath}. */
69
+ export const handle = (request, url) => Effect.gen(function* () {
70
+ if (url.pathname === `${apiPath}query` && request.method === 'POST') {
71
+ const body = yield* Effect.result(request.json);
72
+ if (Result.isFailure(body)) {
73
+ return reply(Query.failure(null, 'InvalidRequest', 'The request body is not JSON.', 'Send a JSON query object.'));
74
+ }
75
+ return reply(yield* answer(body.success));
76
+ }
77
+ if (url.pathname === `${apiPath}export` && request.method === 'GET') {
78
+ const sessionId = url.searchParams.get('sessionId');
79
+ if (sessionId === null)
80
+ return reply(Query.sessionRequired('export'));
81
+ const store = yield* Store;
82
+ const snapshot = yield* store.snapshot(sessionId);
83
+ if (snapshot === undefined)
84
+ return reply(Query.liveSessionNotFound('export', sessionId));
85
+ const source = Query.fromSnapshot(snapshot, yield* Clock.currentTimeMillis);
86
+ return HttpServerResponse.text(Query.toTraceFile(source), {
87
+ contentType: 'application/x-ndjson',
88
+ });
89
+ }
90
+ return reply(Query.failure(null, 'InvalidRequest', `Unknown endpoint ${request.method} ${url.pathname}.`, `Use POST ${apiPath}query or GET ${apiPath}export?sessionId=ID.`));
91
+ });
@@ -20,5 +20,5 @@ export declare const webappPath = "/webapp";
20
20
  * released when that one connection ends, and nothing outlives it.
21
21
  */
22
22
  export declare const handleConnection: (socket: Socket.Socket, path: string) => Effect.Effect<void, never, Store>;
23
- /** Serves instrumented clients, webapp sockets, and the bundled web UI. */
23
+ /** Serves instrumented clients, webapp sockets, the query API and the bundled web UI. */
24
24
  export declare const run: (fetch?: (request: Request) => Promise<Response>) => Effect.Effect<never, never, HttpServer.HttpServer | import("effect/Scope").Scope | Store>;
@@ -11,6 +11,7 @@ import { Effect, Fiber, PubSub, Result } from 'effect';
11
11
  import { HttpServer, HttpServerRequest, HttpServerResponse } from 'effect/unstable/http';
12
12
  import { Socket } from 'effect/unstable/socket';
13
13
  import { clientCodec, collectorCodec, webappCodec, webappRequestCodec } from '../protocol/Codec.js';
14
+ import * as QueryApi from './QueryApi.js';
14
15
  import { Store } from './Store.js';
15
16
  /** Path a webapp client connects on; anything else is an instrumented program. */
16
17
  export const webappPath = '/webapp';
@@ -71,26 +72,29 @@ const handleClient = Effect.fnUntraced(function* (socket) {
71
72
  const store = yield* Store;
72
73
  const pull = yield* readerFor(socket);
73
74
  const writer = yield* socket.writer;
75
+ /** This connection's identity, so the store can tell it from a collision. */
76
+ const connection = Symbol('connection');
74
77
  /** The session this connection belongs to, learned from its `Hello`. */
75
78
  let sessionId;
76
79
  const onMessage = (message) => Effect.gen(function* () {
77
80
  sessionId = message.sessionId;
78
81
  switch (message._tag) {
79
82
  case 'Hello':
80
- yield* store.hello(message);
83
+ yield* store.hello(message, connection);
81
84
  return;
82
85
  case 'Ping':
83
86
  // A failed write means the client is gone; the read loop notices.
84
87
  yield* Effect.ignore(writer.write(collectorCodec.encode({ _tag: 'Pong', sessionId: message.sessionId })));
85
88
  return;
86
89
  default:
87
- yield* store.append(message);
90
+ yield* store.append(message, connection);
88
91
  }
89
92
  });
90
- yield* readLines(pull, clientCodec.decode, onMessage, () => Effect.suspend(() => store.skipLine(sessionId))).pipe(
93
+ yield* readLines(pull, clientCodec.decode, onMessage, () => Effect.suspend(() => store.skipLine(sessionId, connection))).pipe(
91
94
  // However the connection ends — clean close, crash, kill -9 — the session
92
- // is marked ended. A reconnect with the same id resumes it.
93
- Effect.ensuring(Effect.suspend(() => (sessionId === undefined ? Effect.void : store.end(sessionId)))));
95
+ // is marked ended, if this connection still owns it. A reconnect from the
96
+ // same client instance resumes it.
97
+ Effect.ensuring(Effect.suspend(() => sessionId === undefined ? Effect.void : store.end(sessionId, connection))));
94
98
  });
95
99
  /** Handles one webapp client's connection for its lifetime. */
96
100
  const handleWebapp = Effect.fnUntraced(function* (socket) {
@@ -146,12 +150,15 @@ const handleWebapp = Effect.fnUntraced(function* (socket) {
146
150
  * released when that one connection ends, and nothing outlives it.
147
151
  */
148
152
  export const handleConnection = (socket, path) => Effect.scoped(path === webappPath ? handleWebapp(socket) : handleClient(socket));
149
- /** Serves instrumented clients, webapp sockets, and the bundled web UI. */
153
+ /** Serves instrumented clients, webapp sockets, the query API and the bundled web UI. */
150
154
  export const run = (fetch) => Effect.gen(function* () {
151
155
  const server = yield* HttpServer.HttpServer;
152
156
  yield* server.serve(Effect.gen(function* () {
153
157
  const request = yield* HttpServerRequest.HttpServerRequest;
154
- const path = new URL(request.url, 'http://localhost').pathname;
158
+ const url = new URL(request.url, 'http://localhost');
159
+ const path = url.pathname;
160
+ if (path.startsWith(QueryApi.apiPath))
161
+ return yield* QueryApi.handle(request, url);
155
162
  if (request.headers.upgrade?.toLowerCase() === 'websocket') {
156
163
  const socket = yield* request.upgrade;
157
164
  yield* handleConnection(socket, path);
@@ -162,7 +169,7 @@ export const run = (fetch) => Effect.gen(function* () {
162
169
  status: 426,
163
170
  });
164
171
  }
165
- const response = yield* Effect.promise(() => fetch(new Request(new URL(request.url, 'http://localhost').href, {
172
+ const response = yield* Effect.promise(() => fetch(new Request(url.href, {
166
173
  method: request.method,
167
174
  headers: request.headers,
168
175
  })));
@@ -8,6 +8,11 @@
8
8
  * Ingest never blocks on a consumer. The ring drops its oldest message when
9
9
  * full and the live `PubSub` is sliding, so neither a long-running program nor
10
10
  * a stalled webapp client can apply backpressure to the instrumented program.
11
+ *
12
+ * A session belongs to the first client instance that announced its ID. Only
13
+ * that instance's current connection may write to it or end it; a different
14
+ * instance reusing the ID is refused and counted in `Session.conflicts`, so a
15
+ * collision can never merge two runs or overwrite the original trace.
11
16
  */
12
17
  import { Context, Effect, Layer, PubSub } from 'effect';
13
18
  import * as Protocol from '../protocol/Schema.ts';
@@ -21,16 +26,40 @@ export interface SessionSnapshot {
21
26
  readonly droppedMessages: number;
22
27
  /** Lines received for this session that could not be decoded. */
23
28
  readonly skippedLines: number;
29
+ /**
30
+ * Whether the owner sent an instance ID, so a reused ID is refused and
31
+ * counted. `false` for an older client: a reused ID would have merged.
32
+ */
33
+ readonly conflictDetection: boolean;
24
34
  }
35
+ /**
36
+ * One client connection, as the store knows it: an identity token only.
37
+ *
38
+ * Passed with every write so the store can tell the owning connection from a
39
+ * stale one (a reconnect already replaced it) or a refused one (an ID
40
+ * collision).
41
+ */
42
+ export type Connection = symbol;
25
43
  declare const Store_base: Context.ServiceClass<Store, "effect-inspect/collector/Store", {
26
- /** Opens a session, or resumes the existing one when a program reconnects. */
27
- readonly hello: (message: Protocol.Hello) => Effect.Effect<void>;
28
- /** Records one decoded message and fans it out to live subscribers. */
29
- readonly append: (message: Protocol.ClientMessage) => Effect.Effect<void>;
30
- /** Counts one line that could not be decoded. */
31
- readonly skipLine: (sessionId: Protocol.SessionId | undefined) => Effect.Effect<void>;
32
- /** Marks a session ended because its program disconnected. */
33
- readonly end: (sessionId: Protocol.SessionId) => Effect.Effect<void>;
44
+ /**
45
+ * Opens a session for `connection`, or resumes it when the same client
46
+ * instance reconnects. A different instance announcing a known ID is a
47
+ * collision: it is refused and counted, and the session is left untouched.
48
+ */
49
+ readonly hello: (message: Protocol.Hello, connection: Connection) => Effect.Effect<void>;
50
+ /**
51
+ * Records one decoded message and fans it out to live subscribers — only
52
+ * when `connection` owns the message's session.
53
+ */
54
+ readonly append: (message: Protocol.ClientMessage, connection: Connection) => Effect.Effect<void>;
55
+ /** Counts one line that could not be decoded, if `connection` owns the session. */
56
+ readonly skipLine: (sessionId: Protocol.SessionId | undefined, connection: Connection) => Effect.Effect<void>;
57
+ /**
58
+ * Marks a session ended because `connection` closed. A no-op unless it is
59
+ * the owner's current connection, so neither a refused collision nor a
60
+ * connection a reconnect already replaced can end the owner's run.
61
+ */
62
+ readonly end: (sessionId: Protocol.SessionId, connection: Connection) => Effect.Effect<void>;
34
63
  /** Every known session, in the order they first said `Hello`. */
35
64
  readonly sessions: Effect.Effect<ReadonlyArray<Protocol.Session>>;
36
65
  /** One session's retained messages and loss counters. */
@@ -57,14 +86,25 @@ export declare class Store extends Store_base {
57
86
  export declare const make: (options?: {
58
87
  readonly capacity?: number;
59
88
  } | undefined) => Effect.Effect<{
60
- /** Opens a session, or resumes the existing one when a program reconnects. */
61
- readonly hello: (message: Protocol.Hello) => Effect.Effect<void>;
62
- /** Records one decoded message and fans it out to live subscribers. */
63
- readonly append: (message: Protocol.ClientMessage) => Effect.Effect<void>;
64
- /** Counts one line that could not be decoded. */
65
- readonly skipLine: (sessionId: Protocol.SessionId | undefined) => Effect.Effect<void>;
66
- /** Marks a session ended because its program disconnected. */
67
- readonly end: (sessionId: Protocol.SessionId) => Effect.Effect<void>;
89
+ /**
90
+ * Opens a session for `connection`, or resumes it when the same client
91
+ * instance reconnects. A different instance announcing a known ID is a
92
+ * collision: it is refused and counted, and the session is left untouched.
93
+ */
94
+ readonly hello: (message: Protocol.Hello, connection: Connection) => Effect.Effect<void>;
95
+ /**
96
+ * Records one decoded message and fans it out to live subscribers — only
97
+ * when `connection` owns the message's session.
98
+ */
99
+ readonly append: (message: Protocol.ClientMessage, connection: Connection) => Effect.Effect<void>;
100
+ /** Counts one line that could not be decoded, if `connection` owns the session. */
101
+ readonly skipLine: (sessionId: Protocol.SessionId | undefined, connection: Connection) => Effect.Effect<void>;
102
+ /**
103
+ * Marks a session ended because `connection` closed. A no-op unless it is
104
+ * the owner's current connection, so neither a refused collision nor a
105
+ * connection a reconnect already replaced can end the owner's run.
106
+ */
107
+ readonly end: (sessionId: Protocol.SessionId, connection: Connection) => Effect.Effect<void>;
68
108
  /** Every known session, in the order they first said `Hello`. */
69
109
  readonly sessions: Effect.Effect<ReadonlyArray<Protocol.Session>>;
70
110
  /** One session's retained messages and loss counters. */
@@ -8,6 +8,11 @@
8
8
  * Ingest never blocks on a consumer. The ring drops its oldest message when
9
9
  * full and the live `PubSub` is sliding, so neither a long-running program nor
10
10
  * a stalled webapp client can apply backpressure to the instrumented program.
11
+ *
12
+ * A session belongs to the first client instance that announced its ID. Only
13
+ * that instance's current connection may write to it or end it; a different
14
+ * instance reusing the ID is refused and counted in `Session.conflicts`, so a
15
+ * collision can never merge two runs or overwrite the original trace.
11
16
  */
12
17
  import { Clock, Context, Effect, Layer, PubSub } from 'effect';
13
18
  import * as Protocol from '../protocol/Schema.js';
@@ -29,17 +34,31 @@ export const make = Effect.fnUntraced(function* (options) {
29
34
  const sessions = new Map();
30
35
  const changes = yield* PubSub.sliding(1);
31
36
  const notify = PubSub.publish(changes, undefined).pipe(Effect.asVoid);
32
- const hello = (message) => Effect.gen(function* () {
37
+ const hello = (message, connection) => Effect.gen(function* () {
33
38
  const existing = sessions.get(message.sessionId);
34
39
  if (existing !== undefined) {
35
- // A reconnect resumes the session rather than starting a new one, so a
36
- // program that is killed and restarted keeps one continuous trace.
40
+ // Same instance means the client's socket dropped and it dialled back:
41
+ // resume, so one run keeps one continuous trace. Anything else is an
42
+ // independent run that chose the same ID. Refusing it keeps the
43
+ // original trace intact, and counting it lets a query report the
44
+ // collision rather than serve this run's data as the newcomer's.
45
+ if (existing.instanceId !== message.instanceId) {
46
+ existing.session = {
47
+ ...existing.session,
48
+ conflicts: (existing.session.conflicts ?? 0) + 1,
49
+ };
50
+ yield* notify;
51
+ return;
52
+ }
53
+ existing.connection = connection;
37
54
  existing.session = { ...existing.session, active: true };
38
55
  delete existing.session.endedAtEpochMillis;
39
56
  yield* notify;
40
57
  return;
41
58
  }
42
59
  sessions.set(message.sessionId, {
60
+ instanceId: message.instanceId,
61
+ connection,
43
62
  session: {
44
63
  sessionId: message.sessionId,
45
64
  program: message.program,
@@ -56,10 +75,16 @@ export const make = Effect.fnUntraced(function* (options) {
56
75
  });
57
76
  yield* notify;
58
77
  });
59
- const append = (message) => Effect.suspend(() => {
60
- const state = sessions.get(message.sessionId);
61
- // A message for a session that never said Hello has nowhere to go. It is
62
- // a client bug, not a reason to drop the connection.
78
+ /** The session `connection` currently owns under `sessionId`, if any. */
79
+ const owned = (sessionId, connection) => {
80
+ const state = sessionId === undefined ? undefined : sessions.get(sessionId);
81
+ return state?.connection === connection ? state : undefined;
82
+ };
83
+ const append = (message, connection) => Effect.suspend(() => {
84
+ const state = owned(message.sessionId, connection);
85
+ // A message for a session this connection does not own — it never said
86
+ // Hello, or it was refused as a collision — has nowhere to go. It is not
87
+ // a reason to drop the connection either.
63
88
  if (state === undefined)
64
89
  return Effect.void;
65
90
  if (state.ring.length < capacity) {
@@ -74,13 +99,13 @@ export const make = Effect.fnUntraced(function* (options) {
74
99
  });
75
100
  // A line from a connection that never sent a usable `Hello` has no session
76
101
  // to count it against, so it is skipped without a counter.
77
- const skipLine = (sessionId) => Effect.sync(() => {
78
- const state = sessionId === undefined ? undefined : sessions.get(sessionId);
102
+ const skipLine = (sessionId, connection) => Effect.sync(() => {
103
+ const state = owned(sessionId, connection);
79
104
  if (state !== undefined)
80
105
  state.skippedLines += 1;
81
106
  });
82
- const end = (sessionId) => Effect.flatMap(Clock.currentTimeMillis, (now) => {
83
- const state = sessions.get(sessionId);
107
+ const end = (sessionId, connection) => Effect.flatMap(Clock.currentTimeMillis, (now) => {
108
+ const state = owned(sessionId, connection);
84
109
  if (state === undefined || !state.session.active)
85
110
  return Effect.void;
86
111
  state.session = {
@@ -101,6 +126,7 @@ export const make = Effect.fnUntraced(function* (options) {
101
126
  : state.ring.slice(state.head).concat(state.ring.slice(0, state.head)),
102
127
  droppedMessages: state.droppedMessages,
103
128
  skippedLines: state.skippedLines,
129
+ conflictDetection: state.instanceId !== undefined,
104
130
  };
105
131
  });
106
132
  const live = (sessionId) => Effect.sync(() => sessions.get(sessionId)?.live);
@@ -61,6 +61,7 @@ export declare const clientCodec: Codec<{
61
61
  readonly startTime: bigint;
62
62
  readonly wallClockEpochMillis: number;
63
63
  };
64
+ readonly instanceId?: string | undefined;
64
65
  } | {
65
66
  readonly sessionId: string;
66
67
  readonly _tag: "SpanStart";
@@ -225,6 +226,7 @@ export declare const webappCodec: Codec<{
225
226
  };
226
227
  readonly active: boolean;
227
228
  readonly endedAtEpochMillis?: number | undefined;
229
+ readonly conflicts?: number | undefined;
228
230
  }[];
229
231
  } | {
230
232
  readonly sessionId: string;
@@ -240,6 +242,7 @@ export declare const webappCodec: Codec<{
240
242
  readonly startTime: bigint;
241
243
  readonly wallClockEpochMillis: number;
242
244
  };
245
+ readonly instanceId?: string | undefined;
243
246
  } | {
244
247
  readonly sessionId: string;
245
248
  readonly _tag: "SpanStart";
@@ -397,6 +400,7 @@ export declare const webappCodec: Codec<{
397
400
  readonly startTime: bigint;
398
401
  readonly wallClockEpochMillis: number;
399
402
  };
403
+ readonly instanceId?: string | undefined;
400
404
  } | {
401
405
  readonly sessionId: string;
402
406
  readonly _tag: "SpanStart";
@@ -569,7 +573,13 @@ export declare const traceFileHeaderCodec: Codec<{
569
573
  };
570
574
  readonly active: boolean;
571
575
  readonly endedAtEpochMillis?: number | undefined;
576
+ readonly conflicts?: number | undefined;
572
577
  };
573
578
  readonly savedAtEpochMillis: number;
579
+ readonly capture?: {
580
+ readonly droppedMessages: number;
581
+ readonly skippedLines: number;
582
+ readonly conflictDetection: boolean;
583
+ } | undefined;
574
584
  }>;
575
585
  export {};
@@ -34,9 +34,25 @@ export declare const Json: Schema.Codec<Json>;
34
34
  /** Span attributes, log annotations, and metric/event attributes. */
35
35
  export declare const Attributes: Schema.$Record<Schema.String, Schema.Codec<Json, Json, never, never>>;
36
36
  export type Attributes = Schema.Schema.Type<typeof Attributes>;
37
- /** Identifies one run of an instrumented program. */
37
+ /**
38
+ * Identifies one run of an instrumented program.
39
+ *
40
+ * Any string on the wire, so traces from older clients keep decoding. IDs a
41
+ * caller chooses are held to {@link isValidSessionId} by the client instead.
42
+ */
38
43
  export declare const SessionId: Schema.String;
39
44
  export type SessionId = Schema.Schema.Type<typeof SessionId>;
45
+ /** What {@link isValidSessionId} accepts, phrased for diagnostics. */
46
+ export declare const sessionIdRule = "1-128 ASCII letters, digits, \".\", \"_\" or \"-\", starting with a letter or digit";
47
+ /**
48
+ * Whether a caller-chosen session ID is acceptable, such as `checkout-before-1`.
49
+ *
50
+ * Human-readable rather than UUID-shaped, and needs no quoting as a shell
51
+ * argument. No colon, so a chosen ID can never take the `loaded:` form the
52
+ * webapp gives sessions read from a saved file. A generated UUID also
53
+ * satisfies it.
54
+ */
55
+ export declare const isValidSessionId: (id: string) => boolean;
40
56
  /** Mirrors `Tracer.SpanKind`. */
41
57
  export declare const SpanKind: Schema.Literals<readonly ["internal", "server", "client", "producer", "consumer"]>;
42
58
  export type SpanKind = Schema.Schema.Type<typeof SpanKind>;
@@ -67,6 +83,13 @@ export declare const Hello: Schema.Struct<{
67
83
  readonly startTime: Schema.BigIntFromString;
68
84
  readonly wallClockEpochMillis: Schema.Natural;
69
85
  }>;
86
+ /**
87
+ * Random per client instance, fixed for its lifetime and re-sent on every
88
+ * reconnect. Tells a genuine reconnect (same instance) from an independent
89
+ * run that chose the same `sessionId`. Absent from older clients, which are
90
+ * treated as one instance per `sessionId`.
91
+ */
92
+ readonly instanceId: Schema.optional<Schema.String>;
70
93
  }>;
71
94
  export type Hello = Schema.Schema.Type<typeof Hello>;
72
95
  /** The protocol version a `Hello` must carry. Bump on any breaking change. */
@@ -419,6 +442,13 @@ export declare const ClientMessage: Schema.Union<readonly [Schema.Struct<{
419
442
  readonly startTime: Schema.BigIntFromString;
420
443
  readonly wallClockEpochMillis: Schema.Natural;
421
444
  }>;
445
+ /**
446
+ * Random per client instance, fixed for its lifetime and re-sent on every
447
+ * reconnect. Tells a genuine reconnect (same instance) from an independent
448
+ * run that chose the same `sessionId`. Absent from older clients, which are
449
+ * treated as one instance per `sessionId`.
450
+ */
451
+ readonly instanceId: Schema.optional<Schema.String>;
422
452
  }>, Schema.Struct<{
423
453
  readonly sessionId: Schema.String;
424
454
  readonly _tag: Schema.tag<"SpanStart">;
@@ -563,6 +593,12 @@ export declare const Session: Schema.Struct<{
563
593
  }>;
564
594
  readonly active: Schema.Boolean;
565
595
  readonly endedAtEpochMillis: Schema.optional<Schema.Natural>;
596
+ /**
597
+ * Connections refused because a different client instance announced this
598
+ * `sessionId` — an ID collision. Their telemetry was discarded, so this
599
+ * session holds only the first instance's run. Absent when there were none.
600
+ */
601
+ readonly conflicts: Schema.optional<Schema.Natural>;
566
602
  }>;
567
603
  export type Session = Schema.Schema.Type<typeof Session>;
568
604
  /** Every session the collector holds. Sent on connect and on change. */
@@ -579,6 +615,12 @@ export declare const SessionList: Schema.Struct<{
579
615
  }>;
580
616
  readonly active: Schema.Boolean;
581
617
  readonly endedAtEpochMillis: Schema.optional<Schema.Natural>;
618
+ /**
619
+ * Connections refused because a different client instance announced this
620
+ * `sessionId` — an ID collision. Their telemetry was discarded, so this
621
+ * session holds only the first instance's run. Absent when there were none.
622
+ */
623
+ readonly conflicts: Schema.optional<Schema.Natural>;
582
624
  }>>;
583
625
  }>;
584
626
  export type SessionList = Schema.Schema.Type<typeof SessionList>;
@@ -603,6 +645,13 @@ export declare const Backlog: Schema.Struct<{
603
645
  readonly startTime: Schema.BigIntFromString;
604
646
  readonly wallClockEpochMillis: Schema.Natural;
605
647
  }>;
648
+ /**
649
+ * Random per client instance, fixed for its lifetime and re-sent on every
650
+ * reconnect. Tells a genuine reconnect (same instance) from an independent
651
+ * run that chose the same `sessionId`. Absent from older clients, which are
652
+ * treated as one instance per `sessionId`.
653
+ */
654
+ readonly instanceId: Schema.optional<Schema.String>;
606
655
  }>, Schema.Struct<{
607
656
  readonly sessionId: Schema.String;
608
657
  readonly _tag: Schema.tag<"SpanStart">;
@@ -743,6 +792,13 @@ export declare const Live: Schema.Struct<{
743
792
  readonly startTime: Schema.BigIntFromString;
744
793
  readonly wallClockEpochMillis: Schema.Natural;
745
794
  }>;
795
+ /**
796
+ * Random per client instance, fixed for its lifetime and re-sent on every
797
+ * reconnect. Tells a genuine reconnect (same instance) from an independent
798
+ * run that chose the same `sessionId`. Absent from older clients, which are
799
+ * treated as one instance per `sessionId`.
800
+ */
801
+ readonly instanceId: Schema.optional<Schema.String>;
746
802
  }>, Schema.Struct<{
747
803
  readonly sessionId: Schema.String;
748
804
  readonly _tag: Schema.tag<"SpanStart">;
@@ -898,6 +954,12 @@ export declare const WebappMessage: Schema.Union<readonly [Schema.Struct<{
898
954
  }>;
899
955
  readonly active: Schema.Boolean;
900
956
  readonly endedAtEpochMillis: Schema.optional<Schema.Natural>;
957
+ /**
958
+ * Connections refused because a different client instance announced this
959
+ * `sessionId` — an ID collision. Their telemetry was discarded, so this
960
+ * session holds only the first instance's run. Absent when there were none.
961
+ */
962
+ readonly conflicts: Schema.optional<Schema.Natural>;
901
963
  }>>;
902
964
  }>, Schema.Struct<{
903
965
  readonly sessionId: Schema.String;
@@ -913,6 +975,13 @@ export declare const WebappMessage: Schema.Union<readonly [Schema.Struct<{
913
975
  readonly startTime: Schema.BigIntFromString;
914
976
  readonly wallClockEpochMillis: Schema.Natural;
915
977
  }>;
978
+ /**
979
+ * Random per client instance, fixed for its lifetime and re-sent on every
980
+ * reconnect. Tells a genuine reconnect (same instance) from an independent
981
+ * run that chose the same `sessionId`. Absent from older clients, which are
982
+ * treated as one instance per `sessionId`.
983
+ */
984
+ readonly instanceId: Schema.optional<Schema.String>;
916
985
  }>, Schema.Struct<{
917
986
  readonly sessionId: Schema.String;
918
987
  readonly _tag: Schema.tag<"SpanStart">;
@@ -1050,6 +1119,13 @@ export declare const WebappMessage: Schema.Union<readonly [Schema.Struct<{
1050
1119
  readonly startTime: Schema.BigIntFromString;
1051
1120
  readonly wallClockEpochMillis: Schema.Natural;
1052
1121
  }>;
1122
+ /**
1123
+ * Random per client instance, fixed for its lifetime and re-sent on every
1124
+ * reconnect. Tells a genuine reconnect (same instance) from an independent
1125
+ * run that chose the same `sessionId`. Absent from older clients, which are
1126
+ * treated as one instance per `sessionId`.
1127
+ */
1128
+ readonly instanceId: Schema.optional<Schema.String>;
1053
1129
  }>, Schema.Struct<{
1054
1130
  readonly sessionId: Schema.String;
1055
1131
  readonly _tag: Schema.tag<"SpanStart">;
@@ -1200,6 +1276,23 @@ export declare const WebappRequest: Schema.Union<readonly [Schema.Struct<{
1200
1276
  readonly _tag: Schema.tag<"Unsubscribe">;
1201
1277
  }>]>;
1202
1278
  export type WebappRequest = Schema.Schema.Type<typeof WebappRequest>;
1279
+ /**
1280
+ * Collector-side loss counters for one session at snapshot time, carried in
1281
+ * a trace file exported from the collector.
1282
+ */
1283
+ export declare const TraceCapture: Schema.Struct<{
1284
+ /** Messages evicted by the collector's per-session capacity bound. */
1285
+ readonly droppedMessages: Schema.Natural;
1286
+ /** Lines received for the session that could not be decoded. */
1287
+ readonly skippedLines: Schema.Natural;
1288
+ /**
1289
+ * Whether the collector could refuse an independent run reusing this ID.
1290
+ * `false` when the owning client predates instance IDs, so a reused ID
1291
+ * would have merged silently and `session.conflicts` proves nothing.
1292
+ */
1293
+ readonly conflictDetection: Schema.Boolean;
1294
+ }>;
1295
+ export type TraceCapture = Schema.Schema.Type<typeof TraceCapture>;
1203
1296
  /**
1204
1297
  * Line 1 of a saved trace file.
1205
1298
  *
@@ -1229,8 +1322,32 @@ export declare const TraceFileHeader: Schema.Struct<{
1229
1322
  }>;
1230
1323
  readonly active: Schema.Boolean;
1231
1324
  readonly endedAtEpochMillis: Schema.optional<Schema.Natural>;
1325
+ /**
1326
+ * Connections refused because a different client instance announced this
1327
+ * `sessionId` — an ID collision. Their telemetry was discarded, so this
1328
+ * session holds only the first instance's run. Absent when there were none.
1329
+ */
1330
+ readonly conflicts: Schema.optional<Schema.Natural>;
1232
1331
  }>;
1233
1332
  readonly savedAtEpochMillis: Schema.Natural;
1333
+ /**
1334
+ * What the collector knew about loss when the file was exported from it.
1335
+ * Absent from browser saves and older files: their capture completeness is
1336
+ * unknown, not complete. Optional and additive, so older builds, which
1337
+ * ignore unknown header keys, still read these files.
1338
+ */
1339
+ readonly capture: Schema.optional<Schema.Struct<{
1340
+ /** Messages evicted by the collector's per-session capacity bound. */
1341
+ readonly droppedMessages: Schema.Natural;
1342
+ /** Lines received for the session that could not be decoded. */
1343
+ readonly skippedLines: Schema.Natural;
1344
+ /**
1345
+ * Whether the collector could refuse an independent run reusing this ID.
1346
+ * `false` when the owning client predates instance IDs, so a reused ID
1347
+ * would have merged silently and `session.conflicts` proves nothing.
1348
+ */
1349
+ readonly conflictDetection: Schema.Boolean;
1350
+ }>>;
1234
1351
  }>;
1235
1352
  export type TraceFileHeader = Schema.Schema.Type<typeof TraceFileHeader>;
1236
1353
  /** The trace file layout version. Bump only on a change to the header or the line layout. */