@tsplc/debug 0.2.17 → 0.2.19

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/src/ui/recent.ts CHANGED
@@ -1,7 +1,19 @@
1
1
  import type { AppRecord, TimelineEntry, TraceEvent } from "../api.ts";
2
2
  import type { DebugEventSite, DebugManifest } from "../manifest.ts";
3
- import { type View, empty, h, pathLink, recordSourceLink, sourceLink } from "./dom.ts";
4
- import { formatClock, formatDuration, formatProperties, formatValue } from "./format.ts";
3
+ import {
4
+ type View,
5
+ empty,
6
+ h,
7
+ pathLink,
8
+ recordSourceLink,
9
+ sourceLink,
10
+ } from "./dom.ts";
11
+ import {
12
+ formatClock,
13
+ formatDuration,
14
+ formatProperties,
15
+ formatValue,
16
+ } from "./format.ts";
5
17
  import { requestJobLink, requestStatus } from "./jobs.ts";
6
18
  import type { Store } from "./store.ts";
7
19
 
@@ -11,14 +23,14 @@ type Kind = AppRecord["kind"] | Exclude<TimelineEntry["kind"], "app">;
11
23
 
12
24
  const SOURCES: Source[] = ["plc", "server", "agent", "debugger"];
13
25
  const KINDS: Kind[] = [
14
- "event",
15
- "log",
16
- "exception",
17
- "trace",
18
- "request",
19
- "deploy",
20
- "audit",
21
- "connection",
26
+ "event",
27
+ "log",
28
+ "exception",
29
+ "trace",
30
+ "request",
31
+ "deploy",
32
+ "audit",
33
+ "connection",
22
34
  ];
23
35
  const PAGE = 300;
24
36
  const RENDER_INTERVAL_MS = 300;
@@ -27,296 +39,337 @@ const hiddenSources = new Set<Source>();
27
39
  const hiddenKinds = new Set<Kind>();
28
40
 
29
41
  const kindOf = (entry: TimelineEntry): Kind =>
30
- entry.kind === "app" ? entry.record.kind : entry.kind;
42
+ entry.kind === "app" ? entry.record.kind : entry.kind;
31
43
 
32
- const sitesByManifest = new WeakMap<DebugManifest, Map<number, DebugEventSite>>();
44
+ const sitesByManifest = new WeakMap<
45
+ DebugManifest,
46
+ Map<number, DebugEventSite>
47
+ >();
33
48
  const siteOf = (manifest: DebugManifest, id: number) => {
34
- let sites = sitesByManifest.get(manifest);
35
- if (!sites) {
36
- sites = new Map(manifest.events.map((site) => [site.id, site]));
37
- sitesByManifest.set(manifest, sites);
38
- }
39
- return sites.get(id);
49
+ let sites = sitesByManifest.get(manifest);
50
+ if (!sites) {
51
+ sites = new Map(manifest.events.map((site) => [site.id, site]));
52
+ sitesByManifest.set(manifest, sites);
53
+ }
54
+ return sites.get(id);
40
55
  };
41
56
 
42
57
  /** A traced write, worded as what was observed: the value, the scan, and the guards captured with it. */
