Skip to content

Persist and serve pdf captures from the GET _screenshot/ DSL - #6121

Open
lukemelia wants to merge 1 commit into
cs-12976-pagepdf-capture-leg-with-pagebyte-bounds-capture-only-viafrom
cs-12977-persist-pdfs-to-mediacache-and-serve-from-the-get
Open

lukemelia wants to merge 1 commit into
cs-12976-pagepdf-capture-leg-with-pagebyte-bounds-capture-only-viafrom
cs-12977-persist-pdfs-to-mediacache-and-serve-from-the-get

Conversation

@lukemelia

Copy link
Copy Markdown
Contributor

Summary

Third PR in the Cards to PDF chain (CS-12977), stacked on #6120 (which stacks on #6118) — this diff reads clean against #6120.

Durable PDF URLs: {realm}_screenshot/{path}?type=pdf now serves from the ledger, capturing on miss — the use case where cards embed always-current PDF links, since the identity pins the instance generation and an edit re-captures on next fetch.

  • Persist: the capture-only gates from Add the page.pdf capture leg, capture-only via POST /_screenshot-card #6120 lift — the POST handler derives a persist identity for pdf specs (the captures[0].url round-trips through the GET DSL as ?type=pdf), and the worker task persists content_type: application/pdf with null pixel dimensions. No schema change; the ledger, both adapters, and the GC arms are content-type-agnostic (none of the GC candidate queries read content_type).
  • Serve: the GET DSL parse admits type=pdf; the miss path reuses the allowArbitraryScreenshots gate, the congestion pre-check, and the 25s sync-wait / 503 + Retry-After posture unchanged. The service worker's _screenshot/ 503-absorbing retry covers pdf URLs by path prefix, untouched.
  • Content-Disposition: pdf responses carry inline; filename="<card>.pdf" (derived from the source URL's last segment, reduced to the quoted-string-safe charset) so the browser's viewer/download shows a sensible name; image responses stay bare.
  • Telemetry: both boxel:screenshot-perf event types gain a required contentType dimension — request events derive it from the spec (or the served ledger row on the ?name= path), capture events report what the render actually produced (null when it never got that far). The screenshot-captures Grafana dashboard gains a matching contentType template variable applied to all 24 queries; events from before the field existed match the All value (.*).

Testing

  • media-cache-dsl-test.ts (45/45 locally): the pdf GET flow end-to-end (captures on demand, persists as application/pdf with null dims, serves with content-type + content-disposition, repeat is a ledger hit with zero Chrome work, the png identity stays untouched), the task persisting under the pdf spec hash, ?type=pdf round-tripping through the served-query parser, and pdfFilenameFor unit coverage.
  • screenshot-card-test.ts (64/64): a POST pdf capture's persist identity hashes the encoding in and returns the ?type=pdf served URL.
  • media-cache-serving-test.ts and media-cache-gc-test.ts green; runtime-common and realm-server typecheck clean.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Grafana preview

Preview deployed for 1 dashboard in the staging Grafana.
Cross-dashboard drill-throughs still point at the canonical staging dashboards.

Dashboards:

Preview is torn down automatically when this PR is closed or merged.

(Run: https://github.com/cardstack/boxel/actions/runs/35035467518)

@github-actions

github-actions Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Observability diff (vs staging)

Show diff
diff --git a/tmp/remote-canon.HMIcmO/dashboards/boxel-status/screenshot-captures.json b/tmp/committed-canon.00rFGK/dashboards/boxel-status/screenshot-captures.json
index ad87d7e..88b3287 100644
--- a/tmp/remote-canon.HMIcmO/dashboards/boxel-status/screenshot-captures.json
+++ b/tmp/committed-canon.00rFGK/dashboards/boxel-status/screenshot-captures.json
@@ -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\" [5m])) or vector(0)",
+            "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)",
             "queryType": "range",
             "refId": "A"
           }
@@ -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\" [5m])) or vector(0)",
+            "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)",
             "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\" [$__interval]))",
+            "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]))",
             "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\" | unwrap totalMs [$__interval]) by ()",
+            "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 ()",
             "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\" | unwrap totalMs [$__interval]) by ()",
+            "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 ()",
             "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\" | unwrap totalMs [$__interval]) by ()",
+            "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 ()",
             "legendFormat": "p99",
             "queryType": "range",
             "refId": "C"
@@ -275,7 +270,6 @@
           "x": 0,
           "y": 13
         },
