You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Drive AppSignal request-billing on cinegraph/prod down to the Starter tier (250K req/mo ≈ 8,300 req/day) and hold it there for the rolling-30-day window required to downgrade.
Baseline + full audit: #1098. This issue is the implementation playbook — ship all levers in one pass, then watch the drop over several days and tune.
Where we are (measured 2026-06-09, 7-day window): ~18.4M req/mo · oban 54.5% · web 45.4% · graphql 0.06%. One open exception: DBConnection.ConnectionError × 40,916 (replica pool, #1018). Target: 250K/mo = 8,300/day. That's a 73× cut — it requires sampling, not just filtering. This is a deliberate trade: keep all errors + all admin/api/auth + a representative sample of anonymous traffic; stop paying to record every bot page-view.
Why sampling is mandatory (the decisive data)
web broken down by action over 7 days — this is the entire ~279K/day web bill:
Action group
7-day
% of web
People detail pages (/people/:slug, /classic, /legacy, /.../movies, /directors/:id, /.../movies/acting)
1,303,418
66.8%
Movie detail pages (/movies/:slug + /legacy)
476,192
24.4%
Collaborations (/collaborations)
132,322
6.8%
Companies (/companies/:slug)
31,227
1.6%
Auth + admin + api + index + health + sitemap (the stuff worth keeping)
~7,000
0.4%
Two findings that set the strategy:
~98% of the web bill is anonymous content-detail-page hits on a dead-flat 24/7 curve = crawler traffic indexing the slug space (1.15M movies + all people/directors). Real interactive traffic is a rounding error (~7K/wk for auth/admin/api combined).
Starter's 8,300/day budget is less than the real + uncaught crawler residue on these routes even after a bot filter, so we must sample anonymous traffic. Errors are unaffected: CinegraphWeb.Router already does use Honeybadger.Plug (router.ex:3), so dropping a transaction from AppSignal billing never loses error visibility.
The plan — 4 levers, shipped together (Phase A), tuned over days (Phase B)
#
Lever
Mechanism
Expected effect
1
Ignore oban namespace + noise actions
config
−54.5% + health/sitemap/redirect noise → frees the whole budget for web
defmoduleCinegraphWeb.Plugs.AppsignalSamplerdo@moduledoc""" Keeps AppSignal request-billing under the Starter tier (250K/mo). See #1098. Runs right after Plug.Telemetry (endpoint.ex:42), before the router. Order of decisions: 1. Operational surfaces (/admin, /api, /health) -> always tracked, never sampled. 2. Bot / crawler / empty User-Agent -> dropped (Tracer.ignore/0). 3. Everything else (anonymous public traffic) -> keep only `sample_rate` of it. Errors are unaffected: they're captured by Honeybadger (router.ex:3), so dropping a transaction from AppSignal billing never loses error visibility. """@behaviourPlugimportPlug.Conn,only: [get_req_header: 2]@excluded_prefixes["/admin","/api","/health"]@bot_pattern~r/bot|crawler|spider|slurp|googlebot|bingbot|duckduckbot|yandexbot|baiduspider|applebot|petalbot|sogou|facebookexternalhit|twitterbot|linkedinbot|slackbot|discordbot|telegrambot|whatsapp|pinterest|redditbot|embedly|mastodon|ia_archiver|archive\.org_bot|gptbot|chatgpt-user|oai-searchbot|claudebot|anthropic-ai|perplexitybot|amazonbot|bytespider|dataforseo|semrushbot|ahrefsbot|mj12bot|dotbot|headlesschrome|phantomjs|python-requests|go-http-client|node-fetch|axios|okhttp|libwww-perl|curl|wget|scrapy|uptimerobot|pingdom|statuscake/i@implPlugdefinit(opts),do: opts@implPlugdefcall(conn,_opts)docfg=Application.get_env(:cinegraph,__MODULE__,[])conddonotKeyword.get(cfg,:enabled,false)->connnotfilterable_path?(conn.request_path)->conn# keep ops surfaces 100%bot_request?(conn)->ignore(conn)# drop all bots:rand.uniform()<=Keyword.get(cfg,:sample_rate,1.0)->conn# keep sampled fractiontrue->ignore(conn)# drop the restendenddefpignore(conn)doAppsignal.Tracer.ignore()connenddefpfilterable_path?(path),do: notEnum.any?(@excluded_prefixes,&(path==&1orString.starts_with?(path,&1<>"/")))defpbot_request?(conn)docaseget_req_header(conn,"user-agent")do[ua|_]->ua==""orRegex.match?(@bot_pattern,ua)[]->trueendendend
A3. Endpoint wiring
lib/cinegraph_web/endpoint.ex — immediately after Plug.Telemetry (line 42):
Why sample_rate is config, not hardcoded: we don't yet know the true bot-capture rate, so we ship at 0.2, measure the real post-filter number, then tune (one-line change, no code).
A5. Test
test/cinegraph_web/plugs/appsignal_sampler_test.exs — UA matrix (real browser passes, known bots dropped, empty/missing UA dropped), excluded-path pass-through (/admin, /api, /health never ignored), and sample_rate: 0.0 drops a non-bot anonymous request while 1.0 keeps it. Appsignal.Tracer.ignore/0 is a safe no-op when the agent isn't running, so the plug is testable without AppSignal active.
Phase B — measure & tune (the multi-day part)
Deploy A, then watch for 3–5 days (crawler traffic is steady, so a couple of full days is enough to read the new floor). Use the AppSignal Usage tab, or the MCP / dashboard query:
transaction_durationcount, tag namespace=* → confirm oban is gone and read the new web total.
transaction_durationcount, tags action=*, namespace=web → confirm the content routes collapsed and nothing unexpected is leaking.
Tuning rule: after the bot filter settles, read web_per_day. Set sample_rate ≈ 7000 / (web_per_day_at_sample_rate_1.0).
Since A ships at 0.2, the un-sampled anon volume ≈ (observed_anon_per_day) / 0.2; solve for the rate that lands total ≈ 7,000/day (leaves headroom under 8,300). Adjust the one config line and redeploy.
Decision gates:
After A: oban namespace shows ~0 billable. (Confirms Lever 1.)
After A: web/day dropped sharply vs the ~279K/day baseline. (Confirms Lever 2.)
After tune: total /day sits ≤ ~7,500 with headroom. (Confirms Lever 3.)
Spot-check: a real logged-in session + an /admin page + a GraphQL call still appear as tracked transactions. (Confirms we didn't over-sample real signal.)
Phase C — fix the error sources (reduces required sampling + fixes real bugs)
Dead-slug 404 floods — unknown/deleted /people/:slug, /movies/:slug that raise instead of returning a clean 404. Return 404; removes billed exceptions and lets us raise sample_rate (better real-traffic visibility for the same bill).
Bulk-close stale Oban performance tombstones once Lever 1 confirms they've stopped recurring.
Phase D — downgrade
AppSignal allows downgrade only after usage is below the lower plan's allowance for 30+ days (Usage docs).
Once the Usage tab shows rolling-30d ≤ 250K, flip the plan to Starter.
Honest trade: at Starter we are sampling anonymous traffic — we keep 100% of errors (via Honeybadger), 100% of admin/api/auth, and a representative sample of anonymous page-views, while no longer paying to record every individual bot/anon hit. Given that ~98% of web volume is crawler traffic, the lost signal is negligible.
Refs: audit #1098 · replica pool #1018 · precedent razrfly/eventasaurus#5483.
Goal
Drive AppSignal request-billing on
cinegraph/proddown to the Starter tier (250K req/mo ≈ 8,300 req/day) and hold it there for the rolling-30-day window required to downgrade.Baseline + full audit: #1098. This issue is the implementation playbook — ship all levers in one pass, then watch the drop over several days and tune.
Why sampling is mandatory (the decisive data)
webbroken down by action over 7 days — this is the entire ~279K/day web bill:/people/:slug,/classic,/legacy,/.../movies,/directors/:id,/.../movies/acting)/movies/:slug+/legacy)/collaborations)/companies/:slug)Two findings that set the strategy:
Elixir.*Liveactions appear → LiveView connected (WebSocket) mounts are not separately billed. All web billing is plain HTTP route transactions. (This kills the LiveView-leak worry from AppSignal: cut request-billing (~18.4M/mo measured — Oban 54%, bot-heavy web, replica-pool error storm) #1098 Step 2 — nomount/3guard needed.)Starter's 8,300/day budget is less than the real + uncaught crawler residue on these routes even after a bot filter, so we must sample anonymous traffic. Errors are unaffected:
CinegraphWeb.Routeralready doesuse Honeybadger.Plug(router.ex:3), so dropping a transaction from AppSignal billing never loses error visibility.The plan — 4 levers, shipped together (Phase A), tuned over days (Phase B)
obannamespace + noise actionsTracer.ignore/0sample_rateLevers 1–3 ship in one PR ("do all of them in one, see how it drops" — @razrfly). Lever 4 can follow but reduces how hard #3 must sample.
Phase A — implementation (one PR)
A1. AppSignal config — ignore Oban + noise actions
config/config.exscurrently has a bare block at lines 328-331. Extend it (action names verified against the router):A2. Sampler plug — bot filter + anonymous sampling
lib/cinegraph_web/plugs/appsignal_sampler.ex:A3. Endpoint wiring
lib/cinegraph_web/endpoint.ex— immediately afterPlug.Telemetry(line 42):A4. Config switches (off in dev/test, on in prod with a conservative initial rate)
A5. Test
test/cinegraph_web/plugs/appsignal_sampler_test.exs— UA matrix (real browser passes, known bots dropped, empty/missing UA dropped), excluded-path pass-through (/admin,/api,/healthnever ignored), andsample_rate: 0.0drops a non-bot anonymous request while1.0keeps it.Appsignal.Tracer.ignore/0is a safe no-op when the agent isn't running, so the plug is testable without AppSignal active.Phase B — measure & tune (the multi-day part)
Deploy A, then watch for 3–5 days (crawler traffic is steady, so a couple of full days is enough to read the new floor). Use the AppSignal Usage tab, or the MCP / dashboard query:
transaction_durationcount, tagnamespace=*→ confirmobanis gone and read the newwebtotal.transaction_durationcount, tagsaction=*, namespace=web→ confirm the content routes collapsed and nothing unexpected is leaking.Tuning rule: after the bot filter settles, read
web_per_day. Setsample_rate ≈ 7000 / (web_per_day_at_sample_rate_1.0).Since A ships at 0.2, the un-sampled anon volume ≈
(observed_anon_per_day) / 0.2; solve for the rate that lands total ≈ 7,000/day (leaves headroom under 8,300). Adjust the one config line and redeploy.Decision gates:
obannamespace shows ~0 billable. (Confirms Lever 1.)web/daydropped sharply vs the ~279K/day baseline. (Confirms Lever 2.)/daysits ≤ ~7,500 with headroom. (Confirms Lever 3.)/adminpage + a GraphQL call still appear as tracked transactions. (Confirms we didn't over-sample real signal.)Phase C — fix the error sources (reduces required sampling + fixes real bugs)
DBConnection.ConnectionError× 40,916 — replica pool exhaustion (Cinegraph → PgBouncer (16GB tuning done; sibling apps deferred to separate follow-ups) #1018). Billedwebtransactions and a real reliability bug. Fixing it removes the Decision: Supabase Integration Approach - Client Library vs Direct API #1 pollutant at the source./people/:slug,/movies/:slugthat raise instead of returning a clean 404. Return 404; removes billed exceptions and lets us raisesample_rate(better real-traffic visibility for the same bill).Phase D — downgrade
AppSignal allows downgrade only after usage is below the lower plan's allowance for 30+ days (Usage docs).
Checklist
ignore_namespaces: ["oban"]+ health/sitemap/redirectignore_actionsAppsignalSamplerplug (bot filter + anonymous sampling)Plug.Telemetrysample_rate: 0.2)sample_rateto land ≤ ~7,500/dayDBConnection.ConnectionError(Cinegraph → PgBouncer (16GB tuning done; sibling apps deferred to separate follow-ups) #1018) + dead-slug 404s; bulk-close stale incidentsHonest trade: at Starter we are sampling anonymous traffic — we keep 100% of errors (via Honeybadger), 100% of admin/api/auth, and a representative sample of anonymous page-views, while no longer paying to record every individual bot/anon hit. Given that ~98% of web volume is crawler traffic, the lost signal is negligible.
Refs: audit #1098 · replica pool #1018 · precedent razrfly/eventasaurus#5483.