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.
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.aidomain - Privacy-focused: No cookies, GDPR-compliant
Implementation: Query parameters converted to URL path segments for better analytics segmentation.
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.airewrites internally; canonical in HTML points back toanyplot.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.
Filters create dynamic URLs with the following format:
https://anyplot.ai/{category}/{value}/{category}/{value}/...
On spec routes, the spec/language/library prefix is preserved before the filter segments:
https://anyplot.ai/{spec_id}/{language}/{library}/{category}/{value}/...
Ordered categories: lib, spec, plot, data, dom, feat, dep, tech, pat, prep, style
Examples (root homepage):
/?lib=matplotlib→https://anyplot.ai/lib/matplotlib/?lib=matplotlib&plot=scatter→https://anyplot.ai/lib/matplotlib/plot/scatter/?lib=matplotlib,seaborn→https://anyplot.ai/lib/matplotlib,seaborn(OR logic)/?lib=matplotlib&lib=seaborn→https://anyplot.ai/lib/matplotlib/lib/seaborn(AND logic)
Examples (spec routes):
/scatter-basic/python?lib=matplotlib→https://anyplot.ai/scatter-basic/python/lib/matplotlib
Benefits:
- Plausible shows popular filter combinations
- No manual event tracking needed for filter changes
- URL structure clearly shows user's browsing context
| 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, ping) |
/{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
| Event Name | Properties | Where | Description |
|---|---|---|---|
copy_code |
spec, library, method, page |
ImageCard.tsx, SpecPage.tsx, SpecTabs.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
Page values (for user journey tracking):
home: HomePage grid viewspec_overview: SpecPage showing all library implementationsspec_detail: SpecPage showing single library implementation
| 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) |
| 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 "report issue" link |
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?, mode |
FeedbackWidget.tsx | 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. 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. |
| 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.
A single event captures every clickable surface on the chrome and the new
editorial landing page so we can answer "where do users go from / 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 |
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 |
Random methods:
click: Shuffle icon clickedspace: Spacebar presseddoubletap: 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 tabclose: User closed a tab
Tab names: code, specification, implementation, quality
| 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
| 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).
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. |
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.
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.
Social media bots (Twitter, WhatsApp, Teams, etc.) don't execute JavaScript, so og:image requests can only be tracked server-side.
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)
| Event Name | Properties | Description |
|---|---|---|
og_image_view |
page, platform, spec?, language?, library?, filter_*? |
Bot requested og:image |
| 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) |
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
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 |
| 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 |
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.
To see event properties in Plausible dashboard, you MUST register them as custom properties.
Go to: Plausible Dashboard → Site Settings → Custom Properties → Add Property
| 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 |
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 |
method |
Action method (card, image, tab, click, space, doubletap) | copy_code, random_filter |
page |
Page context (home, plots, spec_overview, spec_detail) | copy_code, download_image, og_image_view |
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 |
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 |
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 |
Go to: Plausible Dashboard → Site Settings → Goals → Add Goal
| 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 landing/chrome leads users off the root |
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 |
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) |
Example funnel: Home → Spec → Copy
- Pageview
/(home) - Pageview
/{spec_id}/{language}/{library}(spec detail) copy_codeevent
Recommended custom widgets:
- Top Specs Copied:
copy_codebreakdown byspec - Popular Libraries:
copy_codebreakdown bylibrary - Copy Journey:
copy_codebreakdown bypage - Search Terms:
searchbreakdown byquery - Missing Content:
search_no_resultsbreakdown byquery - View Preference:
grid_resizebreakdown bysize - CWV Performance:
LCP/CLS/INPbreakdown byrating
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 }
Q: Do users copy from search results or spec pages?
- Filter
copy_codebypageproperty home= direct from search/browsespec_overview= after viewing all implementationsspec_detail= after deep-diving into one library
Q: Which library implementations are most downloaded?
- Filter
download_imagebylibraryproperty - Filter by
pageto see if users download from overview or detail
| Event | Properties | Code Location |
|---|---|---|
copy_code |
spec, library, method, page |
ImageCard.tsx, SpecPage.tsx, SpecTabs.tsx |
download_image |
spec, library, page |
SpecPage.tsx |
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 |
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: 30 client-side + 1 server-side = 31 events
Removed events:
potd_dismiss(and thenav_clicksourcespotd_image/potd_title/potd_source_link) died with the dismissiblePlotOfTheDay.tsxbanner — the POTD now lives inPlotOfTheDayTerminal.tsxand only emits thepotd_terminal_*nav_clicksources. Historicalpotd_dismissdata remains visible in Plausible; the goal can be deleted from the dashboard.
Every pageview and event additionally carries a
themeambient prop (dark/light). Set inRootLayoutviasetAnalyticsAmbientPropswhenever the user toggles the theme — registerthemeas a custom property to see the dark-vs-light split per URL.
Any valid specification ID (e.g., scatter-basic, heatmap-correlation, bar-grouped)
matplotlib | seaborn | plotly | bokeh | altair | plotnine | pygal | letsplot
ggplot2 | makie | chartjs | d3 | echarts | highcharts | muix
(Canonical source: LIBRARIES_METADATA in core/constants.py.)
card # ImageCard copy button (home grid)
image # SpecPage image copy button
tab # SpecTabs code tab copy button
click # Random icon clicked
space # Spacebar pressed
doubletap # Mobile double-tap
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
# 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
# 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
code # Python code tab
specification # Spec details tab
implementation # AI implementation review tab
quality # Quality score breakdown tab
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)
normal # Larger cards (1-3 columns)
compact # Smaller cards (2-6 columns)
good # Within recommended thresholds
needs-improvement # Between good and poor thresholds
poor # Exceeds poor threshold
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(inapi/routers/insights.py) - Upstream call:
POST https://plausible.io/api/v2/querywith 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 asPLAUSIBLE_API_KEYfor Cloud Run) - Caching: 1h stale-while-revalidate via
get_or_set_cacheso traffic spikes stay well under Plausible's 600-req/h rate limit. - Graceful degradation: When
PLAUSIBLE_API_KEYis unset or the upstream call fails, the endpoint returnspoints: [](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.
- 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
onTrackEventprop throughout component tree - Stats API consumer:
_fetch_plausible_visitors()inapi/routers/insights.py
Development: Tracking disabled (not on anyplot.ai domain)
Production testing:
- Open https://anyplot.ai
- Open browser console
- Check for Plausible script load
- 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); };- 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_toggleevent +themeambient pageview prop)
- Register all custom properties (see table above, including
rating,action,param,source,platform,filter_*) - 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