Track static-asset 404s per release

Group asset 404s by URL and deploy SHA. Catch stale CDN caches and missing hashed bundles before users see a blank page.

Updated · published

markdown for agents →

difficulty intermediate · time to value 2 minutes · execution on demand

Start from this

Are any static assets 404ing more than expected this week? Group by URL and by deploy SHA so I can see if a release broke a cache key.

Why this matters

Hashed-filename CDN deploys are an under-appreciated source of silent breakage. Webpack/Vite/Turbopack rename app.js to app-a3f29b1.js on every build. If a user has the OLD index.html cached but you’ve already invalidated the OLD bundle URL, they get a 404 — and most apps render a blank page with no thrown error.

Sentry catches thrown exceptions, not 404s on script tags. Cloudflare logs the 404 but you’d have to push those into a log warehouse and query them. Nobody does this until a customer complains “your site is broken on my work laptop.”

Agentry logs the asset 404 as an analytics or log event, joins it with your deploy timeline, and tells you “deploy a3f29b1 is generating 50% of the 404s for stale app-9c3a210.js — your CDN edge in EU has stale TTLs.”

What you get

  • Top 10 asset URLs returning 404s in the last 7 days
  • For each: which deploy invalidated them and how many distinct users are still requesting them
  • A diagnosis (stale edge cache vs missing build vs typo in source)

Walk through it

You

Are any static assets 404ing more than expected this week? Group by URL and deploy SHA.

Agent

Let me check recent deploys for context, then query asset 404s.

Endpoint: GET /v1/projects/:project_id/deploys
Exact shape: /v1/openapi.json?path=%2Fv1%2Fprojects%2F%7Bproject_id%7D%2Fdeploys&method=get
Use query parameters, not a JSON body.
Query/filter concept:
  limit: 5
{
  "deploys": [
    { "sha": "9c3a210", "received_at": 1747353600, "message": "feat: dashboard v2" },
    { "sha": "a3f29b1", "received_at": 1747180000, "message": "fix: pagination" },
    { "sha": "b1e229c", "received_at": 1746920000, "message": "feat: pricing page" }
  ]
}
Endpoint: POST /v1/projects/:project_id/analytics/query
Exact shape: /v1/openapi.json?path=%2Fv1%2Fprojects%2F%7Bproject_id%7D%2Fanalytics%2Fquery&method=post
Custom HogQL goes in the OpenAPI-defined `query` field.
Concept fields:
  query: "SELECT properties.url AS url, count() AS cnt, uniq(distinct_id) AS users FROM events WHERE event = 'asset_404' AND timestamp >= now() - INTERVAL 7 DAY GROUP BY url ORDER BY cnt DESC LIMIT 10"
{
  "rows": [
    { "url": "/assets/app-a3f29b1.js", "cnt": 1284, "users": 412 },
    { "url": "/assets/vendor-a3f29b1.js", "cnt": 1180, "users": 411 },
    { "url": "/assets/logo-old.png", "cnt": 87, "users": 38 },
    { "url": "/assets/app-b1e229c.js", "cnt": 22, "users": 12 }
  ]
}
Agent

The pattern is clear. Deploy 9c3a210 (today) invalidated the a3f29b1 bundle, but 412 users are still requesting app-a3f29b1.js and getting 404s. That means either:

  1. Their index.html is cached at the edge with stale <script src> references, OR
  2. You’re serving index.html with Cache-Control: max-age=3600 somewhere

The logo-old.png is a small separate issue — 38 users, probably a forgotten reference in an email template or social share image.

Recommended fix: serve index.html with Cache-Control: no-cache, must-revalidate and bump CDN edge TTL on the old bundles to 24h instead of immediate purge. Want me to draft a Cloudflare Page Rules config?

The output

Asset 404s (7d), grouped by URL × deploy

URL                                COUNT   USERS   DEPLOY INVALIDATED
/assets/app-a3f29b1.js             1,284     412   9c3a210 (today)
/assets/vendor-a3f29b1.js          1,180     411   9c3a210 (today)
/assets/logo-old.png                  87      38   (not a build asset)
/assets/app-b1e229c.js                22      12   a3f29b1 (2d ago)

