plausible/analytics

Open source, privacy-first web analytics. Lightweight, cookie-free Google Analytics alternative. Self-hosted or cloud.

24,628 stars Elixir 10 components

12 hidden assumptions · 6-stage pipeline · 10 components

Like any codebase, this fullstack makes assumptions it never checks — most are routine. The ones worth your attention are below.

Collects and displays real-time web analytics without cookies or personal data

Website visitors trigger tracking events from injected JavaScript which POST to Phoenix controllers for validation and queuing. Background workers enrich events with geolocation and user agent data before batch insertion into ClickHouse. Dashboard users request analytics data through React components that query Phoenix API endpoints, which aggregate ClickHouse data and return JSON formatted for chart and table visualization.

Under the hood, the system uses 3 feedback loops, 5 data pools, 5 control points to manage its runtime behavior.

A 10-component fullstack. 1417 files analyzed. Data flows through 6 distinct pipeline stages.

Hidden Assumptions

Most of what this code assumes is routine. These 3 are the ones most likely to cause trouble here. The rest are minor; they're under "Show everything".

Worth your attention first

If event target is null, undefined, or doesn't have contains method, modal throws TypeError and breaks click-outside-to-close functionality

Worth your attention first

Circular references in changeset data cause infinite recursion and stack overflow during audit logging, potentially crashing the process

Worth your attention first

In server-side rendering or non-browser environments, accessing document.body.style throws reference error and prevents modal rendering

Show everything (9 more)
Environment

Cross-origin fetch to https://plausible.io/changes.txt will always succeed and return valid text response - assumes network availability, CORS headers, and server uptime

If this fails: Network failures or CORS issues cause silent Promise rejection, leaving changelog notification in inconsistent state with no error handling

assets/js/app.js:changelogNotification
Domain

Browser names from user agent parsing exactly match BROWSER_ICONS keys - assumes consistent string formatting between user agent parser and icon mapping

If this fails: Unrecognized browser names fall back to 'fallback.svg' which may not exist, causing broken image icons in device reports

assets/js/dashboard/stats/devices/index.js:browserIconFor
Temporal

localStorage.lastChangelogUpdate contains valid timestamp number that can be parsed - assumes previous successful fetch stored valid Date.getTime() value

If this fails: Invalid or corrupted timestamp values cause NaN comparisons, potentially showing changelog notification incorrectly or never

assets/js/app.js:showChangelogNotification
Ordering

parseSearch(location.search) returns object with expected filter structure before postProcessFilters runs - assumes URL parsing happens before filter processing

If this fails: Malformed URL search params could pass undefined or invalid filter arrays to postProcessFilters, causing dashboard state corruption

assets/js/dashboard/dashboard-state-context.tsx:useMemo
Scale

Goal names in localStorage keys are URL-safe and don't contain special characters - assumes goal names from API responses are sanitized for storage key usage

If this fails: Goal names with special characters create invalid localStorage keys, causing prop key storage/retrieval to fail silently for those goals

assets/js/dashboard/stats/behaviours/index.js:STORAGE_KEYS.getForPropKeyForGoal
Resource

Window innerWidth is available and returns valid number - assumes browser window object exists with width measurement capability

If this fails: In headless or server environments where window.innerWidth is undefined, modal sizing falls back to DEFAULT_WIDTH but resize handler may throw errors

assets/js/dashboard/stats/modals/modal.js:DEFAULT_WIDTH
Contract

ListReport component expects getFilterInfo to return object with specific shape {prefix: string, filter: [string, string, string[]]} - assumes consistent filter structure contract

If this fails: If getFilterInfo returns different structure, ListReport filter application fails silently or throws errors when constructing dashboard queries

assets/js/dashboard/stats/pages/index.js:getFilterInfo
Environment

BUILD_EXTRA global is defined at compile time and controls conditional module loading - assumes build system sets this flag correctly

If this fails: If BUILD_EXTRA is undefined, maybeRequire() behavior is unpredictable and could break funnel functionality in production builds

