Live evaluation of demo.fanout.run: 33 findings, P0 correctness first
Nobody has claimed this yet.
- Dominant language
- HTML
- Stars
- 4
- Forks
- 0
- Avg merge
- 4h 4m
- Merged PRs (30d)
- 57
Description
Findings from a live evaluation pass over https://demo.fanout.run (image 2026.9.3), driven through the browser as a user would, with every numeric claim verified against the API rather than read off the screen.
The agent itself came out well: from a single service-map click it root-caused a 504 (checkout's PlaceOrder hung ~103s while cart, payment, shipping and email each finished under 15ms; the proxy gave up at its 15s budget and returned 504 UT response_timeout on an order that had actually succeeded). Asked about zero ERROR logs it named the telemetry blind spot instead of answering "0". Dashboard-from-chat works end to end. The list below is everything that got in the way.
Ordered by priority. Each item is independent; P0 items should land before the demo is put in front of anyone else.
P0 — correctness
1. Stat tiles read the last, partial bucket while the card is labelled "Last 1h"
The frontend service card shows Operations 1,281, P95 48.2ms, coloured green. For the same service and window, /api/observability/topology?window=1h returns 15,283 spans and p95 13,691ms, and the card's own Compare tab prints 13.69s → 10.41s. One card contradicts itself by ~285x on latency and ~12x on volume, and paints a 13.7s-P95 service as healthy.
The tile appears to take the most recent 5-minute bucket, which is also incomplete at read time, so it under-reports on both axes.
Fix: aggregate tiles over the labelled window, or label them explicitly as the latest interval and exclude the in-flight bucket.
2. An empty namespace filter reports "Healthy"
Typing a namespace that does not exist (opentelemetry-demo) renders Health: Healthy · 0 services · Error rate 0.00% · 0 operations. No-data is being rendered as good news.
Fix: distinguish "no data" from "healthy" in the health card and every downstream widget. Add namespace autocomplete sourced from known values so the state is hard to reach in the first place.
3. Log severity is not normalised at ingest
Across a 400-entry sample over 6h: INFO 130, empty string 54, INFORMATION 16. System-wide there were zero ERROR or WARN logs in 6h while the platform reported a 1.77% error rate and 9 unhealthy services. Envoy access logs from frontend-proxy arrive with no severity at all; the .NET cart service emits INFORMATION.
Consequences: every severity filter is unreliable, and error-log views are structurally empty even for a system actively returning 504s.
Fix: map OTLP severity_number on ingest, fold INFORMATION into INFO, and give unlabelled records an explicit unspecified value rather than an empty string.
4. The trace picker ignores service scope
A dashboard scoped to checkout pinned a "Slow PlaceOrder Trace" widget that rendered a flagd.evaluation.v1.Service trace — 2 spans, recommendation ↔ flagd, 600.01s. Neither the service nor the operation matches the scope.
5. Health scoring is dominated by long-lived background spans
P95 sits at exactly 600.00s for flagd and fraud-detection, and 510.01s for product-reviews — consumer and poll spans, not request latency. As a result frontend is marked unhealthy despite a 0% error rate and a 3.9ms p50, and the demo permanently reads "Unhealthy · 9 of 20".
Fix: compute request-path percentiles separately from consumer/internal spans, or exclude long-running background span kinds from the health signal.
P1 — gaps that stall an evaluation
- No ingest path in the UI. Signed in as
admin, there is no ingest token, endpoint or copy-paste snippet anywhere. Someone who likes the demo cannot take the next step. - No route from insight to action. After the agent names a failing service there is nothing to click: no "create an alert from this", no pin-to-dashboard, no saved query.
/api/alertsexists and returns data; nothing in the UI reaches it. - No logs explorer. 125,713 logs over 6h are reachable only through the agent or a severity-filtered widget that is empty because of item 3.
- The dashboard generator pins widgets it has not checked. The generated checkout dashboard shipped "Checkout Error Logs" and "Checkout Warnings", both permanently empty. Probe for rows before pinning, or render an explicit empty state at creation time.
- No drill-down from a Recent activity row. Rows highlight on hover like links and do nothing. Clicking a service should lead to its traces and logs.
- Search only matches chat and dashboard titles. Searching
checkoutreturns "No matching chats / No matching dashboards" while a service, its traces and an entire thread about it all exist. - State is not in the URL. Time window and namespace live outside it, so a shared dashboard link never reproduces what the sender was looking at. A bad dashboard id silently renders the default dashboard while keeping the bogus URL; an unknown route silently redirects to
/chat. Both should be a not-found state. - Widget "Remove" deletes immediately with no confirmation and no undo.
P2 — number presentation
- Duration formatting and mixed units.
600.00sshould read10m;240.02sshould read4m 0s. One column currently prints30.00snext to25.0ms, and one axis prints0.0msat zero and100.00sabove it. The "Latency and error correlation" chart exposes a scaling hack in its legend asError rate × 1000instead of using a right-hand percentage axis. - Threshold colouring is noisy. A
0.00%cell renders red; a p50 move of 3.9ms → 4.0ms is flagged as a red 2.2% regression; the errors column flips grey↔red around 1% with no legend. Use explicit thresholds with a key, or a continuous scale. - P95 equals P99 on most rows because values land on histogram bucket bounds (2.5 / 5 / 10 / 25 / 50ms). Interpolate, or mark the values as approximate.
- Downsample long series. The 24h performance chart draws 288 raw points and reads as a hairball.
P3 — design system
- The type scale is not a scale. One screen renders 15 distinct size/weight pairs across 9, 10, 10.4, 11, 12, 14, 15, 16, 20 and 32px. Collapse to roughly six steps and two weights, drop everything below 11px (the 9px
DEFAULTbadge and 10px service-map labels are the ones users actually need to read), and add a step between 20px and 32px. - The dark-mode primary button fails contrast. "New chat", white on violet, measures 3.16:1 — below the 4.5:1 AA threshold for 14px text. The same button in light mode measures 5.54:1.
- Dark-mode muted text is warm on a cool ground (
rgb(191,189,182)on#0b0e14). Match the hue family of the background. - Status is encoded by hue alone in the service map, health bar, service pills and errors column. Add a redundant channel — shape, fill or glyph — for colour-vision deficiency.
- The service map inverts salience. Normal edges are
#8b8e99at 3.5–4.0px width; error edges are#f26d78at 1.5px, so the failing dependencies are the thinnest and palest lines on the canvas. Labels also collide with nodes (otelcol-contriboverload-generator,fraud-detectionoverflagd), and the circular layout routes every chord through the centre. A layered or directed layout would show the real request shape for 20 nodes and 23 edges. - Two status encodings disagree on one row.
adshows a pink "unhealthy" service pill next to a grey0.69%errors value. - Card layout wastes vertical space. The health tile is half empty because it is height-matched to a sparkline tile; Recent activity scrolls internally at 6 rows while the page below it is blank; the generated Performance widget renders at roughly two-thirds width leaving a gutter.
- The error-rate sparkline carries no information. No baseline, no min/max, no axis, plus a decorative hatch fill. A number with a delta would say more in less space.
- The latency heatmap legend overlaps the x-axis tick labels and the colour scale has no unit.
P4 — hygiene
- Ten icon buttons expose no accessible name in the accessibility tree.
- The chat view jumps to the top of the thread after sending, so the answer arrives off-screen.
- The theme-toggle tooltip stays visible after the click.
- A dashboard load double-fetches
dashboards,overview,topologyandperformance. /robots.txtand every unknown path return SPA HTML with a 200. A public demo should serve a real robots file. The Cloudflare beacon also 503s on load.- Intermittent multi-second stalls on the default window: one pass measured 9.0s for
performanceand 12.7s forlogsat 1h, where repeat calls run 440–550ms. Consistent with rollup/maintenance contention on the write mutex. - Timestamps carry no timezone and there is no unit or timezone preference.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reproducing the P0 findings in the browser at demo.fanout.run, then compare the UI with /api/observability/topology?window=1h and the observed log data. Treat the five P0 findings as separate work items; done means the affected cards, filters, trace selection, and health state agree with the underlying data, with the listed empty and long-running cases handled explicitly.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- api, backend, frontend, observability
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100