Skip to content

Latest commit

 

History

History
827 lines (665 loc) · 41.9 KB

File metadata and controls

827 lines (665 loc) · 41.9 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 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=matplotlibhttps://anyplot.ai/lib/matplotlib
  • /?lib=matplotlib&plot=scatterhttps://anyplot.ai/lib/matplotlib/plot/scatter
  • /?lib=matplotlib,seabornhttps://anyplot.ai/lib/matplotlib,seaborn (OR logic)
  • /?lib=matplotlib&lib=seabornhttps://anyplot.ai/lib/matplotlib/lib/seaborn (AND logic)

Examples (spec routes):

  • /scatter-basic/python?lib=matplotlibhttps://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

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, 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

Custom events

Conversions

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 view
  • spec_overview: SpecPage showing all library implementations
  • spec_detail: SpecPage showing single library implementation

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 "report issue" link
tag_click param, value, source SpecTabs.tsx, StatsPage.tsx User clicks a tag chip to filter (sourcespec_detail, stats)
theme_toggle to MastheadRule.tsx User cycles tri-state theme mode (tosystem, 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. modepreview, 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. reactionthumbs_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.

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.

Landing page navigation (nav_click)

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 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
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

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 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)

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
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 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
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

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
  • 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)

Plausible dashboard checklist

  • 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