assets/js/dashboard/stats/behaviours/index.js:BUILD_EXTRA
Temporal

Modal cleanup happens in exact reverse order of setup - assumes removeEventListener calls match addEventListener pairs from componentDidMount

If this fails: If component unmounts during event handling or if event listeners were modified elsewhere, cleanup may miss listeners causing memory leaks

assets/js/dashboard/stats/modals/modal.js:componentWillUnmount

Open the standalone hidden-assumptions report for analytics →

How Data Flows Through the System

Website visitors trigger tracking events from injected JavaScript which POST to Phoenix controllers for validation and queuing. Background workers enrich events with geolocation and user agent data before batch insertion into ClickHouse. Dashboard users request analytics data through React components that query Phoenix API endpoints, which aggregate ClickHouse data and return JSON formatted for chart and table visualization.

  1. Track website visitor events — PlausibleTracker JavaScript on client websites captures page views and custom events, validates against configured domains, and sends HTTP POST requests to /api/event with visitor data but no persistent cookies
  2. Ingest and validate tracking events — IngestController receives tracking POSTs, validates referrer domains against site allowlists, extracts IP addresses for geolocation without storing them, and queues validated events for background processing [HTTP tracking request → Queued tracking event]
  3. Enrich and store analytics events — EventWorker background jobs dequeue tracking events, parse user agent strings for browser/OS data, perform IP geolocation lookups, then batch insert enriched events into ClickHouse events table with session tracking [Queued tracking event → ClickHouse event records]
  4. Request dashboard analytics data — React dashboard components trigger API requests through the api.get() function, serializing DashboardState filters and time periods into query parameters for Phoenix StatsController endpoints [DashboardState → HTTP API request]
  5. Query and aggregate analytics — StatsController validates request parameters, constructs ClickHouse queries through Stats.Query with date ranges and WHERE clauses from filters, executes aggregation queries, and formats results into QueryApiResponse JSON [HTTP API request → QueryApiResponse]
  6. Render dashboard visualizations — React components like VisitorGraph and ListReport consume QueryApiResponse data to render time series charts using Chart.js and tabular reports with Metric formatters, updating the dashboard UI with current analytics [QueryApiResponse → Dashboard UI updates]

Data Models

The data structures that flow between stages — the contracts that hold the system together.

DashboardState assets/js/dashboard/dashboard-state.ts
TypeScript interface with period: DashboardPeriod, filters: Filter[] (triplets of [operator, key, clauses]), date/from/to: Dayjs objects, comparison: ComparisonMode, labels: FilterClauseLabels (ID to human-readable mappings)
Created from URL params and localStorage on page load, updated through user interactions, serialized into API requests and browser navigation
PlausibleSite assets/js/dashboard/site-context.tsx
TypeScript object with domain: string, offset: number (timezone), feature flags like hasGoals/hasProps/funnelsAvailable: boolean, revenueGoals/funnels: arrays, statsBegin/nativeStatsBegin: date strings
Parsed from server-rendered data attributes on initial page load, passed through React context to all dashboard components
Metric assets/js/dashboard/stats/reports/metrics.js
JavaScript class with key: string (API field name), formatter: function (value renderer), renderLabel: function (contextual labels), width: CSS class, meta: object (display hints like plot: true)
Created via factory functions like createVisitors(), configured with dashboard state context, used to render values and labels in tables and charts
QueryApiResponse assets/js/dashboard/api.ts
TypeScript interface with query: {metrics: Metric[], date_range: [string, string]}, results: Array<{metrics: number[], dimensions: string[], comparison: {metrics: number[], change: number[]}}>, meta: Record<string, unknown>
Returned from backend API calls, processed by dashboard components to extract time series data and comparison metrics for visualization
AuditEntry extra/lib/plausible/audit/entry.ex
Ecto schema with name: String.t(), entity/entity_id: String.t(), change: map() (encoded diff), user_id/team_id: integer(), datetime: NaiveDateTime.t(), actor_type: :system | :user
Created automatically when tracked entities change, encoded using Audit.Encoder protocol, persisted to audit_entries table, queried for compliance reporting

System Behavior

How the system operates at runtime — where data accumulates, what loops, what waits, and what controls what.

Data Pools

ClickHouse Events (database)
Time-series storage for all website tracking events with columns for timestamp, page, referrer, browser, location - optimized for analytics aggregations
PostgreSQL Sites (database)
Relational data for user accounts, site configurations, goals, funnels, and audit trails - handles authentication and feature management
Event Processing Queue (queue)
Background job queue for tracking events awaiting enrichment and database insertion - handles traffic spikes and processing delays
Browser Local Storage (cache)
Client-side persistence for dashboard preferences like selected tabs, time periods, and property keys - survives page refreshes
React Component State (in-memory)
Centralized dashboard state holding current filters, time periods, and comparison settings - synchronized with URL and drives all queries

Feedback Loops

Delays

Control Points

Technology Stack

Elixir/Phoenix (framework)
Web framework providing HTTP controllers, real-time channels, and OTP supervision for concurrent event processing
React (framework)
Dashboard UI library with TypeScript, context providers, and functional components for analytics visualization
ClickHouse (database)
Columnar database optimized for analytics queries and time-series data storage with event aggregation
PostgreSQL (database)
Relational database for user accounts, site configuration, goals, and audit trail persistence
Oban (library)
Background job processing for async event enrichment and batch database operations
Chart.js (library)
Time series visualization for visitor graphs and metrics dashboards
Ecto (library)
Database ORM providing schema definitions, changesets, and query composition for PostgreSQL
Alpine.js (library)
Lightweight JavaScript for server-rendered page interactivity in non-dashboard interfaces

Key Components

Explore the interactive analysis

See the full architecture map, data flow, and code patterns visualization.

Analyze on CodeSea

Related Fullstack Repositories

Frequently Asked Questions

What is analytics used for?

Collects and displays real-time web analytics without cookies or personal data plausible/analytics is a 10-component fullstack written in Elixir. Data flows through 6 distinct pipeline stages. The codebase contains 1417 files.

How is analytics architected?

analytics is organized into 5 architecture layers: Tracking Scripts, Ingestion Layer, Analytics Engine, Dashboard UI, and 1 more. Data flows through 6 distinct pipeline stages. This layered structure keeps concerns separated and modules independent.

How does data flow through analytics?

Data moves through 6 stages: Track website visitor events → Ingest and validate tracking events → Enrich and store analytics events → Request dashboard analytics data → Query and aggregate analytics → .... Website visitors trigger tracking events from injected JavaScript which POST to Phoenix controllers for validation and queuing. Background workers enrich events with geolocation and user agent data before batch insertion into ClickHouse. Dashboard users request analytics data through React components that query Phoenix API endpoints, which aggregate ClickHouse data and return JSON formatted for chart and table visualization. This pipeline design reflects a complex multi-stage processing system.

What technologies does analytics use?

The core stack includes Elixir/Phoenix (Web framework providing HTTP controllers, real-time channels, and OTP supervision for concurrent event processing), React (Dashboard UI library with TypeScript, context providers, and functional components for analytics visualization), ClickHouse (Columnar database optimized for analytics queries and time-series data storage with event aggregation), PostgreSQL (Relational database for user accounts, site configuration, goals, and audit trail persistence), Oban (Background job processing for async event enrichment and batch database operations), Chart.js (Time series visualization for visitor graphs and metrics dashboards), and 2 more. A focused set of dependencies that keeps the build manageable.

What system dynamics does analytics have?

analytics exhibits 5 data pools (ClickHouse Events, PostgreSQL Sites), 3 feedback loops, 5 control points, 3 delays. The feedback loops handle polling and retry. These runtime behaviors shape how the system responds to load, failures, and configuration changes.

What design patterns does analytics use?

5 design patterns detected: Context Providers, Generic Report Components, Protocol-based Encoding, Background Job Processing, Modal Routing.

Analyzed on April 20, 2026 by CodeSea. Written by .