-        "id": 6,
         "panels": [],
         "title": "Capture stage breakdown",
         "type": "row"
@@ -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\" | unwrap queueWaitMs [$__interval]) by ()",
+            "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 ()",
             "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\" | unwrap permissionsMs [$__interval]) by ()",
+            "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 ()",
             "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\" | unwrap launchMs [$__interval]) by ()",
+            "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 ()",
             "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\" | unwrap renderMs [$__interval]) by ()",
+            "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 ()",
             "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\" | unwrap navMs [$__interval]) by ()",
+            "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 ()",
             "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\" | unwrap settleMs [$__interval]) by ()",
+            "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 ()",
             "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\" | unwrap imagePaintMs [$__interval]) by ()",
+            "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 ()",
             "legendFormat": "imagePaintMs",
             "queryType": "range",
             "refId": "G"
@@ -396,7 +389,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\" | unwrap screenshotMs [$__interval]) by ()",
+            "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",
             "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\" | unwrap decodeMs [$__interval]) by ()",
+            "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 ()",
             "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\" | unwrap persistMs [$__interval]) by ()",
+            "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 ()",
             "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\" | unwrap queueWaitMs [$__interval]) by ()",
+            "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 ()",
             "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\" | unwrap prerenderMs [$__interval]) by ()",
+            "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 ()",
             "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\" | unwrap renderMs [$__interval]) by ()",
+            "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 ()",
             "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\" | unwrap launchMs [$__interval]) by (tabReused)",
+            "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)",
             "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\" [$__interval]))",
+            "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]))",
             "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\" | unwrap totalMs [$__interval]) by (realmURL)",
+            "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)",
             "legendFormat": "{{realmURL}}",
             "queryType": "range",
             "refId": "A"
@@ -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\" | unwrap totalMs [$__interval]) by (surface)",
+            "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)",
             "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\" | unwrap totalMs [$__range])))",
+            "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])))",
             "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,122 +1101,11 @@
         "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",
       "captures",
@@ -1136,7 +1116,7 @@
         {
           "hide": 2,
           "name": "env",
-          "query": "staging",
+          "query": "__ENV__",
           "skipUrlSync": true,
           "type": "constant"
         },
@@ -1189,6 +1169,38 @@
           ],
           "query": "get-dsl,post",
           "type": "custom"
+        },
+        {
+          "allValue": ".*",
+          "current": {
+            "selected": true,
+            "text": "All",
+            "value": "$__all"
+          },
+          "description": "The capture's output encoding; events from before the field existed match All.",
+          "hide": 0,
+          "includeAll": true,
+          "multi": false,
+          "name": "contentType",
+          "options": [
+            {
+              "selected": true,
+              "text": "All",
+              "value": "$__all"
+            },
+            {
+              "selected": false,
+              "text": "image/png",
+              "value": "image/png"
+            },
+            {
+              "selected": false,
+              "text": "application/pdf",
+              "value": "application/pdf"
+            }
+          ],
+          "query": "image/png,application/pdf",
+          "type": "custom"
         }
       ]
     },
@@ -1198,7 +1210,6 @@
     },
     "timepicker": {},
     "timezone": "browser",
-    "title": "Screenshot Capture Performance",
-    "weekStart": ""
+    "title": "Screenshot Capture Performance"
   }
 }