43
58
  const traceBody = (store: Store, trace: TraceEvent) => {
44
- const site = siteOf(store.manifest, trace.site);
45
- if (!site)
46
- return [
47
- h(
48
- "div",
49
- {},
50
- `Site ${trace.site} is not in this build's manifest; value ${JSON.stringify(trace.value)} in scan ${trace.scan}.`,
51
- ),
52
- ];
53
- return [
54
- h(
55
- "div",
56
- {},
57
- h("span", { class: "tag" }, site.kind),
58
- " ",
59
- pathLink(site.path),
60
- " was ",
61
- h("b", { class: "val" }, formatValue(store.manifest, site.type, trace.value)),
62
- ` in scan ${trace.scan}`,
63
- h("span", { class: "dim" }, ` · ${trace.task}`),
64
- ),
65
- h("div", {}, h("code", {}, site.text), " ", sourceLink(store, site.range)),
66
- trace.operands.length > 0 &&
67
- h(
68
- "div",
69
- { class: "chips" },
70
- h("span", { class: "dim" }, "guards in that scan "),
71
- ...trace.operands.map((operand, index) => {
72
- const declared = site.operands[index];
73
- const value = declared
74
- ? formatValue(store.manifest, declared.type, operand.value)
75
- : JSON.stringify(operand.value);
76
- return h(
77
- "span",
78
- {
79
- class: `chip${operand.stable ? "" : " unstable"}`,
80
- title: operand.stable
81
- ? `${operand.text}, captured at the write`
82
- : `${operand.text}, captured at the write; code between the guard and the write can change it, so the guard may have seen another value`,
83
- },
84
- declared
85
- ? sourceLink(store, declared.range, operand.text)
86
- : h("code", {}, operand.text),
87
- " = ",
88
- operand.path ? pathLink(operand.path, value) : h("span", { class: "val" }, value),
89
- operand.stable ? "" : h("span", { class: "dim" }, " ~"),
90
- );
91
- }),
92
- ),
93
- ];
59
+ const site = siteOf(store.manifest, trace.site);
60
+ if (!site) {
61
+ return [
62
+ h(
63
+ "div",
64
+ {},
65
+ `Site ${trace.site} is not in this build's manifest; value ${JSON.stringify(trace.value)} in scan ${trace.scan}.`,
66
+ ),
67
+ ];
68
+ }
69
+ return [
70
+ h(
71
+ "div",
72
+ {},
73
+ h("span", { class: "tag" }, site.kind),
74
+ " ",
75
+ pathLink(site.path),
76
+ " was ",
77
+ h(
78
+ "b",
79
+ { class: "val" },
80
+ formatValue(store.manifest, site.type, trace.value),
81
+ ),
82
+ ` in scan ${trace.scan}`,
83
+ h("span", { class: "dim" }, ` · ${trace.task}`),
84
+ ),
85
+ h("div", {}, h("code", {}, site.text), " ", sourceLink(store, site.range)),
86
+ trace.operands.length > 0 &&
87
+ h(
88
+ "div",
89
+ { class: "chips" },
90
+ h("span", { class: "dim" }, "guards in that scan "),
91
+ ...trace.operands.map((operand, index) => {
92
+ const declared = site.operands[index];
93
+ const value = declared
94
+ ? formatValue(store.manifest, declared.type, operand.value)
95
+ : JSON.stringify(operand.value);
96
+ return h(
97
+ "span",
98
+ {
99
+ class: `chip${operand.stable ? "" : " unstable"}`,
100
+ title: operand.stable
101
+ ? `${operand.text}, captured at the write`
102
+ : `${operand.text}, captured at the write; code between the guard and the write can change it, so the guard may have seen another value`,
103
+ },
104
+ declared
105
+ ? sourceLink(store, declared.range, operand.text)
106
+ : h("code", {}, operand.text),
107
+ " = ",
108
+ operand.path
109
+ ? pathLink(operand.path, value)
110
+ : h("span", { class: "val" }, value),
111
+ operand.stable ? "" : h("span", { class: "dim" }, " ~"),
112
+ );
113
+ }),
114
+ ),
115
+ ];
94
116
  };
95
117
 
96
118
  /** What the program reported: `bin scanned · barcode=CON-MD3… scanner=3 · route-lookup.ts:120`. */