Diagnosis:
  ~411 users hold a stale index.html pointing at the previous bundle.
  Their next page-load JS fails to download → blank screen.
  This is your top user-facing bug right now.

Suggested fix:
  1. Set `Cache-Control: no-cache, must-revalidate` on index.html (immediate)
  2. Keep old bundle URLs alive for 48h after deploy (CDN side)
  3. Add a service-worker check that auto-reloads on bundle 404

Setting it up

You need some signal when an asset fails to load. Three options, in order of effort:

The browser examples below do not set a User-Agent header: browser fetch attaches its own automatically. Only server, CI, and curl examples should set a custom User-Agent.

Option A — Cheap browser hook (catches scripts only):

window.addEventListener("error", (ev) => {
  // Script tag 404s fire `error` events on the element, not on window
  if (ev.target && (ev.target as HTMLElement).tagName === "SCRIPT") {
    fetch(`https://api.agentry.sh/v1/analytics/`, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${AGENTRY_PUBLIC_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        event: "asset_404",
        distinct_id: visitorId,
        properties: {
          url: (ev.target as HTMLScriptElement).src,
          page: window.location.href,
        },
      }),
    });
  }
}, true); // capture phase — required for resource errors

Option B — fetch wrapper for SPA bundles loaded via dynamic import:

const originalFetch = window.fetch;
window.fetch = async (input, init) => {
  const res = await originalFetch(input, init);
  if (res.status === 404 && typeof input === "string" && input.startsWith("/assets/")) {
    fetch(`https://api.agentry.sh/v1/analytics/`, {
      method: "POST",
      headers: { "Authorization": `Bearer ${AGENTRY_PUBLIC_API_KEY}`, "Content-Type": "application/json" },
      body: JSON.stringify({
        event: "asset_404",
        distinct_id: visitorId,
        properties: { url: input, page: window.location.href },
      }),
    });
  }
  return res;
};

Option C — pipe CDN logs (Cloudflare Logpush, Fastly Real-Time): more setup but catches 100% including images and CSS. Send each 404 line as an asset_404 event with url and referrer.

Variations

  • “Show me asset 404s in EU vs US — is one edge stale and the other fine?”
  • “For the top stale bundle, draft a Cloudflare rule that aliases it to the new one for 24h.”
  • “Find image 404s — these usually point to broken email or OG-image references.”
  • “After a deploy, check on demand for any new bundle-URL pattern that started returning 404.”

Adapt this workflow in your own agent.

Ask your agent to map the starter prompt to your saved signal map, live events, and the current HTTP API before answering.

Agentry gives coding agents production context from errors, product analytics, and deploys through one HTTP API. Fetch and read https://agentry.sh/install.md completely, then install Agentry's full first-time telemetry baseline in this repository. I authorize device-code login, saving the owner key only to ~/.agentry/credentials.json, creating one Agentry project for this repository, and read-only inspection of shipped source plus hidden CI/provider configuration. Deeply analyze the product's business logic and complete value flow before choosing telemetry. Save the complete source-backed plan and show me its exact contents and hash. Until I explicitly approve that plan or request a replacement, do not edit app/CI source, place runtime/CI credentials, commit, exercise product traffic, start proof, push, or deploy. After approval, I authorize only the reviewed targets: place the required scoped browser/server/CI credentials through the established environment or secret mechanism, preserve existing telemetry, implement and test the baseline, commit it, push that reviewed commit when the shipped CI/provider path requires it, exercise safe proof paths with test/non-customer data, and perform one deployment through the reviewed shipped CI/provider path. Ask first if proof would charge money, contact a third party, change real customer data, or require new external access. After the plan is saved, immediately before every onboarding state-changing POST, GET current onboarding state, perform only its single returned next_action, then read state again; do not batch or infer later stages. Continue until status is verified, installation_complete is true, and next_action is null. Keep all secrets, source snapshots, proof markers, and scratch files outside the repository.

+ Full access
5.5 Extra High
  1. 1. Open your repo in Codex, Claude Code, Cursor etc.
  2. 2. Paste the install prompt.
  3. 3. Your agent reads the install doc and shows you an implementation plan for approval.