Asset logos — post-release monitoring
The logo pipeline is fail-soft everywhere: a missing image always degrades to a letter/icon avatar. That means nothing user-visible breaks when it fails — and nothing tells you either, unless you look here. This is the “is it working?” checklist.1. Did the release activate? (check within an hour)
The single most important signal. If prod’sFINNHUB_PROFILE_API_KEY is missing
or plan-gated, the backfill safely skips forever and logos never appear.
Sentry: area:asset_logos + logo_signal:canary_failed
→ Any event means the backfill is doing nothing. Fix the key, then it
self-heals on the next cron run. Recommended: alert on first occurrence.
Or curl the cron directly (response now carries pipeline health):
logoBackfillStopped: "budget"→ the run ran out of route budget for the profile phase. Not a key problem — the price writes still landed and the next run will pick up logos. Investigate only if it fires two runs in a row.logoBackfillEnabled: falseandlogoBackfillStopped: null→ key problem. The canary ran and returned no logo, soFINNHUB_PROFILE_API_KEYis plan-gated or misconfigured. Same failure that fireslogo_signal:canary_failedin Sentry.logosResolved: 0with a non-zerologosAttempted→ the fetch or write path is broken. Plan-gated (403) responses and unknown-symbol responses DO count as resolved — the cron writes the permanent''sentinel for both, so a definitive “no logo exists here” outcome is a resolution, not a miss. The remaining wayslogosResolvedcan stay at 0 while attempts are non-zero are: every profile fetch threw before landing a write (Finnhub 5xx/timeouts, rate-limit exhaustion) or every DB write returned an error. A race in which every candidate was resolved by another writer between the queue snapshot and the update is theoretically possible but essentially never happens — this cron is the only writer of logos at scale.
2. Is the backfill draining? (first ~2 market days)
logoBacklogRemaining should fall by up to ~250 per run and settle near 0.
Coverage query:
SELECT count(*) FILTER (WHERE image_url IS NULL) FROM tracked_tokens WHERE token_id IS NOT NULL;
Sentry: logo_signal:backfill_stalled / token_backfill_stalled fires only
when a run attempted work and resolved none — it self-gates, so a fully-drained
table (the steady state) never alerts.
Tokens CoinGecko never returns (delisted coins) are retired after
IMAGE_LOOKUP_MAX_ATTEMPTS omissions: image_lookup_attempts counts them and
the last strike writes the '' sentinel. Without that they were re-requested
every 2 minutes forever and kept this alert permanently lit. Retired tokens
count as progress in the stall check — a run that only retires stragglers is
finishing work, not failing.
Retirement is a conditional update guarded on image_url IS NULL, because the
'' sentinel is permanent: if a concurrent run resolved a real URL for the
same coin, that write wins and the retirement matches no row.
Stock logo order: the per-run logo queue is chosen up front from the whole
candidate set, legacy api.polygon.io rows first, then capped at
LOGO_FETCHES_PER_RUN. Those legacy rows are the mainstream tickers by
construction (written when the registry only held popular names — it’s why
AAPL and KO sat in that bucket), so household names land in the first runs.
The queue is deliberately built before the price loops split tickers into
bulk (/quote/us) and individual partitions. The bulk partition runs to
completion first, so choosing logo candidates during iteration would let a
legacy ticker missing from the bulk response queue behind every bulk ticker
and miss the budget. Ordering only — every ticker is still priced each run.
3. Are images actually rendering for users?
Two independent signals, because the DB can look healthy while screens don’t: PostHogportfolio_logo_coverage — fired once per portfolio view with
rows_total / rows_with_image. This is the headline metric: the share of
rows that actually show artwork, as users experience it. Chart
sum(rows_with_image) / sum(rows_total) over time; it should climb during the
backfill and then sit flat. A sudden drop means the UI stopped rendering what
the DB has — a field-mapping or sanitizer regression — and it will show up here
before anyone files a ticket.
PostHog portfolio_asset_logo_failed — sampled at 10%, with logo_host.
A spike concentrated on one host = that CDN is down, or a CSP/remotePatterns
regression. Baseline is a low trickle (dead URLs in the long tail).
Sentry logo_signal:host_not_allowlisted — a stored URL was refused
because its host isn’t allowlisted. This is the pipeline’s most dangerous
silent failure: if a provider moves CDN host (CoinGecko has done this), every
logo vanishes at once and no image error fires, because the image never loads.
Deduped per host, so one event = one new unknown host. Alert on any event.
This alert has already paid for itself. On 2026-07-28 it surfaced
maps.googleapis.com — 2,941 properties.photo_url rows (~44% of all stored
property artwork: Street View and static-map images written by the enrichment
pipeline since 2025-03) that had never been allowlisted, so every one rendered a
house icon. Nothing else reported it: a blocked image never loads, so no
onError fires and portfolio_asset_logo_failed stays flat.
Adding a host means all three layers, or it still won’t render:
ALLOWED_LOGO_HOSTS/ALLOWED_LOGO_HOST_SUFFIXESinlib/market-data/logo-url.tsimages.remotePatternsinnext.config.mjs— next/image throws otherwiseimg-srcinlib/content-security-policy.ts
*.googleapis.com would also have covered
storage.googleapis.com — every public GCS bucket on the internet — turning a
stored URL into an arbitrary-image render.
4. Are connected holdings getting registered?
Ticker registration is what gives SnapTrade/Plaid holdings a logo at all. Sentrylogo_signal:registration_failed (tagged provider) fires when a
sync tried to register 3+ new symbols and every one failed — meaning new
connected holdings will never get logos, prices or metadata. Partial failures
are normal (delisted tickers, broker-internal ids) and stay silent.
This alert only means something because of the negative cache. Symbols Finnhub
can’t resolve — overwhelmingly Canadian TSX/TSXV listings arriving from Plaid
without an exchange suffix (AC Air Canada, ATD Couche-Tard) — are recorded
in ticker_registration_failures and skipped after
MAX_REGISTRATION_ATTEMPTS. Before that, ~10k such symbols were retried on
every sync and this alert fired continuously. The skip expires after
REGISTRATION_RETRY_AFTER_DAYS, so a Finnhub plan change heals itself.
To see what’s parked and why:
Ticker registration: checked=… registered=… failed=….
5. Missing badge artwork (product signal, not a failure)
Sentry:area:asset_logos is not used for this one — vehicle badges
report as Vehicle make missing badge logo: <make>, fingerprinted per make, so
each brand is one issue whose event count ranks demand for new artwork. Same
idea applies if we later instrument fund issuers.
6. Is the page itself being used? (experiment health)
The portfolio page is behindportfolio-page-v01, and every interaction now
emits an event — use these to spot a broken surface before it’s reported:
That last one is the most useful funnel on the page: picker tile clicked →
type dialog opened. A drop there means new users can’t add anything, which is
the worst possible failure and otherwise invisible.
Recovering '' sentinels after a Finnhub plan change
The '' sentinel means “resolved, no logo exists” and is never re-asked. That is
correct until our plan changes — on 2026-07-27 the key gained international
coverage, so listings previously written off as plan-gated became resolvable.
Reset only suffixed listings. Verified on that date: 005930.KS, 1300.HK,
19MW.MI, 1GEV.MI (plus VOD.L, NESN.SW, SAP.DE, 7203.T, SHOP.TO)
all now return a logo, while unsuffixed sentinels (AAA, AAAU, AAPW,
ABLS, ABNY) are still genuinely empty — they are ETFs/ETNs misfiled as
stock in the registry. Resetting those would spend the throttled profile
budget re-deriving the same sentinel.
/stock/profile first. A reset that re-derives the same
sentinel is pure cost on a rate-limited key.
Known-good “no logo” cases (do not chase)
- Mutual funds / ETFs have no company logo — they get an issuer badge, or a letter if the issuer isn’t in the pack.
- Non-US listings (
.L,.SA) are plan-gated on profile2 →''sentinel. - Delisted tickers, 529 plan IDs, CUSIP-style bond identifiers — no logo exists.