97
119
  const appBody = (store: Store, task: string, record: AppRecord) => {
98
- const properties = Object.keys(record.properties).length
99
- ? h(
100
- "span",
101
- { class: "mono", title: JSON.stringify(record.properties, null, 1) },
102
- ` · ${formatProperties(record.properties)}`,
103
- )
104
- : "";
105
- const origin = [
106
- " · ",
107
- recordSourceLink(store, record.source),
108
- h("span", { class: "dim" }, ` · ${task}`),
109
- ];
110
- switch (record.kind) {
111
- case "event":
112
- return [h("div", {}, h("b", {}, record.event), properties, ...origin)];
113
- case "log":
114
- return [
115
- h(
116
- "div",
117
- {},
118
- h("span", { class: `tag lvl-${record.level}` }, record.level),
119
- " ",
120
- record.message,
121
- properties,
122
- ...origin,
123
- ),
124
- ];
125
- case "exception":
126
- return [
127
- h(
128
- "div",
129
- {},
130
- h("b", { class: "err" }, record.name ?? `error ${record.code}`),
131
- " · ",
132
- record.handled
133
- ? h("span", { class: "dim" }, "handled")
134
- : h("b", { class: "err" }, "unhandled"),
135
- record.function ? h("span", { class: "mono" }, ` · ${record.function}`) : "",
136
- record.instance ? h("span", {}, " on ", pathLink(record.instance)) : "",
137
- properties,
138
- ...origin,
139
- ),
140
- ];
141
- }
120
+ const properties = Object.keys(record.properties).length
121
+ ? h(
122
+ "span",
123
+ { class: "mono", title: JSON.stringify(record.properties, null, 1) },
124
+ ` · ${formatProperties(record.properties)}`,
125
+ )
126
+ : "";
127
+ const origin = [
128
+ " · ",
129
+ recordSourceLink(store, record.source),
130
+ h("span", { class: "dim" }, ` · ${task}`),
131
+ ];
132
+ switch (record.kind) {
133
+ case "event":
134
+ return [h("div", {}, h("b", {}, record.event), properties, ...origin)];
135
+ case "log":
136
+ return [
137
+ h(
138
+ "div",
139
+ {},
140
+ h("span", { class: `tag lvl-${record.level}` }, record.level),
141
+ " ",
142
+ record.message,
143
+ properties,
144
+ ...origin,
145
+ ),
146
+ ];
147
+ case "exception":
148
+ return [
149
+ h(
150
+ "div",
151
+ {},
152
+ h("b", { class: "err" }, record.name ?? `error ${record.code}`),
153
+ " · ",
154
+ record.handled
155
+ ? h("span", { class: "dim" }, "handled")
156
+ : h("b", { class: "err" }, "unhandled"),
157
+ record.function
158
+ ? h("span", { class: "mono" }, ` · ${record.function}`)
159
+ : "",
160
+ record.instance
161
+ ? h("span", {}, " on ", pathLink(record.instance))
162
+ : "",
163
+ properties,
164
+ ...origin,
165
+ ),
166
+ ];
167
+ }
142
168
  };
143
169
 
144
170
  const byWhom = (entry: { user?: string; host?: string }) =>
145
- entry.user || entry.host
146
- ? h(
147
- "span",
148
- { class: "dim" },
149
- ` by ${entry.user ?? "unknown user"}${entry.host ? ` on ${entry.host}` : ""}`,
150
- )
151
- : "";
171
+ entry.user || entry.host
172
+ ? h(
173
+ "span",
174
+ { class: "dim" },
175
+ ` by ${entry.user ?? "unknown user"}${entry.host ? ` on ${entry.host}` : ""}`,
176
+ )
177
+ : "";
152
178
 