(Run: https://github.com/cardstack/boxel/actions/runs/35035467477)

@github-actions

github-actions Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Host Test Results

    1 files  ±0      1 suites  ±0   2h 32m 42s ⏱️ - 6m 40s
4 806 tests ±0  4 792 ✅ ±0  14 💤 ±0  0 ❌ ±0 
4 821 runs  ±0  4 807 ✅ ±0  14 💤 ±0  0 ❌ ±0 

Results for commit 8836abf. ± Comparison against earlier commit fd76fb0.

Realm Server Test Results

    1 files  ±0    225 suites  ±0   1h 22m 29s ⏱️ +25s
3 073 tests +1  3 073 ✅ +1  0 💤 ±0  0 ❌ ±0 
3 112 runs  +1  3 112 ✅ +1  0 💤 ±0  0 ❌ ±0 

Results for commit 8836abf. ± Comparison against earlier commit fd76fb0.

@lukemelia
lukemelia added this pull request to stack #6122 September 15, 2026 03:42

@lukemelia lukemelia left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖]

Lens. Reviewed the pdf persist/serve path end-to-end against the base branch this stacks on: the capture identity round-trip (POST persist target ⇄ GET DSL served URL ⇄ the worker's re-derived persist hash), the content-type plumbing from render → ledger row → response headers, the reuse of the gate / congestion pre-check / sync-wait posture, and the telemetry + dashboard changes. I did not re-audit the pdf render leg itself (it lands in the base branch) or the arbitrary-screenshot cost model.

Bottom line: no blocking issues. type is part of the canonical spec identity (canonicalCaptureSpecString keeps it via canonicalOverrides, pdf ≠ the png default), so a pdf and a png key distinct ledger rows rather than colliding; the real engine sets contentType: 'application/pdf' in captureScreenshot (prerender/utils.ts), so the persist stores the paged content type instead of the response.contentType ?? 'image/png' fallback; pdfFilenameFor reduces the last path segment to a quoted-string-safe charset with no CRLF anywhere, so the Content-Disposition filename can't inject a header; and the ledger's width/height columns are already nullable, which the paged capture relies on. parseCaptureSpecParams has a single caller (the GET DSL serve path), so lifting the type=pdf refusal doesn't quietly admit pdf on any other surface. The new tests drive the full GET flow (capture-on-demand → persist as application/pdf with null dims → inline serve with filename → repeat is a zero-Chrome ledger hit, png identity untouched) and would fail without the change — the old parse refused type=pdf and the task hashed a pdf render to null and refused to persist.

Adjacent, out of scope. The ScreenshotPrerenderResponse.contentType doc in runtime-common/index.ts still reads "The engine produces only image/png today" — no longer true since the pdf capture leg landed, and this PR's persist now depends on the engine reporting application/pdf. Worth correcting whenever that type is next touched; not this PR's job.

@lukemelia
lukemelia requested review from a team and FadhlanR September 15, 2026 03:55
@lukemelia
lukemelia marked this pull request as ready for review September 15, 2026 03:55
@lukemelia
lukemelia force-pushed the cs-12977-persist-pdfs-to-mediacache-and-serve-from-the-get branch 2 times, most recently from da51bd0 to fd76fb0 Compare September 15, 2026 19:55
A pdf capture now persists to the MediaCache under its own spec identity
(content_type application/pdf, no pixel dimensions) and serves from
{realm}_screenshot/{path}?type=pdf: ledger hit on repeat, capture on miss
behind the same allowArbitraryScreenshots gate, congestion pre-check, and
sync-wait posture every capture uses. An edit re-captures on next fetch
because the identity pins the instance generation — the durable-URL use
case where cards embed always-current pdf links.

PDF responses carry Content-Disposition: inline with a filename derived
from the source card, so the browser viewer shows a sensible name; images
stay bare. The boxel:screenshot-perf events gain a contentType dimension
on both event types (request events derive it from the spec or served
row, capture events report what the render produced), and the screenshot
dashboard's queries grow a matching contentType template variable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lukemelia
lukemelia force-pushed the cs-12977-persist-pdfs-to-mediacache-and-serve-from-the-get branch from fd76fb0 to 8836abf Compare September 15, 2026 23:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants