Skip to content

Latest commit

 

History

History
921 lines (753 loc) · 58.7 KB

plausible.md

File metadata and controls

921 lines (753 loc) · 58.7 KB

Plausible analytics tracking

This document provides a comprehensive overview of Plausible Analytics implementation for anyplot.ai, including all tracked events, user interactions, and the required Plausible dashboard configuration.

Setup

Location: app/index.html

  • Self-hosted Plausible script loaded via /js/script.js
  • Custom endpoint: /api/event (proxied through nginx to avoid adblockers)
  • Manual pageview tracking: autoCapturePageviews: false
  • Production-only: Only tracks on anyplot.ai domain
  • Privacy-focused: No cookies, GDPR-compliant

Page views

Implementation: Query parameters converted to URL path segments for better analytics segmentation.

Multi-language URL strategy

Cutover date: 2026-04-20

Spec URLs follow the three-tier structure /{specId}[/{language}[/{library}]], so the spec slug — the actual SEO entity — sits at the URL root and the language slots between spec and library:

  • Root / and reserved static pages (/plots, /specs, /libraries, /palette, /about, /legal, /mcp, /stats) are un-prefixed.
  • Cross-language hub: /{spec_id} (lists every implementation across all languages).
  • Language overview: /{spec_id}/{language} (e.g. /scatter-basic/python).
  • Implementation detail: /{spec_id}/{language}/{library} (e.g. /scatter-basic/python/matplotlib).
  • Interactive view is no longer a separate route — the detail page toggles between static preview and iframe via ?view=interactive. The query string is not included in Plausible URLs (canonical only).
  • Marketing subdomain python.anyplot.ai rewrites internally; canonical in HTML points back to anyplot.ai/{spec_id}/python[/{library}] so all pageviews show up under the main domain.

Historical data before 2026-04-14 uses un-prefixed paths (/{spec_id}); data between 2026-04-14 and 2026-04-20 uses the legacy /python/{spec_id}[/{library}] prefix. Both ranges remain visible in Plausible but are no longer produced.

Filter-based pageviews

Filters are appended to the page's own path as /{category}/{value} pairs (buildPlausibleUrl() in app/src/hooks/useAnalytics.ts):

https://anyplot.ai/{pathname}/{category}/{value}/{category}/{value}/...

The gallery lives at /plots, so its filter views are recorded under /plots/...; on spec routes the spec/language/library path comes first. Until PR #9625 (2026-07-10) the reserved prefix was stripped, so older data records the same gallery filters without /plots (/lib/matplotlib).

Ordered categories: lang, lib, spec, plot, data, dom, feat, dep, tech, pat, prep, style, language

Examples (gallery):

  • /plots?lib=matplotlib → https://anyplot.ai/plots/lib/matplotlib
  • /plots?lib=matplotlib&plot=scatter → https://anyplot.ai/plots/lib/matplotlib/plot/scatter
  • /plots?lib=matplotlib,seaborn → https://anyplot.ai/plots/lib/matplotlib,seaborn (OR logic)
  • /plots?lib=matplotlib&lib=seaborn → https://anyplot.ai/plots/lib/matplotlib/lib/seaborn (AND logic)
  • /plots?spec=manhattan-gwas → https://anyplot.ai/plots/spec/manhattan-gwas

Examples (spec routes):

  • /scatter-basic?language=python → https://anyplot.ai/scatter-basic/language/python

One spec is therefore recorded under three families of paths: gallery views filtered to it (/plots/spec/{spec_id}, plus any other filter segments), the hub (/{spec_id}, and /{spec_id}/language/{language} when the carousel is scoped) and its implementation pages (/{spec_id}/{language}/{library}). Filter Pages by "contains {spec_id}" to see them together. "contains" also matches longer ids that include this one (line-basic inside sparkline-basic); for such ids use the Stats API matches operator with the id anchored on a path segment, for example [/,]line-basic([/,]|$) — a comma counts as a boundary because OR-filtered gallery views record /plots/spec/a,b.

Benefits:

  • Plausible shows popular filter combinations
  • No manual event tracking needed for filter changes
  • URL structure clearly shows user's browsing context

Pages

URL Description
/ Home page (no filters)
/plots Plots page (filter grid of all implementations)
/specs Specs page (alphabetical spec list)
/libraries Libraries listing
/palette Color palette reference
/about About page
/legal Legal notice, privacy policy, transparency
/mcp MCP server documentation (AI assistant integration)
/stats Platform statistics (library scores, coverage, tags, top implementations)
/map Network map of specs clustered by visual similarity
/debug Pipeline status dashboard (spec coverage, feedback, agent cases, ping); the agent cases section sends no event of its own
/debug/agent Admin-only "Use with my data" agent chat; recorded without its query string, and only in builds with VITE_ENABLE_AGENT_CHAT=true
/agent The public "Use with my data" agent chat (experimental, since 2026-11-02), the same page on the public lane; recorded without its query string, only in builds with VITE_ENABLE_AGENT_CHAT=true and only while the public lane is open
/{spec_id} Cross-language spec hub (all implementations across all languages)
/{spec_id}/{language} Language overview (all libraries for that language)
/{spec_id}/{language}/{library} Implementation detail (preview ↔ interactive toggle)

Total pageview tracking: Automated via trackPageview() in all pages

Custom events

Conversions

Event Name Properties Where Description
copy_code spec, library, method, page ImageCard.tsx, SpecPage.tsx, SpecTabs.tsx, agent-chat/ResultCard.tsx User copies code to clipboard
download_image spec, library, page SpecPage.tsx User downloads PNG image

Copy methods:

  • card: Quick copy button on image card (home grid)
  • image: Copy button on main image (spec page)
  • tab: Copy button in Code tab
  • agent: Copy button on an agent chat result card (the adapted code)

Page values (for user journey tracking):

  • home: HomePage grid view
  • spec_overview: SpecPage showing all library implementations
  • spec_detail: SpecPage showing single library implementation
  • agent_chat: the agent chat, the admin's (/debug/agent) and the public one (/agent)

Agent chat

The "Use with my data" chat (app/src/pages/AgentChatPage.tsx, design in Agent network design) records enum properties only. No event carries message text, pasted data, column names, code or a dataset size: the size travels as a bucket. The events exist only in builds with VITE_ENABLE_AGENT_CHAT=true, and like every event they fire only on anyplot.ai. Since 2026-11-02 the public chat at /agent fires the same events with the same properties; the pageview tells the two lanes apart. On the public lane the feedback form's # include data.csv box starts unticked, so include_data reads no unless the user ticked the box, and since 2026-11-02 it offers no .save(), so agent_session_saved comes only from the admin lane.

Event Name Properties Where Description
agent_open library, source, spec AgentChatPage.tsx The chat page opened, for an admin or on the public lane. source is plot_page when the plot page's entry led there (.use_my_data() → beside the library carousel or beside the code tab's copy button; the .adapt() button on the image until 2026-11-02, under the same value), spec_hub when the spec hub's header entry did (.use_my_data() → at the top of /{spec-id}, which opens matplotlib where it is among the cards shown, else the first of them), landing when the landing page's line did (public lane, on 2026-11-02 only: the line now leads to /plots, tracked as nav_click{source: hero_use_my_data}; the chat still accepts the value), direct otherwise (a typed URL, a bookmark, a reload)
agent_data_parsed status, size_bucket useAgentSession.ts The pasted data came back from the parse. status ∈ ok, too_long, unparseable, data_refused, error (any other refusal; until 2026-11-04 also a formula or code the server answered with 422 formula or 422 code), data_request (since 2026-11-01: the text asked for data instead of holding it, such as "Beispieldaten"; the server answered 422 data_request and the page sent the text to the chat as the user's message, where the assistant generates example data), formula and code (since 2026-11-04: the server answered 422 formula or 422 code; within 2,000 characters the page sent the text to the chat as the user's message, where the assistant computes the formula or runs the code, and longer code stayed in the data box with a line that asks to shorten it), transcribed (the text was not a table, a model read it into one and the page asks the user to confirm it; nothing is stored yet), discarded (the user dropped such a table; .use() instead parses it again and reports ok); size_bucket ∈ lt_1kb, 1_10kb, 10_50kb, 50_200kb, over_200kb (the last one is refused in the browser before anything is sent)
agent_data_confirmed size_bucket useAgentSession.ts The user ended step 1: the stored data and its bindings fit, and the server confirmed them. Since 2026-11-04 it fires inside the .create_plot() click (also .plot_all() and a switch's carry offer), which confirms first and starts the plot only on success, so it counts the clicks on unconfirmed data whose confirmation went through, not a separate step; until then .confirm() was a button of its own. Fires again after each new parse, binding change, data change or undo that the user confirms. size_bucket as for agent_data_parsed; data the assistant generated from a formula without a paste counts as lt_1kb
agent_data_prepared changes useAgentSession.ts The assistant changed the data in a turn (its prepare_data, for example "read Anteil as numbers" or "sum per month") or computed it from a formula (its generate_data, for example "sin(x) from -pi to pi"): the turn's done carried a new data version, which the data panel shows with its change lines and, when an earlier version exists, .undo(). A generated table in a session without data is version 1 and fires this event too, and so does the data panel's .to_long() (a wide table made long, no model call). changes ∈ 1, 2, 3+, the number of change lines (one per operation; a generated table has one, plus one per series with undefined values)
agent_data_undo version useAgentSession.ts The user took back the assistant's last change to the data with .undo(), and the server restored the version before it. version ∈ 1 (back at the paste), 2, 3+: the restored version
agent_data_file kind, encoding, outcome useFileIntake.ts A file reached the data box: dropped on it, chosen with .open() or pasted as a file (since 2026-10-15). A text file is read in the browser; a spreadsheet, Parquet, Feather, Arrow or NumPy file goes to the server, which converts it in the renderer's sandbox (since the binary-file step of 2026-10-15). .parse() afterwards reports agent_data_parsed as for a paste. kind ∈ text, spreadsheet, parquet, arrow, numpy, hdf5, pickle, pdf, archive, binary (by the file's first bytes in the browser; binary for anything else that is not text), unknown (refused before it was read), and for a file the server converted the format it found: xlsx, xlsm, xlsb, xls, ods, parquet, feather, arrow, npy, npz; encoding ∈ utf-8, utf-8-bom, utf-16le, utf-16be, windows-1252, none (a binary file); outcome ∈ filled (into an empty box), replaced (the box had text and the user chose .replace()), kept (the user chose .keep()), sheet (another sheet or .npz array chosen in the sheet picker and read again), refused (a format never read, an empty file, or a file the server refused: encrypted, damaged, over a cap), failed (the server could not convert it now: rate limit, no render slot, renderer unavailable, network), too_large (over 4 MB as a text file or 5 MB as a binary one, or over 1 MB as text), busy (a step was running, or another file was still being read), unreadable (the browser could not read it). The file's name and its sheet names are never sent
agent_plot_rendered library, status, repaired, spec useAgentSession.ts A turn's plot event arrived. status ∈ ok, needs_attention, failed, not_ready; repaired is yes when the run needed its repair round (two attempts), else no
agent_guardrail_block reason useAgentSession.ts A guardrail stopped a request. reason ∈ out_of_scope, needs_data (the request needs data the service would have to fetch), budget (the user's own daily token budget), service_paused (since 2026-11-02: the whole service spent its day budget, from a turn's refusal or the dataset route's 429), unsupported_content, rate_limited and daily_limit (the user's message limits per minute and per day, and since 2026-10-14 the paste limits, which the data panel reports from the dataset route's 429), session_closed (the session closed after three scope refusals), other_library (a request for this plot in another library or language, answered with a pointer to the catalogue), locked_out (the user is paused for the day after three attack verdicts; these are the stream's fixed refusals), data_refused (the pre-filter or the dataset judge refused the pasted data: text addressed to an AI, or the judge's verdict; personal data is never a reason), guard_unavailable (the scope check could not answer and failed closed), other (a refusal code the page does not know yet)
agent_session_saved include_data useAgentSession.ts The admin saved the session as a JSON file with .save() in the chat header's "⋯" menu (admin lane only since 2026-11-02), and the download started. include_data is yes when the # include data.csv box was ticked (the admin's default), so the file holds each version's data file, else no. A failed save fires nothing
agent_result_feedback reaction, include_data, subject useAgentSession.ts The user sent quick feedback with .send(), on a result version from its result card or on the chat itself from the control below the composer, and the BFF stored it as a feedback case. reaction ∈ thumbs_up, thumbs_down, bug, idea, the site feedback's set; include_data is yes when the # include data.csv box was ticked (the admin's default; unticked on the public lane), so the case holds the data file, else no; subject is result for a version and chat for the chat (since 2026-10-12, for a session that made no plot). An edit that replaces a sent case fires again. A failed send fires nothing; the text never travels

copy_code on a result card carries method: agent and page: agent_chat plus the spec and library of the version.

These Plausible events count page interactions in the browser. Since 2026-10-16 the chat also has a server-side usage telemetry in BigQuery, with its own content-free events (data_input, chat_turn, pipeline_run, outcome): the copy, download, save and feedback actions above additionally send an outcome beacon (POST /debug/agent/sessions/{sid}/outcome) that the agents service writes as an outcome line. That beacon is not a Plausible event and fires on every host, not only anyplot.ai. See Agent usage telemetry.

Discovery

Event Name Properties Where Description
search query, category FilterBar.tsx User searches and selects value
search_no_results query FilterBar.tsx Search query returns no results (debounced 200ms)

Feature usage

Event Name Properties Where Description
random_filter category, value, method useFilterState.ts User triggers random filter
filter_remove category, value useFilterState.ts User removes a filter
grid_resize size ToolbarActions.tsx User toggles between normal/compact view
tab_toggle action, tab, library SpecTabs.tsx User opens or closes a tab
plot_rotate spec SpecsListPage.tsx User clicks image on specs page to rotate library
open_interactive spec, library SpecOverview.tsx, SpecDetailView.tsx User opens interactive HTML view
suggest_spec - SpecsListPage.tsx User clicks the spec.suggest() link on the specs list page. The mirror link on the landing page emits nav_click with source: suggest_spec_link instead.
report_issue spec, library? SpecPage.tsx User clicks the "report issue ↗" link on a spec hub page. The in-plot .report() flag was replaced by the 👍/👎 vote buttons, so library is now only set when the hub page has a library selected.
tag_click param, value, source SpecTabs.tsx, StatsPage.tsx User clicks a tag chip to filter (source ∈ spec_detail, stats)
theme_toggle to MastheadRule.tsx User cycles tri-state theme mode (to ∈ system, light, dark). The cycle order is system → light → dark → system.
view_mode_change mode, library SpecDetailView.tsx User toggles preview ↔ interactive view inside a spec detail. mode ∈ preview, interactive. Fires on every toggle in either direction (cf. open_interactive, which only fires when the interactive HTML is opened in a new tab).
library_click source, library LibrariesPage.tsx User clicks a library card on /libraries to navigate to its filtered plots view. source is libraries_page from this entry point.
library_filter source, framework LibrariesPage.tsx User clicks a language/framework filter chip on /libraries (framework carries the chip id; register the prop in the dashboard for breakdowns)
stats_top_impl_click spec, library StatsPage.tsx User clicks a "top implementation" thumbnail on /stats to jump into its spec detail.
map_node_click spec MapPage.tsx User clicks a node on /map (or, on touch, second tap on an already-pinned node) to navigate to its spec detail.
map_node_pin spec MapPage.tsx Touch device only: first tap on a node opens the preview panel + pin marker without navigating. A second tap on the same node fires map_node_click and navigates.
map_search_select spec MapPage.tsx User picks a result from the /map search dropdown (⌘K / Ctrl+K opens it). The camera flies to the node and the preview panel opens.
feedback_opened path FeedbackWidget.tsx User clicks the floating feedback FAB and the quick mini-stack of 👍 / 👎 / 💬 appears (issue #5662). path is window.location.pathname + search at open time.
feedback_submitted path, reaction?, has_contact, spec_id?, library?, mode FeedbackWidget.tsx, SpecDetailView.tsx (via useQuickReaction.ts) User submits a feedback entry. reaction ∈ thumbs_up, thumbs_down, bug, idea (omitted if none selected). mode is "quick" for a one-tap 👍/👎 from the mini-stack, "full" for a submit from the detailed dialog, and "plot_overlay" for a 👍 (top-left) / 👎 (bottom-left) tap directly on the plot in a spec detail view — that one also carries library, because the vote is about one implementation and the server stores library_id + language with it. has_contact is "true"/"false" — the contact field is now a free-form name/email/handle, not strictly an email. spec_id is set when the current route resolves to a spec page.

Diagnostics

Event Properties Source Description
page_not_found path, source NotFoundPage.tsx, SpecPage.tsx A visitor reached a URL the app cannot serve. path is window.location.pathname. source says which kind of miss it was: catch_all (the URL matches no route at all), spec_missing (the spec itself does not exist), impl_missing (the spec exists but not that library/language pair), language_params (a /{spec}/{language} URL arrived without both segments), route_error (the router returned a 404 response).

Read source before path. A catch_all miss is usually a bad inbound link; impl_missing is content that used to exist and no longer does, which is the pattern a library migration leaves behind and the one worth alerting on.

One asymmetry to know about: impl_missing fires without a 404 page ever being shown. SpecPage.tsx redirects those visitors to the spec hub with a language filter, to preserve their intent — so the event is reported from there rather than from NotFoundPage.tsx. Every other source accompanies a rendered 404.

The bot-facing side of the same URLs answers HTTP 404 from api/routers/seo.py, but crawlers never run JavaScript and app/nginx.conf short-circuits bot user agents with return 202 on /api/event, so nothing they do appears here — this event covers humans only.

Navigation clicks (nav_click)

A single event captures every clickable surface on the chrome, the editorial landing page and the gallery so we can answer "where do users go, from which page, and via which UI element". One event, one event-property pair: source (which UI element was clicked) + target (where it leads). Some sources additionally carry spec, library, or value for richer breakdowns.

source value Where Target
nav_specs / nav_plots / nav_map / nav_libraries / nav_stats / nav_palette / nav_mcp NavBar.tsx top-level menu bar
nav_logo NavBar.tsx logo → /
nav_search NavBar.tsx plots.search() button → /plots?focus=search
masthead_logo / masthead_branch / masthead_release MastheadRule.tsx masthead ~/anyplot.ai · main · v1.x.x
breadcrumb MastheadRule.tsx breadcrumb segments on non-landing routes
hero_cta_browse / hero_mcp / hero_github HeroSection.tsx hero call-to-action + secondary links
hero_use_my_data HeroSection.tsx # new: pick any plot, then .use_my_data() → under the hero CTAs, shown only while the agent chat's public lane is open → /plots
potd_terminal_image / potd_terminal_filename / potd_terminal_github PlotOfTheDayTerminal.tsx hero terminal-framed POTD
featured_thumb LandingPage.tsx featured plot grid
library_card LandingPage.tsx library cards (carries value=<library_id>)
section_header SectionHeader.tsx specs.all() / libraries.all() / palette.explore() headers
specs_more_link LandingPage.tsx + N more in the catalogue →
suggest_spec_link LandingPage.tsx spec.suggest() GitHub-issue link
gallery_spec_hub PlotsPage.tsx {spec_id}.compare() → above the grid when /plots is filtered to exactly one spec (carries spec) → /{spec_id}

Random methods:

  • click: Shuffle icon clicked
  • space: Spacebar pressed
  • doubletap: Mobile double-tap gesture

Grid sizes:

  • normal: Larger cards (1-3 columns)
  • compact: Smaller cards (2-6 columns)

Tab toggle actions:

  • open: User opened a tab
  • close: User closed a tab

Tab names: code, specification, implementation, quality

External and internal link events

Event Name Properties Where Description
external_link destination, spec?, library? Footer.tsx, LegalPage.tsx, AboutPage.tsx, LibrariesPage.tsx, SectionHeader.tsx User clicks external link
internal_link destination, spec?, library? Footer.tsx, AboutPage.tsx User clicks internal link

External destinations (Footer): github, github_issue_chooser, linkedin External destinations (LegalPage): linkedin, x, github_personal, kurrentschrift, cite_citadel Internal destinations (Footer): about, legal

Performance (Core Web Vitals)

Event Name Properties Where Description
LCP value, rating reportWebVitals.ts Largest Contentful Paint (rounded to nearest 100ms)
CLS value, rating reportWebVitals.ts Cumulative Layout Shift (2 decimal places)
INP value, rating reportWebVitals.ts Interaction to Next Paint (rounded to nearest 50ms)
FCP value, rating reportWebVitals.ts First Contentful Paint (rounded to nearest 100ms)
TTFB value, rating reportWebVitals.ts Time to First Byte (rounded to nearest 100ms)

Rating values: good, needs-improvement, poor (per web-vitals thresholds)

CWV tracking is production-only and dynamically imported (zero dev/bundle cost).


AI and crawler traffic (separate site)

Agent traffic is recorded against a second Plausible site, bots.anyplot.ai, never the main one. Every Plausible event creates a visitor, so routing these into anyplot.ai would re-introduce the inflation that audit 2026-07-08 (High #7) removed — visitor counts ran ~40% high and the trend lines were unusable. The bot site has no tracking script installed and never will: its events arrive server-side through the Events API, which is why it shows "Setup pending" in the Plausible UI. That is expected, not a broken install.

Event Properties Source Description
bot_fetch assistant, kind, path, status api/main.py (middleware) An AI assistant or search crawler requested a catalogue page. Always recorded on bots.anyplot.ai.
asset_fetch asset, spec, library?, assistant, kind, status api/main.py (middleware) An AI assistant or search crawler fetched ONE thing through the API: an implementation's runnable source (asset=code, GET /specs/{spec}/{library}/code) or a spec's detail (asset=spec, GET /specs/{spec}). Lists, the map, the filter and the machine files are not recorded. Always on bots.anyplot.ai. See the cache caveat below.
og_image_view page, platform, spec?, language?, library?, filter_*?, plus assistant + kind when machine-fetched api/routers/og_images.py A preview image was fetched. Split by audience, see below.

og:image is split by who fetched it

A social or messenger preview means a human shared a link — a product signal that belongs with the human numbers, so those stay on anyplot.ai. A search or AI crawler fetching the same image is not a share, and since robots.txt began permitting /og/ those arrive in volume: recording them on the main site would drown the sharing signal in crawler traffic and inflate visitor counts.

The split uses detect_ai_agent — if it recognises the user agent, the event is machine-side and goes to bots.anyplot.ai carrying the same assistant and kind props as bot_fetch, so both events slice alike. Anything it does not recognise (Twitter, Facebook, Slack, WhatsApp, …) is a share and stays put.

Two things Plausible will silently drop

Both were found by sending probe events against the live API and comparing what appeared, and both make an event vanish with an HTTP 202 ok — Plausible always acknowledges, then discards.

A bot User-Agent. Plausible identifies crawler agents and drops their events. Verified: the same event sent as Claude-User never appears, sent as a browser UA it does. There is no documented bypass. Machine-side events are therefore sent under BOT_SENDER_UA (anyplot-server/1.0); nothing is lost, because the UA only feeds Plausible's browser/OS/device detection — meaningless for a crawler — while the identity travels in assistant and kind.

Our own IP instead of the visitor's. The Events API docs state that Plausible uses "the first valid IP address from the list" and that if you "forward a server, hosting provider, or CDN IP address instead of the actual visitor IP, Plausible's bot filtering will drop the event". Analytics therefore resolves the IP with visitor_ip, not the rate limiter's client_ip: the latter deliberately returns the rightmost forwarded entry, because the leftmost is client-controlled and forging it once let callers poison another user's rate-limit bucket. The rightmost entry is our own infrastructure, so reusing it here would discard every event. api/request_context.py documents why the two must stay separate.

The same trap has a second face on the crawler path. A prerendered page reaches the API through the site's nginx (Cloud Run app → Cloudflare → API), so on that hop cf-connecting-ip is the app container's Google egress address — a hosting-provider IP, dropped — while the crawler itself is only visible as the first x-forwarded-for entry, where the site-side Cloudflare put it. kurrentschrift measured the effect with probe events on 2026-08-28 (34.90.x and 35.204.x dropped, a home IP kept): one counted read in twenty. Since 2026-08-28 visitor_ip therefore reads the forwarded list first and falls back to cf-connecting-ip, and nginx's @seo_proxy forwards the crawler in X-Forwarded-For explicitly. For a direct client behind Cloudflare both headers name the same address, so nothing changes there. If bot_fetch counts ever look implausibly low again, this is the first thing to re-verify: the daily bot-serving-check fires ~25 crawler requests from a GitHub runner at 06:23 UTC and should be visible on the bot site as that many events.

Register the properties on the bots.anyplot.ai site, not on anyplot.ai — property registration is per site, and without it the events still arrive but cannot be broken down, which is the whole point of collecting them. asset_fetch also needs a goal (Custom event) of that name before the dashboard can break it down:

Property Description Used by
assistant Vendor (claude, chatgpt, gemini, mistral, perplexity, meta, amazon, duckduckgo, grok, google, bing, …) bot_fetch, asset_fetch
kind Why it fetched — see the table below bot_fetch, asset_fetch
path Public path that was requested, e.g. /box-basic/python/matplotlib bot_fetch
status Response status as a string (200, 404, …) bot_fetch, asset_fetch
asset What was fetched through the API: code (one implementation's runnable source) or spec (one spec's detail) asset_fetch
spec The spec id, e.g. box-basic asset_fetch
library The library id for a code read, e.g. seaborn; absent on a spec read asset_fetch

asset_fetch counts cache misses, not requests: the two routes answer public, max-age=300 and are also what the SPA fetches, so Cloudflare serves repeat reads from its edge and those never reach the middleware. For sporadic assistant traffic — one assistant, one plot, minutes apart — nearly every read is a miss, so the number is close; it is not exact. The alternative (dropping the edge cache for routes the SPA needs on every spec page) was not worth the precision. Filter on asset, then break down by spec for "which plots do assistants pull the code of", and by library within asset=code.

Filter on status before reading anything else. The event records the request, not a successful read: an assistant asking for a URL that no longer exists is a signal worth keeping — it is how a library migration announces itself — but counting it as a page view would be a lie. This is also why the recording lives in a middleware rather than a router dependency: a dependency runs before the handler and cannot see the response, so every miss was recorded as a read.

Machine-side og_image_view events carry the main site's image properties as well, so register these on bots.anyplot.ai too: page, platform, spec, language, library, and the filter_* family.

kind is the property worth filtering on:

kind Meaning
user_directed A person asked their assistant to open this page. This is a reader
index An assistant's search index is building a corpus; no one is waiting
search A classic search crawler (Googlebot, bingbot, …)
inspection Search Console's URL inspection tool
training A training-corpus crawler

assistant carries the vendor (claude, chatgpt, gemini, mistral, perplexity, meta, amazon, duckduckgo, grok, google, bing, …). The taxonomy lives in AI_AGENTS in api/analytics.py, ordered most-specific first because matching is substring-based: claude-user, claude-searchbot and claudebot mean three different things and a broader claude pattern would collapse them.

Search crawlers are included deliberately. Crawl frequency per engine is otherwise only visible by sampling Search Console's URL inspection one URL at a time — which is how a months-long recrawl gap after the June 2026 outage went unnoticed until someone went looking.

Only /seo-proxy/* paths count as page reads; robots.txt and sitemap.xml live on the same router and are machine files, not catalogue pages. The reported path is always the public URL, never the internal /seo-proxy prefix.

Server-side og:image tracking

Social media bots (Twitter, WhatsApp, Teams, etc.) don't execute JavaScript, so og:image requests can only be tracked server-side.

Architecture

All og:images are routed through the API for tracking:

Bot requests page → nginx detects bot → SEO proxy serves HTML with og:image URL
                                                            ↓
                                          https://api.anyplot.ai/og/{endpoint}.png
                                                            ↓
                                          track_og_image() → Plausible Events API
                                                            ↓
                                          Return image (fire-and-forget tracking)

Implementation: api/analytics.py (server-side Plausible tracking)

og:image event

Event Name Properties Description
og_image_view page, platform, spec?, language?, library?, filter_*? Bot requested og:image

Properties

Property Values Description
page home, plots, spec_overview, spec_detail Page type
platform See list below Detected platform from User-Agent
spec Specification ID Only for spec pages
language Language slug (e.g. python) Only for spec detail pages
library Library ID Only for spec detail pages
filter_* Filter value Dynamic props for filtered URLs (e.g., filter_lib, filter_dom)

Platform detection (27 platforms)

Social Media: twitter, facebook, linkedin, pinterest, reddit, tumblr, mastodon

Messaging Apps: slack, discord, telegram, whatsapp, signal, viber, skype, teams, snapchat

Search Engines: google, bing, yandex, duckduckgo, baidu, apple

Link Preview Services: embedly, quora, outbrain, rogerbot, showyoubot

Fallback: unknown

WhatsApp variant detection

Some apps (Signal, others) use a WhatsApp User-Agent to bypass rate limits (Issue #10060). We distinguish real WhatsApp from spoofed requests by version format:

Platform User-Agent Example Detection
whatsapp WhatsApp/2.23.18.78 i 3+ part version = verified WhatsApp
whatsapp-lite WhatsApp or WhatsApp/2 Simplified UA = Signal or other spoofers

API endpoints

Endpoint Description Tracking
/og/home.png Static og:image for home page page=home, filter_* from query params
/og/plots.png Static og:image for plots page page=plots
/og/{spec_id}.png Collage og:image for spec overview page=spec_overview, spec
/og/{spec_id}/{language}/{library}.png Branded og:image for implementation page=spec_detail, spec, language, library

Filter tracking for shared URLs

When users share filtered URLs (e.g., https://anyplot.ai/?lib=plotly&dom=statistics), the filters are passed to the og:image endpoint:

og:image URL: https://api.anyplot.ai/og/home.png?lib=plotly,matplotlib&dom=statistics
                                                  ↓
Tracked props: { page: "home", platform: "twitter", filter_lib: "plotly,matplotlib", filter_dom: "statistics" }

Note: Each filter category becomes a separate prop (filter_lib, filter_dom, etc.) to handle comma-separated values.


Plausible dashboard configuration

Required custom properties

To see event properties in Plausible dashboard, you MUST register them as custom properties.

Go to: Plausible Dashboard → Site Settings → Custom Properties → Add Property

Properties to register

Property Description Used By Events
spec Plot specification ID copy_code, download_image, plot_rotate, external_link, internal_link, open_interactive, report_issue, tag_click, og_image_view, agent_open, agent_plot_rendered
language Language slug (python, r, julia, javascript) og_image_view
library Library name (matplotlib, seaborn, etc.) copy_code, download_image, external_link, internal_link, open_interactive, tab_toggle, og_image_view, feedback_submitted (plot-overlay votes only), agent_open, agent_plot_rendered
method Action method (card, image, tab, agent, click, space, doubletap) copy_code, random_filter
page Page context (home, plots, spec_overview, spec_detail, agent_chat) copy_code, download_image, og_image_view
status Outcome of an agent chat step (ok, needs_attention, failed, not_ready; for a parse also too_long, unparseable, data_refused, error, data_request, formula, code, transcribed, discarded) agent_data_parsed, agent_plot_rendered
size_bucket Size class of pasted data (lt_1kb, 1_10kb, 10_50kb, 50_200kb, 200kb_1mb, over_1mb; over_200kb until the paste cap rose to 1 MB on 2026-10-14), never the size agent_data_parsed, agent_data_confirmed
changes How many change lines an assistant's data change or generated table carried (1, 2, 3+) agent_data_prepared
version The data version an undo restored (1 is the paste, 2, 3+) agent_data_undo
kind What a file dropped on the agent chat's data box was, by its first bytes (text, spreadsheet, parquet, arrow, numpy, hdf5, pickle, pdf, archive, binary, unknown), or the format the server found when it converted the file (xlsx, xlsm, xlsb, xls, ods, parquet, feather, arrow, npy, npz); on this site only, the bots site's kind is another property agent_data_file
encoding The text encoding the browser read a dropped file with (utf-8, utf-8-bom, utf-16le, utf-16be, windows-1252, none) agent_data_file
outcome What happened to a dropped file (filled, replaced, kept, sheet, refused, failed, too_large, busy, unreadable) agent_data_file
repaired Whether an agent run needed its repair round (yes / no) agent_plot_rendered
reason Why a guardrail stopped an agent chat request (out_of_scope, needs_data, budget, service_paused, unsupported_content, rate_limited, daily_limit, session_closed, other_library, locked_out, data_refused, guard_unavailable, other) agent_guardrail_block
include_data Whether a saved agent chat session, or a feedback case, carries the data file (yes / no) agent_session_saved, agent_result_feedback
reaction The reaction of a feedback entry or of an agent result's quick feedback (thumbs_up, thumbs_down, bug, idea) feedback_submitted, agent_result_feedback
platform Bot/platform name (twitter, whatsapp, teams, etc.) og_image_view
category Filter category (lib, spec, plot, data, dom, feat, dep, tech, pat, prep, style) search, random_filter, filter_remove
value Filter value random_filter, filter_remove, tag_click
query Search query text search, search_no_results
destination Link-target slug — external: github, github_issue_chooser, github_personal, github_bug, github_discussions, github_plot_request, github_releases, linkedin, x, kurrentschrift, cite_citadel, library_docs; internal: about, legal, legal_transparency, palette. SectionHeader external links emit the target hostname instead (e.g. plausible.io). Grep destination: in app/src for the authoritative list. external_link, internal_link
tab Tab name (code, specification, implementation, quality) tab_toggle
action Toggle action (open, close) tab_toggle
size Grid size (normal, compact) grid_resize
param URL parameter name for tag tag_click
source Source UI element / page context tag_click, nav_click, agent_open (plot_page / spec_hub / landing / direct)
framework Library id clicked in the libraries-page filter (needs dashboard registration to appear in breakdowns) library_filter
target Click destination (route or external label) nav_click
subject What an agent chat quick feedback is about (result: a result version, chat: the chat itself) agent_result_feedback
to New mode after toggle (system / light / dark) theme_toggle
mode Spec detail view mode (preview / interactive) view_mode_change
theme Ambient effective theme attached to every pageview & event (dark / light) — resolved from the tri-state mode so OS-followers still split cleanly all events (set in RootLayout via setAnalyticsAmbientProps)
rating CWV rating (good, needs-improvement, poor) LCP, CLS, INP
filter_lib Library filter value (for og:image) og_image_view
filter_spec Specification filter value (for og:image) og_image_view
filter_plot Plot type filter value (for og:image) og_image_view
filter_data Data type filter value (for og:image) og_image_view
filter_dom Domain filter value (for og:image) og_image_view
filter_feat Features filter value (for og:image) og_image_view
filter_dep Dependencies filter value (for og:image) og_image_view
filter_tech Techniques filter value (for og:image) og_image_view
filter_pat Patterns filter value (for og:image) og_image_view
filter_prep Dataprep filter value (for og:image) og_image_view
filter_style Styling filter value (for og:image) og_image_view

Goals configuration

Go to: Plausible Dashboard → Site Settings → Goals → Add Goal

Recommended goals

Goal Name Type Description
copy_code Custom Event Track code copies (primary conversion)
download_image Custom Event Track image downloads
search Custom Event Track successful searches
search_no_results Custom Event Track failed searches (content gaps)
random_filter Custom Event Track random filter usage
filter_remove Custom Event Track filter removal
grid_resize Custom Event Track view preference
tab_toggle Custom Event Track tab interactions
external_link Custom Event Track outbound clicks
internal_link Custom Event Track internal navigation links
open_interactive Custom Event Track interactive mode usage
suggest_spec Custom Event Track spec suggestion clicks
report_issue Custom Event Track issue report clicks
tag_click Custom Event Track tag filter clicks
plot_rotate Custom Event Track plot image rotation on specs page
nav_click Custom Event Track which UI element on the chrome, the landing page or the gallery leads users where
theme_toggle Custom Event Track dark/light theme switches
view_mode_change Custom Event Track preview ↔ interactive toggles in spec detail
library_click Custom Event Track library-card clicks on the libraries page
library_filter Custom Event Track language/framework filter-chip clicks on the libraries page
stats_top_impl_click Custom Event Track clicks on top-quality implementation thumbnails on /stats
map_node_click Custom Event Track navigation clicks from /map into a spec detail
map_node_pin Custom Event Track touch users opening the preview panel on /map (first tap)
map_search_select Custom Event Track use of the /map search-and-fly-to feature
og_image_view Custom Event Track og:image requests from social media bots
agent_open Custom Event Track opens of the agent chat (admin and public) and where they came from
agent_data_parsed Custom Event Track parse outcomes of pasted data in the agent chat
agent_data_confirmed Custom Event Track how often users confirm their data before the agent chat plots it
agent_data_prepared Custom Event Track how often the agent chat's assistant changes the data in step 1 or computes it from a formula
agent_data_undo Custom Event Track how often users take back the assistant's change to their data
agent_data_file Custom Event Track which files users drop on the agent chat's data box, and how often the browser refuses one
agent_plot_rendered Custom Event Track agent chat plot outcomes and repair rounds
agent_guardrail_block Custom Event Track agent chat refusals and failed-closed guard checks
agent_session_saved Custom Event Track how often admins save an agent chat session as a file, and with the data or without
agent_result_feedback Custom Event Track quick feedback on agent chat results by reaction, and how often the case holds the data
LCP Custom Event Largest Contentful Paint (Core Web Vital)
CLS Custom Event Cumulative Layout Shift (Core Web Vital)
INP Custom Event Interaction to Next Paint (Core Web Vital)

Funnels (optional)

Example funnel: Home → Spec → Copy

  1. Pageview / (home)
  2. Pageview /{spec_id}/{language}/{library} (spec detail)
  3. copy_code event

Dashboard widgets

Recommended custom widgets:

  1. Top Specs Copied: copy_code breakdown by spec
  2. Popular Libraries: copy_code breakdown by library
  3. Copy Journey: copy_code breakdown by page
  4. Search Terms: search breakdown by query
  5. Missing Content: search_no_results breakdown by query
  6. View Preference: grid_resize breakdown by size
  7. CWV Performance: LCP / CLS / INP breakdown by rating

User journey tracking

Understanding the page property

The page property tracks where users perform actions to understand their journey:

User lands on anyplot.ai
    │
    ├─→ Home (grid view)
    │   └─→ copy_code { page: 'home' }
    │
    ├─→ Cross-Language Hub (/{spec_id})
    │   └─→ copy_code { page: 'spec_hub' }
    │
    ├─→ Language Overview (/{spec_id}/{language})
    │   └─→ copy_code { page: 'spec_overview' }
    │   └─→ download_image { page: 'spec_overview' }
    │
    └─→ Implementation Detail (/{spec_id}/{language}/{library})
        └─→ copy_code { page: 'spec_detail' }
        └─→ download_image { page: 'spec_detail' }
        └─→ open_interactive { spec, language, library }

Journey examples in Plausible

Q: Do users copy from search results or spec pages?

  • Filter copy_code by page property
  • home = direct from search/browse
  • spec_overview = after viewing all implementations
  • spec_detail = after deep-diving into one library

Q: Which library implementations are most downloaded?

  • Filter download_image by library property
  • Filter by page to see if users download from overview or detail

Complete event reference

Events summary table

Event Properties Code Location
copy_code spec, library, method, page ImageCard.tsx, SpecPage.tsx, SpecTabs.tsx, agent-chat/ResultCard.tsx
download_image spec, library, page SpecPage.tsx
agent_open library, source, spec AgentChatPage.tsx
agent_data_parsed status, size_bucket useAgentSession.ts
agent_data_confirmed size_bucket useAgentSession.ts
agent_data_prepared changes useAgentSession.ts
agent_data_undo version useAgentSession.ts
agent_data_file kind, encoding, outcome useFileIntake.ts
agent_plot_rendered library, status, repaired, spec useAgentSession.ts
agent_guardrail_block reason useAgentSession.ts
agent_session_saved include_data useAgentSession.ts
agent_result_feedback reaction, include_data, subject useAgentSession.ts
search query, category FilterBar.tsx
search_no_results query FilterBar.tsx
random_filter category, value, method useFilterState.ts
filter_remove category, value useFilterState.ts
grid_resize size ToolbarActions.tsx
tab_toggle action, tab, library SpecTabs.tsx
tag_click param, value, source SpecTabs.tsx, StatsPage.tsx
plot_rotate spec SpecsListPage.tsx
open_interactive spec, library SpecOverview.tsx, SpecDetailView.tsx
suggest_spec - SpecsListPage.tsx (LandingPage mirror attributed via nav_click with source: suggest_spec_link)
report_issue spec, library? SpecPage.tsx
external_link destination, spec?, library? Footer.tsx, LegalPage.tsx, AboutPage.tsx, LibrariesPage.tsx, SectionHeader.tsx
internal_link destination, spec?, library? Footer.tsx, AboutPage.tsx
nav_click source, target, spec?, library?, value? NavBar, MastheadRule, HeroSection, SectionHeader, PlotOfTheDayTerminal, LandingPage, PlotsPage
theme_toggle to MastheadRule.tsx
view_mode_change mode, library SpecDetailView.tsx
library_click source, library LibrariesPage.tsx
library_filter source, framework LibrariesPage.tsx
stats_top_impl_click spec, library StatsPage.tsx
map_node_click spec MapPage.tsx
map_node_pin spec MapPage.tsx
map_search_select spec MapPage.tsx
LCP value, rating reportWebVitals.ts
CLS value, rating reportWebVitals.ts
INP value, rating reportWebVitals.ts
FCP value, rating reportWebVitals.ts
TTFB value, rating reportWebVitals.ts
og_image_view page, platform, spec?, language?, library?, filter_*? api/analytics.py (server-side)

Total: 39 client-side + 1 server-side = 40 events (the nine agent_* events exist only in builds with VITE_ENABLE_AGENT_CHAT=true)

Removed events: potd_dismiss (and the nav_click sources potd_image / potd_title / potd_source_link) died with the dismissible PlotOfTheDay.tsx banner — the POTD now lives in PlotOfTheDayTerminal.tsx and only emits the potd_terminal_* nav_click sources. Historical potd_dismiss data remains visible in Plausible; the goal can be deleted from the dashboard.

Every pageview and event additionally carries a theme ambient prop (dark / light). Set in RootLayout via setAnalyticsAmbientProps whenever the user toggles the theme — register theme as a custom property to see the dark-vs-light split per URL.


Property values reference

spec values

Any valid specification ID (e.g., scatter-basic, heatmap-correlation, bar-grouped)

library values

matplotlib | seaborn | plotly | bokeh | altair | plotnine | pygal | letsplot
ggplot2 | makie | chartjs | d3 | echarts | highcharts | muix

(Canonical source: LIBRARIES_METADATA in core/constants.py.)

method values

card      # ImageCard copy button (home grid)
image     # SpecPage image copy button
tab       # SpecTabs code tab copy button
agent     # Agent chat result card copy button
click     # Random icon clicked
space     # Spacebar pressed
doubletap # Mobile double-tap

page values

home          # HomePage grid view (client) or og:image home endpoint (server)
plots         # PlotsPage (server og:image only)
spec_overview # SpecPage showing all libraries
spec_detail   # SpecPage showing single library
agent_chat    # Agent chat result card (copy_code only)

platform values (server-side og:image tracking only)

# Social Media
twitter | facebook | linkedin | pinterest | reddit | tumblr | mastodon

# Messaging Apps
# NB: Signal spoofs the WhatsApp UA (see line 260 above), so the analytics
# pipeline never emits `signal` as a platform value — Signal traffic surfaces
# as `whatsapp-lite`.
slack | discord | telegram | whatsapp | whatsapp-lite | viber | skype | teams | snapchat

# Search Engines
google | bing | yandex | duckduckgo | baidu | apple

# Link Preview Services
embedly | quora | outbrain | rogerbot | showyoubot

# Fallback
unknown

category values

# Spec-level (WHAT is visualized)
lib   # library filter
spec  # specification filter
plot  # plot_type filter
data  # data_type filter
dom   # domain filter
feat  # features filter

# Impl-level (HOW it is implemented)
dep   # dependencies filter
tech  # techniques filter
pat   # patterns filter
prep  # dataprep filter
style # styling filter

tab values

code           # Python code tab
specification  # Spec details tab
implementation # AI implementation review tab
quality        # Quality score breakdown tab

destination values

github          # GitHub repository link (Footer)
stats           # Plausible stats dashboard link (Footer)
linkedin        # LinkedIn profile link (Footer, LegalPage)
x               # X/Twitter profile link (LegalPage)
github_personal # Personal GitHub link (LegalPage)
mcp             # MCP documentation page (internal link, Footer)
legal           # Legal page (internal link, Footer)

size values

normal   # Larger cards (1-3 columns)
compact  # Smaller cards (2-6 columns)

rating values (Core Web Vitals)

good              # Within recommended thresholds
needs-improvement # Between good and poor thresholds
poor              # Exceeds poor threshold

Reading stats — backend → Plausible Stats API

In addition to sending events, the backend reads aggregate visitor data from Plausible to render the unique-visitors chart at the top of the public stats page.

  • Endpoint: GET /insights/visitors (in api/routers/insights.py)
  • Upstream call: POST https://plausible.io/api/v2/query with body {"site_id": "anyplot.ai", "metrics": ["visitors"], "date_range": "28d", "dimensions": ["time:day"]} (28d matches Plausible's own default "Last 28 days" report so totals here align with the dashboard at plausible.io/anyplot.ai)
  • Auth: Authorization: Bearer ${PLAUSIBLE_API_KEY} (Stats API key created in Plausible → Account Settings → API Keys, then mirrored to GCP Secret Manager as PLAUSIBLE_API_KEY for Cloud Run)
  • Caching: 1h stale-while-revalidate via get_or_set_cache so traffic spikes stay well under Plausible's 600-req/h rate limit.
  • Graceful degradation: When PLAUSIBLE_API_KEY is unset or the upstream call fails, the endpoint returns points: [] (empty list). The frontend distinguishes this from "real zeros" — an empty list triggers the "visitor data unavailable" placeholder, while a non-empty list with low/zero values renders the normal 30-bar chart. The dashboard endpoint is unaffected because visitors load on a separate fetch.

Code locations

  • Plausible setup: app/index.html (lines 59-68)
  • Analytics hook: app/src/hooks/useAnalytics.ts
  • Pageview building: buildPlausibleUrl() in useAnalytics.ts
  • Core Web Vitals: app/src/analytics/reportWebVitals.ts
  • Event tracking: Passed via onTrackEvent prop throughout component tree
  • Agent chat events: app/src/hooks/useAgentSession.ts (parse, plot, guardrail), app/src/pages/AgentChatPage.tsx (pageview, open), app/src/sections/agent-chat/ResultCard.tsx (copy_code)
  • Stats API consumer: _fetch_plausible_visitors() in api/routers/insights.py

Testing

Development: Tracking disabled (not on anyplot.ai domain)

Production testing:

  1. Open https://anyplot.ai
  2. Open browser console
  3. Check for Plausible script load
  4. Trigger events and verify in Plausible dashboard (may take 1-2 min)

Debug mode:

// In browser console
window.plausible = function(...args) { console.log('Plausible:', args); };

Implementation checklist

  • Plausible script loaded
  • Manual pageview tracking
  • Filter-based URL generation
  • Code copy events with journey tracking (copy_code + page)
  • Download tracking (download_image + page)
  • Search events (search, search_no_results)
  • Random filter events (random_filter)
  • Filter removal tracking (filter_remove)
  • Grid size toggle tracking (grid_resize)
  • Tab interaction events (tab_toggle)
  • Tag click events (tag_click)
  • External link events (external_link)
  • Internal link events (internal_link)
  • Feature link events (open_interactive, suggest_spec, report_issue)
  • Plot rotation (plot_rotate)
  • Core Web Vitals tracking (LCP, CLS, INP)
  • Server-side og:image tracking (og_image_view) with platform detection
  • Landing-page navigation tracking (nav_click)
  • Theme tracking (theme_toggle event + theme ambient pageview prop)
  • Agent chat events (agent_open, agent_data_parsed, agent_data_confirmed, agent_data_prepared, agent_data_undo, agent_data_file, agent_plot_rendered, agent_guardrail_block, agent_session_saved), enum properties only
  • Agent chat quick feedback (agent_result_feedback), with the feedback control

Plausible dashboard checklist

  • Register all custom properties (see table above, including rating, action, param, source, platform, filter_*, and for the agent chat status, size_bucket, changes, version, repaired, reason, include_data, reaction)
  • Create goals for key events (including LCP, CLS, INP)
  • Set up funnels (optional)
  • Create custom dashboard widgets (optional)

Last Updated: 2026-07-10 Status: Production-ready with full journey tracking, Core Web Vitals, server-side og:image analytics, landing-page nav tracking, and theme analytics