153
179
  const entryBody = (store: Store, entry: TimelineEntry) => {
154
- switch (entry.kind) {
155
- case "app":
156
- return appBody(store, entry.task, entry.record);
157
- case "trace":
158
- return traceBody(store, entry.trace);
159
- case "request": {
160
- const { request } = entry;
161
- return [
162
- h(
163
- "div",
164
- {},
165
- h("b", { class: "mono" }, request.service),
166
- " ",
167
- requestStatus(request),
168
- request.durationMs === undefined ? "" : ` in ${formatDuration(request.durationMs)}`,
169
- request.job === undefined
170
- ? ""
171
- : h("span", {}, " · from ", requestJobLink(store, request.job, request.sequence)),
172
- request.error ? h("div", { class: "err" }, request.error) : "",
173
- ),
174
- ];
175
- }
176
- case "deploy":
177
- return [
178
- h(
179
- "div",
180
- {},
181
- `Release ${entry.release} · ${entry.operation}`,
182
- entry.build ? h("span", { class: "mono dim" }, ` build ${entry.build}`) : "",
183
- byWhom(entry),
184
- ),
185
- ];
186
- case "audit":
187
- return [
188
- h(
189
- "div",
190
- {},
191
- h("b", { class: "mono" }, `tsplc ${entry.command}`),
192
- byWhom(entry),
193
- entry.error ? h("span", { class: "err" }, ` · ${entry.error}`) : "",
194
- ),
195
- h(
196
- "div",
197
- { class: "chips" },
198
- ...Object.entries(entry.values).map(([path, value]) =>
199
- h(
200
- "span",
201
- { class: "chip" },
202
- pathLink(path),
203
- value === "" ? " unforced" : ` = ${value}`,
204
- entry.previous?.[path] === undefined
205
- ? ""
206
- : h("span", { class: "dim" }, ` (was ${entry.previous[path]})`),
207
- ),
208
- ),
209
- ),
210
- ];
211
- case "connection":
212
- return [
213
- h(
214
- "div",
215
- {},
216
- `Reading values: ${entry.state}`,
217
- entry.error ? h("span", { class: "err" }, ` · ${entry.error}`) : "",
218
- ),
219
- ];
220
- }
180
+ switch (entry.kind) {
181
+ case "app":
182
+ return appBody(store, entry.task, entry.record);
183
+ case "trace":
184
+ return traceBody(store, entry.trace);
185
+ case "request": {
186
+ const { request } = entry;
187
+ return [
188
+ h(
189
+ "div",
190
+ {},
191
+ h("b", { class: "mono" }, request.service),
192
+ " ",
193
+ requestStatus(request),
194
+ request.durationMs === undefined
195
+ ? ""
196
+ : ` in ${formatDuration(request.durationMs)}`,
197
+ request.job === undefined
198
+ ? ""
199
+ : h(
200
+ "span",
201
+ {},
202
+ " · from ",
203
+ requestJobLink(store, request.job, request.sequence),
204
+ ),
205
+ request.error ? h("div", { class: "err" }, request.error) : "",
206
+ ),
207
+ ];
208
+ }
209
+ case "deploy":
210
+ return [
211
+ h(
212
+ "div",
213
+ {},
214
+ `Release ${entry.release} · ${entry.operation}`,
215
+ entry.build
216
+ ? h("span", { class: "mono dim" }, ` build ${entry.build}`)
217
+ : "",
218
+ byWhom(entry),
219
+ ),
220
+ ];
221
+ case "audit":
222
+ return [
223
+ h(
224
+ "div",
225
+ {},
226
+ h("b", { class: "mono" }, `tsplc ${entry.command}`),
227
+ byWhom(entry),
228
+ entry.error ? h("span", { class: "err" }, ` · ${entry.error}`) : "",
229
+ ),
230
+ h(
231
+ "div",
232
+ { class: "chips" },
233
+ ...Object.entries(entry.values).map(([path, value]) =>
234
+ h(
235
+ "span",
236
+ { class: "chip" },
237
+ pathLink(path),
238
+ value === "" ? " unforced" : ` = ${value}`,
239
+ entry.previous?.[path] === undefined
240
+ ? ""
241
+ : h("span", { class: "dim" }, ` (was ${entry.previous[path]})`),
242
+ ),
243
+ ),
244
+ ),
245
+ ];
246
+ case "connection":
247
+ return [
248
+ h(
249
+ "div",
250
+ {},
251
+ `Reading values: ${entry.state}`,
252
+ entry.error ? h("span", { class: "err" }, ` · ${entry.error}`) : "",
253
+ ),
254
+ ];
255
+ }
221
256
  };
