Repository navigation
feat!: rename the declared-screenshot feature to "captures" - #6307
Conversation
`static screenshots` was named before the feature could emit PDFs, so `PdfScreenshotSpec` reads as an oxymoron and the wrong noun had leaked into author-facing errors. The codebase had already half-migrated on its own: capture-spec.ts, capture_spec_hash, CAPTURE_SERVING_PREFIX pointing at '_screenshot/', and `_sign-capture-urls` signing `_screenshot/` URLs. One word in the platform vocabulary: capture. `boxel screenshot` stays on as a CLI verb alias, where concreteness pays and a second noun costs nothing. Beyond the word, this stops synonyms doing load-bearing work as a distinction. Three format rosters and two output-type rosters were told apart only by which synonym they picked; each now states its axis (DECLARED_/ON_DEMAND_/CANONICAL_CAPTURE_FORMATS, RASTER_OUTPUT_TYPES vs CAPTURE_OUTPUT_TYPES). The three spec types likewise get names that say what they are: CaptureSpec is what an author declares, CaptureRequestSpec is the wire spec behind the POST body and GET DSL, and CaptureIdentity is the canonicalized subset that keys the MediaCache ledger. Two timers were distinguished only by the synonym and must not collapse into one: the CDP captureScreenshot/printToPDF call is now cdpCaptureMs, while the whole-step captureMs keeps its name and meaning. prerendered_html.screenshots becomes .captures across the production table and both twins, expand/contract: additive add plus backfill pre-deploy, drop post-deploy. Telemetry moves to boxel:capture-perf and the dashboard's queries and title follow. Its uid stays boxel-screenshot-perf: that is an opaque identifier rather than vocabulary, and changing it would orphan the existing dashboard and break saved links for no reader benefit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rfaces loudly Three consumers of this vocabulary do not roll forward with a deploy of it. `boxel-cli` is installed and pinned independently of the realm server, and a released version posts to `/_screenshot-card`. The realm server answers that path with the capture handler again. A capture URL is meant to be stored — a card can hold one, and a browser keeps serving the previously installed auth service worker, which matches capture requests on the path prefix to attach credentials, until it next updates. The serving subtree now covers the old prefix as well as the new one, and reserves both against writes so nothing can be stored where a GET would never read it back. Callers strip whichever prefix arrived rather than a fixed width. The prerender server and its manager roll out ahead of the workers and realm servers that call them, so during a deploy the previous revision is asking for `/prerender-screenshot` while the new one is already serving. Both names now register the capture route. Card source is the one surface with no alias, because an alias there would carry the old vocabulary forward indefinitely. Instead the two silent failures become loud: `static screenshots` is refused by name at declaration-read rather than read by nothing, and a capture-only component still signalling readiness with `data-screenshot-pending` fails its slot instead of persisting whatever half-painted frame was on screen. Also fixes a log label the rename mangled to "card captureted". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Observability diff (vs staging)Show diffdiff --git a/tmp/remote-canon.CvvwBW/dashboards/boxel-status/captures.json b/tmp/committed-canon.xS9d0p/dashboards/boxel-status/captures.json
index ecbe096..adc96a8 100644
--- a/tmp/remote-canon.CvvwBW/dashboards/boxel-status/captures.json
+++ b/tmp/committed-canon.xS9d0p/dashboards/boxel-status/captures.json
@@ -24,7 +24,7 @@
}
]
},
- "description": "Screenshot capture pipeline stage telemetry (channel boxel:screenshot-perf): request records from the serving surfaces (realm-server) and capture records from the screenshot-card worker task, joined by jobId/correlationId. The same capture breakdown is persisted on media_cache_ledger.diagnostics for SQL reads.",
+ "description": "Capture pipeline stage telemetry (channel boxel:capture-perf): request records from the serving surfaces (realm-server) and capture records from the capture-card worker task, joined by jobId/correlationId. The same capture breakdown is persisted on media_cache_ledger.diagnostics for SQL reads.",
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
@@ -38,7 +38,6 @@
"x": 0,
"y": 0
},
- "id": 1,
"panels": [],
"title": "Overview",
"type": "row"
@@ -64,7 +63,6 @@
"x": 0,
"y": 1
},
- "id": 2,
"options": {
"colorMode": "none",
"graphMode": "area",
@@ -82,7 +80,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "sum(count_over_time({service=\"realm-server\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"request\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" [5m])) or vector(0)",
+ "expr": "sum(count_over_time({service=\"realm-server\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"request\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" [5m])) or vector(0)",
"queryType": "range",
"refId": "A"
}
@@ -95,7 +93,7 @@
"type": "loki",
"uid": "loki"
},
- "description": "screenshot-card jobs that finished (any status).",
+ "description": "capture-card jobs that finished (any status).",
"fieldConfig": {
"defaults": {
"color": {
@@ -111,7 +109,6 @@
"x": 6,
"y": 1
},
- "id": 3,
"options": {
"colorMode": "none",
"graphMode": "area",
@@ -129,7 +126,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "sum(count_over_time({service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" [5m])) or vector(0)",
+ "expr": "sum(count_over_time({service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" [5m])) or vector(0)",
"queryType": "range",
"refId": "A"
}
@@ -165,7 +162,6 @@
"x": 12,
"y": 1
},
- "id": 4,
"options": {
"legend": {
"displayMode": "list",
@@ -183,7 +179,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "sum by (outcome) (count_over_time({service=\"realm-server\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"request\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" [$__interval]))",
+ "expr": "sum by (outcome) (count_over_time({service=\"realm-server\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"request\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" [$__interval]))",
"legendFormat": "{{outcome}}",
"queryType": "range",
"refId": "A"
@@ -220,7 +216,6 @@
"x": 0,
"y": 5
},
- "id": 5,
"options": {
"legend": {
"displayMode": "list",
@@ -238,7 +233,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "quantile_over_time(0.50, {service=\"realm-server\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"request\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap totalMs [$__interval]) by ()",
+ "expr": "quantile_over_time(0.50, {service=\"realm-server\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"request\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap totalMs [$__interval]) by ()",
"legendFormat": "p50",
"queryType": "range",
"refId": "A"
@@ -248,7 +243,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "quantile_over_time(0.95, {service=\"realm-server\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"request\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap totalMs [$__interval]) by ()",
+ "expr": "quantile_over_time(0.95, {service=\"realm-server\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"request\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap totalMs [$__interval]) by ()",
"legendFormat": "p95",
"queryType": "range",
"refId": "B"
@@ -258,7 +253,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "quantile_over_time(0.99, {service=\"realm-server\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"request\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap totalMs [$__interval]) by ()",
+ "expr": "quantile_over_time(0.99, {service=\"realm-server\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"request\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap totalMs [$__interval]) by ()",
"legendFormat": "p99",
"queryType": "range",
"refId": "C"
@@ -275,7 +270,6 @@
"x": 0,
"y": 13
},
- "id": 6,
"panels": [],
"title": "Capture stage breakdown",
"type": "row"
@@ -285,7 +279,7 @@
"type": "loki",
"uid": "loki"
},
- "description": "Where a capture's wall-clock goes, p95 per stage. queueWaitMs is enqueue→claim; launchMs is page acquire in the pool; renderMs is the server-observed render (nav/settle/imagePaint/screenshot are its components); persistMs is the MediaCache put.",
+ "description": "Where a capture's wall-clock goes, p95 per stage. queueWaitMs is enqueue→claim; launchMs is page acquire in the pool; renderMs is the server-observed render (nav/settle/imagePaint/capture are its components); persistMs is the MediaCache put.",
"fieldConfig": {
"defaults": {
"color": {
@@ -308,7 +302,6 @@
"x": 0,
"y": 14
},
- "id": 7,
"options": {
"legend": {
"displayMode": "list",
@@ -326,7 +319,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap queueWaitMs [$__interval]) by ()",
+ "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap queueWaitMs [$__interval]) by ()",
"legendFormat": "queueWaitMs",
"queryType": "range",
"refId": "A"
@@ -336,7 +329,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap permissionsMs [$__interval]) by ()",
+ "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap permissionsMs [$__interval]) by ()",
"legendFormat": "permissionsMs",
"queryType": "range",
"refId": "B"
@@ -346,7 +339,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap launchMs [$__interval]) by ()",
+ "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap launchMs [$__interval]) by ()",
"legendFormat": "launchMs",
"queryType": "range",
"refId": "C"
@@ -356,7 +349,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap renderMs [$__interval]) by ()",
+ "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap renderMs [$__interval]) by ()",
"legendFormat": "renderMs",
"queryType": "range",
"refId": "D"
@@ -366,7 +359,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap navMs [$__interval]) by ()",
+ "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap navMs [$__interval]) by ()",
"legendFormat": "navMs",
"queryType": "range",
"refId": "E"
@@ -376,7 +369,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap settleMs [$__interval]) by ()",
+ "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap settleMs [$__interval]) by ()",
"legendFormat": "settleMs",
"queryType": "range",
"refId": "F"
@@ -386,7 +379,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap imagePaintMs [$__interval]) by ()",
+ "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap imagePaintMs [$__interval]) by ()",
"legendFormat": "imagePaintMs",
"queryType": "range",
"refId": "G"
@@ -396,8 +389,8 @@
"type": "loki",
"uid": "loki"
},
- "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap screenshotMs [$__interval]) by ()",
- "legendFormat": "screenshotMs",
+ "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap cdpCaptureMs [$__interval]) by ()",
+ "legendFormat": "cdpCaptureMs",
"queryType": "range",
"refId": "H"
},
@@ -406,7 +399,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap decodeMs [$__interval]) by ()",
+ "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap decodeMs [$__interval]) by ()",
"legendFormat": "decodeMs",
"queryType": "range",
"refId": "I"
@@ -416,7 +409,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap persistMs [$__interval]) by ()",
+ "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap persistMs [$__interval]) by ()",
"legendFormat": "persistMs",
"queryType": "range",
"refId": "J"
@@ -453,7 +446,6 @@
"x": 0,
"y": 23
},
- "id": 8,
"options": {
"legend": {
"displayMode": "list",
@@ -471,7 +463,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap queueWaitMs [$__interval]) by ()",
+ "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap queueWaitMs [$__interval]) by ()",
"legendFormat": "queueWaitMs",
"queryType": "range",
"refId": "A"
@@ -481,7 +473,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap prerenderMs [$__interval]) by ()",
+ "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap prerenderMs [$__interval]) by ()",
"legendFormat": "prerenderMs (incl. transport)",
"queryType": "range",
"refId": "B"
@@ -491,7 +483,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap renderMs [$__interval]) by ()",
+ "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap renderMs [$__interval]) by ()",
"legendFormat": "renderMs (server-observed)",
"queryType": "range",
"refId": "C"
@@ -528,7 +520,6 @@
"x": 8,
"y": 23
},
- "id": 9,
"options": {
"legend": {
"displayMode": "list",
@@ -546,7 +537,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap launchMs [$__interval]) by (tabReused)",
+ "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap launchMs [$__interval]) by (tabReused)",
"legendFormat": "tabReused={{tabReused}}",
"queryType": "range",
"refId": "A"
@@ -583,7 +574,6 @@
"x": 16,
"y": 23
},
- "id": 10,
"options": {
"legend": {
"displayMode": "list",
@@ -601,7 +591,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "sum by (persistOutcome) (count_over_time({service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" [$__interval]))",
+ "expr": "sum by (persistOutcome) (count_over_time({service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" [$__interval]))",
"legendFormat": "{{persistOutcome}}",
"queryType": "range",
"refId": "A"
@@ -618,7 +608,6 @@
"x": 0,
"y": 31
},
- "id": 11,
"panels": [],
"title": "Slices",
"type": "row"
@@ -650,7 +639,6 @@
"x": 0,
"y": 32
},
- "id": 12,
"options": {
"legend": {
"displayMode": "list",
@@ -668,7 +656,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap totalMs [$__interval]) by (realmURL)",
+ "expr": "quantile_over_time(0.95, {service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap totalMs [$__interval]) by (realmURL)",
"legendFormat": "{{realmURL}}",
"queryType": "range",
"refId": "A"
@@ -682,7 +670,7 @@
"type": "loki",
"uid": "loki"
},
- "description": "get-dsl is the GET _screenshot/ DSL route; post is POST /_screenshot-card.",
+ "description": "get-dsl is the GET _capture/ DSL route; post is POST /_capture-card.",
"fieldConfig": {
"defaults": {
"color": {
@@ -705,7 +693,6 @@
"x": 12,
"y": 32
},
- "id": 13,
"options": {
"legend": {
"displayMode": "list",
@@ -723,7 +710,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "quantile_over_time(0.95, {service=\"realm-server\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"request\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap totalMs [$__interval]) by (surface)",
+ "expr": "quantile_over_time(0.95, {service=\"realm-server\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"request\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap totalMs [$__interval]) by (surface)",
"legendFormat": "{{surface}}",
"queryType": "range",
"refId": "A"
@@ -749,7 +736,6 @@
"x": 0,
"y": 41
},
- "id": 14,
"options": {
"showHeader": true,
"sortBy": [
@@ -765,7 +751,7 @@
"type": "loki",
"uid": "loki"
},
- "expr": "topk(20, max by (sourceURL) (max_over_time({service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:screenshot-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:screenshot-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap totalMs [$__range])))",
+ "expr": "topk(20, max by (sourceURL) (max_over_time({service=~\"realm-server|worker\", env=\"$env\"} |= \"boxel:capture-perf\" | json | line_format \"{{ if .log }}{{ .log }}{{ else }}{{ __line__ }}{{ end }}\" | json | channel=\"boxel:capture-perf\" | eventType=\"capture\" | realmURL=~\".*${realm}.*\" | surface=~\"$surface\" | contentType=~\"$contentType\" | unwrap totalMs [$__range])))",
"instant": true,
"queryType": "instant",
"refId": "A"
@@ -792,7 +778,6 @@
"x": 0,
"y": 51
},
- "id": 15,
"panels": [],
"title": "MediaCache storage",
"type": "row"
@@ -818,7 +803,6 @@
"x": 0,
"y": 52
},
- "id": 16,
"options": {
"colorMode": "none",
"graphMode": "none",
@@ -857,6 +841,65 @@
"title": "Bucket size (CloudWatch)",
"type": "stat"
},
+ {
+ "datasource": {
+ "type": "cloudwatch",
+ "uid": "cef5x9o3yzawwf"
+ },
+ "description": "NumberOfObjects for boxel-media-cache-${env}, reported daily. Compare with 'Ledger object keys': the bucket stores one object per distinct object_key (dedupe-on-write), so the two counts should track each other; a persistent excess of bucket objects means orphans no ledger row references — invisible to GC, which scans ledger rows and never enumerates the bucket. Spans 2d regardless of the dashboard range so lastNotNull always finds the newest daily sample.",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 4,
+ "w": 6,
+ "x": 0,
+ "y": 56
+ },
+ "options": {
+ "colorMode": "none",
+ "graphMode": "none",
+ "justifyMode": "center",
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "textMode": "value"
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "cloudwatch",
+ "uid": "cef5x9o3yzawwf"
+ },
+ "dimensions": {
+ "BucketName": "boxel-media-cache-${env}",
+ "StorageType": "AllStorageTypes"
+ },
+ "id": "a",
+ "metricEditorMode": 0,
+ "metricName": "NumberOfObjects",
+ "metricQueryType": 0,
+ "namespace": "AWS/S3",
+ "period": "86400",
+ "refId": "A",
+ "region": "default",
+ "statistic": "Maximum"
+ }
+ ],
+ "timeFrom": "2d",
+ "title": "Bucket objects (CloudWatch)",
+ "type": "stat"
+ },
{
"datasource": {
"type": "grafana-postgresql-datasource",
@@ -878,7 +921,6 @@
"x": 6,
"y": 52
},
- "id": 18,
"options": {
"colorMode": "none",
"graphMode": "none",
@@ -908,6 +950,56 @@
"title": "Ledger-referenced bytes",
"type": "stat"
},
+ {
+ "datasource": {
+ "type": "grafana-postgresql-datasource",
+ "uid": "cef5v5sl9k7i8f"
+ },
+ "description": "COUNT(DISTINCT object_key) in media_cache_ledger — how many objects the ledger references. The bucket-object counterpart to 'Ledger-referenced bytes'.",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 4,
+ "w": 6,
+ "x": 6,
+ "y": 56
+ },
+ "options": {
+ "colorMode": "none",
+ "graphMode": "none",
+ "justifyMode": "center",
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "textMode": "value"
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "grafana-postgresql-datasource",
+ "uid": "cef5v5sl9k7i8f"
+ },
+ "editorMode": "code",
+ "format": "table",
+ "rawQuery": true,
+ "rawSql": "SELECT COUNT(DISTINCT object_key) AS ledger_keys\n FROM media_cache_ledger;",
+ "refId": "A"
+ }
+ ],
+ "title": "Ledger object keys",
+ "type": "stat"
+ },
{
"datasource": {
"type": "cloudwatch",
@@ -954,7 +1046,6 @@
"x": 12,
"y": 52
},
- "id": 20,
"options": {
"legend": {
"calcs": [],
@@ -1010,124 +1101,13 @@
"timeFrom": "30d",
"title": "Bucket growth (daily)",
"type": "timeseries"
- },
- {
- "datasource": {
- "type": "cloudwatch",
- "uid": "cef5x9o3yzawwf"
- },
- "description": "NumberOfObjects for boxel-media-cache-${env}, reported daily. Compare with 'Ledger object keys': the bucket stores one object per distinct object_key (dedupe-on-write), so the two counts should track each other; a persistent excess of bucket objects means orphans no ledger row references — invisible to GC, which scans ledger rows and never enumerates the bucket. Spans 2d regardless of the dashboard range so lastNotNull always finds the newest daily sample.",
- "fieldConfig": {
- "defaults": {
- "color": {
- "mode": "palette-classic"
- },
- "unit": "short"
- },
- "overrides": []
- },
- "gridPos": {
- "h": 4,
- "w": 6,
- "x": 0,
- "y": 56
- },
- "id": 17,
- "options": {
- "colorMode": "none",
- "graphMode": "none",
- "justifyMode": "center",
- "reduceOptions": {
- "calcs": [
- "lastNotNull"
- ],
- "fields": "",
- "values": false
- },
- "textMode": "value"
- },
- "targets": [
- {
- "datasource": {
- "type": "cloudwatch",
- "uid": "cef5x9o3yzawwf"
- },
- "dimensions": {
- "BucketName": "boxel-media-cache-${env}",
- "StorageType": "AllStorageTypes"
- },
- "id": "a",
- "metricEditorMode": 0,
- "metricName": "NumberOfObjects",
- "metricQueryType": 0,
- "namespace": "AWS/S3",
- "period": "86400",
- "refId": "A",
- "region": "default",
- "statistic": "Maximum"
- }
- ],
- "timeFrom": "2d",
- "title": "Bucket objects (CloudWatch)",
- "type": "stat"
- },
- {
- "datasource": {
- "type": "grafana-postgresql-datasource",
- "uid": "cef5v5sl9k7i8f"
- },
- "description": "COUNT(DISTINCT object_key) in media_cache_ledger — how many objects the ledger references. The bucket-object counterpart to 'Ledger-referenced bytes'.",
- "fieldConfig": {
- "defaults": {
- "color": {
- "mode": "palette-classic"
- },
- "unit": "short"
- },
- "overrides": []
- },
- "gridPos": {
- "h": 4,
- "w": 6,
- "x": 6,
- "y": 56
- },
- "id": 19,
- "options": {
- "colorMode": "none",
- "graphMode": "none",
- "justifyMode": "center",
- "reduceOptions": {
- "calcs": [
- "lastNotNull"
- ],
- "fields": "",
- "values": false
- },
- "textMode": "value"
- },
- "targets": [
- {
- "datasource": {
- "type": "grafana-postgresql-datasource",
- "uid": "cef5v5sl9k7i8f"
- },
- "editorMode": "code",
- "format": "table",
- "rawQuery": true,
- "rawSql": "SELECT COUNT(DISTINCT object_key) AS ledger_keys\n FROM media_cache_ledger;",
- "refId": "A"
- }
- ],
- "title": "Ledger object keys",
- "type": "stat"
}
],
"preload": false,
"refresh": "1m",
- "schemaVersion": 42,
+ "schemaVersion": 41,
"tags": [
- "screenshot",
+ "capture",
"captures",
"media-cache"
],
@@ -1136,7 +1116,7 @@
{
"hide": 2,
"name": "env",
- "query": "staging",
+ "query": "__ENV__",
"skipUrlSync": true,
"type": "constant"
},
@@ -1230,7 +1210,6 @@
},
"timepicker": {},
"timezone": "browser",
- "title": "Screenshot Capture Performance",
- "weekStart": ""
+ "title": "Capture Performance"
}
}
diff --git a/tmp/remote-canon.CvvwBW/dashboards/boxel-status/service-prerender-manager.json b/tmp/committed-canon.xS9d0p/dashboards/boxel-status/service-prerender-manager.json
index 517ba1c..1d6e321 100644
--- a/tmp/remote-canon.CvvwBW/dashboards/boxel-status/service-prerender-manager.json
+++ b/tmp/committed-canon.xS9d0p/dashboards/boxel-status/service-prerender-manager.json
@@ -1671,7 +1671,7 @@
"type": "loki",
"uid": "loki"
},
- "description": "Proxy request volume broken out by endpoint, from `proxying ${label} prerender request` log lines (manager-app.ts:1071). Lets you see whether the manager is mostly handling `visit` (per-card prerender), `module` (compile-time module prerender), `command` (LLM tool-call execution), or `screenshot` traffic — useful when correlating CPU / latency spikes with workload mix.",
+ "description": "Proxy request volume broken out by endpoint, from `proxying ${label} prerender request` log lines (manager-app.ts:1071). Lets you see whether the manager is mostly handling `visit` (per-card prerender), `module` (compile-time module prerender), `command` (LLM tool-call execution), or `capture` traffic — useful when correlating CPU / latency spikes with workload mix.",
"fieldConfig": {
"defaults": {
"color": {
@@ -1777,8 +1777,8 @@
"type": "loki",
"uid": "loki"
},
- "expr": "sum(count_over_time({service=\"prerender-manager\"} |~ \"proxying screenshot prerender request\" [1m]))",
- "legendFormat": "screenshot",
+ "expr": "sum(count_over_time({service=\"prerender-manager\"} |~ \"proxying capture prerender request\" [1m]))",
+ "legendFormat": "capture",
"queryType": "range",
"refId": "D"
}
@@ -2618,7 +2618,7 @@
{
"hide": 2,
"name": "env",
- "query": "staging",
+ "query": "__ENV__",
"skipUrlSync": true,
"type": "constant"
},
diff --git a/tmp/remote-canon.CvvwBW/dashboards/boxel-status/service-worker.json b/tmp/committed-canon.xS9d0p/dashboards/boxel-status/service-worker.json
index 14d31ea..380a576 100644
--- a/tmp/remote-canon.CvvwBW/dashboards/boxel-status/service-worker.json
+++ b/tmp/committed-canon.xS9d0p/dashboards/boxel-status/service-worker.json
@@ -1229,7 +1229,7 @@
"type": "grafana-postgresql-datasource",
"uid": "cef5v5sl9k7i8f"
},
- "description": "Resolved jobs per minute by `job_type`, taken from the `jobs` table. Lets you see whether the worker fleet is mostly indexing or whether cron-driven jobs (`daily-credit-grant`, `screenshot-card`, `run-command`) are crowding the queue.",
+ "description": "Resolved jobs per minute by `job_type`, taken from the `jobs` table. Lets you see whether the worker fleet is mostly indexing or whether cron-driven jobs (`daily-credit-grant`, `capture-card`, `run-command`) are crowding the queue.",
"fieldConfig": {
"defaults": {
"color": {
@@ -1811,7 +1811,7 @@
{
"hide": 2,
"name": "env",
- "query": "staging",
+ "query": "__ENV__",
"skipUrlSync": true,
"type": "constant"
}
(Run: https://github.com/cardstack/boxel/actions/runs/36788585037) |
Preview deploymentsHost Test Results 1 files ± 0 1 suites ±0 1h 30m 42s ⏱️ + 1h 18m 15s Results for commit cb30358. ± Comparison against earlier commit 042fc67. Realm Server Test Results 1 files ±0 291 suites ±0 2h 21m 55s ⏱️ - 2m 0s Results for commit cb30358. ± Comparison against earlier commit 042fc67. |
lukemelia
left a comment
There was a problem hiding this comment.
[Claude Code 🤖] This review looked at what survives the rolling deploy: the prerendered_html expand/contract, the wire and prefix aliases against the deploy order in deploy-release.yml, and the consumers of the old names outside this repo's deploy. It did not re-read the mechanical rename across the ~175 files.
Bottom line: one blocking issue. Rows that old-revision workers commit during the rollout lose their capture manifests when screenshots is dropped, silently, and the GC then deletes their objects. The fix is one statement in the removal migration.
- Before the drop, re-copy the manifests that old-revision workers wrote after the backfill. Details are in the thread on the removal migration. Blocking.
- Shim
screenshot-cardto the capture-card module. Releasedboxel-cliplugin skills keep teaching that import. Details are in the thread onpackages/host/app/tools/index.ts. Non-blocking; it's a decision. boxel captureis a new CLI verb, which makes thisfeat:(minor) rather thanrefactor:(patch) forboxel-cli. Non-blocking.- A few new comments describe the old name historically rather than stating the alias as a contract:
LEGACY_CAPTURE_SERVING_PREFIXinpaths.ts("before they were named captures"), the/_screenshot-cardroute inroutes.ts, and the additive migration's "renamed fromscreenshotsnow that…". Something like "The legacy spelling ofCAPTURE_SERVING_PREFIX, still answered because…" says the same thing timelessly. Non-blocking.
Host and realm-server shards are still running at the time of this review.
| // Removal half of the `screenshots` → `captures` rename. Runs post-deploy, | ||
| // once the realm-server rollout has stabilized and no task still reading | ||
| // `screenshots` is in flight. The additive migration already copied every | ||
| // manifest into `captures`, so nothing is lost here. | ||
|
|
||
| exports.shorthands = undefined; | ||
|
|
||
| exports.up = (pgm) => { | ||
| for (let table of [ | ||
| 'prerendered_html', | ||
| 'prerendered_html_pending', | ||
| 'prerendered_html_working', | ||
| ]) { | ||
| pgm.dropColumns(table, ['screenshots']); | ||
| } | ||
| }; |
There was a problem hiding this comment.
[Claude Code 🤖] "Nothing is lost here" doesn't hold for rows committed between migrate-db and the old worker tasks draining. promotePrerenderedHtmlPending copies every column in getColumnNames('prerendered_html') (ON CONFLICT … SET name=EXCLUDED.name), and that list is read from the live schema on main too. So once the additive migration has run, an old-revision worker's commit writes its fresh manifest to screenshots and copies the pending row's NULL captures over the backfilled value. This drop then discards the only copy.
What follows: those rows serve no declared captures until their next invalidation. The reconcile sweep has no arm for a missing manifest. Once MEDIA_CACHE_GC_MIN_AGE_MS passes, gcUnreferencedPredicate treats their ledger objects as unreferenced and deletes them. That includes every image/file thumb indexed by an old worker during the deploy.
The same column-copy mechanism separates the two writers cleanly. New-revision commits leave screenshots NULL, because their pending rows never set it. So a pre-drop copy restores exactly the old-revision writes and never resurrects a manifest the new code cleared:
pgm.sql(
`UPDATE ${table} SET captures = screenshots
WHERE captures IS NULL AND screenshots IS NOT NULL`,
);
pgm.dropColumns(table, ['screenshots']);The additive migration's comment ("the previous code revision keeps reading screenshots") could also say that it keeps writing it, and that its commits clear captures.
Regression. Blocking: silent data loss on the deploy that ships this.
There was a problem hiding this comment.
Willing to pay this price.
| 'screenshot-card', | ||
| ScreenshotCardToolModule, | ||
| ); | ||
| shimHostToolModule(virtualNetwork, 'capture-card', CaptureCardToolModule); |
There was a problem hiding this comment.
[Claude Code 🤖] The "no alias for card source" decision doesn't account for one set of consumers: the released boxel-cli versions this PR keeps POST /_screenshot-card for also ship plugin skills that teach import ScreenshotCardTool from '@cardstack/boxel-host/tools/screenshot-card' (integrate-screenshot-card-format) and boxel run-command @cardstack/boxel-host/commands/screenshot-card/default (boxel-command). Agents on those versions will keep generating code that fails to resolve, with a module-not-found error rather than the named refusal static screenshots gets.
One more line covers both spellings, since shimHostToolModule registers tools/ and commands/:
| shimHostToolModule(virtualNetwork, 'capture-card', CaptureCardToolModule); | |
| shimHostToolModule(virtualNetwork, 'capture-card', CaptureCardToolModule); | |
| shimHostToolModule(virtualNetwork, 'screenshot-card', CaptureCardToolModule); |
It also removes the landing-order constraint with the catalog companion PR, which otherwise can't deploy atomically with this one.
Not a bug in the rename itself. Non-blocking; it's a decision.
There was a problem hiding this comment.
Willing to pay this price too
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ve the legacy capture tool module Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three conflicts, all where main removed the orphaned realm-copy index path (#6331) that this branch had only renamed through: - `index-writer.ts`: `copyFrom`, `copyPrerenderedHtmlFrom` and the declared-lane ledger duplication are gone on main, so the rename of their bodies goes with them. The `capture-spec` import keeps main's type-only form, since `captureLedgerSourceURL`'s only caller was in the removed block, and carries this branch's name for the manifest type, which the rest of the file still uses. - `index-writer-test.ts`: the `copyFrom` tests cover the removed path. - `pg-queue.ts`: main dropped the `tasks/copy` job registration; this branch renamed the capture job's module. Both apply. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
richardhjtan
left a comment
There was a problem hiding this comment.
[Claude Code 🤖] This review traced whether every consumer of the old screenshot vocabulary — the CLI verb, the wire routes and serving prefixes, the DB columns, card-source declarations, and the host tool module — either moved or kept a compatibility path, and re-verified the two already-settled threads (the manifest-column drop's documented data loss, and the declined screenshot-card host-tool alias) still match their accepted resolutions on the current head.
One gap in the alias sweep: see the inline comment on capture.ts — the CLI's --spec batch output filename changed with no compatibility path, which the feat: title doesn't account for.
Everything else held up: /_screenshot-card, _screenshot/ (both the serving prefix and its write-reservation, which checks both spellings), and /prerender-screenshot all still answer; the additive/removal migration pair backfills correctly and documents the deploy-window loss the author already accepted; the regenerated SQLite schema matches; static screenshots is refused by name with a test pinning the message; and captureLedgerSourceURL isn't left orphaned by the rebase that dropped the realm-copy path.
The companion PRs (cardstack/boxel-catalog#753, cardstack/boxel-skills#150) are both still open, matching the merge-order dependency the description already discloses — not a new finding.
Matrix Client Tests (2, 3) is red, but both failing specs (tools.spec.ts's "Automatic Switch Command" flow, profile.spec.ts's login timeout) are unrelated to anything renamed here — reads as flaky infra, not a regression from this PR.
| // Wait between busy-retries when the server's 503 carries no Retry-After. | ||
| const DEFAULT_RETRY_AFTER_SECONDS = 5; | ||
| const MANIFEST_FILE_NAME = 'screenshot-manifest.json'; | ||
| const MANIFEST_FILE_NAME = 'capture-manifest.json'; |
There was a problem hiding this comment.
[Claude Code 🤖] The batch --spec manifest's filename changed with no compatibility path: screenshot-manifest.json → capture-manifest.json (this line; the JSON shape itself — {ok, captures: [...]} — was already this shape on main, only the filename moves). Every other externally-observed surface in this PR — the /_screenshot-card route, the _screenshot/ prefix, the boxel screenshot verb itself — kept a deliberate alias; a script that runs boxel screenshot --spec … --out <dir> and then reads <dir>/screenshot-manifest.json finds nothing there after an upgrade, with no error surfaced.
That's a breaking change to a published CLI's output contract, which the current feat: title doesn't capture — compute-release.ts only bumps major on feat!: or a BREAKING CHANGE: footer, and packages/boxel-cli/src/ is touched here, so the title drives the version this merge publishes.
Either retitle with feat!: (and a footer naming the break for the changelog), or close the gap the way the rest of the PR does — write (or symlink) the manifest under both names for a deprecation window.
Regression; blocking on the title/versioning decision before merge.
There was a problem hiding this comment.
[Claude Code 🤖] Went with the retitle rather than a dual-write: the title is now feat!:, and the description has a BREAKING CHANGE: footer naming the screenshot-manifest.json → capture-manifest.json move, so the changelog carries it.
# Conflicts: # packages/host/memory-baseline.json # packages/host/tests/test-module-timings.json # packages/realm-server/tests/helpers/index.ts # packages/realm-server/tests/realm-endpoints/capture-test.ts # packages/realm-server/tests/realm-endpoints/coarse-permission-outcome-test.ts # packages/realm-server/tests/test-module-timings.json # packages/runtime-common/realm.ts
main drops the shared prerendered_html_working table in a removal migration, so the captures rename no longer adds to or drops from it: a database where that drop already ran would otherwise fail the older- timestamped rename migrations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
[Claude Code 🤖] CI triage for the red checks on this run (CI #35214): Realm Server Tests (2, 6): not caused by this PR. The shard ran 701 tests and failed one:
Realm Server Test Results is the aggregate of the shards, so it clears with shard 2. percy/-cardstack-host: "1 visual change needs review". This means a snapshot is waiting for approval; no test failed. I didn't find any user-visible text in this PR's template changes that a snapshot would show, so the diff needs a look on the Percy build before it's approved. |
Picks up #6389, which fixes realm-policy-non-grantable-test on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Carries main's new capture-card code onto the captures names, including the realm-authority test main added under the old shared-test export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # packages/realm-server/prerender/utils.ts
habdelra
left a comment
There was a problem hiding this comment.
[Claude Code 🤖] This review checked whether the rename holds across the boundaries it has to survive: rolling-deploy ordering between services, stored URLs and config, persisted JSON keys, and string-matched consumers. It didn't re-check the rename's internal consistency, which typecheck and CI already cover.
Three blocking gaps, all in compatibility the PR otherwise sets out to keep. The rename itself holds up.
- The
screenshot-cardjob type is stranded during the deploy, and the stranded rows never leave the queue. A one-line worker alias fixes it. Seeruntime-common/worker.ts. - The rename to
allowArbitraryCapturessilently closes the gate on realms whose config still saysallowArbitraryScreenshots. Either confirm none do, or read both keys. Seebase/realm-config.gts. - The legacy
_screenshot/prefix is served, but the host signer,serve-index, and the auth service worker match only_capture/. Seeruntime-common/paths.ts. - The diagnostics retry keys are renamed without rewriting existing rows (non-blocking). See
prerender-html-reconcile.ts. - The
data-screenshot-pendingguard has no test and missesdata-screenshot-failed(non-blocking). Seeprerender/utils.ts. - The skill directory and name don't match, and the dashboard name is wrong (non-blocking). See
SKILL.md.
The cost you accepted on the removal migration has two more paths into it during the deploy window. Neither needs action if that decision stands:
- The
/prerender-visitbody attribute is renamed with no alias, and so is the response key. The new prerender server ignoresscreenshotsfrom previous-revision workers, so those visits skip their declared captures. - The new prerender server runs against the previous revision's base (
data-screenshot-pending) until the realm server rolls. PDF, video, and 3D poster slots fail during that window, and the reconcile retry arm recovers them afterward.
Adjacent, out of scope: software-factory/src/render-gate.ts is an in-repo consumer that still posts to /_screenshot-card. That route's only test asserts a 401 from jwtMiddleware, so it doesn't show which handler the alias reaches. It's worth pinning if the alias is meant to outlive this deploy.
Generated by Claude Code
| 'capture-card': () => | ||
| this.#queue.register(`capture-card`, Tasks['captureCard'](taskArgs)), |
There was a problem hiding this comment.
[Claude Code 🤖] The job type rename leaves screenshot-card jobs that nothing will ever claim. The claim SQL in pg-queue.ts filters j.job_type IN (<registered handlers>), and deploy-release.yml rolls workers (deploy-worker, then post-deploy-worker) before deploy-realm-server. Previous-revision realm servers keep serving for that whole window, and every screenshot-card job they publish from GET _screenshot/ or POST /_screenshot-card stays unfulfilled permanently. Each such request 503s after its sync wait. The rows never go away, so _queue-status's pending_job_count over-reports from then on.
This is the same rolling-deploy class as the /prerender-screenshot alias the PR already keeps. The args are wire-identical (ScreenshotCardArgs and CaptureCardArgs have the same fields), so a one-release alias is enough:
'capture-card': () =>
this.#queue.register(`capture-card`, Tasks['captureCard'](taskArgs)),
// Previous-revision realm servers publish under the old name until they drain.
'screenshot-card': () =>
this.#queue.register(`screenshot-card`, Tasks['captureCard'](taskArgs)),Add 'screenshot-card' back to the worker-job-registration-test list. A removal-phase UPDATE jobs SET job_type = 'capture-card' WHERE job_type = 'screenshot-card' AND status = 'unfulfilled' would work as an alternative, but only for cleanup: the requests made during the window would still fail.
Regression. Blocking unless you'd rather accept it the way the other deploy-window costs were accepted. The stranded rows are the difference: they outlast the deploy.
Generated by Claude Code
There was a problem hiding this comment.
[Claude Code 🤖] Added the alias in c0dd491. screenshot-card is registered against the captureCard task, and worker-job-registration-test lists it again. Previous-revision realm servers that publish under the old name during the rollout now get their jobs claimed.
| // request time, so editing this takes effect with the index update, no | ||
| // restart. | ||
| @field allowArbitraryScreenshots = contains(BooleanField); | ||
| @field allowArbitraryCaptures = contains(BooleanField); |
There was a problem hiding this comment.
[Claude Code 🤖] Renaming this field closes the arbitrary-capture gate, with no warning, on every deployed realm that opted in. allowsArbitraryCaptures() in realm.ts reads only attributes?.allowArbitraryCaptures. A realm config that still says "allowArbitraryScreenshots": true therefore reads as gated, and every uncached DSL capture on that realm returns 403 "does not allow arbitrary captures". Only experiments-realm/realm.json was migrated. The PR's reasoning that no one has adopted the feature covers card source. It doesn't cover realm config, which is user data.
Which do you want?
- Confirm that no deployed realm config sets the old key. A query against
boxel_indexfor the realm-config instances would settle it. If none do, this is fine as-is. - Or read both keys in
allowsArbitraryCaptures()(=== trueon either) until the configs are rewritten.
Regression. Blocking until one of those is chosen.
Generated by Claude Code
There was a problem hiding this comment.
[Claude Code 🤖] Went with reading both keys (e9cde5e). RealmConfig keeps allowArbitraryScreenshots as a described legacy field, so the index still carries it, and allowsArbitraryCaptures() opens the gate when either key is true. A new media-cache-dsl-test case seeds a config with only the old key and asserts that an uncached capture renders.
| // The legacy spelling of `CAPTURE_SERVING_PREFIX`. Still answered, because a | ||
| // durable capture URL is meant to be stored: a card may hold one minted under | ||
| // this spelling, and a browser keeps serving an auth service worker that | ||
| // matches capture requests on this prefix to attach credentials until it next | ||
| // updates. Reserved against | ||
| // writes for as long as it is served, so nothing can be stored where a GET | ||
| // would never read it back. | ||
| export const LEGACY_CAPTURE_SERVING_PREFIX = '_screenshot/'; |
There was a problem hiding this comment.
[Claude Code 🤖] The realm serves the legacy prefix, but three consumers outside the realm match only CAPTURE_SERVING_PREFIX, so a stored _screenshot/ URL still breaks. These are exactly the cards this constant is kept for:
host/app/services/capture-url-signer.tsrealmRootOfCaptureURLthrows "is not a capture URL" on a_screenshot/URL.getSignedUrlcalls it before making any request, solib/signed-capture.gtsrenders an error and retries for any legacy URL on a private realm. The server-side signer inrealm.tsdoes accept both prefixes, so only the host half is missing.realm-server/handlers/serve-index.tsisCaptureServingRequestcheckspathname.includes('/' + CAPTURE_SERVING_PREFIX). Atext/htmlnavigation to a legacy URL, such as opening a PDF capture in a tab, gets the host app shell instead of the capture.host/public/auth-service-worker.jsCAPTURE_PATH_SEGMENTis/_capture/only. Credentials are still attached, but the 503/Retry-After absorption no longer engages, so a legacy<img>whose capture is still rendering shows as broken.
All three can reuse isCaptureServingPath or the CAPTURE_SERVING_PREFIXES list. The service worker can't import it, so it needs a second segment constant. The comment above is also off: the old service worker keys only 503 absorption on the prefix, not credential attachment. Credentials are attached to any realm URL (the fetch listener), so that part of the rationale doesn't hold.
Regression against the compatibility the PR description promises. Blocking.
Generated by Claude Code
There was a problem hiding this comment.
[Claude Code 🤖] Fixed in 447ed58. All three now match the legacy prefix too:
realmRootOfCaptureURLresolves againstCAPTURE_SERVING_PREFIXES, which is now exported. The unit test covers a_screenshot/URL.isCaptureServingRequestpre-filters on either prefix. A new index-responses test sends a document navigation to a_screenshot/URL and asserts that the realm answers, not the shell.- The service worker has a
LEGACY_CAPTURE_PATH_SEGMENT, andisCaptureRoutechecks both. The mirror and the shipped-worker test cover it.
I also corrected the LEGACY_CAPTURE_SERVING_PREFIX comment. It no longer claims the old worker keys credential attachment on the prefix.
| AND jsonb_typeof(ph.diagnostics->'captureErrors') = 'array' | ||
| AND COALESCE((ph.diagnostics->>'captureFailureRenders')::int, 1) | ||
| < ${DECLARED_CAPTURE_RETRY_CAP} |
There was a problem hiding this comment.
[Claude Code 🤖] The diagnostics keys were renamed (screenshotErrors → captureErrors, screenshotCaptureFailureRenders → captureFailureRenders), but no migration rewrites the rows that already exist. Any published row with pending declared-capture failures at deploy time drops out of this retry arm permanently, and its captures stay missing until the source is invalidated some other way. For the same reason, the prior-state read in index-writer.ts (priorDiagnostics?.captureErrors) resets the retry counters on those rows.
To fix it, add a key rewrite to the additive migration on prerendered_html and prerendered_html_pending, for example:
UPDATE prerendered_html
SET diagnostics = (diagnostics - 'screenshotErrors' - 'screenshotCaptureFailureRenders')
|| jsonb_strip_nulls(jsonb_build_object(
'captureErrors', diagnostics->'screenshotErrors',
'captureFailureRenders', diagnostics->'screenshotCaptureFailureRenders'))
WHERE diagnostics ?| array['screenshotErrors', 'screenshotCaptureFailureRenders'];If you accept the loss instead, say so in the migration comment.
Regression. Non-blocking: it affects only rows that were mid-retry, and the next edit recovers them.
Generated by Claude Code
There was a problem hiding this comment.
[Claude Code 🤖] Added the rewrite in 074bbc3. I put it in the removal migration rather than the additive one. The previous revision reads and writes the old keys until it drains, so running afterwards also catches the rows it wrote during the rollout, and it leaves the old reconcile working in the meantime. It covers both prerendered_html and prerendered_html_pending.
| // A component still signalling readiness under the attribute's former | ||
| // name reads here as no signal at all, so the wait above clears at once | ||
| // and the capture persists whatever half-painted frame is on screen — | ||
| // plausible bytes, wrong content, no error anywhere. Fail the slot | ||
| // instead, naming the attribute to rename. | ||
| if ( | ||
| await page.evaluate( | ||
| () => document.querySelector('[data-screenshot-pending]') != null, | ||
| ) | ||
| ) { | ||
| return buildInvalidRenderResponseError( | ||
| page, | ||
| `capture-only component for capture "${name}" signals readiness with data-screenshot-pending, which is no longer read — rename it to data-capture-pending`, | ||
| { title: 'Capture render never painted' }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
[Claude Code 🤖] This guard has two gaps.
- No test covers it. Nothing in the test directories renders a component that sets
data-screenshot-pending, so this guard is one of the two loud failures the PR relies on and is currently pinned only by this comment. A prerendering test that renders a capture-only component setting the old attribute and asserts the slot error would cover it. - It misses
data-screenshot-failed. A component that clears the old pending attribute and sets the old failure attribute passes the[data-capture-failed]wait, and its broken frame is persisted without any error. That is the same silent failure this block exists to prevent. Checking[data-screenshot-pending], [data-screenshot-failed]in one selector covers both.
Non-blocking.
Generated by Claude Code
There was a problem hiding this comment.
[Claude Code 🤖] Both fixed in bd4aec6. The guard now looks for data-screenshot-pending and data-screenshot-failed and names whichever it finds, along with its rename. A new declared-captures-indexing-test case declares a slot for each attribute next to a working sibling. It asserts that both slots fail with the rename in their error while the sibling still captures.
| @@ -1,38 +1,38 @@ | |||
| --- | |||
| name: screenshot-perf-diagnosis | |||
| description: Diagnose why a screenshot capture took a given amount of time, from the per-capture stage telemetry the screenshot pipeline emits — one JSON log line per event on the `boxel:screenshot-perf` channel (the same `| json` convention as `boxel:client-perf`), plus the same `capture` breakdown persisted onto the capture's `media_cache_ledger.diagnostics` row so it is readable by SQL. Two event types cover the pipeline's processes — a `request` event from a serving surface (the GET `{realm}_screenshot/…` DSL route or `POST /_screenshot-card`) recording what the caller experienced (hit / rendered / congested / timeout / gated / error) and where the surface wall-clock went, and a `capture` event from the worker's `screenshot-card` task recording queue wait, the prerender stage breakdown, and the persist leg — joined on `jobId`, joined back to the realm-server's `realm:requests` lines by the `x-boxel-logging-correlation-id` `correlationId`, and joined to the prerender server/manager logs by the `prerenderRequestId`. Covers (1) a "this screenshot was slow" complaint given a served URL, a (realm + cardId + capture spec) triple, a matrix user/session, a time window, or a correlation id — locate the timing record(s) and break the wall-clock across the stages (queue-wait, page-acquire launch-vs-reuse, settle, image-paint wait, `page.screenshot`, persist upload-vs-dedupe-hit, serve) and name the dominant one; (2) separating a ledger hit (zero Chrome work, `outcome=hit`, no `capture` event) from an on-demand render (`outcome=rendered`); (3) separating a congested per-realm lane (a fail-fast `outcome=congested` 503 from the queue-depth pre-check, or a long `jobWaitMs`/`queueWaitMs` behind the serialized `screenshot:{realmURL}` lane) from a genuinely slow render (`renderMs`); (4) separating a cold-pool Chrome launch (`tabReused=false`, high `launchMs`/`tabStartupMs`) from a warm affinity reuse; (5) separating a real S3 upload (`persistOutcome=uploaded`) from a dedupe-on-write hit (`persistOutcome=deduped`); (6) separating a cache miss forced by a generation bump — the instance's index row was re-stamped (its own edit, a dependency change, or a realm-wide reindex), so a prior `media_cache_ledger` row exists for the same `(realm_url, source_url, capture_spec_hash)` at a lower `source_generation` — from a genuinely new capture spec (no prior row for that `capture_spec_hash`); and (7) recognizing an `outcome=timeout` (the sync wait expired with a 503 + Retry-After; the job keeps running, persists, and the caller's retry becomes a hit) and an `outcome=gated` (the realm's `allowArbitraryScreenshots` gate is closed; existing captures still serve); and (8) diagnosing a slow or failed **PDF** capture (`type=pdf`) — the output encoding is a `contentType` dimension (`application/pdf` vs the raster `image/*`) on both event types, the ledger row, and the dashboard's `contentType` template variable, so a "this PDF was slow" complaint runs the same hit-vs-render / stage-breakdown runbook scoped to `application/pdf`, with pagination cost landing in the render's capture stage and an over-bounds document (past the 20-page / 10 MB caps) surfacing as a `status=error` capture / `outcome=error` request rather than a slow success. Reads the "Screenshot Capture Performance" Grafana dashboard (grafanactl name `boxel-screenshot-perf`: capture latency by stage p50/p95, hit-vs-render and queue-vs-render splits, warm/cold page-acquire, persist outcome, sliced by realm, surface, and contentType encoding). For staging/prod this layers on `aws-access` (the AWS session + SSM port-forward to the read-only DB) and `tail-logs` (the Loki wrapper); hand off to `prerender-sizing` when the bottleneck is pool contention and to `indexing-diagnostics` for the prerender render internals (the `Timings`/`PoolMeta` the render itself records). Use when someone says a screenshot was slow, a capture URL took seconds, a `_screenshot/` request 503'd, or asks why a specific capture cost what it did. | |||
| name: capture-perf-diagnosis | |||
There was a problem hiding this comment.
[Claude Code 🤖] The frontmatter name: capture-perf-diagnosis no longer matches the directory, which is still screenshot-perf-diagnosis. The skill is listed under the directory name, so search-shape-diagnosis/SKILL.md's new reference to capture-perf-diagnosis points at nothing. Rename the directory to match.
Two more spots in this file:
- Line 124 says "Capture Capture Performance" and grafanactl name
boxel-capture-perf. The dashboard JSON (boxel-status/captures.json) keepsmetadata.name: boxel-screenshot-perf, which is deliberate, and its title is "Capture Performance". - The same doubled word appears in the
packages/runtime-common/index.tscomment "Capture-capture renders only".
Non-blocking.
Generated by Claude Code
There was a problem hiding this comment.
[Claude Code 🤖] Fixed in 4c0832e. The directory is now capture-perf-diagnosis, so the reference from search-shape-diagnosis resolves. The dashboard is named "Capture Performance" with grafanactl name boxel-screenshot-perf, and the doubled word in the index.ts comment is gone.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Zc264o35GJXEcQSG39bwm
…and service worker Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Zc264o35GJXEcQSG39bwm
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Zc264o35GJXEcQSG39bwm
…t-deploy Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Zc264o35GJXEcQSG39bwm
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Zc264o35GJXEcQSG39bwm
…rd name Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Zc264o35GJXEcQSG39bwm
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Zc264o35GJXEcQSG39bwm
… integration-surfaces reference Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Zc264o35GJXEcQSG39bwm
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Zc264o35GJXEcQSG39bwm
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Zc264o35GJXEcQSG39bwm
Background and Goal
CS-13193.
static screenshotswas named before the feature could emit PDFs. Now that it can, the name is wrong rather than merely imprecise —PdfScreenshotSpecreads as an oxymoron, and the wrong noun had leaked into author-facing errors ("a pdf screenshot's format must be 'isolated' or 'embedded'"). A 20-page document laid out under@pageand@media printis not a shot of a screen.The codebase had already half-migrated on its own. In runtime-common + base + host/app, bare "capture" outnumbered "screenshot" roughly 2.5:1; the modules were already
capture-spec.tsandcapture-url-token.ts, the column alreadycapture_spec_hash, the path constant alreadyCAPTURE_SERVING_PREFIX— pointing at'_screenshot/'.QUERY /_sign-capture-urlsalready signed_screenshot/URLs. "Screenshot" survived concentrated in the author-facing declaration and the URL prefix.No card author has adopted the feature and no durable
_screenshot/URLs exist in user data, so this is the cheapest this rename will ever be.One word in the platform vocabulary:
capture.boxel screenshotstays on as a CLI verb alias, where concreteness pays and a second noun costs nothing.Beyond the word
The larger defect was that distinct concepts were told apart only by which synonym they picked, so a reader had to read the comment above each one. Three format rosters and two output-type rosters now state their axis instead:
SCREENSHOT_FORMATSON_DEMAND_CAPTURE_FORMATSCAPTURE_FORMATSCANONICAL_CAPTURE_FORMATSDECLARED_SCREENSHOT_FORMATSDECLARED_CAPTURE_FORMATSSCREENSHOT_IMAGE_TYPESRASTER_OUTPUT_TYPESThree spec types likewise collided on the good name; each now says what it is.
CaptureSpecis what an author declares,CaptureRequestSpecis the wire spec behind the POST body and GET DSL, andCaptureIdentityis the canonicalized subset that keys the MediaCache ledger.Where to start
packages/runtime-common/capture-spec.ts— the roster and spec-type renames above.packages/base/card-api.gts—static captures,CaptureSpec,captureURLs,data-capture-pending.packages/postgres/migrations/1790289919732_*andmigrations-removal/1790289920732_*—prerendered_html.screenshots→.capturesacross the production table and both twins, expand/contract: additive add plus backfill pre-deploy, drop post-deploy.packages/realm-server/prerender/utils.ts— see the timer note below.Two things worth a reviewer's attention
screenshotMsdid not becomecaptureMs.captureMsalready existed inprerender/utils.tsas a different metric (whole-step elapsed), whilescreenshotMstimed only the CDPcaptureScreenshot/printToPDFcall. Merging them would have silently corrupted the perf data with no test failure. The CDP call is nowcdpCaptureMs;captureMskeeps its name and meaning.The Grafana dashboard's uid stays
boxel-screenshot-perf. Its title, filename and every LogQL query are renamed, but the uid is an opaque identifier rather than vocabulary — grafanactl has no prune, so changing it would orphan the existing dashboard and break saved links for no reader benefit. Historical panels will read empty for the Loki retention window, which is expected and was chosen deliberately.What still answers to the old names
Three consumers of this vocabulary do not roll forward with a deploy of it, so the wire paths keep both names:
POST /_screenshot-card—boxel-cliis installed and pinned independently of the realm server, and a released version posts here. Same handler._screenshot/serving prefix — a capture URL is meant to be stored, and a browser keeps serving the previously installed auth service worker (which matches capture requests on the prefix to attach credentials) until it next updates. Served, and reserved against writes, under both prefixes./prerender-screenshot— the prerender server and manager roll out ahead of the workers and realm servers that call them, so the previous revision asks for the old path while the new one is already serving.Card source gets no alias, because one there would carry the old vocabulary forward indefinitely. The two silent failures are made loud instead:
static screenshotsis refused by name at declaration-read rather than read by nothing, and a capture-only component still signalling withdata-screenshot-pendingfails its slot instead of persisting a half-painted frame.Left as "screenshot" on purpose
Puppeteer's own
page.screenshot/handle.screenshot; the CLI alias;boxel-cli/CHANGELOG.md;experiments-realm/transition-tray.gts(CSS view-transition captures, unrelated);ai-assistant-evals,software-factory,boxel-icons. Applied migrations were left immutable, so1787603632440's comment still names the old type — a migration records the state at the time it ran.Companion PRs
The host tool module moved, so these must land together or the catalog demo resolves to nothing:
listing-create's importTesting
Typecheck clean in host, realm-server, runtime-common and boxel-cli; eslint and prettier clean; migration name lint and the removal-phase guard pass; eslint-plugin-boxel 131/131; boxel-cli 638/640 (the 2 failures are a missing Playwright browser locally). CLI alias verified:
boxel capture|screenshot. The host and realm-server browser suites need the full dev stack — CI is the arbiter.BREAKING CHANGE:
boxel capture --spec … --out <dir>(and itsboxel screenshotalias) now writes its batch manifest to<dir>/capture-manifest.jsoninstead of<dir>/screenshot-manifest.json. The JSON shape is unchanged. Scripts that read the old filename need to read the new one.🤖 Generated with Claude Code