222
257
 
223
258
  export const recentEntry = (store: Store, entry: TimelineEntry) =>
224
- h(
225
- "div",
226
- { class: `entry k-${kindOf(entry)}` },
227
- h(
228
- "span",
229
- { class: "when mono" },
230
- formatClock(entry.at),
231
- entry.approximate
232
- ? h(
233
- "span",
234
- {
235
- class: "approx",
236
- title:
237
- "Placed through another clock, so its order against other sources is approximate",
238
- },
239
- "≈",
240
- )
241
- : "",
242
- ),
243
- h("span", { class: `source src-${entry.source}` }, entry.source),
244
- h("div", { class: "body" }, ...entryBody(store, entry)),
245
- );
259
+ h(
260
+ "div",
261
+ { class: `entry k-${kindOf(entry)}` },
262
+ h(
263
+ "span",
264
+ { class: "when mono" },
265
+ formatClock(entry.at),
266
+ entry.approximate
267
+ ? h(
268
+ "span",
269
+ {
270
+ class: "approx",
271
+ title:
272
+ "Placed through another clock, so its order against other sources is approximate",
273
+ },
274
+ "≈",
275
+ )
276
+ : "",
277
+ ),
278
+ h("span", { class: `source src-${entry.source}` }, entry.source),
279
+ h("div", { class: "body" }, ...entryBody(store, entry)),
280
+ );
246
281
 
247
282
  const clockNote = (store: Store) => {
248
- const clock = store.session.clock;
249
- return `PLC records are placed on the app server's clock from their age when it received them${
250
- clock ? `, within ±${clock.uncertaintyMs.toFixed(1)} ms` : ""
251
- }, so their order against other sources is approximate (≈).`;
283
+ const clock = store.session.clock;
284
+ return `PLC records are placed on the app server's clock from their age when it received them${
285
+ clock ? `, within ±${clock.uncertaintyMs.toFixed(1)} ms` : ""
286
+ }, so their order against other sources is approximate (≈).`;
252
287
  };
253
288
 
254
289
  export const recentView = (store: Store): View => {
255
- let shown = PAGE;
256
- let renderTimer: ReturnType<typeof setTimeout> | undefined;
257
- const list = h("div", { class: "entries" });
258
- const more = h("button", { onclick: () => ((shown += PAGE), render()) }, `Show ${PAGE} older`);
259
- const note = h("p", { class: "hint" });
290
+ let shown = PAGE;
291
+ let renderTimer: ReturnType<typeof setTimeout> | undefined;
292
+ const list = h("div", { class: "entries" });
293
+ const more = h(
294
+ "button",
295
+ {
296
+ onclick: () => {
297
+ shown += PAGE;
298
+ render();
299
+ },
300
+ },
301
+ `Show ${PAGE} older`,
302
+ );
303
+ const note = h("p", { class: "hint" });
260
304
 
261
- const toggles = <T extends string>(values: T[], hidden: Set<T>) =>
262
- values.map((value) => {
263
- const input = h("input", { type: "checkbox", checked: !hidden.has(value) });
264
- input.addEventListener("change", () => {
265
- if (input.checked) hidden.delete(value);
266
- else hidden.add(value);
267
- render();
268
- });
269
- return h("label", { class: "check" }, input, value);
270
- });
305
+ const toggles = <T extends string>(values: T[], hidden: Set<T>) =>
306
+ values.map((value) => {
307
+ const input = h("input", {
308
+ type: "checkbox",
309
+ checked: !hidden.has(value),
310
+ });
311
+ input.addEventListener("change", () => {
312
+ if (input.checked) {
313
+ hidden.delete(value);
314
+ } else {
315
+ hidden.add(value);
316
+ }
317
+ render();
318
+ });
319
+ return h("label", { class: "check" }, input, value);
320
+ });
271
321
 
272
- const render = () => {
273
- clearTimeout(renderTimer);
274
- renderTimer = undefined;
275
- note.textContent = clockNote(store);
276
- const visible = store.timeline.filter(
277
- (entry) => !hiddenSources.has(entry.source) && !hiddenKinds.has(kindOf(entry)),
278
- );
279
- list.replaceChildren(
280
- ...(visible.length
281
- ? visible
282
- .slice(-shown)
283
- .reverse()
284
- .map((entry) => recentEntry(store, entry))
285
- : [empty("Nothing in the recent window matches these filters.")]),
286
- );
287
- more.hidden = visible.length <= shown;
288
- };
322
+ const render = () => {
323
+ clearTimeout(renderTimer);
324
+ renderTimer = undefined;
325
+ note.textContent = clockNote(store);
326
+ const visible = store.timeline.filter(
327
+ (entry) =>
328
+ !hiddenSources.has(entry.source) && !hiddenKinds.has(kindOf(entry)),
329
+ );
330
+ list.replaceChildren(
331
+ ...(visible.length
332
+ ? visible
333
+ .slice(-shown)
334
+ .reverse()
335
+ .map((entry) => recentEntry(store, entry))
336
+ : [empty("Nothing in the recent window matches these filters.")]),
337
+ );
338
+ more.hidden = visible.length <= shown;
339
+ };
289
340
 
290
- render();
291
- store.show();
292
- return {
293
- element: h(
294
- "div",
295
- { class: "page recent" },
296
- h("h1", {}, "Recent"),
297
- h(
298
- "p",
299
- { class: "hint" },
300
- "The app server's recent window: what the program reported, its traced writes when the trace is on, and the requests it made, with the agent's deployments and audited commands. History lives in the app's telemetry sink, such as PostHog.",
301
- ),
302
- h(
303
- "div",
304
- { class: "toolbar" },
305
- h("span", { class: "dim" }, "Source"),
306
- ...toggles(SOURCES, hiddenSources),
307
- h("span", { class: "dim sep" }, "Kind"),
308
- ...toggles(KINDS, hiddenKinds),
309
- ),
310
- note,
311
- list,
312
- more,
313
- ),
314
- update(topics) {
315
- if (!topics.has("timeline") && !topics.has("session")) return;
316
- renderTimer ??= setTimeout(render, RENDER_INTERVAL_MS);
317
- },
318
- dispose() {
319
- clearTimeout(renderTimer);
320
- },
321
- };
341
+ render();
342
+ store.show();
343
+ return {
344
+ element: h(
345
+ "div",
346
+ { class: "page recent" },
347
+ h("h1", {}, "Recent"),
348
+ h(
349
+ "p",
350
+ { class: "hint" },
351
+ "The app server's recent window: what the program reported, its traced writes when the trace is on, and the requests it made, with the agent's deployments and audited commands. History lives in the app's telemetry sink, such as PostHog.",
352
+ ),
353
+ h(
354
+ "div",
355
+ { class: "toolbar" },
356
+ h("span", { class: "dim" }, "Source"),
357
+ ...toggles(SOURCES, hiddenSources),
358
+ h("span", { class: "dim sep" }, "Kind"),
359
+ ...toggles(KINDS, hiddenKinds),
360
+ ),
361
+ note,
362
+ list,
363
+ more,
364
+ ),
365
+ update(topics) {
366
+ if (!topics.has("timeline") && !topics.has("session")) {
367
+ return;
368
+ }
369
+ renderTimer ??= setTimeout(render, RENDER_INTERVAL_MS);
370
+ },
371
+ dispose() {
372
+ clearTimeout(renderTimer);
373
+ },
374
+ };
322
375
  };