# Technical Documentation: rybbit-io/rybbit

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/rybbit-io/rybbit)

### Overview (README.md)

<p align="center">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="docs/public/rybbit/horizontal_white.svg">
    <source media="(prefers-color-scheme: light)" srcset="docs/public/rybbit/horizontal_black.svg">
    <img src="docs/public/rybbit/horizontal_black.svg" height="70" alt="Rybbit">
  </picture>
    <p align="center">Open Source Web & Product Analytics</p>

<p align="center">
    <a href="https://rybbit.com" target="_blank">Website</a> |
    <a href="https://demo.rybbit.com/81" target="_blank">Demo</a> |
    <a href="https://rybbit.com/docs" target="_blank">Documentation</a> |
    <a href="https://discord.gg/DEhGb4hYBj" target="_blank">Discord</a> |
    <a href="https://github.com/rybbit-io/rybbit?tab=AGPL-3.0-1-ov-file" target="_blank">License (AGPL-3)</a> |
    <a href="https://github.com/rybbit-io/rybbit/blob/master/CONTRIBUTE.md" target="_blank">Contribute</a>
</p>

<a href="https://rybbit.com/" target="_blank">Rybbit</a> is the modern open source and privacy friendly alternative to Google Analytics. It takes only a couple minutes to setup and is super intuitive to use.

<p align="center">
  <a href="https://www.producthunt.com/products/rybbit?embed=true&utm_source=badge-top-post-badge&utm_medium=badge&utm_source=badge-rybbit" target="_blank">
    <img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=1028220&theme=standard&period=daily" alt="Rybbit - Open Source Google Analytics Replacement | Product Hunt" width="250" height="54" />
  </a>
</p>

<p align="center">
  <strong><a href="https://demo.rybbit.com/81">🔍 View Live Demo</a></strong> - See Rybbit running on a real-life production site.
</p>

<img width="1299" height="797" alt="Screenshot 2025-10-16 at 7 25 20 PM" src="https://github.com/user-attachments/assets/7988ba04-64ee-4410-9972-847d278efa2f" />

<hr>

## 🚀 Getting Started

There are two ways to start using Rybbit:

| Option                                                   | Description                                                   |
| -------------------------------------------------------- | ------------------------------------------------------------- |
| **[Hosted Service](https://rybbit.com)**                 | The fastest way to get started                                |
| **[Self-Hosting](https://rybbit.com/docs/self-hosting)** | Deploy and manage Rybbit on your own VPS for complete control |

📚 Explore our [documentation](https://rybbit.com/docs) to learn more about installation, configuration, and usage.

To work on the client and backend locally, see the [pnpm workspace setup](CONTRIBUTE.md#local-development).

<hr>

## ✨ Key Features

- All key web analytics metrics including sessions, unique users, pageviews, bounce rate, session duration
- Session replays
- No cookies & privacy friendly
- Customizable goals. retention, user journeys, and funnels dashboards
- Advanced filtering across 15+ dimensions
- Custom events with JSON properties
- 3 level location tracking (country -> region -> city) + advanced map visualizations
- Real time dashboard
- Support for organizations and unlimited number of sites

<hr>

## 📊 Comparison

See how Rybbit compares to other analytics solutions:

| Feature                           | Rybbit | GA4        | Plausible  | Cloudflare |
| --------------------------------- | ------ | ---------- | ---------- | ---------- |
| **Open Source**                   | ✅     | ❌         | ✅         | ❌         |
| **Self-Hosting**                  | ✅     | ❌         | ✅\*       | ❌         |
| **Cookieless & Privacy friendly** | ✅     | ❌         | ✅         | ✅         |
| **Advanced Maps**                 | ✅     | ❌         | ❌         | ❌         |
| **Advanced Filters**              | ✅     | ⚠️ Limited | ⚠️ Limited | ❌         |
| **Web Vitals**                    | ✅\*\* | ❌         | ❌         | ❌         |
| **Session Details**               | ✅     | ❌         | ❌         | ❌         |
| **User Profiles**                 | ✅     | ❌         | ❌         | ❌         |
| **Session Replays**               | ✅     | ❌         | ❌         | ❌         |
| **Funnels**                       | ✅     | ✅         | ✅\*\*     | ❌         |
| **User Journeys**                 | ✅     | ✅         | ❌         | ❌         |
| **Retention Analysis**            | ✅     | ✅         | ❌         | ❌         |
| **Goals & Events**                | ✅     | ✅         | ✅         | ❌         |
| **Real-time Dashboard**           | ✅     | ✅         | ✅         | ✅         |
| **Custom Events (JSON)**          | ✅     | ✅         | ⚠️ Limited | ❌         |
| **Error Tracking**                | ✅     | ❌         | ❌         | ❌         |
| **Public Dashboards**             | ✅     | ❌         | ✅         | ❌         |
| **Organizations**                 | ✅     | ✅         | ✅         | ✅         |
| **Frog 🐸**                       | ✅     | ❌         | ❌         | ❌         |

\* Plausible's Community Edition has very limited features compared to their cloud version

\*\* Only available on paid tiers

<hr>

## 📊 Dashboard Preview

### Map

<img width="1299" height="797" alt="Screenshot 2025-10-16 at 7 27 12 PM" src="https://github.com/user-attachments/assets/52643121-4f4d-4bbe-9910-2516226bf421" />
<img width="1299" height="797" alt="Screenshot 2025-10-16 at 7 30 39 PM" src="https://github.com/user-attachments/assets/153585d5-7ff6-43bb-8bf6-fecfbe240ef8" />
<img width="1299" height="797" alt="Screenshot 2025-10-16 at 7 30 00 PM" src="https://github.com/user-attachments/assets/3873bc6b-9f79-4e9a-b083-0676a7d52b1d" />
<img width="1299" height="797" alt="Screenshot 2025-10-16 at 7 30 23 PM" src="https://github.com/user-attachments/assets/04ceca1d-a4af-4f52-856c-f054deeb1023" />

## Session Replay

<img width="1299" height="797" alt="Screenshot 2025-10-16 at 7 29 18 PM" src="https://github.com/user-attachments/assets/6906aef0-8ce0-41f9-9bbb-b175ef158df3" />

### Journeys

<img width="1299" height="797" alt="Screenshot 2025-10-16 at 7 34 13 PM" src="https://github.com/user-attachments/assets/4cd465df-8f91-43bf-a121-647ae2c9e8e1" />

### Funnels

<img width="1299" height="797" alt="Screenshot 2025-10-16 at 7 34 05 PM" src="https://github.com/user-attachments/assets/e3b083c2-4f18-4c8c-bc97-9491a6402439" />

### Goals

<img width="1299" height="797" alt="Screenshot 2025-10-16 at 7 28 26 PM" src="https://github.com/user-attachments/assets/d34c5c00-1dec-4864-9115-bfa9e9811d60" />

### Users

<img width="1299" height="797" alt="Screenshot 2025-10-16 at 7 28 54 PM" src="https://github.com/user-attachments/assets/f10c1bba-6658-4b6a-94a6-dcba351e4eea" />

### Errors

<img width="1299" height="797" alt="Screenshot 2025-10-16 at 7 40 56 PM" src="https://github.com/user-attachments/assets/efd60f04-c203-45ea-981d-14f34ec2b1e1" />

### Retention

<img width="1299" height="797" alt="Screenshot 2025-10-16 at 7 34 20 PM" src="https://github.com/user-attachments/assets/c56a31ac-7af5-419b-82a5-d3aedb68debc" />

### Web Vitals (Cloud only)

<img width="1299" height="797" alt="Screenshot 2025-10-16 at 7 28 01 PM" src="https://github.com/user-attachments/assets/d88b8be0-cd30-4dd6-a0e5-5446c890c0f9" />

<hr>

## ⭐ Star History

[](https://star-history.dera.page/#rybbit-io/rybbit&Date)

---

### README (`docs/README.md`)

# docs-v2

This is a Next.js application generated with
[Create Fumadocs](https://github.com/fuma-nama/fumadocs).

Run development server:

```bash
npm run dev
# or
pnpm dev
# or
yarn dev
```

Open http://localhost:3000 with your browser to see the result.

## Explore

In the project, you can see:

- `lib/source.ts`: Code for content source adapter, [`loader()`](https://fumadocs.dev/docs/headless/source-api) provides the interface to access your content.
- `app/layout.config.tsx`: Shared options for layouts, optional but preferred to keep.

| Route                     | Description                                            |
| ------------------------- | ------------------------------------------------------ |
| `app/(home)`              | The route group for your landing page and other pages. |
| `app/docs`                | The documentation layout and pages.                    |
| `app/api/search/route.ts` | The Route Handler for search.                          |

### Fumadocs MDX

A `source.config.ts` config file has been included, you can customise different options like frontmatter schema.

Read the [Introduction](https://fumadocs.dev/docs/mdx) for further details.

## Learn More

To learn more about Next.js and Fumadocs, take a look at the following
resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
  features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
- [Fumadocs](https://fumadocs.vercel.app) - learn about Fumadocs

---

### Content Docs (Docs) Index (`docs/content/docs/(docs)/index.mdx`)

---
title: Introduction
description: Introduction to Rybbit
full: true
---

**Rybbit** is a powerful open source web and product analytics platform. Check out our [live demo](https://demo.rybbit.com/81) using a real production site.

<DocsHomeCards />

## Getting Started

Choose the option that's right for you:

- **Cloud Version:** Sign up for the [hosted version](https://app.rybbit.io/signup). Get up and running in minutes.
- **Self-Host:** Follow our [self hosting guide](/docs/self-hosting) to set up Rybbit on your own server.

And then learn how to add the [tracking script](/docs/script) to your website to start collecting data.

## Code

Rybbit is open sourced under the AGPL-3.0 license. You can find the code on [GitHub](https://github.com/rybbit-io/rybbit).

## Community

Join our [Discord server](https://discord.gg/DEhGb4hYBj) or talk to me on [X](https://x.com/yang_frog). I'm always looking for feedback and ideas to make Rybbit as awesome as possible.

---

### Content Docs Api Getting Started (`docs/content/docs/api/getting-started.mdx`)

---
title: Getting Started
description: Authentication, common parameters, and best practices for the Rybbit Stats API
---

import { Callout } from 'fumadocs-ui/components/callout';
import { TypeTable } from 'fumadocs-ui/components/type-table';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';

<Callout type="warn">
The API is currently in beta. There may be breaking changes as we continue to improve and expand the API.
</Callout>

## API Playground

The easiest way to explore and test the API is using the built-in **API Playground** in your Rybbit dashboard. Access it by navigating to any site and clicking "API Playground" in the sidebar.

The playground provides:
- **Endpoint browser** - Browse all available API endpoints
- **Parameter controls** - Configure request parameters with a visual interface
- **Exact time ranges** - Test date ranges with optional start and end times
- **Live responses** - See real API responses with your actual data
- **Code generation** - Copy ready-to-use code snippets

This is the recommended way to familiarize yourself with the API before integrating it into your applications.

## Guides

Looking for what to *build* with the API? These end-to-end recipes chain endpoints together to solve a real problem:

- [**Automated Weekly Report**](/docs/api/weekly-report) — a scheduled digest of last week's traffic, week-over-week change, and top pages and sources.
- [**Find Where Users Drop Off**](/docs/api/funnel-dropoff) — locate the biggest leak in a funnel, then see where the people who abandon go instead.
- [**Compare Channel Performance**](/docs/api/channel-performance) — rank acquisition channels by how well they convert, not just how much traffic they send.
- [**Build a Live Visitor Feed**](/docs/api/live-feed) — poll the events endpoint for a real-time activity stream you can render or alert on.
- [**Export Events to a Warehouse**](/docs/api/export-events) — backfill your full event history and keep it in sync with an incremental job.

## Authentication

All API requests must include authentication using one of the following methods:

### Bearer Token (Recommended)

```bash
Authorization: Bearer your_api_key_here
```

### Query Parameter (Testing Only)

```bash
?api_key=your_api_key_here
```

<Callout type="warn">
Query parameters expose API keys in server logs and browser history. Use only for quick testing.
</Callout>

## Generating API Keys

Rybbit has two kinds of API keys:

- **Organization API keys** (recommended for integrations) — the organization's own credential. They can access every site in the organization and keep working when team members leave. Only organization admins and owners can create and manage them.
- **Personal API keys** — act as you, with exactly your access, including any site restrictions, across every organization you belong to. Best for personal scripts and for connecting MCP clients.

### Organization keys

1. Navigate to **Settings → Organization** in your Rybbit dashboard
2. Scroll to the **Organization API Keys** section (visible to admins and owners)
3. Enter a name for the key and click **Create**
4. Copy the key immediately (it won't be shown again)

### Personal keys

1. Navigate to **Settings → Account** in your Rybbit dashboard
2. Scroll to the **Personal API Keys** section
3. Enter a name for the key and click **Create**
4. Copy the key immediately (it won't be shown again)

Both kinds support restricting a key to specific permissions when you create it — leave restrictions off for a full-access key.

### Rate Limits (Cloud Only)

Requests are limited in two ways: a **burst** limit that is the same on every plan, and a **daily quota** that depends on your plan.

| Plan | Burst | Daily quota |
|------|-------|-------------|
| **Free / Basic** | API keys not available | — |
| **Standard** | 50 requests per 10 seconds | 5,000 requests per day |
| **Pro** | 50 requests per 10 seconds | 25,000 requests per day |

The burst limit is a bucket of 50 requests that refills continuously at 5 requests per second. You can spend all 50 at once, then keep going at 5 per second — so short spikes are fine and there is no window boundary to line up with.

The daily quota resets at **00:00 UTC**.

Requests refused by the burst limit don't count against your daily quota.

#### Which budget a request is charged to

Limits apply per **credential owner**, never per key — creating more keys splits an existing budget rather than adding to it:

| Credential | Budget |
|------------|--------|
| **Organization key** (`rb_org_`) | The organization's. All of an organization's keys share it. |
| **Personal key** | Yours. All of your personal keys share one budget, across every organization you belong to. |
| **OAuth token** (MCP) | Yours, the same budget as your personal keys. |

A personal key's quota follows the most generous plan among the organizations you belong to.

Self-hosted instances are not rate limited at all — neither tier applies, and no rate limit headers are sent.

#### Reading your remaining budget

Analytics and management endpoints report your current budget on every response, so you can pace a client without waiting to be rejected:

| Header | Meaning |
|--------|---------|
| `X-RateLimit-Burst-Limit` / `-Remaining` | Burst bucket size and tokens left |
| `X-RateLimit-Burst-Reset` | Seconds until the bucket is full again |
| `X-RateLimit-Daily-Limit` / `-Remaining` | Daily quota and requests left today |
| `X-RateLimit-Daily-Reset` | Seconds until 00:00 UTC |
| `RateLimit-Limit` / `-Remaining` / `-Reset` | Whichever of the two is closest to running out |

Treat these as advisory: a few endpoints resolve credentials outside the standard path and omit them. `Retry-After` on a `429` is always present.

#### When you hit a limit

A `429` names the limit you hit and tells you how long to wait, in seconds:

```json
{
  "error": "Rate limit exceeded",
  "scope": "daily",
  "limit": 5000,
  "retryAfter": 34200
}
```

`scope` is `burst` or `daily`, and the same value is in the `Retry-After` header. The difference matters: a `burst` rejection clears in about a second, while a `daily` one lasts until the quota resets. Back off accordingly rather than retrying straight away.

---

## Common Parameters

The following parameters are shared across all analytics endpoints.

### Time Parameters

All endpoints require date-based, exact datetime, or relative time parameters:

<TypeTable
  type={{
    start_date: {
      description: 'Start date for the query period',
      type: 'string',
      typeDescription: 'Format: YYYY-MM-DD (e.g., "2024-01-15")',
      required: false,
    },
    end_date: {
      description: 'End date for the query period',
      type: 'string',
      typeDescription: 'Format: YYYY-MM-DD (e.g., "2024-01-31")',
      required: false,
    },
    time_zone: {
      description: 'IANA timezone identifier',
      type: 'string',
      typeDescription: 'e.g., "America/New_York", "Europe/London", "Asia/Tokyo". Required with date and exact datetime ranges.',
      required: false,
    },
    start_datetime: {
      description: 'Exact UTC start timestamp for the query period',
      type: 'string',
      typeDescription: 'Format: YYYY-MM-DD HH:mm:ss (e.g., "2024-01-15 13:00:00")',
      required: false,
    },
    end_datetime: {
      description: 'Exact UTC end timestamp for the query period. This boundary is exclusive.',
      type: 'string',
      typeDescription: 'Format: YYYY-MM-DD HH:mm:ss (e.g., "2024-01-15 15:00:00")',
      required: false,
    },
    past_minutes_start: {
      description: 'Start of relative time range (older boundary)',
      type: 'number',
      typeDescription: 'Minutes ago from now. Must be greater than past_minutes_end.',
      required: false,
    },
    past_minutes_end: {
      description: 'End of relative time range (newer boundary)',
      type: 'number',
      typeDescription: 'Minutes ago from now. Must be less than past_minutes_start.',
      required: false,
    },
  }}
/>

**Note:** You must provide either:
- All three date parameters (`start_date`, `end_date`, `time_zone`), OR
- All three exact datetime parameters (`start_datetime`, `end_datetime`, `time_zone`), OR
- Both relative parameters (`past_minutes_start`, `past_minutes_end`)

#### Examples

**Date-based query (last 30 days):**
```
?start_date=2024-01-01&end_date=2024-01-31&time_zone=America/New_York
```

**Exact datetime query (8 AM to 10 AM in New York on Jan 15, sent as UTC):**
```
?start_datetime=2024-01-15%2013:00:00&end_datetime=2024-01-15%2015:00:00&time_zone=America/New_York
```

**Relative query (last 60 minutes):**
```
?past_minutes_start=60&past_minutes_end=0
```

### Filter Parameters

The `filters` parameter accepts a JSON-encoded array of filter objects to narrow down your data.

You can also pass `segment_id`, the ID of a [saved segment](/docs/segments), instead of or alongside `filters`. The segment's filters are expanded on the server and ANDed with any `filters` you pass. Use the [Segments endpoints](/docs/api/segments/list) to find IDs. Public and private-link callers can only use segments marked as shown on the public dashboard.

```
?start_date=2024-01-01&end_date=2024-01-31&time_zone=UTC&segment_id=12
```

#### Filter Object Structure

<TypeTable
  type={{
    parameter: {
      description: 'The dimension to filter on',
      type: 'FilterParameter',
      typeDescription: 'See Filter Parameters table below',
      required: true,
    },
    type: {
      description: 'The type of comparison to perform',
      type: 'FilterType',
      typeDescription: '"equals" | "not_equals" | "contains" | "not_contains" | "regex" | "not_regex" | "greater_than" | "less_than"',
      required: true,
    },
    value: {
      description: 'Array of values to match against',
      type: 'Array<string | number>',
      typeDescription: 'Multiple values are treated as OR conditions',
      required: true,
    },
  }}
/>

#### Filter Types

<TypeTable
  type={{
    equals: {
      description: 'Exact match',
      type: 'string',
      typeDescription: 'Value must exactly match',
    },
    not_equals: {
      description: 'Exclude exact matches',
      type: 'string',
      typeDescription: 'Value must not match',
    },
    contains: {
      description: 'Substring match',
      type: 'string',
      typeDescription: 'Value must contain the substring (uses SQL LIKE %value%)',
    },
    not_contains: {
      description: 'Exclude substring matches',
      type: 'string',
      typeDescription: 'Value must not contain the substring',
    },
    regex: {
      description: 'Regular expression match',
      type: 'string',
      typeDescription: 'Value must match the regex pattern (max 500 characters)',
    },
    not_regex: {
      description: 'Exclude regex matches',
      type: 'string',
      typeDescription: 'Value must not match the regex pattern',
    },
    greater_than: {
      description: 'Numeric greater than',
      type: 'number',
      typeDescription: 'For numeric fields like lat, lon',
    },
    less_than: {
      description: 'Numeric less than',
      type: 'number',
      typeDescription: 'For numeric fields like lat, lon',
    },
  }}
/>

#### Available Filter Parameters

**Browser & Device**

<TypeTable
  type={{
    browser: {
      description: 'Browser name',
      type: 'string',
      typeDescription: 'e.g., "Chrome", "Firefox", "Safari"',
    },
    browser_version: {
      description: 'Browser with version',
      type: 'string',
      typeDescription: 'e.g., "Chrome 120"',
    },
    operating_system: {
      description: 'Operating system name',
      type: 'string',
      typeDescription: 'e.g., "Windows", "macOS", "Linux"',
    },
    operating_system_version: {
      description: 'OS with version',
      type: 'string',
      typeDescription: 'e.g., "Windows 10/11", "macOS 14"',
    },
    device_type: {
      description: 'Device category',
      type: 'string',
      typeDescription: '"desktop", "mobile", "tablet"',
    },
    dimensions: {
      description: 'Screen dimensions',
      type: 'string',
      typeDescription: 'e.g., "1920x1080", "390x844"',
    },
    language: {
      description: 'Browser language',
      type: 'string',
      typeDescription: 'e.g., "en-US", "es-ES"',
    },
  }}
/>

**Location**

<TypeTable
  type={{
    country: {
      description: 'Country code',
      type: 'string',
      typeDescription: 'ISO 3166-1 alpha-2 code, e.g., "US", "GB"',
    },
    region: {
      description: 'Geographic region/state',
      type: 'string',
      typeDescription: 'e.g., "California", "England"',
    },
    city: {
      description: 'City name with region prefix',
      type: 'string',
      typeDescription: 'Format: "Region-City", e.g., "CA-San Francisco"',
    },
    timezone: {
      description: 'User timezone',
      type: 'string',
      typeDescription: 'IANA timezone, e.g., "America/New_York"',
    },
    lat: {
      description: 'Latitude',
      type: 'number',
      typeDescription: 'Supports greater_than, less_than, equals (with 0.001 tolerance)',
    },
    lon: {
      description: 'Longitude',
      type: 'number',
      typeDescription: 'Supports greater_than, less_than, equals (with 0.001 tolerance)',
    },
  }}
/>

**Page & Traffic**

<TypeTable
  type={{
    pathname: {
      description: 'URL pathname',
      type: 'string',
      typeDescription: 'e.g., "/blog/post-title"',
    },
    page_title: {
      description: 'HTML page title',
      type: 'string',
    },
    hostname: {
      description: 'Website hostname',
      type: 'string',
      typeDescription: 'e.g., "example.com"',
    },
    querystring: {
      description: 'URL query string',
      type: 'string',
      typeDescription: 'e.g., "?ref=twitter"',
    },
    referrer: {
      description: 'Referrer domain (without www)',
      type: 'string',
      typeDescription: 'e.g., "google.com", "twitter.com"',
    },
    entry_page: {
      description: 'First page in session (landing page)',
      type: 'string',
    },
    exit_page: {
      description: 'Last page in session',
      type: 'string',
    },
    channel: {
      description: 'Marketing channel',
      type: 'string',
      typeDescription: 'e.g., "Direct", "Organic Search", "Social"',
    },
  }}
/>

**UTM Parameters**

<TypeTable
  type={{
    utm_source: {
      description: 'UTM source parameter',
      type: 'string',
      typeDescription: 'e.g., "google", "newsletter"',
    },
    utm_medium: {
      description: 'UTM medium parameter',
      type: 'string',
      typeDescription: 'e.g., "cpc", "email"',
    },
    utm_campaign: {
      description: 'UTM campaign parameter',
      type: 'string',
    },
    utm_term: {
      description: 'UTM term parameter',
      type: 'string',
    },
    utm_content: {
      description: 'UTM content parameter',
      type: 'string',
    },
  }}
/>

**User & Events**

<TypeTable
  type={{
    user_id: {
      description: 'User identifier',
      type: 'string',
      typeDescription: 'Matches both device fingerprint and custom identified user ID',
    },
    event_name: {
      description: 'Custom event name',
      type: 'string',
      typeDescription: 'Filters to sessions containing the event',
    },
  }}
/>

#### Filter Examples

<Tabs items={['Single Filter', 'Multiple Filters', 'Contains', 'Regex', 'Numeric', 'URL-encoded']}>
  <Tab value="Single Filter">
```json
[
  {
    "parameter": "country",
    "type": "equals",
    "value": ["US"]
  }
]
```
  </Tab>
  <Tab value="Multiple Filters">
Multiple filters use AND logic:
```json
[
  {
    "parameter": "country",
    "type": "equals",
    "value": ["US", "CA"]
  },
  {
    "parameter": "device_type",
    "type": "equals",
    "value": ["mobile"]
  }
]
```
  </Tab>
  <Tab value="Contains">
Substring match:
```json
[
  {
    "parameter": "pathname",
    "type": "contains",
    "value": ["/blog"]
  }
]
```
  </Tab>
  <Tab value="Regex">
Pattern match:
```json
[
  {
    "parameter": "pathname",
    "type": "regex",
    "value": ["^/products/[0-9]+$"]
  }
]
```
  </Tab>
  <Tab value="Numeric">
Geolocation comparison:
```json
[
  {
    "parameter": "lat",
    "type": "greater_than",
    "value": ["37.0"]
  },
  {
    "parameter": "lat",
    "type": "less_than",
    "value": ["38.0"]
  }
]
```
  </Tab>
  <Tab value="URL-encoded">
```
?filters=%5B%7B%22parameter%22%3A%22country%22%2C%22type%22%3A%22equals%22%2C%22value%22%3A%5B%22US%22%5D%7D%5D
```
  </Tab>
</Tabs>

---

## Error Responses

All endpoints return standard HTTP status codes:

- **200** - Success
- **400** - Bad Request (invalid parameters)
- **401** - Unauthorized (missing or invalid API key)
- **403** - Forbidden (no access to site)
- **404** - Not Found (site doesn't exist)
- **429** - Too Many Requests (rate limit exceeded)
- **500** - Internal Server Error

### Error Response Format

```json
{
  "error": "Error message describing what went wrong"
}
```

---

### Content Partials Guides README (`docs/content/partials/guides/README.md`)

# Integration guide standard

Every page under `content/docs/(docs)/guides/` follows `TEMPLATE.mdx` in this folder.
`npm run lint:guides` checks every guide against the rules below and runs in CI.

## Rules

1. **Frontmatter**: `title` is the platform name as the vendor spells it. `description` is
   `Add Rybbit analytics to your <Platform> site` (use `app`, `store`, `docs`, `blog` when
   that is the better noun). `category` is one of the values in `scripts/check-guides.mjs`.
   `icon` is an `@icons-pack/react-simple-icons` export name (`SiWordpress`); omit it when
   the brand has no icon.
2. **Skeleton**: intro paragraph, then exactly three `<Step>`s titled
   `Get your tracking snippet`, `Add the snippet to <Platform>`, `Verify installation`.
   Optional `## Track custom events` and `## Troubleshooting`, then `## Next steps`.
3. **Shared content comes from partials**, never retyped:
   - `get-snippet.mdx` – the canonical snippet and the self-hosting note.
   - `verify.mdx` – the verification checklist.
   - `next-steps.mdx` – cross-links.
   Include with `<include>../../../partials/guides/<name>.mdx</include>` (one more `../`
   from a subfolder such as `react/`). Includes are inlined at build time, so they also
   reach the `.mdx` / `llms.txt` endpoints that AI agents read.
4. **Snippet form**: when a guide must show the tag inline (a template file, a config
   value, a JS injector), use the URL form `script.js?siteId=YOUR_SITE_ID` with `defer`.
   Never the legacy `data-site-id` attribute: script optimisers strip `data-*` attributes.
5. **Only platform-specific content belongs in the guide.** Generic explanations of what
   Rybbit is, how events work, GDPR, ad blockers or proxies are linked, not repeated.
6. **Don't invent UI.** Menu paths and file names must be real. If you cannot confirm
   where custom head code goes on a platform, say what the setting is usually called and
   point to the vendor's own page rather than guessing a path.
7. **Tone**: second person, present tense, imperative steps. No "powerful", "seamless",
   "easily". A guide is typically 40–120 lines; length comes from real platform detail,
   not padding.
8. **Imports**: double quotes, `Steps` before `Callout` before `Tabs`, only what is used.
9. **Sidebar**: add the slug to `guides/meta.json` under the matching `---Category---`
   separator, alphabetically. The index page (`guides/index.mdx`) groups by frontmatter
   `category` automatically.

## Adding a guide

```
cp content/partials/guides/TEMPLATE.mdx "content/docs/(docs)/guides/<slug>.mdx"
# fill it in, then
npm run lint:guides
```

---

### Content Docs (Docs) Guides Index (`docs/content/docs/(docs)/guides/index.mdx`)

---
title: Integration guides
description: Step-by-step instructions for adding Rybbit to every major website platform, framework and CMS
---

Rybbit works on any site that can load a script tag. Each guide below shows exactly where the tracking snippet goes on that platform and how to confirm it is working.

Don't see your platform? Paste the snippet from **Site Settings → Tracking Script** into the `<head>` of every page, or follow the [tracking script](/docs/script) reference.

<GuideIndex />

---

### Content Docs (Docs) Mcp Index (`docs/content/docs/(docs)/mcp/index.mdx`)

---
title: MCP
description: Let an AI assistant explore and manage your Rybbit analytics through the hosted Model Context Protocol server
---

import { Callout } from "fumadocs-ui/components/callout";
import { Cards, Card } from "fumadocs-ui/components/card";
import { SiClaude } from "@icons-pack/react-simple-icons";

Rybbit includes a hosted [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) endpoint. It gives an AI assistant read and write access to your analytics: exploring traffic and behavior data, and managing sites, goals, funnels, organization members, teams, and user profiles — through the same API the dashboard uses, with the same access checks, roles, and rate limits.

Your MCP client connects to Rybbit over HTTPS and sends your API key with each protocol request. Rybbit verifies the key before processing any message, and every tool call is authorized against the key user's role and site access.

## Connect a client

If your client supports the MCP authorization flow — Claude Code, Codex, Claude Desktop, and opencode all do — you can [connect with OAuth](#or-connect-with-oauth) and skip API keys entirely. The steps below describe the API-key path, which works in every client and supports scoped credentials.

### 1. Create an API key

1. Open **Settings → Account** in Rybbit.
2. Create a key in the **Personal API Keys** section.
3. Copy it immediately. Rybbit only shows a new key once.

A key created without permissions can do everything its user can do. To hand an agent a narrower credential, create the key with scoped permissions (`POST /api/user/api-keys` with a `permissions` object, e.g. `{"analytics": ["read"], "goals": ["read", "write"]}`): the MCP tool list and every API call are then limited to those `resource:action` scopes. `write` implies `read` on the same resource, and scopes never elevate — org admin/owner requirements still apply. Deleting the key revokes the connection.

### 2. Add the endpoint

Use this Streamable HTTP URL for Rybbit Cloud:

```text
https://app.rybbit.io/api/mcp
```

For a self-hosted installation, replace the host with your configured `BASE_URL`:

```text
https://analytics.example.com/api/mcp
```

Configure your client to send this header on every request:

```text
Authorization: Bearer <RYBBIT_API_KEY>
```

MCP clients use different configuration formats. A typical remote-server configuration has this shape:

```json
{
  "mcpServers": {
    "rybbit": {
      "url": "https://app.rybbit.io/api/mcp",
      "headers": {
        "Authorization": "Bearer <RYBBIT_API_KEY>"
      }
    }
  }
}
```

Use your client's secret or environment-variable support when available instead of saving the key in a file. Query-string API keys are not accepted by the MCP endpoint. Per-client walkthroughs — Claude Code, Codex, Claude Desktop, Cursor, VS Code, opencode — are in [Client guides](#client-guides) below.

### Or connect with OAuth

Clients that support the MCP authorization flow can skip API keys entirely: add the endpoint URL with no headers, and the client discovers Rybbit's authorization server through the standard well-known metadata (`/.well-known/oauth-protected-resource`), registers itself, and opens a browser window for you to log in and approve access. The resulting access token acts with your user's role, exactly like an API key would.

OAuth grants support the same `resource:action` scopes as API keys (advertised in `scopes_supported`). A grant that requests only the standard OIDC scopes is unrestricted; a grant that requests custom scopes like `analytics:read` is limited to them. The requesting client controls which scopes it asks for — consent is currently approve-or-deny as a whole.

### 3. Start with context

Ask the assistant to call `list_sites`, or prompt it with something like:

> List my Rybbit sites, summarize last month's traffic for the docs site, and create a goal for signups.

`list_sites` returns the numeric `site_id` and `organization_id` required by the other tools, plus the key's role in each organization. It does not return API keys or member email addresses.

## Client guides

Coding agents can use the endpoint to pull analytics into your development workflow — checking whether an error you just fixed still occurs in production, or looking up which pages matter before a refactor — and chat clients like Claude Desktop turn it into a conversational analytics interface. Follow the guide for your client:

<Cards>
  <Card
    icon={<SiClaude />}
    title="Claude Code"
    description="One command with OAuth or an API key, plus a no-secrets team setup"
    href="/docs/mcp/claude-code"
  />
  <Card
    title="Codex"
    description="codex mcp add, then OAuth login or a key from the environment"
    href="/docs/mcp/codex"
  />
  <Card
    icon={<SiClaude />}
    title="Claude Desktop"
    description="Add a custom connector on claude.ai, desktop, and mobile"
    href="/docs/mcp/claude-desktop"
  />
  <Card
    title="Cursor"
    description="mcp.json with an API key header"
    href="/docs/mcp/cursor"
  />
  <Card
    title="VS Code"
    description="Copilot agent mode, with the key kept out of the config file"
    href="/docs/mcp/vscode"
  />
  <Card
    title="opencode"
    description="Remote server config with OAuth login or env-based headers"
    href="/docs/mcp/opencode"
  />
</Cards>

Any other MCP client that supports Streamable HTTP works with the generic configuration from [step 2](#2-add-the-endpoint).

## Available tools

### Analytics (read)

| Tool                      | Returns                                                                                 |
| ------------------------- | --------------------------------------------------------------------------------------- |
| `get_overview`            | Sessions, pageviews, users, pages per session, bounce rate, and session duration        |
| `get_overview_timeseries` | The overview KPIs bucketed over time                                                    |
| `get_breakdown`           | Sessions broken down by one dimension (pages, referrers, countries, devices, UTM, ...)  |
| `get_live_stats`          | Visitors active on the site right now                                                   |
| `get_event_names`         | Custom event names tracked on the site with counts                                      |
| `get_errors`              | JavaScript errors grouped by name/message with occurrence counts                        |
| `get_web_vitals`          | Core Web Vitals percentiles (LCP, CLS, INP, FCP, TTFB)                                  |
| `get_retention`           | User retention cohort table                                                             |
| `get_journeys`            | Most common page-to-page navigation paths                                               |
| `get_annotations`         | Notes pinned to dates on the traffic chart (launches, deploys, campaigns, outages)      |
| `list_segments`           | Saved segments: named, reusable filter sets for a site or the whole organization        |
| `apply_segment`           | Overview KPIs for visitors matching a saved segment, with its filters for reuse         |

### Search Console (read)

| Tool                        | Returns                                                                                          |
| --------------------------- | ------------------------------------------------------------------------------------------------ |
| `get_search_console_status` | Whether Google Search Console is connected for the site, and which property                      |
| `get_search_console_data`   | Clicks, impressions, CTR, and average position grouped by query, page, country, device, or date  |

Search Console tools read the Google Search Console property connected to the site under **Site settings > Integrations** (Rybbit Cloud); no Google Cloud project or separate MCP server is needed. `get_search_console_data` groups by up to three dimensions (`query`, `page`, `country`, `device`, `date`), accepts ANDed filters such as `{"dimension": "page", "operator": "contains", "expression": "/pricing"}`, and returns 100 rows by default (up to 25,000, paged with `start_row`). Google's numbers lag about two days; pass `data_state: "all"` to include fresh, still-changing data. For how this compares with self-run Search Console servers, and prompts that work well, see the [Google Search Console MCP guide](/blog/google-search-console-mcp).

### Sites

| Tool                 | Does                                                                                    |
| -------------------- | ---------------------------------------------------------------------------------------- |
| `list_sites`         | List organizations and sites the key can access (call this first)                       |
| `get_site`           | One site's full configuration                                                           |
| `create_site`        | Add a new site to an organization *(admin)*                                             |
| `update_site_config` | Change site settings: name, domain, tracking features, exclusions, tags *(editor)*      |
| `delete_site`        | Permanently delete a site and its data *(admin, destructive)*                           |

### Goals & funnels

| Tool             | Does                                                                  |
| ---------------- | ---------------------------------------------------------------------- |
| `get_goals`      | Conversion goals with conversion stats                                |
| `create_goal`    | Create a path, event, or autocapture goal                             |
| `update_goal`    | Replace a goal's definition                                           |
| `delete_goal`    | Permanently delete a goal *(destructive)*                             |
| `get_funnels`    | Saved funnel definitions                                              |
| `analyze_funnel` | Compute an ad-hoc funnel without saving it                            |
| `save_funnel`    | Save a funnel, or update one by passing `funnel_id`                   |
| `delete_funnel`  | Permanently delete a saved funnel *(destructive)*                     |

### People

| Tool                 | Does                                                                             |
| -------------------- | --------------------------------------------------------------------------------- |
| `get_users`          | Person inventory with per-user aggregates and traits (sortable, searchable)      |
| `get_user`           | One person's profile: traits, linked devices, vitals, locations                  |
| `identify_user`      | Link an anonymous device to your user ID and merge traits                        |
| `update_user_traits` | Replace a person's traits wholesale                                              |
| `delete_user`        | GDPR erasure of one person's analytics data *(editor, destructive)*              |

### Organization & teams

| Tool                        | Does                                                              |
| --------------------------- | ------------------------------------------------------------------ |
| `list_members`              | Organization members with roles, site access, and teams          |
| `add_member`                | Add an existing Rybbit user to the organization *(admin)*        |
| `update_member_site_access` | Restrict a member to specific sites *(admin)*                    |
| `list_teams`                | Teams with members and site access                               |
| `create_team`               | Create a team *(admin)*                                          |
| `update_team`               | Rename a team or replace its members/sites *(admin)*             |
| `delete_team`               | Permanently delete a team *(admin, destructive)*                 |

### Raw data & SQL

| Tool               | Does                                                                          |
| ------------------ | ------------------------------------------------------------------------------ |
| `get_sessions`     | Recent visitor sessions with full attribution                                 |
| `get_session`      | One session's detail plus its event timeline                                  |
| `get_events`       | Raw recent events, newest first                                               |
| `get_query_schema` | The ClickHouse schema and rules for `run_query`                               |
| `run_query`        | Read-only ClickHouse SQL against the site-scoped `scoped_events` table        |

Analytics tools accept optional time inputs (`start_date`/`end_date` with an IANA `time_zone`, or `past_minutes`) and the same filters as the dashboard. Omitting time inputs queries all time; an unrecognized `time_zone` is rejected before the call is made. `get_sessions`, `get_events`, and `get_users` accept an optional `fields` list to return only the named row columns, which keeps large result sets small.

## Permissions and roles

The MCP surface mirrors the REST API's permission model:

- **Read tools** work for any role with access to the site, including viewers.
- **Write tools** need the member role or above on the site (goals, funnels, `identify_user`, `update_user_traits`, `save_funnel`).
- **Tools marked *(editor)*** need the editor role or above on the site.
- **Tools marked *(admin)*** require the API key's user to hold the **admin or owner** role in the organization. `add_member` can't grant a role above the key's own, so only an owner's key can grant `owner`.

A key acts with its user's role on each site, including [site-specific roles](/docs/organization-settings#member-roles) from site restrictions and teams, and it reaches exactly the sites that user can see. The REST layer enforces all of this: an underprivileged key gets a 403 `"Insufficient role"` naming the role required.

To give an assistant a read-only or read-mostly view, create a **scoped** API key (see step 1): grant only the `resource:action` scopes it needs — e.g. `{"analytics": ["read"], "sessions": ["read"]}` for a reporting agent. The MCP tool list is filtered to those scopes, and any out-of-scope call returns `403 { "error": "Insufficient scope", "required": "goals:write" }`. Alternatively, connect with a viewer's key: it hides nothing but refuses every write.

OAuth clients request scopes through the standard grant. Rybbit advertises the same `resource:action` scopes in its discovery metadata; a grant carrying no custom scopes (or only the standard OIDC scopes) is treated as full access, for backward compatibility.

## Destructive tools

`delete_goal`, `delete_funnel`, `delete_site`, `delete_team`, and `delete_user` permanently destroy data and are marked with the MCP `destructiveHint` annotation, so well-behaved clients ask for confirmation before invoking them. `delete_site` and `delete_user` (the GDPR-erasure path) also remove recorded events and replays, with erasure completing asynchronously.

## Data and security boundary

- The API key is verified before any MCP message is processed, and each tool call re-runs the corresponding REST route's own access checks, role requirements, and rate limits.
- The surface returns whatever the REST API returns for your role — including session-level records and, where a site has IP tracking enabled, visitor IP addresses. Connect only AI clients you trust with that data, and use a separate, revocable API key per integration.
- Text originating in tracked traffic (page titles, paths, referrers, event names, traits) is stripped of control and bidi-override characters. AI clients should still treat these values as untrusted data, never as instructions.
- `run_query` is restricted to a read-only, site-scoped table with row and execution-time caps.
- The server does not contain an AI model or send analytics to an AI provider. Data only leaves Rybbit in response to the MCP client you configure.
- The endpoint is stateless. Each HTTP request is independently authenticated.

## Troubleshooting

- **401 Unauthorized:** The client did not send a Bearer credential, or the key/token is invalid, expired, or revoked. OAuth-capable clients re-authenticate automatically via the `WWW-Authenticate` challenge.
- **OAuth login loop or "table does not exist" (self-hosted):** Run `npm run db:push` in `server/` to create the OAuth tables, then retry.
- **403 in tool results:** The key's user lacks access to that site or organization, or holds too low a role for the tool (`"Insufficient role"` names the role required). Check the `role` field in `list_sites`.
- **403 "Insufficient scope":** The credential was created without the `resource:action` scope this tool needs (the `required` field names it). Use a key or OAuth grant that includes it.
- **429 Too Many Requests:** The API key exceeded its plan's rate limit.
- **404 "GSC not connected" from Search Console tools:** Connect Google Search Console for that site under Site settings > Integrations, then retry.
- **503 Service Unavailable:** Rybbit could not verify the key. Retry the request; do not replace a known-valid key based on this response.
- **405 Method Not Allowed on GET:** This is expected. The stateless endpoint accepts MCP messages over `POST`.
- **Site access error:** Call `list_sites` again. Membership or site permissions may have changed.

---

### Content Docs (Docs) Proxy Guide Index (`docs/content/docs/(docs)/proxy-guide/index.mdx`)

---
title: Proxy Setup Guide
description: Bypass ad blockers and improve tracking accuracy by proxying Rybbit through your own domain
---

import { Cards, Card } from 'fumadocs-ui/components/card';
import { Callout } from 'fumadocs-ui/components/callout';
import {
  SiAstro,
  SiCloudflare,
  SiExpress,
  SiLaravel,
  SiNextdotjs,
  SiNginx,
  SiVercel
} from '@icons-pack/react-simple-icons';

Proxying Rybbit's tracking script through your own domain helps bypass ad blockers and improves tracking accuracy by making all analytics requests appear as first-party traffic.

## What is Proxy Setup?

When you add Rybbit's tracking script directly to your website using the default installation:

```html
<script src="https://app.rybbit.io/api/script.js" async data-site-id="YOUR_SITE_ID"></script>
```

The browser loads the script from `app.rybbit.io`, which ad blockers can identify and block as third-party analytics. Even though Rybbit is privacy-focused and doesn't use cookies, some ad blockers use aggressive blocking rules that target any analytics domains.

**Proxying solves this** by serving the tracking script and handling tracking requests through your own domain (e.g., `yourdomain.com/analytics/script.js`), making all tracking appear as first-party traffic.

<Callout type="info">
Rybbit is fully compliant with GDPR, CCPA, and other privacy regulations. It uses cookieless tracking based on IP + User Agent hashing with optional daily rotating salts. Proxying is about improving data accuracy, not circumventing privacy laws.
</Callout>

## Why Proxy Rybbit?

### Improve Tracking Accuracy

Ad blockers are becoming increasingly aggressive and may block analytics scripts regardless of their privacy practices. By proxying through your own domain:

- **Bypass ad blocker detection**: Requests appear as first-party traffic to your own domain
- **Increase data completeness**: Typical improvement of 10-30% in tracked sessions
- **Maintain accurate metrics**: Get a true picture of your website's performance

### Stay Privacy-Compliant

Proxying doesn't compromise user privacy:

- No cookies are used (Rybbit remains cookieless)
- User data is still processed according to your privacy policy
- GDPR, CCPA, and other privacy regulations remain fully satisfied
- Users can still opt out through browser settings or Do Not Track

### Better Performance (Optional)

With proper caching configuration, proxying can also improve performance:

- Serve scripts from your CDN or edge network
- Reduce DNS lookups (one less domain to resolve)
- Cache static scripts closer to your users

## How It Works

Here's the flow when you proxy Rybbit:

1. **User visits your website**: Your HTML includes the tracking script from your domain
   ```html
   <script src="https://yourdomain.com/analytics/script.js" data-site-id="123"></script>
   ```

2. **Script loads from your domain**: Your server proxies the request to Rybbit's servers
   - Request: `yourdomain.com/analytics/script.js` → proxied to → `app.rybbit.io/api/script.js`

3. **Script auto-discovers analytics host**: The Rybbit script automatically detects the domain it was loaded from
   ```typescript
   // The script extracts the host from its own src attribute
   const analyticsHost = src.split("/script.js")[0];
   // Result: "https://yourdomain.com/analytics"
   ```

4. **Tracking data sent through your domain**: All analytics events are sent to your domain and proxied to Rybbit
   - Event tracking: `yourdomain.com/analytics/track` → proxied to → `app.rybbit.io/api/track`
   - Session replay: `yourdomain.com/analytics/session-replay/record/{siteId}` → proxied
   - Configuration: `yourdomain.com/analytics/site/tracking-config/{siteId}` → proxied

5. **Data appears in your Rybbit dashboard**: No changes needed in the Rybbit interface

<Callout type="warn">
**Bandwidth Consideration**: All tracking requests will flow through your server infrastructure, which will increase bandwidth usage. For most sites this is minimal (typically 10-50KB per session), but consider this when evaluating hosting costs.
</Callout>

## Choose Your Framework

Select the framework or platform that matches your setup:

<Cards>
  <Card
    icon={<SiNextdotjs />}
    title="Next.js"
    href="/docs/proxy-guide/next-js"
    className="flex items-center gap-2"
  />
  <Card
    icon={<SiNginx />}
    title="Nginx"
    href="/docs/proxy-guide/nginx"
    className="flex items-center gap-2"
  />
  <Card
    title="Caddy"
    href="/docs/proxy-guide/caddy"
    className="flex items-center gap-2"
  />
  <Card
    icon={<SiVercel />}
    title="Vercel"
    href="/docs/proxy-guide/vercel"
    className="flex items-center gap-2"
  />
  <Card
    icon={<SiExpress />}
    title="Express.js"
    href="/docs/proxy-guide/express"
    className="flex items-center gap-2"
  />
  <Card
    icon={<SiLaravel />}
    title="Laravel"
    href="/docs/proxy-guide/laravel"
    className="flex items-center gap-2"
  />
  <Card
    icon={<SiAstro />}
    title="Astro"
    href="/docs/proxy-guide/astro"
    className="flex items-center gap-2"
  />
  <Card
    icon={<SiCloudflare />}
    title="Cloudflare Workers"
    href="/docs/proxy-guide/cloudflare-workers"
    className="flex items-center gap-2"
  />
</Cards>

## Generic Implementation

If your framework isn't listed above, check out our [generic implementation guide](/docs/proxy-guide/get-started) that covers the core concepts applicable to any reverse proxy or server setup.

## Need Help?

If you encounter issues during setup, check our [troubleshooting guide](/docs/proxy-guide/troubleshooting) for common problems and solutions.

---

### Content Docs Api Bots Overview (`docs/content/docs/api/bots/overview.mdx`)

---
title: Get Bot Overview
description: Returns aggregate bot-traffic metrics with a breakdown by detection layer.
full: true
method: GET
---

import { TypeTable } from 'fumadocs-ui/components/type-table';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';

<div className="grid grid-cols-1 xl:grid-cols-2 gap-8">
<div>

```
GET /api/sites/:site/bots/overview
```

Returns aggregate bot-traffic metrics with a breakdown by detection layer.

### Path Parameters

<TypeTable
  type={{
    site: {
      description: 'Site ID or site identifier',
      type: 'string | number',
      required: true,
    },
  }}
/>

### Query Parameters

Accepts all [Common Parameters](/docs/api/getting-started#common-parameters) (time and filters).

Bot filtering supports a subset of filter parameters: `browser`, `browser_version`, `operating_system`, `operating_system_version`, `country`, `region`, `city`, `device_type`, `referrer`, `hostname`, `pathname`, `querystring`, `dimensions`, `user_id`, `lat`, and `lon`.

<TypeTable
  type={{
    layer: {
      description: 'Restrict results to a single detection layer',
      type: 'string',
      typeDescription: '"ua_pattern" | "header_heuristics" | "client_signals" | "bot_asn" | "rate_anomaly"',
      required: false,
    },
  }}
/>

### Response

<TypeTable
  type={{
    bot_requests: {
      description: 'Total bot requests',
      type: 'number',
    },
    total_events: {
      description: 'Total requests (bots + humans)',
      type: 'number',
    },
    bot_percentage: {
      description: 'Bot requests as a percentage of all requests',
      type: 'number',
      typeDescription: 'Range: 0-100',
    },
    ai_requests: {
      description: 'Requests from named AI systems: training crawlers, answer-engine indexers, and user-triggered agents',
      type: 'number',
    },
    ai_agent_requests: {
      description: 'Requests from user-triggered AI agents (a person asked an assistant to open the page)',
      type: 'number',
    },
    ai_crawler_requests: {
      description: 'Requests from AI training crawlers and answer-engine indexers',
      type: 'number',
    },
    ua_pattern: {
      description: 'Bot requests flagged by the user-agent pattern layer',
      type: 'number',
    },
    header_heuristics: {
      description: 'Bot requests flagged by the header-heuristics layer',
      type: 'number',
    },
    client_signals: {
      description: 'Bot requests flagged by the client-signals layer',
      type: 'number',
    },
    bot_asn: {
      description: 'Bot requests flagged by the known-bot-ASN layer',
      type: 'number',
    },
    rate_anomaly: {
      description: 'Bot requests flagged by the rate-anomaly layer',
      type: 'number',
    },
  }}
/>

</div>
<div className="flex flex-col gap-4">

<Tabs items={['cURL', 'JavaScript', 'Python', 'PHP', 'Ruby', 'Go', 'Rust', 'Java', '.NET']}>
  <Tab value="cURL">
```bash title="Request"
curl -X GET "https://app.rybbit.io/api/sites/123/bots/overview?start_date=2024-01-01&end_date=2024-01-31&time_zone=America/New_York" \
  -H "Authorization: Bearer your_api_key_here"
```
  </Tab>
  <Tab value="JavaScript">
```javascript title="Request"
const response = await fetch(
  'https://app.rybbit.io/api/sites/123/bots/overview?start_date=2024-01-01&end_date=2024-01-31&time_zone=America/New_York',
  {
    headers: {
      'Authorization': 'Bearer your_api_key_here'
    }
  }
);

const data = await response.json();
```
  </Tab>
  <Tab value="Python">
```python title="Request"
import requests

response = requests.get(
    'https://app.rybbit.io/api/sites/123/bots/overview',
    params={
        'start_date': '2024-01-01',
        'end_date': '2024-01-31',
        'time_zone': 'America/New_York'
    },
    headers={
        'Authorization': 'Bearer your_api_key_here'
    }
)

data = response.json()
```
  </Tab>
  <Tab value="PHP">
```php title="Request"
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://app.rybbit.io/api/sites/123/bots/overview?start_date=2024-01-01&end_date=2024-01-31&time_zone=America/New_York');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer your_api_key_here'
]);

$response = curl_exec($ch);
curl_close($ch);

$data = json_decode($response, true);
```
  </Tab>
  <Tab value="Ruby">
```ruby title="Request"
require 'net/http'
require 'json'

uri = URI('https://app.rybbit.io/api/sites/123/bots/overview?start_date=2024-01-01&end_date=2024-01-31&time_zone=America/New_York')
req = Net::HTTP::Get.new(uri)
req['Authorization'] = 'Bearer your_api_key_here'

res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)
```
  </Tab>
  <Tab value="Go">
```go title="Request"
req, _ := http.NewRequest("GET", "https://app.rybbit.io/api/sites/123/bots/overview?start_date=2024-01-01&end_date=2024-01-31&time_zone=America/New_York", nil)
req.Header.Set("Authorization", "Bearer your_api_key_here")

client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()

var data map[string]interface{}
json.NewDecoder(resp.Body).Decode(&data)
```
  </Tab>
  <Tab value="Rust">
```rust title="Request"
let client = reqwest::Client::new();
let res = client
    .get("https://app.rybbit.io/api/sites/123/bots/overview?start_date=2024-01-01&end_date=2024-01-31&time_zone=America/New_York")
    .header("Authorization", "Bearer your_api_key_here")
    .send()
    .await?;

let data: serde_json::Value = res.json().await?;
```
  </Tab>
  <Tab value="Java">
```java title="Request"
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://app.rybbit.io/api/sites/123/bots/overview?start_date=2024-01-01&end_date=2024-01-31&time_zone=America/New_York"))
    .header("Authorization", "Bearer your_api_key_here")
    .GET()
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
```
  </Tab>
  <Tab value=".NET">
```csharp title="Request"
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer your_api_key_here");

var response = await client.GetAsync("https://app.rybbit.io/api/sites/123/bots/overview?start_date=2024-01-01&end_date=2024-01-31&time_zone=America/New_York");
var data = await response.Content.ReadAsStringAsync();
```
  </Tab>
</Tabs>

```json title="Response"
{
  "data": {
    "bot_requests": 1840,
    "total_events": 48933,
    "bot_percentage": 3.76,
    "ai_requests": 940,
    "ai_agent_requests": 180,
    "ai_crawler_requests": 760,
    "ua_pattern": 1200,
    "header_heuristics": 410,
    "client_signals": 95,
    "bot_asn": 120,
    "rate_anomaly": 15
  }
}
```

</div>
</div>

---

### Content Docs Api Overview Overview (`docs/content/docs/api/overview/overview.mdx`)

---
title: Get Overview
description: Returns high-level analytics metrics for a site.
full: true
method: GET
---

import { TypeTable } from 'fumadocs-ui/components/type-table';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';

<div className="grid grid-cols-1 xl:grid-cols-2 gap-8">
<div>

```
GET /api/sites/:site/overview
```

Returns high-level analytics metrics for a site.

### Path Parameters

<TypeTable
  type={{
    site: {
      description: 'Site ID or site identifier',
      type: 'string | number',
      required: true,
    },
  }}
/>

### Query Parameters

Accepts all [Common Parameters](/docs/api/getting-started#common-parameters) (time and filters).

### Response

<TypeTable
  type={{
    sessions: {
      description: 'Total number of sessions',
      type: 'number',
    },
    pageviews: {
      description: 'Total number of pageviews',
      type: 'number',
    },
    users: {
      description: 'Total unique users',
      type: 'number',
    },
    pages_per_session: {
      description: 'Average pages viewed per session',
      type: 'number',
    },
    bounce_rate: {
      description: 'Percentage of single-page sessions',
      type: 'number',
      typeDescription: 'Range: 0-100',
    },
    session_duration: {
      description: 'Average session duration in seconds',
      type: 'number',
    },
  }}
/>

</div>
<div className="flex flex-col gap-4">

<Tabs items={['cURL', 'JavaScript', 'Python', 'PHP', 'Ruby', 'Go', 'Rust', 'Java', '.NET']}>
  <Tab value="cURL">
```bash title="Request"
curl -X GET "https://app.rybbit.io/api/sites/123/overview?start_date=2024-01-01&end_date=2024-01-31&time_zone=America/New_York" \
  -H "Authorization: Bearer your_api_key_here"
```
  </Tab>
  <Tab value="JavaScript">
```javascript title="Request"
const response = await fetch(
  'https://app.rybbit.io/api/sites/123/overview?start_date=2024-01-01&end_date=2024-01-31&time_zone=America/New_York',
  {
    headers: {
      'Authorization': 'Bearer your_api_key_here'
    }
  }
);

const data = await response.json();
```
  </Tab>
  <Tab value="Python">
```python title="Request"
import requests

response = requests.get(
    'https://app.rybbit.io/api/sites/123/overview',
    params={
        'start_date': '2024-01-01',
        'end_date': '2024-01-31',
        'time_zone': 'America/New_York'
    },
    headers={
        'Authorization': 'Bearer your_api_key_here'
    }
)

data = response.json()
```
  </Tab>
  <Tab value="PHP">
```php title="Request"
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://app.rybbit.io/api/sites/123/overview?start_date=2024-01-01&end_date=2024-01-31&time_zone=America/New_York');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer your_api_key_here'
]);

$response = curl_exec($ch);
curl_close($ch);

$data = json_decode($response, true);
```
  </Tab>
  <Tab value="Ruby">
```ruby title="Request"
require 'net/http'
require 'json'

uri = URI('https://app.rybbit.io/api/sites/123/overview?start_date=2024-01-01&end_date=2024-01-31&time_zone=America/New_York')
req = Net::HTTP::Get.new(uri)
req['Authorization'] = 'Bearer your_api_key_here'

res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)
```
  </Tab>
  <Tab value="Go">
```go title="Request"
req, _ := http.NewRequest("GET", "https://app.rybbit.io/api/sites/123/overview?start_date=2024-01-01&end_date=2024-01-31&time_zone=America/New_York", nil)
req.Header.Set("Authorization", "Bearer your_api_key_here")

client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()

var data map[string]interface{}
json.NewDecoder(resp.Body).Decode(&data)
```
  </Tab>
  <Tab value="Rust">
```rust title="Request"
let client = reqwest::Client::new();
let res = client
    .get("https://app.rybbit.io/api/sites/123/overview?start_date=2024-01-01&end_date=2024-01-31&time_zone=America/New_York")
    .header("Authorization", "Bearer your_api_key_here")
    .send()
    .await?;

let data: serde_json::Value = res.json().await?;
```
  </Tab>
  <Tab value="Java">
```java title="Request"
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://app.rybbit.io/api/sites/123/overview?start_date=2024-01-01&end_date=2024-01-31&time_zone=America/New_York"))
    .header("Authorization", "Bearer your_api_key_here")
    .GET()
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
```
  </Tab>
  <Tab value=".NET">
```csharp title="Request"
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer your_api_key_here");

var response = await client.GetAsync("https://app.rybbit.io/api/sites/123/overview?start_date=2024-01-01&end_date=2024-01-31&time_zone=America/New_York");
var data = await response.Content.ReadAsStringAsync();
```
  </Tab>
</Tabs>

```json title="Response"
{
  "data": {
    "sessions": 15420,
    "pageviews": 48933,
    "users": 12847,
    "pages_per_session": 3.17,
    "bounce_rate": 42.8,
    "session_duration": 182.5
  }
}
```

</div>
</div>

---

### Content Docs Api Performance Overview (`docs/content/docs/api/performance/overview.mdx`)

---
title: Get Performance Overview
description: Returns aggregate performance metrics with percentile breakdowns for all Core Web Vitals.
full: true
method: GET
---

import { TypeTable } from 'fumadocs-ui/components/type-table';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';

<div className="grid grid-cols-1 xl:grid-cols-2 gap-8">
<div>

```
GET /api/sites/:site/performance/overview
```

Returns aggregate performance metrics with percentile breakdowns for all Core Web Vitals.

### Path Parameters

<TypeTable
  type={{
    site: {
      description: 'Site ID or site identifier',
      type: 'string | number',
      required: true,
    },
  }}
/>

### Query Parameters

Accepts all [Common Parameters](/docs/api/getting-started#common-parameters) (time and filters).

### Response

<TypeTable
  type={{
    data: {
      description: 'Performance metrics object',
      type: 'PerformanceMetrics',
    },
  }}
/>

#### PerformanceMetrics Object

Each metric (lcp, cls, inp, fcp, ttfb) includes the following percentiles:

<TypeTable
  type={{
    lcp_p50: {
      description: 'LCP 50th percentile (median)',
      type: 'number',
      typeDescription: 'milliseconds',
    },
    lcp_p75: {
      description: 'LCP 75th percentile',
      type: 'number',
      typeDescription: 'milliseconds',
    },
    lcp_p90: {
      description: 'LCP 90th percentile',
      type: 'number',
      typeDescription: 'milliseconds',
    },
    lcp_p99: {
      description: 'LCP 99th percentile',
      type: 'number',
      typeDescription: 'milliseconds',
    },
    total_performance_events: {
      description: 'Total number of performance measurements',
      type: 'number',
    },
  }}
/>

</div>
<div className="flex flex-col gap-4">

<Tabs items={['cURL', 'JavaScript', 'Python', 'PHP', 'Ruby', 'Go', 'Rust', 'Java', '.NET']}>
  <Tab value="cURL">
```bash title="Request"
curl -X GET "https://app.rybbit.io/api/sites/1/performance/overview?start_date=2024-01-01&end_date=2024-01-31" \
  -H "Authorization: Bearer your_api_key_here"
```
  </Tab>
  <Tab value="JavaScript">
```javascript title="Request"
const response = await fetch(
  'https://app.rybbit.io/api/sites/1/performance/overview?start_date=2024-01-01&end_date=2024-01-31',
  {
    headers: {
      'Authorization': 'Bearer your_api_key_here'
    }
  }
);

const data = await response.json();
```
  </Tab>
  <Tab value="Python">
```python title="Request"
import requests

response = requests.get(
    'https://app.rybbit.io/api/sites/1/performance/overview',
    params={
        'start_date': '2024-01-01',
        'end_date': '2024-01-31'
    },
    headers={
        'Authorization': 'Bearer your_api_key_here'
    }
)

data = response.json()
```
  </Tab>
  <Tab value="PHP">
```php title="Request"
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://app.rybbit.io/api/sites/1/performance/overview?start_date=2024-01-01&end_date=2024-01-31');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer your_api_key_here'
]);

$response = curl_exec($ch);
curl_close($ch);

$data = json_decode($response, true);
```
  </Tab>
  <Tab value="Ruby">
```ruby title="Request"
require 'net/http'
require 'json'

uri = URI('https://app.rybbit.io/api/sites/1/performance/overview?start_date=2024-01-01&end_date=2024-01-31')
req = Net::HTTP::Get.new(uri)
req['Authorization'] = 'Bearer your_api_key_here'

res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)
```
  </Tab>
  <Tab value="Go">
```go title="Request"
req, _ := http.NewRequest("GET", "https://app.rybbit.io/api/sites/1/performance/overview?start_date=2024-01-01&end_date=2024-01-31", nil)
req.Header.Set("Authorization", "Bearer your_api_key_here")

client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()

var data map[string]interface{}
json.NewDecoder(resp.Body).Decode(&data)
```
  </Tab>
  <Tab value="Rust">
```rust title="Request"
let client = reqwest::Client::new();
let res = client
    .get("https://app.rybbit.io/api/sites/1/performance/overview?start_date=2024-01-01&end_date=2024-01-31")
    .header("Authorization", "Bearer your_api_key_here")
    .send()
    .await?;

let data: serde_json::Value = res.json().await?;
```
  </Tab>
  <Tab value="Java">
```java title="Request"
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://app.rybbit.io/api/sites/1/performance/overview?start_date=2024-01-01&end_date=2024-01-31"))
    .header("Authorization", "Bearer your_api_key_here")
    .GET()
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
```
  </Tab>
  <Tab value=".NET">
```csharp title="Request"
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer your_api_key_here");

var response = await client.GetAsync("https://app.rybbit.io/api/sites/1/performance/overview?start_date=2024-01-01&end_date=2024-01-31");
var data = await response.Content.ReadAsStringAsync();
```
  </Tab>
</Tabs>

```json title="Response"
{
  "data": {
    "lcp_p50": 1850,
    "lcp_p75": 2400,
    "lcp_p90": 3200,
    "lcp_p99": 5100,
    "cls_p50": 0.05,
    "cls_p75": 0.1,
    "cls_p90": 0.18,
    "cls_p99": 0.35,
    "inp_p50": 120,
    "inp_p75": 180,
    "inp_p90": 280,
    "inp_p99": 450,
    "fcp_p50": 1200,
    "fcp_p75": 1600,
    "fcp_p90": 2100,
    "fcp_p99": 3500,
    "ttfb_p50": 450,
    "ttfb_p75": 650,
    "ttfb_p90": 950,
    "ttfb_p99": 1800,
    "total_performance_events": 48521
  }
}
```

</div>
</div>

---

### CLAUDE (`docs/CLAUDE.md`)

# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

- Dev: `npm run dev` (Next.js with Turbopack on port 3003)
- Build: `npm run build`
- Production: `npm start`
- Type Check: `tsc --noEmit`
- Lint integration guides: `npm run lint:guides` (rules in `content/partials/guides/README.md`; copy `TEMPLATE.mdx` for a new guide)
## Project Overview

This is the documentation and marketing website for Rybbit, built with Next.js 16 and Fumadocs. The site includes:

- Documentation pages (using Fumadocs MDX)
- Blog (separate content collection)
- Marketing pages (home, pricing, features, comparison pages)
- Analytics tools (SEO, ROI calculators, privacy policy builder, etc.)
- LLM-friendly documentation endpoints
- The domain is rybbit.com

## Code Architecture

### Content Management

- **Fumadocs MDX**: Documentation system using `fumadocs-mdx` for MDX processing
- **Source Configuration**: `source.config.ts` defines two content collections:
  - `docs`: Documentation pages in `content/docs/`
  - `blog`: Blog posts in `content/blog/` with extended frontmatter (date, author, image, tags)
- **Source Adapters**:
  - `src/lib/source.ts`: Docs source loader (base URL: `/docs`)
  - `src/lib/blog-source.ts`: Blog source loader (base URL: `/blog`)
- **Auto-generated**: `.source/` directory contains generated TypeScript from MDX files

### Route Structure

- `app/(home)/*`: Marketing pages (landing, pricing, features, comparison pages, tools)
- `app/docs/[[...slug]]/`: Documentation pages using catch-all routing
- `app/blog/`: Blog listing and individual posts
- `app/api/search/`: Documentation search powered by Orama
- `app/api/tools/`: API endpoints for tool functionality (SEO generators, analytics detector, etc.)
- `app/llms.mdx/` and `app/llms-full.txt/`: LLM-optimized documentation endpoints

### Internationalization (i18n)

- **Library**: `next-intl` v4 with experimental extract mode enabled
- **Supported Locales**: `en` (default), `de`, `fr`, `zh`, `es`, `pl`, `it`, `ko`, `pt`, `ja`
- **Message Files**: `messages/{locale}.json` — one JSON file per locale with ~242 keys
- **Config Files** in `src/i18n/`:
  - `routing.ts`: Locale list and prefix strategy (`as-needed` — no prefix for default `en`)
  - `request.ts`: Server-side locale resolution with fallback to `en`
  - `navigation.ts`: Locale-aware `Link`, `redirect`, `usePathname`, `useRouter` exports
- **Middleware**: `src/proxy.ts` uses `next-intl/middleware` for automatic locale detection from URL or `Accept-Language` header
- **URL Structure**: Default locale has no prefix (`/pricing`); others are prefixed (`/fr/pricing`)
- **Fumadocs i18n**: `src/lib/i18n.ts` configures the Fumadocs doc tree with the same locales
- **Usage in components**: `const t = useExtracted()` from `next-intl`, then `t("key")` or `t("Welcome {name}", { name })`
- **Language Switcher**: `src/components/LanguageSwitcher.tsx` — client component dropdown for all 10 locales
- **Adding translations**: Add keys to `messages/en.json`, then run `npm run extract` to sync all locale files
- **Automated translation**: `.github/workflows/translate-docs.yml` detects untranslated strings on push to master and opens a PR with AI-generated translations

### Path Aliases

- `@/*`: Maps to `src/*`
- `@/.source`: Maps to `.source/index.ts` (generated MDX content)

## Code Conventions

- Fumadocs for documentation infrastructure; MDX components defined in `src/mdx-components.tsx`
- Tailwind CSS v4 with dark mode support

## Important Notes

- Content files in `content/docs/` and `content/blog/` are processed by Fumadocs MDX
- The `.source/` directory is auto-generated and should not be edited manually
- Next.js rewrites map `/docs/:path*.mdx` to `/llms.mdx/:path*` for LLM consumption
- Remote images allowed from: pbs.twimg.com, abs.twimg.com, ui-avatars.com, cdn.outrank.so, www.google.com

---

### .Design Sync Docs ActivitySlider (`.design-sync/docs/ActivitySlider.md`)

---
category: Replay
---

The session-replay scrubber: a Radix slider whose track shows where the visitor was active and whose rail above it plots the meaningful events (clicks, navigations, typing, rage clicks, console errors) as colored 8px dots, so you can jump straight to the interesting moment. It is the "activity" view of the replay player; `TimelineSlider` is the plain thin transport rail used when there is no event data.

Single part: `ActivitySlider`. It extends every Radix `Slider.Root` prop (`value`/`defaultValue` as `[ms]`, `max`, `step`, `onValueChange`, `disabled`) plus three of its own:
- `duration` (ms, the recording length; ALSO pass it as `max` so the thumb and the rail agree),
- `activityPeriods` (`{ start, end }[]` in ms; drawn as neutral-600 segments on the neutral-700 idle track),
- `events` (raw rrweb events `{ timestamp, type, data }`; the slider runs them through `getMeaningfulEvents` from the replay module, so feed it what the player already has: type 4 Meta for session start/navigation, type 3 source 2 for clicks, source 5 for input, type 6 console plugin for logs).

Marker hues: navigation/console log blue-400, click/double click violet-500, right click fuchsia-500, rage click red-500, typing amber-500, resize cyan-500, console warning yellow-500, console error red-500. Each dot has a 2px ring in the panel color (`ring-neutral-900` in dark) so overlapping dots stay distinct, and its `title` shows the label and offset on hover (not capturable statically). Over 160 markers the plain clicks/typing are sampled while every navigation, rage click and console event is kept. The played portion of the track is emerald (`accent-500`); the thumb is a 16px white disc with an emerald border.

Conventions: mount it inside the player card (neutral-900) with the session identity on the left and `m:ss / m:ss` on the right in 12px muted tabular figures; drive `value` from the player's current time.

```tsx
import { ActivitySlider } from "@rybbit/ui";

<ActivitySlider
  duration={duration}
  max={duration}
  step={100}
  value={[currentTime]}
  onValueChange={([t]) => player.seek(t)}
  activityPeriods={[{ start: 0, end: 30_000 }, { start: 34_000, end: 78_000 }]}
  events={rrwebEvents}
/>
```

---

### .Design Sync Docs Alert (`.design-sync/docs/Alert.md`)

---
category: Feedback
---

Inline, non-dismissing status message that lives in the page flow (a settings panel, the top of a report, an onboarding step). Use it for state the user needs to see when they arrive: tracking detected, a plan limit approaching, a failed verification. For a transient confirmation after an action use `Toaster`/`toast`; for something that must be acknowledged use `AlertDialog`.

Parts: `Alert` (root, `role="alert"`, full width, 4.8px radius, 16px/12px padding, tinted fill) → a lucide icon as the first child (absolutely positioned top-left, 16px, tinted to the variant; the text shifts right 28px automatically) → `AlertTitle` (14px medium, one line) → `AlertDescription` (14px, wraps; nested `<p>` get relaxed leading, so an action row can follow the text).

Variants (`variant` on `Alert`): `default` (neutral-800 fill, gray icon) · `info` (blue tint) · `success` (emerald tint) · `warning` (yellow tint) · `destructive` (red tint). There is no border; the hue is carried by the 12-15% tint and the icon only, so the alert sits flat on the panel like everything else in the instrument. Title in sentence case, description says the consequence or the next step; title-only and description-only are both fine for one-liners. Give the icon `className="h-4 w-4"`.

```tsx
import { Alert, AlertDescription, AlertTitle } from "@rybbit/ui";
import { AlertTriangle } from "lucide-react";

<Alert variant="warning">
  <AlertTriangle className="h-4 w-4" />
  <AlertTitle>Approaching your event limit</AlertTitle>
  <AlertDescription>You have used 91% of 1M monthly events. Upgrade to keep collecting after the cap.</AlertDescription>
</Alert>
```

---

### .Design Sync Docs AlertDialog (`.design-sync/docs/AlertDialog.md`)

---
category: Overlays
---

Focus-trapped confirmation prompt (Radix AlertDialog). Unlike `Dialog`, it has no close X, cannot be dismissed by clicking the overlay, and always ends in a choice. Use it before an irreversible or costly action (delete a site, leave an organization, regenerate a key); use `Dialog` for forms and everything else.

Parts: `AlertDialog` (root, `open`/`onOpenChange`) → `AlertDialogTrigger` (`asChild` around a Button) → `AlertDialogContent` (centered 512px panel) → `AlertDialogHeader` / `AlertDialogTitle` / `AlertDialogDescription` → optional body → `AlertDialogFooter` with `AlertDialogCancel` (already styled as an `outline` Button, closes on click) and `AlertDialogAction` (accepts Button `variant` and `size`; closes on click, so attach the real work to `onClick`).

Conventions: title is a question naming the object ("Delete tomato.gg?"); description states the consequence and whether it can be undone; the cancel label is a verb ("Keep site"), not "No". The action is `variant="destructive"` for deletes and `variant="accent"` for non-destructive confirms. Never put more than two buttons in the footer.

```tsx
<AlertDialog>
  <AlertDialogTrigger asChild>
    <Button variant="destructive">Delete site</Button>
  </AlertDialogTrigger>
  <AlertDialogContent>
    <AlertDialogHeader>
      <AlertDialogTitle>Delete tomato.gg?</AlertDialogTitle>
      <AlertDialogDescription>
        This removes the site and its 2.4M events. This cannot be undone.
      </AlertDialogDescription>
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogCancel>Keep site</AlertDialogCancel>
      <AlertDialogAction variant="destructive" onClick={deleteSite}>Delete site</AlertDialogAction>
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>
```

---

### .Design Sync Docs Badge (`.design-sync/docs/Badge.md`)

---
category: Data display
---

Small inline status label: 12px medium text, 2px/6px padding, 2.8px radius (`rounded-md`), inline-flex. One export, `Badge`, with a `variant` prop; it renders a `div`, so it is not clickable — wrap it or use a `Button size="xs"` when it must act.

Variants:
- `default` — neutral-800 fill, neutral-100 text. Plan names, generic labels ("Pro").
- `secondary` — one notch quieter (panel fill, neutral-300 text). Counts and low-emphasis tags.
- `success` — emerald 20% tint, emerald-400 text. "Tracking", "Enabled", "Verified".
- `warning` — yellow tint. "No data", "Paused", thresholds.
- `destructive` — red tint. "Bot", "Blocked", "Failed".
- `info` — blue tint. "Beta", "New", informational only (never for data-series color).
- `outline` — neutral-700 border, transparent fill. Environment / category tags.
- `ghost` — no border or fill. Inline keywords.

Conventions: colored variants are semantic, so use them only when the state has that meaning — a status column is the canonical place. Put a 12px lucide icon (`className="h-3 w-3"`) as the first child and add `gap-1`; add `tabular-nums` for counts and `font-mono` for paths. Badges do not change size; if you need a larger pill, it is a `Button`.

```tsx
<Badge variant="success">Tracking</Badge>
<Badge variant="warning">No data</Badge>
<Badge variant="destructive" className="gap-1"><Bot className="h-3 w-3" /> Bot</Badge>
<Badge variant="secondary" className="tabular-nums">2,140</Badge>
```

---

### .Design Sync Docs BasicTabs (`.design-sync/docs/BasicTabs.md`)

---
category: Navigation
---

Underline tabs (Radix), the quiet sibling of `Tabs`. There is no filled strip: triggers are muted text sitting on a transparent 32px row, and the active one turns bright with a 2px bottom border. Use it inside cards and section headers to switch a breakdown (Countries / Regions / Cities, Channels / Referrers) where a pill strip would be too heavy; use `Tabs` for top-level view switching.

Parts (exported with the `BasicTabs` prefix because the source file shares names with `tabs.tsx`): `BasicTabs` (root; `defaultValue` or `value` / `onValueChange`) → `BasicTabsList` (`inline-flex` row with 12px gaps) → `BasicTabsTrigger` (`value`, `disabled`) and `BasicTabsContent` (`value`; adds `mt-2`).

Conventions: short one-word labels; an optional count after the label as `<span className="ml-1 text-xs text-neutral-500 tabular-nums">48</span>`; keep the list flush-left with the card's content edge. Never mix `BasicTabs` and `Tabs` parts in one tree.

```tsx
<BasicTabs defaultValue="countries">
  <BasicTabsList>
    <BasicTabsTrigger value="countries">Countries</BasicTabsTrigger>
    <BasicTabsTrigger value="regions">Regions</BasicTabsTrigger>
    <BasicTabsTrigger value="cities">Cities</BasicTabsTrigger>
  </BasicTabsList>
  <BasicTabsContent value="countries">{/* ranked list */}</BasicTabsContent>
  <BasicTabsContent value="regions">…</BasicTabsContent>
  <BasicTabsContent value="cities">…</BasicTabsContent>
</BasicTabs>
```

---

### .Design Sync Docs Breadcrumb (`.design-sync/docs/Breadcrumb.md`)

---
category: Navigation
---

Location trail for deep pages (a user profile, a funnel, an org settings sub-page). Ancestors are muted links, the current page is bright plain text, and a small chevron separates them. Use it only when the page is two or more levels below a sidebar destination; top-level report pages do not need one.

Parts: `Breadcrumb` (`<nav aria-label="breadcrumb">`) → `BreadcrumbList` (`<ol>`, 14px muted text, wraps) → `BreadcrumbItem` (`<li>`) containing either `BreadcrumbLink` (`href`, or `asChild` to wrap a router `Link`) or `BreadcrumbPage` (current page, `aria-current="page"`). Put a `BreadcrumbSeparator` between items: with no children it renders a 14px `ChevronRight`; pass any node (e.g. `<Slash />`) to replace it. `BreadcrumbEllipsis` is a 36px "…" placeholder for collapsed middle levels; wrap it in its own `BreadcrumbItem`.

Conventions: the site domain (`tomato.gg`) is the first crumb inside a site; keep at most four visible crumbs and collapse the rest with the ellipsis; the last item is always a `BreadcrumbPage`, never a link. Hover brightening on links is a live state and is not shown statically.

```tsx
<Breadcrumb>
  <BreadcrumbList>
    <BreadcrumbItem>
      <BreadcrumbLink href="/tomato.gg">tomato.gg</BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbLink href="/tomato.gg/users">Users</BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbPage>usr_8f3a91c2</BreadcrumbPage>
    </BreadcrumbItem>
  </BreadcrumbList>
</Breadcrumb>
```

---

### .Design Sync Docs Button (`.design-sync/docs/Button.md`)

---
category: Actions
---

The primary control. Nine visual variants on one shared shape (36px tall, 4.8px radius, 14px medium text, 4px icon gap).

- `accent` — the ONE emerald primary action on a surface ("Add site", "Save", "Publish"). At most one per view.
- `default` — the raised gray chrome button (neutral-850 fill, neutral-750 border in dark). The everyday secondary action.
- `secondary` — one notch quieter than `default` (panel-colored fill).
- `outline` — bordered, transparent; toolbar and filter chrome.
- `ghost` — no border or fill until hover; icon buttons, "Cancel", inline actions.
- `link` — text only with underline on hover; navigation-like actions.
- `success` / `warning` / `destructive` — state-colored actions (emerald, yellow, red). Use for confirmations, never as decoration.

Sizes: `lg` (40px) · `default` (36px) · `sm` (32px) · `xs` (24px) · `icon` (36px square) · `smIcon` (28px square). Put a lucide icon as the first child; the button sizes it to 16px automatically.

`asChild` renders the styles onto the child element (e.g. an `<a>`), keeping one DOM node.

```tsx
<Button variant="accent"><Plus /> New goal</Button>
<Button variant="outline" size="sm">Export CSV</Button>
<Button size="icon" variant="ghost" aria-label="Settings"><Settings /></Button>
```

---

### .Design Sync Docs ButtonGroup (`.design-sync/docs/ButtonGroup.md`)

---
category: Actions
---

Joins adjacent `Button`s (and inputs/select triggers) into one control: inner corners are squared and the shared border is collapsed so a Day/Week/Month switch, a zoom toolbar or a split "range + chevron" trigger reads as a single piece of chrome. Use it whenever two or more related actions sit flush; use plain `flex gap-2` for unrelated buttons.

Parts: `ButtonGroup` (root, `role="group"`, `flex w-fit items-stretch`; `orientation="horizontal"` (default) removes left borders/radii between children, `"vertical"` stacks them and removes the top ones) → children are ordinary `Button`s. `ButtonGroupSeparator` is a hairline `Separator` (neutral-800, stretches to the group height) for splitting clusters inside one group. `ButtonGroupText` is a static label cell with the same height and border (neutral-800 fill; `asChild` to render it as a label or link); put a lucide icon first and it is sized to 16px. Nesting a `ButtonGroup` directly inside a `ButtonGroup` inserts an 8px gap between the clusters, which is how you build a filter bar.

Conventions: members share one `size` (`sm` for filter chips, `icon` for tool rails). Use `variant="outline"` for the members and `variant="default"` (the raised neutral-850 chrome) for the selected one; `accent` only for the single primary action at the end of a group. `buttonGroupVariants` is exported if you need the classes on a custom element.

```tsx
import { Button, ButtonGroup, ButtonGroupSeparator, ButtonGroupText } from "@rybbit/ui";
import { Calendar, ChevronDown, Download } from "lucide-react";

<ButtonGroup>
  <Button variant="outline" size="sm">Day</Button>
  <Button variant="default" size="sm">Week</Button>
  <Button variant="outline" size="sm">Month</Button>
</ButtonGroup>

<ButtonGroup>
  <ButtonGroupText><Calendar /> Last 30 days</ButtonGroupText>
  <Button variant="outline" aria-label="Change range"><ChevronDown /></Button>
  <ButtonGroupSeparator />
  <Button variant="outline"><Download /> Export</Button>
</ButtonGroup>
```

---

### .Design Sync Docs Calendar (`.design-sync/docs/Calendar.md`)

---
category: Forms
---

Month-grid date picker (react-day-picker v9) that lives inside the date-range popover and any "pick a day" dialog. One component plus its day cell: `Calendar` (all DayPicker props pass through) and `CalendarDayButton` (the `ghost`/`icon` Button used per day, swappable via `components`). Key props: `mode="single"` with `selected: Date` / `onSelect(date)`, or `mode="range"` with `selected: { from, to }` / `onSelect(range)`; `defaultMonth` (always set it so the render is deterministic); `disabled` (a matcher such as `{ after: new Date() }` to block future days); `captionLayout="label"` (default, static "May 2024") or `"dropdown"` (month + year selects, needs `startMonth`/`endMonth`); `numberOfMonths={2}` for a side-by-side range picker; `showOutsideDays` (default true).

Appearance: a `w-fit` neutral-850 panel with 12px padding and 32px cells (`--cell-size`), muted weekday letters, the selected day as a light (neutral-200) pill with dark text, today as a neutral-700 pill, range middles as a continuous neutral-700 band with square inner corners, disabled days at 50% opacity. Inside a `PopoverContent` or `CardContent` the panel background goes transparent to sit on the host. Keep it for picking days; pair it with the preset range list (Last 7/30/90 days) rather than replacing it, and use `Input type="time"` for times.

```tsx
const [range, setRange] = React.useState<{ from?: Date; to?: Date }>();

<Calendar
  mode="range"
  selected={range}
  onSelect={setRange}
  defaultMonth={new Date(2024, 4, 1)}
  disabled={{ after: new Date() }}
  numberOfMonths={2}
/>
```

---

### .Design Sync Docs Card (`.design-sync/docs/Card.md`)

---
category: Data display
---

The panel. Every dashboard section, stat tile, and settings block sits in a `Card`: a neutral-900 surface one step off the canvas, 1px neutral-850 border, 4.8px radius, `overflow-hidden`, no shadow. Flat parts, all optional:

- `Card` — the surface (`relative`, so loaders and absolute children anchor to it).
- `CardHeader` — vertical stack with 16px padding; holds `CardTitle` (16px semibold, tight tracking) and `CardDescription` (14px, neutral-400 muted).
- `CardContent` — 16px padding with the top padding removed (`p-4 pt-0`) so it butts against the header. Put your rows, table, or chart here.
- `CardFooter` — horizontal flex row, same padding rule; put `Button`s here (`justify-end gap-2` for an action row, ONE `accent`).
- `CardLoader` — an indeterminate progress bar for "refetching in place". It is `absolute top-0 left-0 w-full` with a -15px top margin, so render it as the first child of a `Card` (or inside any `relative` container) and the card's `overflow-hidden` clips it to a thin line along the top edge. It needs no props; it reads the theme via next-themes and colors itself neutral-400 in dark mode. Use it with `Skeleton` content underneath on first load, or alone over stale data while refetching.

Conventions: stat tiles do not use `CardHeader` — they are a `Card` with `px-3 py-2`, a 12px `text-muted-foreground` label, and a 24px `font-medium` figure (delta in `text-emerald-400` / `text-red-400`). Lists inside `CardContent` use `border-b border-neutral-800` hairlines between rows, not nested cards. Never nest a `Card` in a `Card`.

```tsx
<Card>
  <CardLoader />
  <CardHeader>
    <CardTitle>Top pages</CardTitle>
    <CardDescription>Last 7 days on tomato.gg</CardDescription>
  </CardHeader>
  <CardContent className="flex flex-col gap-2">
    <Skeleton className="h-4 w-full" />
    <Skeleton className="h-4 w-full" />
  </CardContent>
  <CardFooter className="justify-end gap-2">
    <Button variant="ghost" size="sm">Cancel</Button>
    <Button variant="accent" size="sm">Save changes</Button>
  </CardFooter>
</Card>
```

---

### .Design Sync Docs Carousel (`.design-sync/docs/Carousel.md`)

---
category: Data display
---

Horizontal (or vertical) slide deck driven by Embla, for a row of stat cards, onboarding steps or screenshots that would not fit side by side. In an analytics UI reach for it rarely: a grid or a scrollable row is usually clearer; use a carousel when the slides are peers and only a few need to be visible at once.

Parts: `Carousel` (root, `role="region"`; props `orientation="horizontal" | "vertical"`, `opts` passed straight to Embla, e.g. `{ loop: true, startIndex: 1, align: "start" }`, `plugins`, `setApi` to receive the `CarouselApi` for programmatic scrolling; arrow keys work out of the box) → `CarouselContent` (the overflow-hidden viewport; its inner flex track has a -16px start margin so items are spaced by their own 16px padding) → `CarouselItem` (one slide, `basis-full` by default; set `style={{ flexBasis: "33.3333%" }}` or a `basis-*` class for N-up) → `CarouselPrevious` / `CarouselNext` (32px round `outline` icon buttons, absolutely positioned 48px OUTSIDE the track: `-left-12` / `-right-12`, or above/below and rotated when vertical; they disable themselves at the ends unless `loop` is on). Both buttons accept every `Button` prop, so pass `variant`/`className` to reposition them inside the track if the parent has no gutter.

Conventions: reserve a 64px side gutter in the parent for the arrows; give a vertical `CarouselContent` an explicit height; slides are `Card`s with the stat-tile layout (12px muted label, 24px semibold tabular number, emerald/red delta line). Keep the visible slide count at 1-3.

```tsx
import { Card, CardContent, Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@rybbit/ui";

<div style={{ padding: "16px 64px" }}>
  <Carousel opts={{ align: "start" }}>
    <CarouselContent>
      {stats.map((s) => (
        <CarouselItem key={s.label} style={{ flexBasis: "33.3333%" }}>
          <Card>
            <CardContent className="p-4">
              <div className="text-xs font-medium text-neutral-400">{s.label}</div>
              <div className="mt-1 text-2xl font-semibold tabular-nums">{s.value}</div>
              <div className="mt-1 text-xs text-emerald-400">{s.delta} vs. previous 30 days</div>
            </CardContent>
          </Card>
        </CarouselItem>
      ))}
    </CarouselContent>
    <CarouselPrevious />
    <CarouselNext />
  </Carousel>
</div>
```

---

### .Design Sync Docs Checkbox (`.design-sync/docs/Checkbox.md`)

---
category: Forms
---

Radix checkbox, 16px square, 4.8px radius. Unchecked: 1px `neutral-700` border on a faint `neutral-900/30` fill. Checked: emerald (`accent-500`) border and fill with a white lucide `Check`. Focus shows a 3px emerald ring. Use it for independent on/off choices in a list (columns to include, sites to notify, filters to apply); use `Switch` for a single setting that applies immediately, and `RadioGroup` for mutually exclusive options.

Parts: `Checkbox` only (indicator is internal). Props: `checked` / `defaultChecked`, `onCheckedChange`, `disabled`, `id`, `name`, `className`.

Conventions:
- Always give it an `id` and a sibling `Label htmlFor` in a `flex items-center gap-2` row; the label toggles it.
- With a description: `flex items-start gap-3`, add `className="mt-0.5"` to the checkbox so it aligns with the first text line, and put the description in `text-xs text-neutral-400`.
- Option lists live in a `grid gap-2.5` inside a `rounded-lg border border-neutral-800 bg-neutral-900 p-4` panel with a `text-sm font-medium` heading; option labels use `font-normal`.
- `disabled` fades the box to 50%; add `opacity-70` to its label. Hover and indeterminate are not shown in the previews.

```tsx
<div className="flex items-center gap-2">
  <Checkbox id="exclude-bots" defaultChecked />
  <Label htmlFor="exclude-bots">Exclude known bots</Label>
</div>

<div className="flex items-start gap-3">
  <Checkbox id="salt" className="mt-0.5" />
  <div className="grid gap-1">
    <Label htmlFor="salt">Salt user IDs daily</Label>
    <p className="text-xs text-neutral-400">Returning-visitor stats reset at midnight UTC.</p>
  </div>
</div>
```

---

### .Design Sync Docs Command (`.design-sync/docs/Command.md`)

---
category: Overlays
---

Searchable command list built on cmdk. Typing in `CommandInput` filters `CommandItem`s by their text (or `value` / `keywords`), arrow keys move the highlight, Enter fires `onSelect`. Use it for the ⌘K palette, site switchers, and any pick-from-many list that needs a search box; for a plain menu of actions use `DropdownMenu`, and for a form value use `Select`.

Parts: `Command` (root; transparent background, so wrap it in your own panel such as `rounded-lg border border-neutral-800 bg-neutral-900`, or use `CommandDialog`) → `CommandInput` (search icon + input, hairline bottom border; `placeholder`, `value` / `onValueChange`) → `CommandList` (scroll container capped at 300px) containing `CommandEmpty` (shown only when nothing matches), `CommandGroup` (`heading` renders a small muted label), `CommandItem` (`value`, `keywords`, `onSelect`, `disabled`; highlighted row is a raised neutral), `CommandShortcut` (right-aligned key hint) and `CommandSeparator`. `CommandDialog` wraps the same children in a `Dialog` (`open` / `onOpenChange`, `title` / `description` for screen readers, `showCloseButton`) with taller 48px rows.

Conventions: put a 16px lucide icon with `mr-2 size-4 text-neutral-400` before item text; group by kind (Sites / Go to / Actions) with a separator between groups; keep the first item selected by default (cmdk does this). Keyboard navigation and the filtered state are live behaviors and are not shown statically.

```tsx
<CommandDialog open={open} onOpenChange={setOpen} title="Command palette">
  <CommandInput placeholder="Search sites, pages, or commands…" />
  <CommandList>
    <CommandEmpty>No results found.</CommandEmpty>
    <CommandGroup heading="Sites">
      <CommandItem onSelect={() => go("/tomato.gg")}>
        <Globe className="mr-2 size-4 text-neutral-400" /> tomato.gg
      </CommandItem>
    </CommandGroup>
    <CommandSeparator />
    <CommandGroup heading="Go to">
      <CommandItem>
        <Users className="mr-2 size-4 text-neutral-400" /> Sessions
        <CommandShortcut>⌘2</CommandShortcut>
      </CommandItem>
    </CommandGroup>
  </CommandList>
</CommandDialog>
```

---

### .Design Sync Docs Dialog (`.design-sync/docs/Dialog.md`)

---
category: Overlays
---

Modal dialog (Radix). Compose the flat parts: `Dialog` (root, controls `open`) → `DialogTrigger` (wraps the opener, `asChild` for a Button) → `DialogContent` (portalled panel, 512px max, close X built in; `hideClose` removes it) → `DialogHeader` / `DialogTitle` / `DialogDescription` → body → `DialogFooter` (actions, right-aligned on desktop). `DialogClose` wraps any element that should dismiss. `DialogContentFullScreen` is the edge-to-edge editor surface with no overlay or padding.

Rules of thumb: title in sentence case; description explains the consequence; footer holds a `ghost`/`outline` cancel and ONE `accent` (or `destructive`) confirm. For screen-size-adaptive dialogs use `ResponsiveDialog` (dialog on desktop, drawer on mobile). For a plain confirm/cancel prompt with focus trapping use `AlertDialog`.

```tsx
<Dialog>
  <DialogTrigger asChild><Button variant="accent">Add site</Button></DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Add a website</DialogTitle>
      <DialogDescription>Enter the domain you want to track.</DialogDescription>
    </DialogHeader>
    <Input placeholder="example.com" />
    <DialogFooter>
      <DialogClose asChild><Button variant="ghost">Cancel</Button></DialogClose>
      <Button variant="accent">Add site</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>
```

---

### .Design Sync Docs Drawer (`.design-sync/docs/Drawer.md`)

---
category: Overlays
---

Touch-first panel built on vaul: slides up from the bottom with a drag handle and swipe-to-dismiss. Use it on mobile layouts for short forms and detail cards (a goal editor, a session summary, a filter list). On desktop prefer `Dialog` or `Sheet`; `ResponsiveDialog` switches between the two for you.

Parts: `Drawer` (root, `open`/`onOpenChange`; `direction="bottom" | "top" | "left" | "right"`, default `bottom`) → `DrawerTrigger` (`asChild` around a Button) → `DrawerContent` (portalled panel with overlay; the drag handle is rendered automatically for `bottom`) → `DrawerHeader` / `DrawerTitle` / `DrawerDescription` → your content → `DrawerFooter` (stacked, full-width actions pushed to the bottom with `mt-auto`). `DrawerClose` wraps any element that should dismiss.

Conventions: header and footer carry their own `p-4`, so pad body content with `px-4` to line up. Footer buttons stack vertically at full width — put the `accent` action first and a `ghost` cancel below it. Bottom drawers center their header text; side drawers (`left`/`right`) behave like a `Sheet` and are capped at `max-w-sm`. Keep bottom drawers under ~60% of the viewport; anything taller belongs in a page.

```tsx
<Drawer>
  <DrawerTrigger asChild><Button variant="accent">New goal</Button></DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>New goal</DrawerTitle>
      <DrawerDescription>Count a conversion whenever a visitor reaches this page.</DrawerDescription>
    </DrawerHeader>
    <div className="grid gap-3 px-4">
      <Label htmlFor="path">Path</Label>
      <Input id="path" defaultValue="/welcome" />
    </div>
    <DrawerFooter>
      <Button variant="accent">Create goal</Button>
      <DrawerClose asChild><Button variant="ghost">Cancel</Button></DrawerClose>
    </DrawerFooter>
  </DrawerContent>
</Drawer>
```

---

### .Design Sync Docs DropdownMenu (`.design-sync/docs/DropdownMenu.md`)

---
category: Overlays
---

Contextual action menu (Radix) that opens from a trigger and closes on select or outside click. Use it for per-row/per-site actions, column toggles, and quick single-choice settings; use `Select` when the value is a form field, and `Popover` when the content is not a list of commands.

Parts: `DropdownMenu` (root, `open` / `onOpenChange`) → `DropdownMenuTrigger` (renders as a Button and accepts Button `variant` / `size`; pass `asChild` to supply your own element, or `unstyled` to opt out of button styling) → `DropdownMenuContent` (portalled panel, `align="start" | "end"`, `sideOffset` 4 by default; give it a width like `className="w-56"`). Inside: `DropdownMenuLabel` (section heading), `DropdownMenuGroup`, `DropdownMenuItem` (a leading lucide icon is auto-sized to 16px; `inset` indents to align with checkbox rows; `disabled` dims it), `DropdownMenuShortcut` (right-aligned key hint), `DropdownMenuSeparator`, `DropdownMenuCheckboxItem` (`checked` / `onCheckedChange`, emerald check indicator), `DropdownMenuRadioGroup` + `DropdownMenuRadioItem` (`value`, emerald dot), and `DropdownMenuSub` + `DropdownMenuSubTrigger` (chevron built in) + `DropdownMenuSubContent` for a nested level. `DropdownMenuPortal` is only needed when you portal sub-content yourself.

Conventions: one label per section, separators between sections, the destructive action last and colored with `text-red-400`. Keep menus to ~8 rows; put value pickers with many options in `Select`. Hover/focus highlight is a raised neutral, never emerald: emerald appears only on the check/radio indicator.

```tsx
<DropdownMenu>
  <DropdownMenuTrigger variant="outline" size="sm">
    tomato.gg <ChevronDown className="size-4" />
  </DropdownMenuTrigger>
  <DropdownMenuContent align="start" className="w-56">
    <DropdownMenuLabel>tomato.gg</DropdownMenuLabel>
    <DropdownMenuSeparator />
    <DropdownMenuItem>
      <Settings /> Site settings
      <DropdownMenuShortcut>⌘,</DropdownMenuShortcut>
    </DropdownMenuItem>
    <DropdownMenuCheckboxItem checked>Show bounce rate</DropdownMenuCheckboxItem>
    <DropdownMenuSeparator />
    <DropdownMenuItem className="text-red-400 focus:text-red-400">
      <Trash2 /> Delete site
    </DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>
```

---

### .Design Sync Docs Form (`.design-sync/docs/Form.md`)

---
category: Forms
---

The react-hook-form wiring layer for every settings and create dialog. `Form` is react-hook-form's `FormProvider`: spread the object from `useForm()` into it, then put a real `<form onSubmit={form.handleSubmit(fn)}>` inside. Each field is `FormField` (`control={form.control}`, `name`, and a `render={({ field }) => ...}` that spreads `field` onto the input) → `FormItem` (a `space-y-2` stack that owns the ids) → `FormLabel` (turns red on error) → `FormControl` (a Slot that stamps `id`, `aria-describedby`, `aria-invalid` on the single child control) → `FormDescription` (muted 12.8px helper) → `FormMessage` (red 12.8px; renders the field's error message automatically, or its children, or nothing). `useFormField()` exposes the ids and error inside custom controls.

Conventions: one `FormItem` per field, `grid gap-5` (or `space-y-4`) between them, actions in a right-aligned `flex justify-end gap-2` row with a `ghost` cancel and ONE `accent` submit. Bind non-input controls via their own props: a `Select` takes `value={field.value} onValueChange={field.onChange}` with `FormControl` wrapping the `SelectTrigger`; a `Switch`/`Checkbox` takes `checked={field.value} onCheckedChange={field.onChange}`. Show errors from validation, or set them with `form.setError("domain", { message })`. Use plain `Label` + `Input` only for a single uncontrolled field outside a form.

```tsx
const form = useForm({ defaultValues: { domain: "" } });

<Form {...form}>
  <form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-5">
    <FormField
      control={form.control}
      name="domain"
      rules={{ required: "Domain is required." }}
      render={({ field }) => (
        <FormItem>
          <FormLabel>Domain</FormLabel>
          <FormControl><Input placeholder="example.com" {...field} /></FormControl>
          <FormDescription>The hostname the tracking script will report from.</FormDescription>
          <FormMessage />
        </FormItem>
      )}
    />
    <div className="flex justify-end gap-2">
      <Button type="button" variant="ghost">Cancel</Button>
      <Button type="submit" variant="accent">Add site</Button>
    </div>
  </form>
</Form>
```

---

### .Design Sync Docs Input (`.design-sync/docs/Input.md`)

---
category: Forms
---

Single-line text field. Transparent fill, 1px `neutral-800` border, 4.8px radius, placeholder in `neutral-400`, and a 1px `neutral-300` ring on focus. Use it for every free-text, number, URL and password field in settings and dialogs; pair it with `Label` above via `htmlFor`.

Parts: `Input` only. It forwards every native `<input>` prop (`type`, `placeholder`, `defaultValue`, `disabled`, `aria-invalid`, `ref`).

Conventions:
- `inputSize="default"` (36px, 14px text) for forms; `inputSize="sm"` (28px, 12px text) for toolbar filters and table headers.
- `isSearch` renders a leading lucide `Search` icon and pads the text past it. Use it for every "Search / Filter …" box.
- `aria-invalid="true"` turns the border red; put the error text in a `text-xs text-red-400` line below.
- `disabled` drops the field to 50% opacity. Width comes from the container: it is `w-full`, so constrain with `className="w-32"` or a grid.
- Stack `Label` + `Input` in a `grid gap-1.5`; use `grid grid-cols-2 gap-3` for side-by-side fields. Prefer `Textarea` for multi-line, `Select` for enumerations.

```tsx
<div className="grid gap-1.5">
  <Label htmlFor="domain">Domain</Label>
  <Input id="domain" placeholder="example.com" defaultValue="tomato.gg" />
</div>

<Input isSearch inputSize="sm" placeholder="Search pages…" />

<div className="grid gap-1.5">
  <Input aria-invalid="true" defaultValue="not a domain" />
  <p className="text-xs text-red-400">Enter a valid domain, like tomato.gg</p>
</div>
```

---

### .Design Sync Docs Label (`.design-sync/docs/Label.md`)

---
category: Forms
---

Form label built on Radix Label: 14px, medium weight, `leading-none`, inherits the foreground color. Always wire it to its control with `htmlFor` + `id` so clicking the text focuses the input or toggles the checkbox/switch. It is the only text style for field names; helper text below a field is a plain `p.text-xs.text-neutral-400`.

Parts: `Label` only. Forwards every native `<label>` prop and `className`.

Conventions:
- Above a field: `grid gap-1.5` with `Label` first, then `Input` / `Textarea` / `Select`.
- Beside a toggle: `flex items-center gap-2` with the `Checkbox` / `RadioGroupItem` / `Switch` first, then the `Label`. For lists of options use `className="font-normal"` so only the group heading is medium.
- When the control has `peer` and is `disabled`, the label auto-fades to 70% (`peer-disabled:opacity-70`); otherwise add `className="opacity-70"` yourself.
- Required marker: `<span className="text-red-400">*</span>` inside the label. A trailing hint sits on the same row with `flex items-baseline justify-between`.

```tsx
<div className="grid gap-1.5">
  <Label htmlFor="site-name">Site name</Label>
  <Input id="site-name" defaultValue="rybbit.com" />
  <p className="text-xs text-neutral-400">Shown in the site switcher.</p>
</div>

<div className="flex items-center gap-2">
  <Checkbox id="exclude-bots" defaultChecked />
  <Label htmlFor="exclude-bots">Exclude known bots</Label>
</div>
```

---

### .Design Sync Docs MultiSelect (`.design-sync/docs/MultiSelect.md`)

---
category: Forms
---

Searchable multi-value picker for filters (countries, browsers, pages, sites): an `outline` combobox button that grows to hold the chosen values as `secondary` badges with an X to remove, and a Popover + Command list with a search box and check marks. One component, no sub-parts: `MultiSelect`. Props: `options: { value, label, disabled? }[]`, `value: string[]` + `onValueChange(values)` (controlled; keep the array in state), `placeholder` (empty-state text, default "Select items..."), `searchPlaceholder`, `emptyText` ("No items found."), `disabled`, `className` (trigger), `badgeClassName` (chips).

Conventions: the trigger is `w-full` and at least 36px tall; chips wrap onto extra rows rather than truncating, so give it a bounded width (form column or `w-96`). The list opens on click only (no `open` prop) and filters by label substring; selecting toggles membership and keeps the list open. Use it when more than one value can be active at once; for a single choice use `Select`; for free text use `InputWithSuggestions`.

```tsx
const countries = [
  { value: "US", label: "United States" },
  { value: "DE", label: "Germany" },
  { value: "GB", label: "United Kingdom" },
];
const [selected, setSelected] = React.useState<string[]>(["US"]);

<MultiSelect
  options={countries}
  value={selected}
  onValueChange={setSelected}
  placeholder="Filter by country"
  searchPlaceholder="Search countries"
  emptyText="No country matches."
/>
```

---

### .Design Sync Docs NavigationMenu (`.design-sync/docs/NavigationMenu.md`)

---
category: Navigation
---

Horizontal top-level nav bar (Radix) whose triggers open a content panel that drops from below the bar. It is for marketing/site-wide headers with a few sections, not for the dashboard sidebar (which is a plain list) and not for per-item actions (`DropdownMenu`). Plain links live in the same bar via `NavigationMenuLink` styled with `navigationMenuTriggerStyle()`.

Parts: `NavigationMenu` (root; renders the `NavigationMenuViewport` for you. Uncontrolled by default; pass `value` / `onValueChange` to force an item open) → `NavigationMenuList` (flex row) → `NavigationMenuItem` (give it `value` when controlling) containing either `NavigationMenuTrigger` (36px button, chevron built in and flips when open) + `NavigationMenuContent` (the panel body; size it yourself, e.g. `w-[400px] p-2`), or a bare `NavigationMenuLink` (`href`, `asChild`). `NavigationMenuIndicator` is an optional caret under the active trigger. The panel is a `bg-neutral-950` surface with a hairline `border-neutral-800`.

Conventions: 2-4 triggers plus 1-2 plain links; inside a panel list links as `block rounded-md p-3 hover:bg-neutral-800` blocks with a 14px medium title and a 12px `text-neutral-400` description. Give the container enough height below the bar for the panel. Hover-to-open and the slide animation are live behaviors; the preview forces the panel open with `value`.

```tsx
<NavigationMenu>
  <NavigationMenuList>
    <NavigationMenuItem>
      <NavigationMenuTrigger>Reports</NavigationMenuTrigger>
      <NavigationMenuContent>
        <div className="grid w-[400px] gap-1 p-2">
          <NavigationMenuLink asChild>
            <a href="/overview" className="block rounded-md p-3 hover:bg-neutral-800">
              <div className="text-sm font-medium">Overview</div>
              <p className="mt-1 text-xs text-neutral-400">Visitors, pageviews and top pages.</p>
            </a>
          </NavigationMenuLink>
        </div>
      </NavigationMenuContent>
    </NavigationMenuItem>
    <NavigationMenuItem>
      <NavigationMenuLink href="/docs" className={navigationMenuTriggerStyle()}>Docs</NavigationMenuLink>
    </NavigationMenuItem>
  </NavigationMenuList>
</NavigationMenu>
```

---

### .Design Sync Docs Popover (`.design-sync/docs/Popover.md`)

---
category: Overlays
---

Non-modal floating panel anchored to a trigger (Radix Popover). Use it for small, focused controls that belong next to the thing they change: date-range presets, a filter's value picker, the site selector, a one-paragraph explanation. Use `DropdownMenu` for a list of commands, `Tooltip` for a label that needs no interaction, and `Dialog` for anything that needs a title and footer.

Parts: `Popover` (root, `open`/`onOpenChange`) → `PopoverTrigger` (`asChild` around a Button) → `PopoverContent` (portalled panel: `w-72 p-4 rounded-md`, raised neutral surface; props `align="start" | "center" | "end"` (default `center`), `side`, `sideOffset` (default 4), `onOpenAutoFocus`). `PopoverAnchor` positions the panel against an element other than the trigger.

Conventions: prefer `align="start"` so the panel lines up with the trigger's left edge. Override width per use (`w-64`, `w-80`, or `w-[var(--radix-popover-trigger-width)]` to match the trigger); pass `p-0` when the content is a `Command` list or a `Calendar`. Inside, use `text-sm`, muted labels in `text-xs text-neutral-400`, and a right-aligned `size="sm"` action row. Inputs inside inherit a slightly lighter border automatically.

```tsx
<Popover>
  <PopoverTrigger asChild>
    <Button variant="outline" size="sm"><CalendarIcon /> Last 7 days</Button>
  </PopoverTrigger>
  <PopoverContent align="start" className="w-64">
    <div className="grid gap-1">
      {["Today", "Last 7 days", "Last 30 days"].map(p => (
        <button key={p} className="rounded-md px-2 py-1.5 text-left text-sm hover:bg-neutral-700/60">{p}</button>
      ))}
    </div>
    <Separator className="my-3 dark:bg-neutral-700" />
    <div className="grid grid-cols-2 gap-2">
      <Input defaultValue="2026-09-06" className="h-8 text-xs" />
      <Input defaultValue="2026-09-13" className="h-8 text-xs" />
    </div>
  </PopoverContent>
</Popover>
```

---

### .Design Sync Docs Progress (`.design-sync/docs/Progress.md`)

---
category: Data display
---

Determinate progress / usage meter (Radix Progress). One export, `Progress`: an 8px-tall, fully rounded neutral-800 track with an emerald (`accent-500`) indicator that slides in by `value` percent. Props: `value` (0-100, `undefined` renders empty), `max` (Radix, default 100), `className` on the track. There is no indeterminate mode — for "loading" use `CardLoader` or `Skeleton`.

Conventions: the bar carries no text, so always pair it with a label and a figure. The canonical usage meter is a `flex justify-between text-sm` row (name left, `812K / 1M` in `tabular-nums text-neutral-400` right), the bar, then a 12px muted note. For inline rows (countries, browsers) shrink the track with `h-1.5` and give the bar `flex-1` between a fixed-width name and a right-aligned percentage. Emerald here means "amount used / achieved", not success; do not recolor the indicator per row (chart series use periwinkle in Nivo, not this component). Use `Slider` when the user should change the value.

```tsx
<div className="flex flex-col gap-2">
  <div className="flex items-center justify-between text-sm">
    <span className="font-medium">Events this month</span>
    <span className="tabular-nums text-neutral-400">812K / 1M</span>
  </div>
  <Progress value={81} />
  <div className="text-xs text-muted-foreground">Resets in 9 days · Pro plan</div>
</div>
```

---

### .Design Sync Docs RadioGroup (`.design-sync/docs/RadioGroup.md`)

---
category: Forms
---

Radix radio group for one-of-N choices that need every option visible (retention period, chart interval, visitor scope). Items are 16px circles: 1px `neutral-700` border when idle, emerald (`accent-500`) fill with a white dot when selected, 3px emerald focus ring. Use `Select` instead when there are more than ~5 options or space is tight; use `Checkbox` when choices are independent.

Parts:
- `RadioGroup` — the root (`grid gap-2` by default). Props: `value` / `defaultValue`, `onValueChange`, `disabled`, `name`, `className`.
- `RadioGroupItem` — one option. Props: `value` (required), `id`, `disabled`, `className`. Indicator is internal.

Conventions:
- Each item sits in a `flex items-center gap-2` row with a `Label htmlFor` matching its `id`; option labels use `className="font-normal"`, the group heading is `text-sm font-medium` above.
- Horizontal: `<RadioGroup className="flex gap-4">` for short enums like hour / day / week.
- With descriptions: `flex items-start gap-3`, `className="mt-0.5"` on the item, description in `text-xs text-neutral-400`.
- `disabled` on an item fades it to 50%; add `opacity-70` to that label.

```tsx
<RadioGroup defaultValue="365">
  <div className="flex items-center gap-2">
    <RadioGroupItem value="90" id="r-90" />
    <Label htmlFor="r-90" className="font-normal">90 days</Label>
  </div>
  <div className="flex items-center gap-2">
    <RadioGroupItem value="365" id="r-365" />
    <Label htmlFor="r-365" className="font-normal">1 year</Label>
  </div>
</RadioGroup>
```

---

### .Design Sync Docs ResponsiveDialog (`.design-sync/docs/ResponsiveDialog.md`)

---
category: Overlays
---

One overlay, two presentations: at viewports ≥640px it renders a centered Radix `Dialog`; below that it renders a vaul `Drawer` (bottom sheet with drag handle). Use it for any dialog that also has to work on phones — invite a member, edit a goal, expand a dashboard card. If you only ever need one presentation, use `Dialog`, `Sheet` or `Drawer` directly.

Parts: `ResponsiveDialog` (root; `open`/`onOpenChange`, plus `direction` for the drawer side, `onlyDialog` / `onlyDrawer` to pin a presentation, `dismissible={false}` to block overlay/Escape, `alert` to make it an un-dismissable confirm with no X) → `ResponsiveDialogTrigger` → `ResponsiveDialogContent` (`showCloseButton`, `closeButtonClassName`, `dragHandleClassName`) → `ResponsiveDialogHeader` / `ResponsiveDialogTitle` / `ResponsiveDialogDescription` → body → `ResponsiveDialogFooter` (row on desktop, reversed column on mobile). `ResponsiveDialogClose` wraps a dismissing element. `useResponsiveDialog()` (call inside the root) returns `{ modal, dismissible, direction, onlyDrawer, onlyDialog, alert }` for custom parts.

Conventions: `ResponsiveDialogContent` sets no width or padding, so always pass them — `className="max-w-md p-6"` for a form, `max-w-[1000px] w-[calc(100vw-2rem)] p-4` for a wide card expansion. In drawer mode add `px-4 pb-4` instead. Same footer rule as Dialog: one `accent`/`destructive` confirm, one `ghost`/`outline` cancel.

```tsx
<ResponsiveDialog>
  <ResponsiveDialogTrigger asChild><Button variant="accent">Invite</Button></ResponsiveDialogTrigger>
  <ResponsiveDialogContent className="max-w-md p-6">
    <ResponsiveDialogHeader>
      <ResponsiveDialogTitle>Invite a teammate</ResponsiveDialogTitle>
      <ResponsiveDialogDescription>They get access to every site in Tomato Labs.</ResponsiveDialogDescription>
    </ResponsiveDialogHeader>
    <Input placeholder="name@company.com" />
    <ResponsiveDialogFooter>
      <ResponsiveDialogClose asChild><Button variant="ghost">Cancel</Button></ResponsiveDialogClose>
      <Button variant="accent">Send invite</Button>
    </ResponsiveDialogFooter>
  </ResponsiveDialogContent>
</ResponsiveDialog>
```

---

### .Design Sync Docs ScrollArea (`.design-sync/docs/ScrollArea.md`)

---
category: Data display
---

Custom-scrollbar container for any fixed-height list or wide row (Radix ScrollArea, with a touch-device fallback to native scrolling). Parts: `ScrollArea` (root + viewport; already includes a vertical `ScrollBar`, a corner, and an edge-fade mask) and `ScrollBar` (add one with `orientation="horizontal"` as a child when the content is wider than the panel).

`ScrollArea` props beyond Radix's: `viewportClassName` (classes on the scrolling viewport, e.g. `pr-3`), `maskHeight` (px, default 30; `0` disables the fade), `maskClassName`. Radix props pass through — `type="always"` keeps the bar visible, the default (`hover`, `scrollHideDelay={0}`) reveals it only while the pointer is over the area. The mask is a panel-colored (`neutral-900`) gradient that appears only on edges with more content, so it tells the user "there is more" without a visible scrollbar.

Conventions: the root needs a height (`h-64`, `h-[314px]`, or `h-full min-h-0` inside a flex column) or nothing scrolls; put padding on the inner content, not the root, so the mask and bar sit at the true edge. Use it for dashboard lists (countries, pages, bot rows), dialog bodies, and chip rows; use plain `Table` for short lists that fit. Rows inside are ordinary `flex justify-between` lines with `tabular-nums` figures. The mask assumes the panel is `neutral-900`; on the raw canvas set `maskClassName` or `maskHeight={0}`.

```tsx
<ScrollArea className="h-64 rounded-lg border border-neutral-800 bg-neutral-900">
  <div className="flex flex-col p-2 text-sm">
    {countries.map(([name, n]) => (
      <div key={name} className="flex items-center justify-between px-2 py-1.5">
        <span>{name}</span>
        <span className="tabular-nums text-neutral-400">{n.toLocaleString()}</span>
      </div>
    ))}
  </div>
</ScrollArea>

<ScrollArea className="rounded-lg border border-neutral-800">
  <div className="flex w-max gap-2 p-3">{chips}</div>
  <ScrollBar orientation="horizontal" />
</ScrollArea>
```

---

### .Design Sync Docs Select (`.design-sync/docs/Select.md`)

---
category: Forms
---

Single-value dropdown (Radix Select) for picking one option from a short, known list: time range, site, timezone, metric. Compose the flat parts: `Select` (root; `value`/`defaultValue`, `onValueChange`, `open`, `disabled`) → `SelectTrigger` (the 36px bordered field; `size="sm"` for the 28px toolbar variant) containing `SelectValue` (shows the chosen item's text or its `placeholder`) → `SelectContent` (portalled popper list, dark neutral-800 panel; pass the same `size`) → `SelectItem` (`value` required; the chosen one shows an emerald check on the right; `disabled` dims it). Group with `SelectGroup` + `SelectLabel` heading and separate groups with `SelectSeparator`. `SelectScrollUpButton` / `SelectScrollDownButton` are already inside `SelectContent`.

Conventions: the trigger is `w-full` by default, so size it with the parent. Keep `size` consistent across Trigger/Content/Item/Label. Use Select when there are 3-12 mutually exclusive options; for 2 options use a `RadioGroup` or `Switch`, for many searchable options use `MultiSelect`/`Command`, for free text with hints use `InputWithSuggestions`. Inside a `Form`, wrap the `SelectTrigger` in `FormControl` and bind `value={field.value} onValueChange={field.onChange}`.

```tsx
<Select defaultValue="7d" onValueChange={setRange}>
  <SelectTrigger>
    <SelectValue placeholder="Pick a range" />
  </SelectTrigger>
  <SelectContent>
    <SelectGroup>
      <SelectLabel>Recent</SelectLabel>
      <SelectItem value="24h">Last 24 hours</SelectItem>
      <SelectItem value="7d">Last 7 days</SelectItem>
      <SelectItem value="30d">Last 30 days</SelectItem>
    </SelectGroup>
    <SelectSeparator />
    <SelectItem value="all">All time</SelectItem>
  </SelectContent>
</Select>
```

---

### .Design Sync Docs Separator (`.design-sync/docs/Separator.md`)

---
category: Data display
---

A 1px hairline (Radix Separator) in neutral-800, the same tone as table row borders. One export, `Separator`. Props: `orientation` (`"horizontal"` default → `h-[1px] w-full`; `"vertical"` → `h-full w-[1px]`), `decorative` (default `true`, so it is hidden from assistive tech; set `false` only when the division is semantic), and `className` for margins or an explicit height.

Conventions: horizontal separators divide text blocks inside a `Card` or settings page — give them vertical breathing room with `my-4` (or `my-3` in dense lists). Between list rows prefer `border-b border-neutral-800` on the row itself rather than a `Separator` per row. A vertical separator is `h-full`, so its parent must have a height: either make the row a fixed-height flex container (`flex h-4 items-center`) or set the height directly (`className="h-5"`) when dividing toolbar button groups. Do not use separators as decoration between every element; the grayscale ramp and spacing should do most of the dividing.

```tsx
<div className="text-sm font-medium">Tracking script</div>
<Separator className="my-4" />
<div className="text-sm font-medium">Data retention</div>

<div className="flex h-9 items-center gap-2">
  <Button variant="ghost" size="sm">Filter</Button>
  <Separator orientation="vertical" className="h-5" />
  <Button variant="ghost" size="sm">Export</Button>
</div>
```

---

### .Design Sync Docs Sheet (`.design-sync/docs/Sheet.md`)

---
category: Overlays
---

Edge-anchored panel that slides in over a dimmed page (Radix Dialog under the hood). Use it for secondary work that keeps the page in context: site settings, a site switcher, a filter editor, a bulk-action strip. For a centered task use `Dialog`; for a touch-first bottom panel with drag-to-dismiss use `Drawer`.

Parts: `Sheet` (root, `open`/`onOpenChange`) → `SheetTrigger` (`asChild` around a Button) → `SheetContent` (portalled panel; `side="right" | "left" | "top" | "bottom"`, default `right`; `showClose={false}` hides the built-in X) → `SheetHeader` / `SheetTitle` / `SheetDescription` → your content → `SheetFooter` (right-aligned actions). `SheetClose` wraps any element that should dismiss.

Conventions: `right` is the default and the one to reach for; `left` reads as navigation; `bottom`/`top` are full-width strips, so keep them short (a title line plus actions). Side panels are `w-3/4` capped at `max-w-sm` (384px), so lay the body out as a single column with `grid gap-4 py-6`. Footer holds one `accent` confirm plus a `ghost`/`outline` cancel. Group settings rows as bordered cards (`rounded-lg border border-neutral-800 p-3`) with a `Switch` on the right.

```tsx
<Sheet>
  <SheetTrigger asChild><Button variant="outline">Settings</Button></SheetTrigger>
  <SheetContent>
    <SheetHeader>
      <SheetTitle>Site settings</SheetTitle>
      <SheetDescription>tomato.gg</SheetDescription>
    </SheetHeader>
    <div className="grid gap-4 py-6">
      <div className="grid gap-1.5">
        <Label htmlFor="domain">Domain</Label>
        <Input id="domain" defaultValue="tomato.gg" />
      </div>
    </div>
    <SheetFooter>
      <SheetClose asChild><Button variant="ghost">Cancel</Button></SheetClose>
      <Button variant="accent">Save changes</Button>
    </SheetFooter>
  </SheetContent>
</Sheet>
```

---

### .Design Sync Docs Skeleton (`.design-sync/docs/Skeleton.md`)

---
category: Data display
---

Loading placeholder. `Skeleton` is a single `div` with `animate-pulse`, 2.8px radius, and a neutral-850 fill (one step above the card surface); it has no intrinsic size, so every instance needs a height and width class. Compose skeletons to mirror the shape of the content they replace — the layout should not jump when real data lands.

Conventions:
- Stat tile: keep the real label, replace the figure with `h-9 w-[60px]` and the delta with `h-5 w-[50px]` (this is exactly what the overview does).
- Table / list: one `h-4` bar per row, `flex-1` for the name column and a fixed `w-12` for the figure column; 5-6 rows.
- Text: `h-5` for a title, `h-4 w-full` body lines, make the last line shorter (`w-64`).
- Avatar: `h-8 w-8 rounded-full`.
- Put skeletons inside the same `Card` / `CardContent` the data will use; for an in-place refetch over existing data use `CardLoader` instead.

```tsx
<Card>
  <div className="flex flex-col px-3 py-2">
    <div className="text-xs font-medium text-muted-foreground">Unique visitors</div>
    <div className="flex items-center justify-between">
      <Skeleton className="h-9 w-[60px] rounded-md" />
      <Skeleton className="h-5 w-[50px] rounded-md" />
    </div>
  </div>
</Card>
```

---

### .Design Sync Docs Slider (`.design-sync/docs/Slider.md`)

---
category: Forms
---

Numeric range control (Radix Slider) for settings that are tuned rather than typed: replay sample rate, session-duration filter, retention window. One component, no sub-parts: `Slider`. Props are Radix's: `defaultValue` / `value` as a number array (one entry per thumb, so `[40]` is a single thumb and `[30, 300]` is a two-thumb range), `onValueChange(values)`, `min` / `max` (default 0-100), `step`, `disabled`, `orientation`. It stretches to `w-full`.

Appearance: a 6px neutral-800 track, the filled range in emerald (`accent-500`), 16px near-black thumbs with a neutral-800 border. There is no built-in value label; pair it with a `Label` and put the current value in a muted `text-sm tabular-nums text-neutral-400` span on the same row. `disabled` blocks interaction but draws the same (Radix puts the disabled state on a span, so the thumb's opacity rule never matches); dim the whole row yourself if the state must read visually. Use `Slider` for approximate values where seeing the position matters; use an `Input type="number"` when the exact figure matters.

```tsx
const [rate, setRate] = React.useState([40]);

<div className="flex items-center justify-between mb-3">
  <Label>Session replay sample rate</Label>
  <span className="text-sm tabular-nums text-neutral-400">{rate[0]}%</span>
</div>
<Slider value={rate} onValueChange={setRate} max={100} step={5} />

// two thumbs = range
<Slider defaultValue={[30, 300]} min={0} max={600} step={10} />
```

---

### .Design Sync Docs Switch (`.design-sync/docs/Switch.md`)

---
category: Forms
---

Radix toggle for a single setting that takes effect immediately (session replay on/off, public dashboard, live mode). Track is 36×20px with a 16px white thumb: emerald (`accent-500`) when on, `neutral-800` when off, 50% opacity when disabled. Use `Checkbox` when the choice is submitted with a form or is one of many, and `RadioGroup` for mutually exclusive options.

Parts: `Switch` only (thumb is internal). Props: `checked` / `defaultChecked`, `onCheckedChange`, `disabled`, `id`, `name`, `className`. It carries the `peer` class, so a following `Label` fades automatically when the switch is disabled.

Conventions:
- Inline: `flex items-center gap-2` with the `Switch` first, then `Label htmlFor`.
- Settings list (the common case): rows of `flex items-center justify-between gap-4 px-4 py-3` inside a `divide-y divide-neutral-800 rounded-lg border border-neutral-800 bg-neutral-900` panel; label + `text-xs text-neutral-400` description on the left, switch on the right.
- One switch per row; never stack switches horizontally. Keep the label a noun phrase ("Web vitals"), not a verb.
- Hover/pressed transitions are not shown in previews.

```tsx
<div className="flex items-center justify-between gap-4 px-4 py-3">
  <div className="grid gap-0.5">
    <Label htmlFor="replay">Session replay</Label>
    <p className="text-xs text-neutral-400">Record sessions for the last 7 days.</p>
  </div>
  <Switch id="replay" defaultChecked />
</div>
```

---

### .Design Sync Docs Table (`.design-sync/docs/Table.md`)

---
category: Data display
---

Data table for dense analytics lists. Parts: `Table` (scroll wrapper + `<table>`) → `TableHeader` (neutral-850 band) → `TableRow` → `TableHead`; `TableBody` → `TableRow` → `TableCell`; optional `TableFooter` (totals) and `TableCaption` (below the table, muted). `TableSortIndicator` renders the sort chevrons for a sortable column: `sortDirection="asc" | "desc"` shows a blue arrow, `undefined` shows the neutral double chevron, `canSort={false}` hides it.

Conventions: 14px text, 8px cell padding, hairline row borders, no zebra striping. Right-align numeric columns and add `tabular-nums`. Keep the first column a name/path in `font-medium`. Use `Badge` for status cells and `Skeleton` rows while loading.

```tsx
<Table>
  <TableHeader>
    <TableRow>
      <TableHead>Page</TableHead>
      <TableHead className="text-right">Visitors <TableSortIndicator sortDirection="desc" /></TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell className="font-medium">/pricing</TableCell>
      <TableCell className="text-right tabular-nums">6,120</TableCell>
    </TableRow>
  </TableBody>
</Table>
```

---

### .Design Sync Docs Tabs (`.design-sync/docs/Tabs.md`)

---
category: Navigation
---

Segmented tabs (Radix). A filled `TabsList` pill strip where the active `TabsTrigger` lifts to a darker surface; each `TabsContent` panel shows only while its `value` is active. Use it to switch between views of one thing (Overview / Sessions / Goals, Desktop / Mobile). For the lighter underline style used inside report cards use `BasicTabs`.

Parts: `Tabs` (root; `defaultValue` for uncontrolled, `value` / `onValueChange` for controlled) → `TabsList` (36px strip, `bg-neutral-800`, `inline-flex` by default; add `grid w-full grid-cols-N` to stretch triggers evenly) → `TabsTrigger` (`value`, `disabled`; active state is `bg-neutral-950` with bright text) and `TabsContent` (`value`; adds `mt-2`).

Conventions: 2-5 triggers, sentence-case labels, no icons. Keep the list left-aligned above its content; stretch it full-width only for two-way toggles. Disable a trigger for a feature the plan lacks rather than hiding it. Focus rings are keyboard-only and are not shown in the static preview.

```tsx
<Tabs defaultValue="overview">
  <TabsList>
    <TabsTrigger value="overview">Overview</TabsTrigger>
    <TabsTrigger value="sessions">Sessions</TabsTrigger>
    <TabsTrigger value="goals">Goals</TabsTrigger>
    <TabsTrigger value="replay" disabled>Replay</TabsTrigger>
  </TabsList>
  <TabsContent value="overview">
    <div className="grid grid-cols-2 gap-3">{/* stat cards */}</div>
  </TabsContent>
  <TabsContent value="sessions">…</TabsContent>
  <TabsContent value="goals">…</TabsContent>
</Tabs>
```

---

### .Design Sync Docs Textarea (`.design-sync/docs/Textarea.md`)

---
category: Forms
---

Multi-line text field for notes, descriptions and pasted snippets. Same chassis as `Input` (transparent fill, 1px `neutral-800` border, `neutral-400` placeholder, ring on focus) with a 60px minimum height and vertical resize. Use it for annotation notes, site descriptions, alert messages, and raw filter/config text; use `Input` for anything that fits on one line.

Parts: `Textarea` only. Forwards every native `<textarea>` prop (`rows`, `placeholder`, `defaultValue`, `disabled`, `aria-invalid`, `ref`).

Conventions:
- Default height is `min-h-[60px]`; set `rows={5}` (or `className="min-h-32"`) for longer content.
- Text is 14px at md+ (`text-base` on phones so iOS doesn't zoom). Add `font-mono text-xs` for code-like content such as rule DSLs.
- There is no invalid style baked in (unlike `Input`): pass `aria-invalid="true"` for accessibility plus `className="dark:border-red-400"` for the red border (the `dark:` prefix is required: the component's own `dark:border-neutral-800` overrides a bare `border-red-400`), and put the message in a `text-xs text-red-400` line below. A right-aligned `text-xs text-neutral-400 tabular-nums` counter goes on the same row.
- `disabled` fades to 50% opacity. Always pair with a `Label` via `htmlFor`.

```tsx
<div className="grid gap-1.5">
  <Label htmlFor="note">Annotation note</Label>
  <Textarea id="note" placeholder="What changed on this date?" />
</div>

<Textarea rows={5} className="font-mono text-xs" defaultValue={"path contains /docs\nAND country is Germany"} />
```

---

### .Design Sync Docs TimelineSlider (`.design-sync/docs/TimelineSlider.md`)

---
category: Replay
---

The minimal playback rail of the session-replay transport bar: a 2px neutral-800 track, an emerald (`accent-500`) played range and a 10px emerald-300 thumb that scales up on hover and shows a grab cursor. Use it for any "position in time" control (replay progress, a funnel-step scrubber); use `Slider` for a value input with a visible track height, and `ActivitySlider` when the rail should also plot events and activity.

Single part: `TimelineSlider`, a thin wrapper over Radix `Slider.Root`, so the whole Radix API applies: `value` / `defaultValue` as a one-element array in the unit you choose (milliseconds for replay), `max` (the duration), `min`, `step`, `onValueChange`, `onValueCommit` (seek on release), `disabled`, `orientation`. Pass `className="flex-1"` so it fills the space between the elapsed and total times.

Conventions: sit it in a `flex items-center gap-3` bar on a neutral-900 panel with ghost `smIcon` transport buttons on the left, `m:ss` elapsed and total in 12px muted tabular figures on either side, and a playback-rate label at the end. Hover/active scaling can't be rendered statically. The thumb hue is the only chromatic element on the bar; everything else stays neutral.

```tsx
import { Button, TimelineSlider } from "@rybbit/ui";
import { Pause } from "lucide-react";

<div className="flex items-center gap-3 rounded-lg border border-neutral-800 bg-neutral-900 px-4 py-3">
  <Button size="smIcon" variant="ghost" aria-label="Pause"><Pause /></Button>
  <span className="text-xs tabular-nums text-neutral-400">0:42</span>
  <TimelineSlider
    className="flex-1"
    max={duration}
    step={100}
    value={[currentTime]}
    onValueChange={([t]) => setCurrentTime(t)}
    onValueCommit={([t]) => player.seek(t)}
  />
  <span className="text-xs tabular-nums text-neutral-400">3:00</span>
</div>
```

---

### .Design Sync Docs Toaster (`.design-sync/docs/Toaster.md`)

---
category: Feedback
---

Transient confirmations after an action ("Settings saved", "Couldn't verify domain"). Mount ONE `<Toaster />` at the app root (it is a configured react-hot-toast host: bottom-right, neutral-850 card with neutral-200 text in dark mode, 4.8px radius, 14px medium) and fire messages from anywhere with the `toast` function. Toasts stack newest at the bottom and auto-dismiss (success ~2s, default/error ~4s), so keep the message to one line that reads on its own. For state that must persist on the page use `Alert`; for something the user must acknowledge use `AlertDialog`.

Parts: `Toaster` (the host, no props) and `toast` (the API). The API takes a plain string: `toast(message)` (no icon), `toast.success(message)` (emerald check), `toast.error(message)` (red cross), `toast.info(message)` (an "i" glyph), `toast.dismiss(id?)` (dismiss one or all), `toast.custom(node, opts)` (escape hatch for a fully custom body). Every call returns the toast id.

Conventions: one toast per user action; name the object ("Goal “Signup completed” created", "Settings saved for tomato.gg"); an error says what to do next. Never use a toast for validation (that belongs in `FormMessage`) or for anything the user has to read later.

```tsx
import { Button, Toaster, toast } from "@rybbit/ui";

// once, at the root
<Toaster />

// anywhere
<Button
  variant="accent"
  onClick={async () => {
    try {
      await saveSettings();
      toast.success("Settings saved for tomato.gg");
    } catch {
      toast.error("Couldn't save settings. Try again.");
    }
  }}
>
  Save changes
</Button>
```

---

### .Design Sync Docs Tooltip (`.design-sync/docs/Tooltip.md`)

---
category: Overlays
---

Hover/focus label (Radix Tooltip). Use it to name icon-only buttons and to explain a metric or setting in one or two short lines. It is not interactive: if the content needs a link, a button or more than a couple of sentences, use `Popover` instead. `RybbitTheme` already mounts the required `TooltipProvider`, so never add another.

Parts: `Tooltip` (root; `open` for a controlled state, `delayDuration` default 300ms) → `TooltipTrigger` (`asChild` around the Button or icon) → `TooltipContent` (portalled: `text-xs`, raised neutral surface with a hairline border; props `side="top" | "right" | "bottom" | "left"` (default `top`), `align`, `sideOffset` default 4). `TooltipProvider` is exported but only for apps that opt out of `RybbitTheme`.

Conventions: keep the default `top` unless the trigger sits at a viewport edge. Content is sentence case, no trailing period for a single label. For a two-line explainer, put a `font-medium` heading first and the body in `text-neutral-400`, and cap width with `max-w-56`. Always give icon-only triggers an `aria-label` that matches the tooltip text. The hover-in state cannot be rendered statically; the previews use `open`.

```tsx
<Tooltip>
  <TooltipTrigger asChild>
    <Button variant="outline" size="icon" aria-label="Site settings">
      <Settings />
    </Button>
  </TooltipTrigger>
  <TooltipContent>Site settings</TooltipContent>
</Tooltip>

<Tooltip>
  <TooltipTrigger asChild>
    <button type="button" aria-label="What is bounce rate?"><Info className="h-3.5 w-3.5" /></button>
  </TooltipTrigger>
  <TooltipContent side="right" align="start" className="max-w-56">
    <p className="font-medium">Bounce rate</p>
    <p className="mt-1 text-neutral-400">Sessions with one pageview and no events.</p>
  </TooltipContent>
</Tooltip>
```

---

### Public Security (`docs/public/security.txt`)

# security.txt for rybbit.com       
# We accept vulnerability reports. We do not offer monetary or bug bounty rewards.

Canonical: https://rybbit.com/.well-known/security.txt
Contact: mailto:hello@rybbit.com
Preferred-Languages: en
Expires: 2027-01-01T00:00:00Z

---

### Server Docs Better Auth 1.7.3 (`server/docs/better-auth-1.7.3.md`)

# Better Auth 1.7.3 migration

This upgrade moves the client, server, API key and SSO packages from 1.6.25 to
1.7.3. MCP OAuth now uses `@better-auth/mcp`, `@better-auth/oauth-provider` and
the JWT plugin, with an explicit consent page at `/auth/consent`.
`@better-auth/infra` keeps its separate 0.1.x version. The MCP SDK and stateless
transport remain on their existing versions; dynamic client registration stays
enabled.

## Database and cutover

Migration `0018_better_auth_173.sql` is additive. It creates the OAuth/JWT tables,
copies registered clients, hashes their previously plaintext secrets, and adds
team membership keys and member counts. Counts are backfilled; existing nullable
membership keys use Better Auth's compatibility lookup. New memberships use the
same key calculation as Better Auth.

Auth uses the native PostgreSQL/Kysely adapter, so its array fields are stored
as JSONB. Drizzle's native PostgreSQL arrays are not compatible with that
adapter's serialization. The Drizzle schema and snapshot reflect this.

1. Back up PostgreSQL and record the current application version. Run only the
   opening `DO` block from migration 0018 as a read-only preflight: it checks
   duplicate account identities and parses every nonempty legacy client metadata
   value with PostgreSQL's JSON parser. Fix any reported record before cutover.
2. Stop the old backend workers before migrating. Client registrations and team
   membership changes must not race the data copy and count backfill.
3. Apply migration 0018 with the existing file-based migration runner. The Docker
   backend entrypoint already runs `npm run db:migrate` before starting. For a
   manual deployment, run that command from `server/` against the deployment's
   database before starting the new backend.
4. Deploy the matching client and server. Keep `BASE_URL` set to the public site
   origin and keep the existing `BETTER_AUTH_SECRET`. OAuth requires HTTPS, with
   an exception for HTTP loopback during development. Plain HTTP LAN deployments
   keep session and API key authentication but do not enable OAuth.
5. Check password login, Google/GitHub sign-in where configured, existing
   sessions/API keys, team updates, and an MCP connection through discovery,
   login, consent and a tool call. Sign out of the linked session and confirm
   its MCP bearer is rejected on the next request. The explicit `BASE_URL` must
   produce the registered `/api/auth/callback/google` and
   `/api/auth/callback/github` URLs; check this in both development and production.

No live database migration is part of preparing this change.

The migration stops before schema writes if accounts contain duplicate
`(providerId, accountId)` pairs or legacy OAuth client metadata is invalid JSON.
The metadata error identifies the affected `oauthApplication.id` without logging
its contents. Resolve these records explicitly before retrying. Do not apply an
account `issuer` backfill from the 1.7.0–1.7.2 instructions: upstream reverted
that migration in 1.7.3.

## MCP client behavior

- Existing OAuth clients must reconnect and approve fresh consent. Old access
  tokens, refresh tokens and consent are retained in the legacy tables but are
  not accepted by the new provider. Password sessions and API keys are not
  rewritten by this migration.
- Registered client IDs and redirect URIs are preserved. Imported confidential
  clients use `client_secret_basic`; the old schema did not record their token
  authentication method. Clients that previously posted a secret must switch to
  HTTP Basic or register again with their intended method.
- New native clients with loopback redirects must register
  `application_type: "native"` and `token_endpoint_auth_method: "none"`.
  Other public clients must also explicitly send
  `token_endpoint_auth_method: "none"`. Omitting it now creates a confidential client using
  `client_secret_basic`, even for unauthenticated registration. Such a client
  receives a secret and must authenticate with it or subsequent token requests
  fail with `invalid_client`.
- Discovery advertises the new `/api/auth/oauth2/*` endpoints. Clients caching
  the old endpoints should rediscover the server.
- Resource tokens are verified for signature, issuer, expiry, audience,
  client state and validity of the linked session. These Bearer routes reject
  proof-bound tokens.
  The single resource remains `<BASE_URL>/api/mcp`; per-client resource join
  enforcement is disabled so imported clients can request it.
- Access tokens are JWTs with the upstream default lifetime of one hour. There
  is no per-access-token database revocation check: `/oauth2/revoke` rejects JWT
  access tokens with `unsupported_token_type`. Revoking a refresh token prevents
  future refreshes but leaves already-issued access JWTs valid until expiry.
  Deleting or disabling the OAuth client, or signing out of the linked session,
  makes those JWTs fail verification. Do not describe a refresh-token revocation
  or consent deletion as immediately disconnecting existing access tokens.
- Existing scope semantics remain: a grant with only standard OIDC scopes has
  full API access within the user's permissions. The consent page states this
  explicitly. Custom scopes narrow that access.

## Rollback

The old OAuth tables remain available, and the new columns are additive. A
rollback needs a coordinated stop of the new workers before restoring the old
application version. New client registrations and consent are not copied back;
those clients will need to register or reconnect again. Old tokens retained in
the legacy tables can become usable again under the old binary, including tokens
whose corresponding access was revoked after cutover. Account for those grants
before restoring OAuth traffic. Recompute team member counts before returning
to 1.7 after any period of running 1.6.

## Verification

The team's internal `memberCount` reservation counter can drift after user
deletions that cascade to memberships. Rybbit's team lists read the membership
rows, and `maximumMembersPerTeam` is not configured. Reconcile this counter on
all deletion paths before enabling that limit; it is not an authoritative count
for reporting. This remains a follow-up rather than a cutover requirement.

The disposable PostgreSQL integration test applies every migration through 0018
using the production Kysely adapter. It checks client migration and secret
hashing, duplicate-account and malformed-metadata rejection, discovery, signed
consent, PKCE token exchange, refresh and its revocation, JWT revocation limits,
bearer verification, sign-out revocation, pre-migration
sessions/API keys, team keys and email OTP password recovery. Client tests cover
consent, rejection, error retry and preservation of the signed login query.

Validation used Node 24: the server suite passed with four workers, the client
suite passed, both packages type-checked, and the client production build passed
with `npm run build -- --webpack`. A headless browser check also exercised the
real client plugin's signed-query submission and the mobile consent layout.
Local Turbopack builds cannot resolve the existing `@rybbit/shared` symlink
outside the client root; the production Dockerfile already materializes that
package before building. `npm run lint` is blocked by the existing empty ESLint
configuration scanning generated `.next` files and encountering undefined rules.
The 17 new consent messages are translated in all 11 non-English locales, with
their interpolation placeholders and existing translations preserved.

Upstream references: [1.7 upgrade guide](https://better-auth.com/docs/guides/1-7-upgrade-guide),
[MCP plugin](https://better-auth.com/docs/plugins/mcp), and
[changelog](https://better-auth.com/changelog).

---

### Content Blog 5k Stars (`docs/content/blog/5k-stars.mdx`)

---
title: "How Rybbit Got 5,000 GitHub Stars in 9 Days"
date: 2025-05-26
updated: 2026-09-28
description: "The launch story behind Rybbit's first 5,000 GitHub stars: what we posted, where it spread, and what we'd do again."
keyword: "github stars"
tags: ['growth']
author: Rybbit Team
---



## The backstory

I run an open source web analytics platform with a freemium hosted version. I have a gaming stats site that gets around 1.5 million visits a month, so I needed serious web analytics. The existing platforms were either too simple (just a basic dashboard) or too complicated and enterprisey, so I decided to build my own web analytics platform since I love stats anyway.

I started working on it in January 2025, and it took around 110 days for me to launch. Rybbit being open source made the distribution problem so much easier, as I could freely advertise it in many places since it was self-hostable and free to use.

## The launch strategy

I made posts on 2 major subreddits - r/SideProject and r/selfhosted, plus a few smaller subs. Pretty much every post reached #1 in hot and I probably got 150k free impressions from this.

**TL;DR** - all I did was make like 4 Reddit posts with the same headline, got 150k free impressions, and then proceeded to go viral on X, Hacker News, Bluesky, Mastodon, and some random Asian language sites from other people sharing my project.

## What I think I did right

### 1. Full iframe embed as hero section
I put a full iframe embed of my demo site as my hero section. A lot of analytics platforms have a demo site, but I went a step further by embedding the entire demo site into the landing page so any visitor is immediately greeted by a full demo of the product. I haven't ever seen anyone do this, but I think this is an insane hack if this suits your product.

### 2. Visual-first GitHub README
I included a ton of product screenshots on my GitHub README and kept the text content short since I had a landing and docs page. People have short attention spans and aren't gonna read a wall of text.

### 3. Catchy positioning
I used the post title "I built an open source Google Analytics replacement" - simple but effective positioning that immediately communicates the value.

### 4. Strong visual assets
I created a good OpenGraph image that showed the best features of the product, which helped with social sharing.

### 5. Polish matters
I had a genuinely good & polished product with a fully functional landing page, docs, self-hosting guide, and SaaS product that I personally use every single day. I doubt I would've gotten as much traction if I released a barely-working MVP after a few weeks.

## What didn't work

I made a couple launch posts from my X account, but I only had a couple hundred followers so nobody cared (though I didn't expect to go viral on X in the first place).

Some big Discord servers like the Next.js server have #showcase channels. Literally nobody reads these.

## The results and reality check

I optimized my launch for reach instead of revenue, so I wasn't surprised that I didn't get a huge surge of paid plan signups. The vast majority of people are self-hosting it since my most viral post was in r/selfhosted. I'm just playing the numbers game and hoping that a small single-digit percentage of people who see it will pay for the cloud version.

So far I have around 700 total signups and $900 in total revenue in 3 weeks - not life-changing money, but solid validation.

## What's next

The virality cooled down after the first week, but the organic growth is still going strong. I'm at 5.8k stars 3 weeks post-launch, so the daily gain is still around ~50 stars. I expect to hit 10k stars in around 3 months.

---

### Content Blog Best Google Analytics Alternatives (`docs/content/blog/best-google-analytics-alternatives.mdx`)

---
title: "9 Best Google Analytics Alternatives in 2026 (Free & Paid)"
description: "Nine Google Analytics alternatives compared on pricing, privacy, features and self-hosting, including free and open-source options. Pricing verified in 2026."
keyword: "google analytics alternatives"
date: 2026-07-19
updated: 2026-09-28
tags: ["analytics", "privacy", "comparisons"]
author: Rybbit Team
---

Google Analytics is still the default, and still the tool people most want to leave. Since the forced GA4 migration, the complaints have been remarkably consistent: the interface buries simple questions under 150+ reports, busy properties get sampled data, cookie-based tracking forces consent banners that silently drop a third or more of your real traffic, and several EU data-protection authorities have ruled that Google Analytics data transfers violated GDPR.

The good news is that the alternatives in 2026 aren't compromises anymore. Several of them ship features GA4 has never had (session replay, error tracking, uncomplicated funnels) while staying cookie-free and consent-banner-free.

Full disclosure up front: we build [Rybbit](https://rybbit.com), the first tool on this list. We've kept every claim about the other tools factual (pricing and limits below come from each vendor's own published pages as of July 2026), and we'll tell you honestly when another tool is the better fit.

## Quick comparison

| Tool | Starting price | Free tier | Open source / self-host | Best for |
| --- | --- | --- | --- | --- |
| [Rybbit](#1-rybbit) | $19/mo (100k events) | ❌ (7-day trial) | ✅ Free | Teams that want GA4-level depth without the complexity |
| [Plausible](#2-plausible) | $9/mo (10k pageviews) | 30-day trial | ✅ Community Edition | Simple, lightweight traffic stats |
| [Matomo](#3-matomo) | €29/mo cloud (50k hits) | Free on-premise | ✅ Free | Feature parity with old Universal Analytics |
| [Fathom](#4-fathom-analytics) | Slider pricing ($45/mo at 500k) | 7-day trial | ❌ | Set-and-forget simple stats |
| [Umami](#5-umami) | $20/mo (1M events) | ✅ Free Hobby plan | ✅ Free (MIT) | Developers and hobby projects |
| [PostHog](#6-posthog) | Usage-based after free tier | ✅ 1M events/mo free | Partially | Product teams that want the full toolkit |
| [Simple Analytics](#7-simple-analytics) | $20/mo | ✅ For hobby sites | ❌ | EU-hosted simplicity |
| [Pirsch](#8-pirsch) | $6/mo (10k pageviews) | 30-day trial | Enterprise only | Budget-friendly privacy analytics |
| [Cloudflare](#9-cloudflare-web-analytics) | Free | ✅ Entirely free | ❌ | A free rough signal, not a GA replacement |

## Why people are leaving GA4

Before the list, it's worth being precise about what you're escaping, because it determines which alternative fits:

- **Consent loss.** GA4 needs cookies, so GDPR/ePrivacy requires a consent banner in most jurisdictions. A large share of visitors decline. Teams that switch to cookieless analytics routinely find their real traffic is 30–60% higher than GA4 reported.
- **Sampling and retention.** Complex reports on busy properties get sampled, and standard GA4 properties keep event-level data for at most 14 months. Unsampled exports live in GA360, which starts around $50,000/year.
- **Complexity.** GA4 is an event warehouse with a reporting UI, not an analytics product. Most teams need a dashboard, not a data modeling project.
- **Legal risk.** Data-protection authorities in Austria, France, and Italy all found GA's EU–US transfers unlawful in the Schrems II era. The regulatory direction hasn't reversed.

If you searched for a **GA4 alternative** specifically because of one of these, jump to the tool that fixes it: consent loss and legal risk → any cookieless tool below; sampling → [Rybbit](#1-rybbit) or [Matomo](#3-matomo); complexity → [Plausible](#2-plausible), [Fathom](#4-fathom-analytics), or [Pirsch](#8-pirsch).

## 1. Rybbit



**Best for: teams that want GA4-level depth (funnels, session replay, error tracking) without GA4's complexity or cookie banners.**

[Rybbit](https://rybbit.com) is an open-source, cookie-free analytics platform that deliberately sits between the "simple stats" tools and the heavyweight product-analytics suites. The dashboard reads like Plausible (one page, instantly legible), but underneath it ships the features that usually force teams back to Google: [funnels](/features/funnels), [user journeys](/features/user-journeys), [session replay](/features/session-replay), [error tracking](/features/error-tracking), [Web Vitals](/features/web-vitals), user profiles, and custom events with attributes.

Because it's cookie-free with daily-rotating visitor hashes, you don't need a consent banner, and you see every visitor, including the ones GA4's banner was hiding from you. There's no sampling at any traffic level. Independent reviewers have come to the same conclusion: Travis of travis.media called Rybbit [the best Google Analytics alternative](https://travis.media/blog/rybbit-best-google-analytics-alternative/) after testing it against the field.

[Pricing](/pricing) starts at $19/month for 100k events with a 7-day free trial; [session replay](/features/session-replay) and unlimited sites come with Pro, from $39/month. It's also fully open source and [free to self-host](/docs/self-hosting) with Docker. The main limitation is that there's no historical import from GA4 (Plausible, for one, [offers a GA importer](https://plausible.io/docs/google-analytics-import)); Rybbit's importers cover Plausible, Umami, and Simple Analytics. Rybbit is also younger than Matomo or Plausible, though it ships things Plausible lacks, such as session replay and error tracking.

## 2. Plausible



**Best for: simple, lightweight traffic stats with the longest track record among privacy tools.**

Plausible is the tool that made privacy-first analytics mainstream: a single-page dashboard, a script it advertises as ~54× smaller than GA's, and cookieless tracking that needs no consent banner. It's open source, EU-hosted, and independent.

The trade-off is depth. Plausible intentionally stops at traffic analytics: goals and simple funnels exist, but there's no session replay, no error tracking, no user-level view. If your questions are "where do visitors come from and what do they visit," it answers beautifully; if you ask "why did signups drop," you'll need another tool alongside it.

Starter is $9/month for 10k pageviews on one site; Growth at $14/month and Business at $19/month add sites and team members. The trial runs 30 days with no card required, annual billing gets you two months free, and self-hosters have a free Community Edition. Two things to watch: pageview-based pricing climbs with traffic, and the feature set is deliberately minimal.

See our detailed [Rybbit vs Plausible](/compare/plausible) comparison.

## 3. Matomo



**Best for: teams that want the closest thing to old Universal Analytics, or a mature free self-hosted option.**

Matomo (formerly Piwik) is the veteran of this list, nearly two decades old, with the largest feature surface of any GA alternative: heatmaps, A/B testing, tag manager, and hundreds of plugins. Its free on-premise Community Edition has no hit limits, which has made it the default for public-sector and compliance-heavy deployments.

The honest downside is age: the interface feels like the era it came from, and the full feature set on-premise means assembling paid plugin bundles (Team bundles from €275). Cloud pricing is hit-based and EU-hosted (Frankfurt) with no data sampling.

Cloud starts at €29/month for 50k hits with a no-card free trial, while the on-premise Community Edition is free forever, and it's the most battle-tested self-hosted analytics there is. Weigh that against the dated UX, plugin-based pricing complexity, and a heavier footprint to run than newer tools.

See [Rybbit vs Matomo](/compare/matomo).

For a head-to-head with GA4, see [Matomo vs Google Analytics](/compare/matomo-vs-google-analytics).

## 4. Fathom Analytics



**Best for: set-and-forget simple stats from a long-running independent company.**

Fathom was one of the first paid privacy-analytics products, and it stays deliberately simple: one dashboard, cookieless tracking, forever data retention, and every plan includes at least 50 sites. It's a polished, stable product from a small company that's been profitable for years.

Pricing is a slider by pageviews: $45/month at 500k pageviews (the entry 100k tier is priced lower on their slider), with a 7-day trial and two months free on annual billing. There's no free tier (their FAQ is upfront about that) and no self-hosting. Simple by design also means no funnels on the level of product tools and no replay.

See [Rybbit vs Fathom](/compare/fathom).

## 5. Umami



**Best for: developers and hobby projects that want free, minimal, MIT-licensed analytics.**

Umami is a lightweight open-source analytics tool that developers love: trivial to self-host, MIT-licensed, and its cloud has a genuinely free Hobby plan. The product covers pageviews, referrers, custom events, and basic reports, and stops there.

The Hobby cloud plan is free forever; Pro is $20/month for 1M events, with a 14-day trial on paid plans. Self-hosting is free and among the easiest to run (a small Node app plus a database). Keep expectations in check, though: reporting depth is minimal, the team behind it is small, and most of its adoption is the free tier.

See [Rybbit vs Umami](/compare/umami).

## 6. PostHog

**Best for: product engineering teams that want analytics, feature flags, A/B testing, and a data warehouse in one platform.**

PostHog comes at this list from the other direction: it's a product-analytics platform that happens to include a web analytics dashboard. If you want feature flags, experiments, SQL access to raw events, and session replay in a single vendor, it's the strongest option, and its free tier is huge: 1M events every month, no card required.

The flip side: PostHog is a lot. The pricing is per-product and usage-based (from $0.00005/event after the free tier), which is cheap at small scale and requires spreadsheet attention at large scale, and the interface has GA4-ish complexity because it's solving a bigger problem than web analytics.

You get 1M events/month free, then usage-based billing, with a card only needed past the free tier. A hobby self-deploy exists, but the real product is cloud. Both the complexity and the cookie/consent posture depend on how you configure it, and web analytics is a side dish, not the meal.

See [Rybbit vs PostHog](/compare/posthog).

## 7. Simple Analytics



**Best for: EU-based companies that want the most privacy-conservative option.**

Amsterdam-based Simple Analytics is exactly what the name says, with a notable 2026 twist: it now positions itself as a *complement* that fixes the traffic GA4 misses, arguing GA can under-report by up to 60%. It's EU-hosted end to end, has a free tier for hobby sites, and an AI chat interface over your stats.

The hobby tier is free (1 user, 5 websites, 1 month of history); paid plans start at $20/month with usage-based sliders, and the 14-day trial needs no card. Self-hosting is not offered. Watch the per-user pricing if you have a team, and note the short history on the free tier. Like Plausible and Fathom, it's simple by design.

See [Rybbit vs Simple Analytics](/compare/simpleanalytics).

## 8. Pirsch

**Best for: the lowest-priced paid entry point in privacy analytics.**

Pirsch is a German-built, cookieless analytics tool with developer-friendly touches (server-side tracking via SDKs is a first-class feature) and aggressive pricing: $6/month covers 10k pageviews across up to 50 websites with unlimited team members and unlimited retention. The $12/month Plus tier adds funnels and A/B testing. It also ships importers for GA, Plausible, and Fathom.

Plans start at $6/month with a 30-day no-card trial. Self-hosting is gated to the paid Enterprise tier; the code isn't free OSS. And the feature set is smaller than Rybbit's or PostHog's, with no session replay or error tracking.

## 9. Cloudflare Web Analytics

**Best for: a free rough signal when you can't justify any budget, not a GA replacement.**

Cloudflare's free web analytics is cookieless and takes one minute to enable if you're already behind Cloudflare. But know what you're getting: per Cloudflare's own developer docs, dashboard stats are based on a ~10% sample of page-load events and retention is limited to 30 days. There are no custom events, goals, or funnels.

The price is free. The limitations are the point: sampled reporting, ~30-day retention, and minimal features make it fine as a sanity check and unusable as your only analytics.

## Which Google Analytics alternative should you pick?

- Depth without the pain (funnels, replay, errors, and a dashboard anyone can read): **[Rybbit](/pricing)**. That's the exact gap we built it for.
- The simplest possible stats page: **Plausible**, **Fathom**, or **Pirsch** (cheapest), in roughly that order of polish.
- Maximum features, and you don't mind the learning curve: **PostHog** (modern, product-focused) or **Matomo** (mature, UA-like).
- Free forever: **Umami's** Hobby cloud tier, or self-host **Rybbit**, **Umami**, **Plausible CE**, or **Matomo**, all [genuinely free to run yourself](/docs/self-hosting).
- Zero budget and zero setup: **Cloudflare**, eyes open about sampling.

## FAQ

**Do I still need a cookie banner with these tools?**
Not for the cookieless ones (Rybbit, Plausible, Fathom, Umami, Simple Analytics, Pirsch, Cloudflare) when used as designed: they don't set cookies or track individuals across sites. GA4 and full-configuration PostHog generally do require consent in the EU.

**Can I import my Google Analytics history?**
Effectively no. Google only exports GA4 data to BigQuery, and no alternative can rebuild its reports from that. The standard move is to run your new tool in parallel with GA4 for a few weeks, then archive the GA4 property. (Rybbit does [import data](/docs) from Plausible, Umami, and Simple Analytics if you're switching between alternatives.)

**Is Google Analytics illegal in Europe?**
It's not banned, but DPAs in Austria, France, and Italy ruled its EU–US data transfers unlawful under GDPR, and using it compliantly requires consent banners plus careful configuration. Cookieless alternatives remove most of that burden.

**What's the best free Google Analytics alternative?**
Self-hosting an open-source tool (Rybbit, Umami, Plausible CE, Matomo) gives you the most for free if you can run Docker. If you want free *cloud* hosting, Umami's Hobby plan and PostHog's 1M-events tier are the real ones; Cloudflare is free but sampled.

---

*[Start a free Rybbit trial](https://app.rybbit.io/signup) or explore the [live demo](https://demo.rybbit.com/81) to see what GA4 has been hiding. Setup is one script tag and takes about five minutes.*

---

### Content Blog Best Session Replay Tools (`docs/content/blog/best-session-replay-tools.mdx`)

---
title: "10 Best Session Replay Tools in 2026 (Free & Open Source)"
description: "Compare 10 session replay tools for 2026: free options like Clarity, open source picks like Rybbit and OpenReplay, plus verified prices and limits."
keyword: "session replay tools"
date: 2026-09-13
updated: 2026-09-27
tags: ["session replay", "comparisons", "tools"]
author: Rybbit Team
---

Session replay tools record what a visitor does on your site (clicks, scrolls, page changes, masked typing) and let you watch it back like a video. This guide compares ten of them on price, limits, privacy defaults, and whether you can run them on your own servers.

Disclosure up front: we build [Rybbit](https://rybbit.com), one of the tools on this list. Every claim about the other nine comes from that vendor's own pricing or documentation pages, linked inline and checked in September 2026. Where a vendor doesn't publish a number, we say so rather than guess.

## Quick comparison

| Tool | Best for | Free tier | Starting price (Sept 2026) | Self-host | Open source |
| --- | --- | --- | --- | --- | --- |
| [Rybbit](#rybbit) | Replay inside privacy-first analytics | No (7-day trial) | Pro from $39/mo (not on Standard) | Yes, free | Yes, AGPL-3.0 |
| [Microsoft Clarity](#microsoft-clarity) | Free, unlimited recordings | Yes, unlimited | $0 | No | No |
| [Hotjar / Contentsquare](#hotjar-contentsquare) | Heatmaps, replay and surveys | Yes, 10k replays/mo | Growth from $49/mo | No | No |
| [FullStory](#fullstory) | Enterprise experience analytics | Yes, 30k sessions/mo | Quote only | No | No |
| [LogRocket](#logrocket) | Front-end debugging | Yes, 1k sessions/mo | Core from $83/mo (10k sessions) | Enterprise only | No |
| [PostHog](#posthog) | Product analytics suite | Yes, 5k recordings/mo | $0.005 per recording after | Yes, with caveats | Yes, MIT core |
| [OpenReplay](#openreplay) | Self-hosted replay for developers | Self-hosted only | Cloud from $199/mo | Yes, free | Yes, AGPL-3.0 core |
| [Lucky Orange](#lucky-orange) | Small-business CRO bundle | No (7-day trial) | $39/mo (3,500 sessions) | No | No |
| [Mouseflow](#mouseflow) | Friction and form analytics | Yes, 500 sessions/mo | $25/mo (5,000 sessions) | No | No |
| [Sentry Session Replay](#sentry-session-replay) | Replay attached to errors | Yes, 50 replays/mo | Team $26/mo plus usage | Yes | Fair Source (FSL) |

## How to choose a session replay tool

**Privacy controls.** What is masked *before* data leaves the browser, and is that the default? Sentry masks all text, inputs and media unless you loosen it. Clarity masks numbers, emails and every input box. PostHog and Rybbit mask input values by default but show page text unless you tag it. LogRocket only guarantees that passwords are never recorded.

**Sampling and limits.** Vendors meter replay per session (Mouseflow, Lucky Orange, LogRocket), per recording (PostHog, Sentry), or with a replay cap separate from the session or event quota (Contentsquare, and Rybbit, whose Pro plan includes replays equal to 10% of its event allowance). Retention runs from 30 days (Clarity, Rybbit cloud, Sentry's free plan) to 12 months (FullStory's free plan, Mouseflow Premium).

**Bundled or standalone.** Replay is most useful next to the data that sent you there: a funnel drop-off, a JavaScript error, a rage-click. Rybbit, PostHog, Sentry and LogRocket bundle replay with analytics or error tracking; Clarity, Hotjar, Mouseflow and Lucky Orange pair it with heatmaps.

**Self-hosting.** If recordings can't leave your infrastructure, the shortlist is Rybbit, OpenReplay, PostHog (with caveats) and Sentry. Everything else here is cloud only.

**Cost at 100k sessions a month.** Starting prices hide the curve. This is what each tool's published pricing works out to at roughly 100,000 sessions or recordings a month, computed from the pages linked in each section:

| Tool | Approximate monthly cost at 100k sessions |
| --- | --- |
| Rybbit | $39 to $99 on Pro (billed by events: 100k to 500k) |
| Microsoft Clarity | $0 |
| Hotjar / Contentsquare | $409 on Growth, but replays are capped at 20k |
| FullStory | Quote only (the free plan stops at 30k sessions) |
| LogRocket | Not published (25k sessions starts at $176) |
| PostHog | About $272 (5k free, then tiered per recording) |
| OpenReplay | $199 Dedicated, no recording limits |
| Lucky Orange | $839 billed annually (Scale, the 300k tier) |
| Mouseflow | $319 (Premium) |
| Sentry | About $383 (Team plan plus pay-as-you-go replays) |

## Rybbit

[Rybbit](https://rybbit.com) is open-source, privacy-first web analytics with [session replay](/features/session-replay) built into the same dashboard as funnels, user journeys, error tracking, Web Vitals and user profiles. Replay records DOM mutations rather than video. You filter recordings by minimum duration, browser, device, country, referrer, channel or user ID, then press play; each replay carries a breadcrumb timeline of pages, clicks and custom events.



Replay is opt-in per site ([feature guide](/docs/feature-guides/session-replay)), all input values are masked by default, and the [script attributes](/docs/script) let you block elements entirely, ignore interactions on specific fields, or mask text by selector. The analytics side is cookieless, so there is no consent banner to lose visitors behind.

The honest limits: on Rybbit Cloud, replay is a **Pro-plan feature**. Standard (from $19/month) doesn't include it; Pro starts at $39/month for 100k events, or $26/month billed annually, per the [pricing page](/pricing). Plans start with a 7-day trial (a card is collected at signup and not charged until the trial ends). Cloud replays are kept for 30 days. There are no heatmaps, and replay is web only; the [React Native SDK](/docs/sdks/react-native) tracks events but doesn't record screens. If you [self-host](/docs/self-hosting), everything including replay is free under AGPL-3.0 and you set your own retention.

- **Pros:** replay, analytics, errors and funnels in one tool; inputs masked by default; replay quota grows with the plan (10,000 a month at 100k events); free self-hosting with replay included
- **Cons:** replay needs the Pro plan on cloud; 30-day cloud retention; no heatmaps; no native mobile replay
- **Pricing (as of September 2026):** Standard from $19/month, Pro (with replay) from $39/month at 100k events; self-hosted free. Source: [rybbit.com/pricing](/pricing)

## Microsoft Clarity

[Clarity](https://clarity.microsoft.com) is what people mean by "free session replay". Microsoft's [FAQ](https://learn.microsoft.com/en-us/clarity/faq) says: "Clarity is a free service forever. You never encounter traffic limits or are compelled to upgrade to a paid version", with no sampling and no credit card. Recordings, heatmaps and ML insights are included, and there are [mobile SDKs](https://learn.microsoft.com/en-us/clarity/mobile-sdk/) for Android, iOS, React Native, Flutter and Cordova/Ionic.

Recordings are kept for 30 days (favorites and a random sample stay up to 9 months). Data lives with Microsoft, with no self-hosted option. And the same FAQ notes that for visitors in the EEA, UK and Switzerland, "valid consent is required" to use recordings, and without it Clarity limits data collection in those regions.

Masking defaults are sensible: [Balanced mode](https://learn.microsoft.com/en-us/clarity/setup-and-installation/clarity-masking) masks numbers and email addresses, input boxes and drop-downs are masked in every mode, and Strict mode masks everything.

- **Pros:** free with no traffic limits; no sampling; heatmaps and native mobile SDKs included
- **Cons:** 30-day recording retention; consent required for EEA/UK/Swiss visitors; cloud only, hosted by Microsoft
- **Pricing (as of September 2026):** $0, no paid tiers. Source: [Clarity FAQ](https://learn.microsoft.com/en-us/clarity/faq)

## Hotjar (Contentsquare)

Hotjar no longer exists as a separate product. [Contentsquare's Hotjar page](https://contentsquare.com/hotjar/) says the two platforms "have merged into a single, more powerful experience intelligence platform", and hotjar.com/pricing now redirects to [contentsquare.com/pricing](https://contentsquare.com/pricing/). New signups get Contentsquare's Free, Growth, Pro and Enterprise plans.

Free gives 200,000 monthly sessions, unlimited heatmaps, basic funnels and error monitoring, on 1 project. The catch is that replay is a separate, fixed allowance of 10,000 captures a month, which the pricing page says "can't be increased". Growth starts at $49/month for 7,000 sessions and 7,000 replays and reaches $409/month at 100,000 sessions, but the replay cap tops out at 20,000 on every Growth tier. Growth keeps session data for 13 months and replays for 2 months; Contentsquare's [free-plan post](https://contentsquare.com/blog/contentsquare-free-plan/) lists 1 month of retention on Free. Annual billing takes 20% off.

Privacy defaults carry over from Hotjar: keystrokes are suppressed by default and replaced with asterisks, and you add `data-hj-allow` to fields you want captured, per the [Hotjar help center](https://help.hotjar.com/hc/en-us/articles/36820034730641-How-to-Show-Elements-and-Keystrokes-in-Data-Collection). Contentsquare describes Experience Analytics as covering "sites and mobile apps". There is no self-hosted edition.

- **Pros:** generous free session quota; heatmaps, surveys and replay together; 15-day Growth trial without a card
- **Cons:** replay allowance is fixed and small relative to sessions; 2-month replay retention on Growth; cloud only
- **Pricing (as of September 2026):** Free (200k sessions, 10k replays); Growth from $49/month. Source: [contentsquare.com/pricing](https://contentsquare.com/pricing/)

For heatmaps and surveys as well as replay, see our [Hotjar alternatives](/blog/hotjar-alternatives) guide.

## FullStory

[FullStory](https://www.fullstory.com) is the enterprise option: replay plus product analytics, funnels and retroactive data history. The paid tiers (Business, Advanced, Enterprise) have no published prices; each reads "Request pricing & demo" on the [pricing page](https://www.fullstory.com/pricing/). Mobile analytics is an add-on on paid plans.

What is published is the free plan: FullstoryFree gives "30,000 sessions per month" with "12 months of data retention" for up to 10 users, no credit card required.

Privacy is chosen at setup from three modes: Full Capture, Form Privacy, and [Private by Default](https://help.fullstory.com/hc/en-us/articles/360044349073), under which "no text is captured or sent outside the user's browser unless it is explicitly allowlisted as safe to capture".

- **Pros:** 30k free sessions with 12-month retention; strong privacy mode; mature enterprise analytics
- **Cons:** no public pricing above the free plan; cloud only; mobile is an add-on
- **Pricing (as of September 2026):** Free (30k sessions/month); paid plans by quote. Source: [fullstory.com/pricing](https://www.fullstory.com/pricing/)

For more options by use case, see our [FullStory alternatives](/blog/fullstory-alternatives) guide.

## LogRocket

[LogRocket](https://logrocket.com) is replay for engineers. Recordings include console logs, network requests and exceptions, and the [mobile SDKs](https://logrocket.com/products/mobile) cover Android, iOS, React Native, Flutter, Ionic and Expo. Analytics, error and log events are unlimited on every plan; you pay for sessions captured.

The [pricing page](https://logrocket.com/pricing) calculator starts with a Free plan for 1,000 sessions a month, then Core at $83/month for 10,000 sessions and $176/month for 25,000; paid features also come with a 14-day trial. A Conditional Recording option captures roughly a quarter of sessions, and Pro features are "included free above 100K sessions/mo". Self-hosting is an Enterprise option for regulated industries.

On privacy, LogRocket's [documentation](https://docs.logrocket.com/docs/privacy) states that password fields are never recorded; masking anything else uses a `data-private` attribute or the SDK's sanitizers, so the safe default is your responsibility.

- **Pros:** deep debugging context per session; broad mobile coverage; unlimited non-session events
- **Cons:** the Free plan stops at 1k sessions, and 25k sessions costs $176/month; masking beyond passwords is opt-in; self-host is Enterprise only
- **Pricing (as of September 2026):** Free for 1k sessions/month; Core from $83/month for 10k sessions; 14-day trial. Source: [logrocket.com/pricing](https://logrocket.com/pricing)

## PostHog

[PostHog](https://posthog.com) bundles replay with product analytics, feature flags, experiments and surveys on usage-based pricing. The [replay pricing page](https://posthog.com/docs/session-replay/pricing) gives 5,000 web recordings and 2,500 mobile recordings free every month, no card required, then charges $0.0050 per web recording from 5k to 15k, falling to $0.0015 above 500k. Mobile recordings cost $0.0100 each on the first paid tier.

Retention follows your plan: [up to 30 days](https://posthog.com/docs/session-replay/data-retention) free and up to 90 days on pay-as-you-go. Inputs are [masked by default](https://posthog.com/docs/session-replay/privacy); page text isn't, and you mask it with `maskTextSelector` or `maskTextFn`.

The core is MIT-licensed with a one-command "hobby" Docker Compose deployment, but PostHog's [self-host page](https://posthog.com/docs/self-host) calls self-hosted deployments "officially unsupported" and steers anyone expecting more than roughly 300k events or 1k recordings a month to PostHog Cloud. We compare the two products further in [Rybbit vs PostHog](/compare/posthog).

- **Pros:** real free tier renewed monthly; replay next to funnels, flags and experiments; transparent volume pricing
- **Cons:** breadth brings complexity; self-hosting is unsupported and not meant for scale; page text unmasked by default
- **Pricing (as of September 2026):** 5,000 web recordings free per month, then from $0.005 per recording. Source: [posthog.com/docs/session-replay/pricing](https://posthog.com/docs/session-replay/pricing)

## OpenReplay

[OpenReplay](https://openreplay.com) is the self-hosting purist's replay tool. The core is AGPL-3.0 (an `ee/` directory carries a separate license, per the [repository](https://github.com/openreplay/openreplay/blob/main/LICENSE)), and the [pricing page](https://openreplay.com/pricing.html) says "Open-Source is free — you host it on your own infrastructure". It is aimed squarely at developers, with [iOS and Android SDKs](https://docs.openreplay.com/en/ios-sdk/sanitization/).

The cloud option is a single "Dedicated" plan: "Starting from $199/mo", billed hourly at $0.276, a dedicated VM in its own VPC "with no limits on users, recordings, or retention", with a 7-day trial and no card. Enterprise is custom.

Self-hosting asks for at least 2 vCPUs, 8 GB of RAM and 50 GB of storage per the [Docker guide](https://docs.openreplay.com/en/deployment/deploy-docker/). Privacy defaults are good: inputs are obscured by default, emails are obscured in inputs and text, and the [docs](https://docs.openreplay.com/en/sdk/sanitize-data/) note that obscured data "will never leave the user's browser". Numbers in text are not obscured unless you turn that on.

- **Pros:** free open-source self-hosting with no recording limits; developer-grade context; strong default sanitization
- **Cons:** cloud starts at $199/month; needs an 8 GB server at minimum; no shared cloud tier for small sites
- **Pricing (as of September 2026):** self-hosted free; Dedicated cloud from $199/month. Source: [openreplay.com/pricing.html](https://openreplay.com/pricing.html)

## Lucky Orange

[Lucky Orange](https://www.luckyorange.com) is a conversion-optimization bundle for small businesses: recordings, heatmaps, funnels, surveys and an AI layer it calls Discovery, with unlimited team members on every plan. [Pricing](https://www.luckyorange.com/pricing) is by monthly sessions: Build is $39/month, or $32/month paid annually, for 3,500 sessions; Grow is $72/month paid annually for 10,000; Expand is $199/month paid annually ($249 monthly) for 50,000; Scale is $839/month paid annually ($1,049 monthly) for 300,000; Enterprise starts at 700,000. Extra sites cost $5 each.

There is no free plan, only a 7-day trial with every feature. Base storage "starts at 60 days" with 90, 180 and 365-day upgrades. It is cloud only, and the pricing page doesn't mention native app recording.

- **Pros:** affordable entry price with everything included; unlimited seats; surveys in the same tool
- **Cons:** no free plan; 60-day base retention; large jump from the 50k to the 300k tier; cloud only
- **Pricing (as of September 2026):** from $39/month ($32 billed annually) for 3,500 sessions. Source: [luckyorange.com/pricing](https://www.luckyorange.com/pricing)

## Mouseflow

[Mouseflow](https://mouseflow.com) offers replay and heatmaps, plus friction scoring, form analytics and journey analysis on higher tiers. The [pricing page](https://mouseflow.com/pricing/) is public and simple. Free is $0 for 500 sessions a month, 1 month of retention, 1 website. Essential is $25/month for 5,000 sessions and 3 months of retention. Advanced is $109/month for 25,000 sessions and 6 months. Premium is $319/month for 100,000 sessions and 12 months. Enterprise is custom. The page advertises up to 35% off for annual billing and a 14-day money-back guarantee, and all plans include unlimited users.

Privacy defaults are conservative; Mouseflow's [help center](https://help.mouseflow.com/en/articles/9055379-mouseflow-and-the-privacy-of-the-website-visitors) says it does not record keystrokes by default, and you whitelist individual fields from the UI. It is cloud only, and the pricing page doesn't mention native app SDKs.

- **Pros:** clear public pricing; 12-month retention on Premium; keystrokes excluded by default
- **Cons:** the 500-session free plan is a demo, not a tier; cloud only; web only
- **Pricing (as of September 2026):** Free (500 sessions); Essential $25/month for 5,000 sessions. Source: [mouseflow.com/pricing](https://mouseflow.com/pricing/)

## Sentry Session Replay

[Sentry](https://sentry.io) is error monitoring first, and its replay exists to show the seconds before an exception. The default setup in the [JavaScript docs](https://docs.sentry.io/platforms/javascript/session-replay/) records 10% of sessions and 100% of sessions with an error, and replay is [available](https://docs.sentry.io/product/explore/session-replay/) for web plus Android, iOS, React Native and Flutter.

The free Developer plan (one user) and the paid Team ($26/month billed annually) and Business ($80/month billed annually) plans each include 50 replays a month on the [pricing page](https://sentry.io/pricing/). Beyond that you pay as you go; the [pricing docs](https://docs.sentry.io/pricing/) list Team replays at $0.00375 each up to 5k and $0.003563 each from 5k to 100k. Replays are [retained](https://docs.sentry.io/security-legal-pii/security/data-retention-periods/) for 30 days on Developer and 90 days on paid plans.

Defaults are the strictest here: `maskAllText`, `maskAllInputs` and `blockAllMedia` are all `true` out of the box, per the [privacy docs](https://docs.sentry.io/platforms/javascript/session-replay/privacy/). Self-hosting exists under the Functional Source License, needs at least 4 CPU cores and 16 GB of RAM per the [self-hosted docs](https://develop.sentry.dev/self-hosted/), and has included replay since release 23.3.1 according to the project's [issue tracker](https://github.com/getsentry/self-hosted/issues/1873).

- **Pros:** replay linked to the exact error; mask-everything defaults; sampling built in; self-hostable
- **Cons:** 50 included replays; per-replay costs add up; built for debugging errors rather than UX research
- **Pricing (as of September 2026):** Developer free (50 replays); Team $26/month billed annually plus $0.00375 per extra replay. Source: [sentry.io/pricing](https://sentry.io/pricing/)

## Which one should you pick

- **You want replay and web analytics in one privacy-first tool, or need to self-host without a giant server:** Rybbit.
- **You have no budget and can live with 30-day retention and a consent banner in Europe:** Microsoft Clarity.
- **You're a marketer who wants heatmaps, surveys and replay with a big free session quota:** Hotjar / Contentsquare.
- **Your engineers want console logs and network calls next to every recording:** LogRocket, or Sentry Session Replay if you already use Sentry and only need replays for errored sessions.
- **You want the widest product-analytics suite with a real free tier, and cloud hosting is fine:** PostHog.
- **Recordings must never leave your infrastructure and you have a spare 8 GB box:** OpenReplay.
- **You need 12 months of replay history at a published price:** Mouseflow Premium, or FullStory's free plan if 30k sessions is enough.

One tool to skip: Smartlook. Its [homepage](https://www.smartlook.com/) now carries a Cisco end-of-life notice, with no new subscriptions since 31 May 2026, no renewals after 31 August 2026, and the platform decommissioned on 30 September 2027. If you already use Amplitude, its [pricing page](https://amplitude.com/pricing) lists 10,000 replay sessions a month on the free plan with 1 month of retention.

For the analytics side of the decision, see our guides to the [best web analytics tools](/blog/best-web-analytics-tools) and the [best Google Analytics alternatives](/blog/best-google-analytics-alternatives).

## FAQ

### Is there a free session replay tool?

Yes, several. Microsoft Clarity is free with no traffic limits. Contentsquare's Free plan includes 10,000 replay captures a month, PostHog gives 5,000 web recordings a month, LogRocket's Free plan covers 1,000 sessions a month, and FullStory's free plan covers 30,000 sessions with 12-month retention. Rybbit and OpenReplay are free if you self-host; Rybbit Cloud has a 7-day trial rather than a free tier.

### Is session replay software the same as session recording software?

Yes. Vendors use "session recording", "session replay" and "visitor recordings" for the same thing: capturing DOM changes and interactions in the browser and reconstructing them for playback.

### Is session replay GDPR compliant?

It can be, but your configuration makes it so, not the tool. Mask inputs and personal text before data leaves the browser, record only with a lawful basis, honor consent where you rely on it, keep retention short, and sign a processing agreement with the vendor. Self-hosting (Rybbit, OpenReplay, PostHog, Sentry) removes the vendor-transfer question entirely.

### Which session replay tools are open source?

Rybbit (AGPL-3.0), OpenReplay (AGPL-3.0 core with a separately licensed `ee/` directory) and PostHog (MIT core) are open source and self-hostable. Sentry is source-available under the Functional Source License, which is not an OSI-approved open-source license.

### Does session replay slow down a website?

Modern tools record DOM mutations, not screenshots or video, and load their capture script asynchronously, so the effect on page load is small. Bandwidth on long, interaction-heavy sessions is the real cost, which is why tools offer sampling: Sentry defaults to 10% of sessions, and Rybbit's script attributes let you throttle mouse-move and scroll events and slim the recorded DOM.

---

### Content Blog Best Web Analytics Tools (`docs/content/blog/best-web-analytics-tools.mdx`)

---
title: "10 Best Web Analytics Tools in 2026, Compared by Use Case"
description: "The best web analytics tools in 2026 by use case: privacy-first trackers, product analytics suites and free options, each with verified pricing."
keyword: "best web analytics tools"
date: 2026-07-19
updated: 2026-09-28
tags: ["analytics", "tools", "comparisons"]
author: Rybbit Team
---

Every list of web analytics tools pretends there's one "best" answer. There isn't. A solo blogger checking referrers, a SaaS team debugging a signup funnel, and an EU retailer with a nervous legal department need genuinely different products.

So this guide is organized around the question that actually decides the purchase: **what do you need your analytics to do?** We build one of these tools ([Rybbit](https://rybbit.com)), so we'll flag that bias where it matters. Every price and limit below comes from the vendor's own published pages, checked July 2026.

## The short version

| If you need... | Use | Price anchor |
| --- | --- | --- |
| Depth + simplicity in one tool | [Rybbit](#rybbit) | $19/mo, 100k events |
| The simplest possible dashboard | [Plausible](#plausible), [Fathom](#fathom-analytics) | $9/mo · slider ($45 at 500k) |
| The cheapest paid plan | [Pirsch](#pirsch) | $6/mo, 10k pageviews |
| Full product-analytics suite | [PostHog](#posthog) | 1M events/mo free, then usage-based |
| Free ads-ecosystem default | [Google Analytics 4](#google-analytics-4) | Free (GA360 ≈ $50k/yr) |
| Free forever, cloud-hosted | [Umami](#umami) | $0 Hobby plan |
| Maximum features, self-hosted, free | [Matomo](#matomo) | Free on-premise; cloud €29/mo |
| Strictest EU privacy posture | [Simple Analytics](#simple-analytics) | Free hobby tier; $20/mo paid |
| Zero budget, zero setup | [Cloudflare](#cloudflare-web-analytics) | Free (sampled) |

## How to choose a web analytics tool

Four questions sort the whole market:

1. **Traffic stats or behavior?** Counting visits, referrers, and top pages is the easy half. Understanding *behavior* (funnels, session replay, user journeys, errors) is where tools diverge sharply. Simple trackers stop at the first half.
2. **Cookies or cookieless?** Cookie-based tools (GA4 chief among them) require consent banners in most of the world, and every declined banner is a visitor deleted from your data. Cookieless tools see everyone and need no banner.
3. **Cloud or self-hosted?** If analytics data legally can't leave your infrastructure, your shortlist is the open-source tools: Rybbit, Matomo, Plausible CE, Umami.
4. **Who reads the dashboard?** If the answer includes non-analysts, weight simplicity heavily. The most common analytics failure is a tool nobody opens, not missing data.

## Rybbit



**The all-rounder: product-analytics depth on a dashboard anyone can read.**

Most tools on this list force a choice between simple-but-shallow and deep-but-painful. [Rybbit](https://rybbit.com) exists to refuse that choice: a single-page, real-time dashboard in the Plausible tradition, with [funnels](/features/funnels), [session replay](/features/session-replay), [user journeys](/features/user-journeys), [error tracking](/features/error-tracking), [Web Vitals](/features/web-vitals), and user profiles built in. That feature set otherwise requires a PostHog-class suite. Tracking is cookieless (no consent banner, no lost visitors, no sampling), the whole platform is open source, and there's a [live demo with real data](https://demo.rybbit.com/81) you can click around right now.

**Pricing:** from $19/month for 100k events on [Standard](/pricing); session replay and unlimited sites come with Pro, from $39/month. 7-day trial. Self-hosting is [free with Docker](/docs/self-hosting).
**Skip it if:** you need years of GA-style ad-attribution workflows, or feature flags and experiments in the same tool (that's PostHog territory).

## Google Analytics 4

**The default, and the tool everyone on this page is measured against.**

GA4 is free, integrates natively with Google Ads, and exports raw events to BigQuery. For paid-acquisition teams inside Google's ecosystem, it remains genuinely hard to replace. It's on this list on merit.

But treat its price with suspicion: "free" is funded by your visitors' data feeding Google's ad network. The operational costs are consent banners (and the 30–60% of traffic they cost you), report sampling on busy properties, 14-month standard retention, and an interface that has made "how do I just see my top pages?" a support-forum genre. Unsampled data means GA360, at roughly $50,000/year. If those are your pain points, our [detailed GA4 comparison](/compare/google-analytics) breaks down the switch, or see the full [guide to Google Analytics alternatives](/blog/best-google-analytics-alternatives).

## Plausible



**The standard-bearer for simple, privacy-first analytics.**

One page of stats, a tiny script (their claim: ~54× smaller than GA's), cookieless by design, EU-owned and EU-hosted. Plausible proved a paid GA alternative could be a real business, and a decade of polish shows. Its scope is deliberately narrow (traffic analytics, goals, basic funnels), which is either its best feature or its dealbreaker depending on your needs. **From $9/month** (10k pageviews), 30-day no-card trial, free self-hosted Community Edition. [Rybbit vs Plausible →](/compare/plausible)

## Matomo



**The open-source veteran with the largest feature list.**

Heatmaps, A/B tests, a tag manager, hundreds of plugins, and a free on-premise edition with unlimited hits: Matomo (ex-Piwik) is what "we replaced Google Analytics" meant for a decade, and it's still the compliance-department favorite (cloud data lives in Frankfurt, no sampling). The cost is a dated interface and pricing complexity on-premise, where advanced features come in paid plugin bundles from €275. **Cloud from €29/month** for 50k hits; no-card trial. [Rybbit vs Matomo →](/compare/matomo)

For a head-to-head with GA4, see [Matomo vs Google Analytics](/compare/matomo-vs-google-analytics).

## PostHog

**The product-analytics suite that swallowed web analytics.**

PostHog bundles product analytics, session replay, feature flags, experiments, surveys, and a SQL-queryable event warehouse, with a web analytics dashboard on the side. For engineering-led product teams it's the most capable platform here, and the free tier is the market's most generous: **1M events every month, no card**. Past that it's usage-based (from $0.00005/event), which needs monitoring at scale. The trade: GA4-class complexity, because it's solving a much bigger problem than "how's my traffic." [Rybbit vs PostHog →](/compare/posthog)

For the per-product rates and worked examples, see [PostHog pricing, explained](/blog/posthog-pricing).

## Fathom Analytics



**Simple stats from one of the original privacy-analytics companies.**

Cookieless, one clean dashboard, forever data retention, and at least 50 sites on every plan. Fathom's pitch is stability: a profitable independent company that's been doing exactly this since before it was a category. Pricing is a pageview slider (**$45/month at 500k pageviews**; smaller tiers cost less), 7-day trial, no free tier, no self-hosting. [Rybbit vs Fathom →](/compare/fathom)

## Umami



**Free, minimal, MIT-licensed: the developer's hobby-project default.**

Umami is the easiest analytics tool to self-host (a small Node app), and its cloud **Hobby plan is free forever**, with Pro at **$20/month for 1M events**. Reporting is deliberately minimal (pageviews, referrers, events), and that's the point. If your need is "decent stats on my side projects for $0," this is the answer. [Rybbit vs Umami →](/compare/umami)

## Simple Analytics



**The most privacy-conservative option, made and hosted in the EU.**

Simple Analytics collects the least data of any paid tool here and wears it proudly. In 2026 it's also carved out a clever niche: positioning itself as the *fix* for GA4's consent-loss problem (their number: GA can miss up to 60% of traffic). **Free for hobby sites** (5 sites, 1 month history); paid from **$20/month per user** with usage sliders; 14-day no-card trial. No self-hosting. [Rybbit vs Simple Analytics →](/compare/simpleanalytics)

## Pirsch

**The lowest-cost paid entry in privacy analytics.**

German-built, cookieless, with first-class server-side tracking SDKs and importers for GA, Plausible, and Fathom. **$6/month** buys 10k pageviews across up to 50 sites with unlimited team members and retention; $12/month adds funnels and A/B testing; 30-day no-card trial. Self-hosting exists but only behind the Enterprise tier. For tight budgets it's the best dollar-for-dollar deal on this list.

## Cloudflare Web Analytics

**Free and instant, as long as you understand what it isn't.**

If your site is behind Cloudflare, this is one toggle. It's cookieless and costs nothing, but Cloudflare's own docs note the dashboard is built on a ~10% sample of page-load events with roughly 30-day retention, and there are no goals, events, or funnels. Use it as a free second opinion, not as your analytics.

## What we'd actually do

- Under 10k visits a month and a $0 budget: Umami's free cloud tier, or self-host if you enjoy it.
- A business site where decisions ride on the data: [Rybbit](/pricing). You'll use the funnels and replay within the first week, and cookieless tracking means the numbers are actually complete.
- A content site that just needs clean traffic stats: Plausible or Fathom. Pirsch if price wins.
- An engineering-led product team: PostHog, or Rybbit if you want your PMs and marketers to actually open the dashboard.
- A regulated or self-host-only environment: Rybbit or Matomo, both [free to run on your own infrastructure](/docs/self-hosting).

## FAQ

**What's the difference between web analytics and product analytics?**
Web analytics answers traffic questions (visitors, sources, pages); product analytics answers behavior questions (activation, retention, funnels) usually tied to identified users. GA4 and PostHog are product-analytics tools wearing web-analytics clothes; Plausible and Fathom are pure web analytics; Rybbit deliberately covers both sides for websites and apps.

**Are free web analytics tools good enough?**
Often, yes. Umami and PostHog's free tiers are real products, and self-hosting an open-source tool costs only your server. The free options to be wary of are the ones with invisible costs: GA4 (consent loss, complexity, your users' data) and Cloudflare (sampling, 30-day window).

**Can I run two analytics tools at once?**
Yes, and during any migration you should. Scripts from different vendors don't conflict, and a few weeks of parallel data is the only honest way to compare them. It's also the standard way to [leave Google Analytics](/blog/best-google-analytics-alternatives) without losing your bearings.

**Do cookieless analytics tools comply with GDPR?**
The cookieless tools here (Rybbit, Plausible, Fathom, Umami, Simple Analytics, Pirsch) are built to run without consent banners: no cookies, no cross-site identifiers, no personal profiles. Self-hosting adds the strongest guarantee, since data never touches a third party at all.

---

*Want the deep version of any matchup? Every comparison above links to a feature-by-feature breakdown, or start with [Rybbit vs Google Analytics](/compare/google-analytics), the switch most teams are weighing.*

---

### Content Blog Bounce Rate (`docs/content/blog/bounce-rate.mdx`)

---
title: "What Is Bounce Rate? How to Calculate and Reduce It"
description: "Bounce rate is the share of sessions that view one page and leave. Learn how it's calculated, what a good bounce rate looks like, and how to lower yours."
keyword: "bounce rate"
date: 2025-11-21
updated: 2026-09-28
tags: ['analytics', 'metrics']
author: Rybbit Team
---

Bounce rate measures the percentage of visitors who leave your site after viewing only one page. For anyone running a website, this number matters, it shows whether people find what they're looking for or bail immediately. Rybbit makes tracking bounce rate simple, and I'll walk you through everything you need to know.

## Understanding Bounce Rate

A bounce happens when someone lands on your site and leaves without clicking anywhere else. That's it. The bounce rate is just the percentage of these single-page sessions compared to total sessions (the [bounce rate calculator](/tools/bounce-rate-calculator) does the division for you). To understand bounce rate in the broader context of [web analytics](/blog/what-is-web-analytics), it's one of the most fundamental metrics you'll track.

High bounce rates aren't always terrible. Someone might read your entire 2,000-word guide, get their answer, and close the tab feeling satisfied. But if users hit your homepage and immediately leave? That signals a real problem.

## Accessing Your Bounce Rate Data

Once you've added the Rybbit tracking script (takes about five minutes), bounce rate data appears immediately in your dashboard.

### Dashboard Access

Log into [app.rybbit.io](https://app.rybbit.io) and select your website from the list.




### Main Metrics View

Your bounce rate sits right on the main dashboard next to:
- Unique Users
- [Sessions](/docs/feature-guides/sessions) (see our [sessions guide](/docs/feature-guides/sessions) for deeper analysis)
- Pageviews
- Pages Per Session
- Session Duration

The interface stays clean and simple. No digging through buried menus for your key numbers are front and center.

## The Math Behind Bounce Rate

Rybbit calculates bounce rate with this formula:

`
Bounce Rate = (Single-Page Sessions ÷ Total Sessions) × 100
`

**Real example:** You had 1,000 sessions yesterday. 350 were single-page visits. Your bounce rate: 35%.

For more on session behavior and user engagement patterns, see our [Sessions Tab Guide](/docs/feature-guides/sessions), [Churn Rate vs Retention Rate article](/blog/retention-analysis), and [Website Traffic Drop Analysis](/blog/website-traffic-drop).

## Segmenting Your Data

Rybbit's filtering capabilities let you dig into the "why" behind your bounce rate numbers.

### Traffic Source Comparison

Click "Referrer" or "UTM Source" to compare channels:

- **Organic Search:** Your SEO pages converting?
- **Social Media:** How do Twitter, LinkedIn, and Facebook visitors behave?
- **Direct Traffic:** Performance from bookmarks and typed URLs
- **Paid Campaigns:** Getting ROI from paid traffic?



### Device Breakdown

Mobile users behave differently than desktop users. Filter by device type:
- Desktop
- Mobile



If mobile bounce rates run significantly higher, your mobile experience needs work.

### Geographic Patterns

The Location Filter shows bounce rates by country, region, and city. Useful for spotting regional issues or informing localization choices.



### Advanced Filtering Options

Rybbit offers 15+ dimensions for detailed segmentation:
- Browser and Browser Version
- Operating System
- Screen Resolution
- Language
- [UTM parameters](/tools/utm-builder) (Campaign, Medium, Term, Content)

You can easily select the right filter at the top of the page of your main dashboard



## Real-Time Monitoring

Rybbit shows bounce rate changes as they happen. This matters during:
- Campaign launches
- New content releases
- A/B tests
- Site updates

## Historical Trend Analysis

Looking at bounce rate over time reveals patterns and shows whether your optimizations work.

### Time Range Selection

Pick your date range from the top of your dashboard:
- Today
- Yesterday
- Last 7 days
- Last 30 days
- Last 90 days
- Custom date range



### Pattern Recognition

The bounce rate graph shows you what's happening over time. Watch for:
- **Spikes:** Something caused a sudden jump—what was it?
- **Declines:** Did your recent changes help?
- **Weekly rhythms:** Certain days consistently different?
- **Seasonal shifts:** How does time of year affect behavior?

### Setting Up a Funnel

1. Navigate to Funnels
2. Click "Create Funnel"
3. Map your steps (Landing Page → Product Page → Cart → Checkout)
4. Analyze drop-off rates

Your first step drop-off equals your bounce rate for that entry point. For a comprehensive guide on using funnels to measure conversions, see our [sales funnel measurement guide](/blog/funnel-analysis).

## Core Web Vitals Monitoring

Page performance impacts bounce rate. Rybbit automatically measures [Core Web Vitals](/features/web-vitals):
- **LCP (Largest Contentful Paint):** How fast the main content loads
- **FID (First Input Delay):** How quickly the page responds to interactions
- **CLS (Cumulative Layout Shift):** Visual stability during loading



Poor scores here often match high bounce rates.

## Actionable Ways to Reduce Bounce Rate

Now that you know how to track bounce rate, here's how to improve it:

### 1. Improve Load Speed
Check Web Vitals data, find slow pages, fix them.

### 2. Align Content with Expectations
Review referrer data, do landing pages deliver what traffic sources promise?

### 3. Optimize Mobile Experience
High mobile bounce rates mean your mobile site needs attention now. If you need to understand how different devices are performing, check out our [e-commerce analytics guide](/blog/ecommerce-analytics) which covers device-specific optimization.

### 4. Clarify Next Steps
Watch [session replays](/features/session-replay) to see if users understand where to go next. Session replays are invaluable for understanding [user behavior patterns](/docs/feature-guides/sessions) that metrics alone can't reveal.

### 5. Match Content to Headlines
Blog posts with high bounces often fail to deliver what titles promise.

### 6. Fix Technical Issues
Session replays expose broken elements, confusing navigation, error messages. If you're seeing sudden bounce rate spikes, consult our [website traffic drop analysis guide](/blog/website-traffic-drop) to diagnose what went wrong.

### 7. Improve Audience Targeting
Use UTM tracking to measure campaign-level bounce rates and adjust targeting. Understanding how different [traffic sources perform](/blog/marketing-attribution-tools) helps you optimize your acquisition strategy.

## Comparing Rybbit to Alternatives

Switching from Google Analytics, Plausible, or Umami? Key differences:

- **Content-friendly:** Long-form content doesn't get unfairly penalized
- **Complete data:** No sampling—you see every session
- **Instant updates:** Real-time data without processing delays
- **Privacy-first:** No cookies required, no consent banners needed

## Support Resources

Need help with bounce rate or other Rybbit features?

- **Documentation:** [rybbit.com/docs](https://rybbit.com/docs)
- **Discord Community:** Join other Rybbit Users at [discord.gg/DEhGb4hYBj](https://discord.gg/DEhGb4hYBj)
- **Twitter/X:** Reach out to [@yang_frog](https://x.com/yang_frog)
- **GitHub:** [github.com/rybbit-io/rybbit](https://github.com/rybbit-io/rybbit)

## Final Thoughts

Bounce rate reveals a lot about user engagement. Rybbit makes the data accessible and actionable. Real-time tracking, session replays, detailed filters, and privacy-focused analytics give you everything needed to convert insights into improvements.

What works:
1. **Regular monitoring:** Check bounce rate trends each week
2. **Deep segmentation:** Examine sources, devices, and pages—not just overall numbers
3. **Session replay review:** Watch real user behavior to understand the story
4. **Change testing:** Implement improvements and measure results
5. **Privacy respect:** Get accurate data without invasive tracking

Ready to start? Start a [free trial](/pricing) or explore the [live demo](https://demo.rybbit.com/81) featuring a real site handling millions of events annually.

---

**Additional Resources:**
- [Getting Started with Rybbit Analytics](/docs)
- [Self-Hosting Guide](/docs/self-hosting)
- [Tracking Custom Events](/docs/track-events)
- [Framework Integration Guides](/docs) (React, Next.js, Vue, plus 20+ frameworks)
- [Ecommerce Analytics: The Metrics and Reports That Matter](/blog/ecommerce-analytics)
- [What Is Web Analytics? A Complete Guide](/blog/what-is-web-analytics)
- [Funnel Analysis: How to Find Where Users Drop Off](/blog/funnel-analysis)

---

### Content Blog Ecommerce Analytics (`docs/content/blog/ecommerce-analytics.mdx`)

---
title: "Ecommerce Analytics: The Metrics and Reports That Matter"
date: 2025-11-22
updated: 2026-09-28
description: "A practical guide to ecommerce analytics: the metrics worth tracking, how to set up conversion events, and which reports show where sales are won or lost."
keyword: "ecommerce analytics"
tags: ["analytics", "ecommerce", "conversion"]
author: Rybbit Team
---

If you run an [e-commerce website](/for-ecommerce), understanding your customer behavior and [sales funnel](/features/funnels) is crucial for growth. Whether you're running a small online store or a large marketplace, having the right analytics in place can mean the difference between scaling successfully and struggling to understand why your sales are declining.

E-commerce analytics are a specialized application of [web analytics](/blog/what-is-web-analytics), focused specifically on understanding purchase behavior, conversion rates, and [customer journeys](/features/user-journeys) through your store.

This is where Rybbit Analytics comes in. Unlike traditional analytics platforms that overwhelm you with data you don't need, Rybbit gives you actionable insights into your e-commerce metrics while respecting your customers' privacy and keeping your data under your control.

## Why E-commerce Sites Need Better Analytics

Most e-commerce platforms come with basic built-in analytics, but they're often limited in scope. You might see how many people visited your product pages, but can you easily answer these critical questions:

- Which marketing channels are actually driving purchases, not just traffic?
- What's the conversion rate for mobile vs desktop users?
- Are customers from certain countries more likely to complete a purchase?
- Which product pages have high traffic but low conversion rates?
- How is your site's performance affecting your sales?
- What's the customer journey from landing to checkout?

Traditional analytics tools like Google Analytics can answer some of these questions, but they come with significant downsides: complex setup, privacy concerns, cookie consent banners, data ownership issues, and an overwhelming interface that requires training to navigate effectively.

## What Makes Rybbit Different for E-commerce

Rybbit is built from the ground up to be simple, privacy-focused, and actionable. Here's what that means for your e-commerce business:

**No Cookie Consent Banners Required** - Rybbit doesn't use cookies, which means you don't need those annoying cookie banners that hurt your conversion rates. Your checkout flow stays clean and simple.

**Lightning Fast** - The Rybbit script is designed to be lightweight and minimize performance impact on your product pages and checkout process. Load time matters for e-commerce conversions.

**You Own Your Data** - With the self-hosted option, all your customer data stays on your servers. No third-party access, no data sharing, complete control.

**Simple, Actionable Metrics** - Rybbit focuses on the metrics that actually matter for e-commerce, without drowning you in vanity metrics.

## Setting Up Rybbit for Your E-commerce Site

Getting started with Rybbit is straightforward. You can either use the hosted version or self-host it (which many e-commerce businesses prefer for data sovereignty).

### Installation

Add the Rybbit tracking script to your site's header. For most e-commerce platforms, this goes in your theme's template file. Refer to the [official Rybbit documentation](/docs/script) for the correct installation steps.

That's it. No complex configuration, no tag managers required, no cookies to configure.

### Tracking E-commerce Events

While Rybbit automatically tracks page views, you'll want to track key e-commerce events like:

- Product views
- Add to cart
- Checkout initiated
- Purchase completed

See the [Rybbit event tracking documentation](/docs/track-events) for specific implementation details on how to track these events properly for your platform.

## Key Metrics for E-commerce Success

### 1. Traffic Sources and Quality

Understanding where your traffic comes from is table stakes, but Rybbit makes it easy to see which sources actually convert.



Using Rybbit's channel filter, you can quickly see traffic from:

- Organic search
- Paid advertising
- Social media
- Email campaigns
- Direct traffic
- Referrals

But more importantly, you can combine this with your purchase events to see which channels drive actual revenue, not just visits. This helps you allocate your marketing budget more effectively.

### 2. Geographic Performance

If you're selling internationally, geographic data is crucial. Rybbit makes it easy to analyze performance by country.



You can use geographic filtering to:

- Identify high-converting markets worth investing in
- Spot countries with high traffic but low conversion (maybe you need better shipping options or payment methods)
- Optimize your ad spend by focusing on profitable regions
- Plan inventory and shipping logistics based on where your customers are

### 3. Device and Platform Analysis

E-commerce conversion rates often vary significantly between desktop and mobile. Rybbit lets you filter by device type to understand these differences.



Common insights you might discover:

- Mobile traffic is high but conversion is low → your mobile checkout might need work
- Tablet users have higher average order values → consider tablet-specific promotions
- Desktop users browse more pages before purchasing → optimize your navigation for desktop

### 4. Page Performance Analysis

Page speed directly impacts e-commerce conversions.



Rybbit's performance tab shows you the Largest Contentful Paint (LCP) for each page. If your product pages or checkout pages have poor performance metrics, you should optimize them. The performance dashboard makes it easy to identify which pages need optimization.

**What to look for:**

- Product pages with high traffic but slow load times → optimize images, reduce scripts
- Checkout pages with performance issues → this is costing you direct sales
- Category pages that load slowly → customers will bounce before seeing your products

### 5. Conversion Funnel Insights

One of the most powerful uses of Rybbit for e-commerce is tracking your conversion funnel. Similar to how [content marketing funnels](/blog/funnel-analysis) work for awareness and consideration stages, your e-commerce funnel tracks the path from product discovery to purchase. Using the pages tab, you can see the customer journey:



The embedded graphs make it easy to spot where customers are dropping off:

- High traffic to product pages but low "add to cart" pages → product pages need better CTAs
- Many add to cart events but few checkout initiations → shipping costs revealed too late?
- Checkout started but not completed → payment issues or complicated forms?

### 6. Time-Based Patterns

Understanding when your customers shop helps you optimize promotions and advertising.



Using Rybbit's date range filters, you can:

- Compare this month vs last month to track growth
- Identify seasonal patterns in your business
- See if your recent marketing campaigns are working
- Spot unusual drops or spikes that need investigation

### 7. Referrer Analysis

Beyond broad traffic sources, Rybbit lets you drill down into specific referrers to see exactly which sites, influencers, or affiliate partners are driving traffic.



This is especially valuable for:

- Affiliate programs - see which affiliates drive the most valuable traffic
- Influencer marketing - track which influencers actually drive sales
- Content marketing - identify which guest posts or mentions are worth pursuing
- Partner relationships - measure the value of co-marketing efforts

## Real-World E-commerce Scenarios

### Scenario 1: Optimizing Marketing Spend

When analyzing which traffic sources drive actual revenue (not just clicks), you can make more informed decisions about how to allocate your marketing budget. Using Rybbit's channel filtering combined with conversion tracking helps you see which acquisition channels produce the highest-quality users.

### Scenario 2: Fixing Mobile Conversion Issues

If you notice that mobile users make up a large portion of your traffic but a smaller portion of your conversions, Rybbit's device filtering helps you identify where the problem is. Common issues include:

- Checkout experience problems on mobile
- Performance issues on mobile devices
- Form complexity or navigation problems on small screens

By identifying where mobile users drop off, you can prioritize fixes to improve mobile conversion rates.

### Scenario 3: International Expansion

Using Rybbit's geographic filtering, you can see which countries or regions show strong engagement and conversion patterns. This helps you:

- Identify markets worth investing in
- Spot countries with traffic but low conversion (signaling a potential need for localization)
- Prioritize international optimizations based on data, not assumptions

### Scenario 4: Product Page Optimization

By analyzing individual product pages with Rybbit, you can identify patterns:

- Which pages get high traffic but have poor conversion rates
- What content attributes correlate with higher conversions (detailed descriptions, multiple images, videos, etc.)
- Which optimization efforts have the most impact

For a comprehensive guide on understanding your entire purchase journey, see our [sales funnel guide](/blog/funnel-analysis), which covers how to track and optimize all stages from awareness to purchase.

## Privacy and E-commerce: Why It Matters

E-commerce businesses collect sensitive customer data - names, addresses, payment information. Using privacy-respecting analytics like Rybbit sends a positive signal to customers:

**No Data Sharing** - Customer behavior data stays with you, never sold to advertisers

**GDPR and Privacy Compliant** - Rybbit is built to respect privacy laws by default

**No Cookie Banners** - Better user experience means better conversion rates

**Build Trust** - Privacy-conscious customers appreciate businesses that respect their data

In an era where data breaches make headlines regularly, being able to tell your customers that you use privacy-first analytics is a competitive advantage.

## Comparing Rybbit to Other E-commerce Analytics

### vs. Google Analytics

**Google Analytics:**

- Requires complex e-commerce tracking setup
- Needs cookie consent banners
- Data belongs to Google
- Overwhelming interface with hundreds of metrics
- Slower script impacts page speed

**Rybbit:**

- Simple event tracking
- No cookie banners needed
- You own all data
- Focused on actionable metrics
- Minimal performance impact

### vs. Platform Built-in Analytics (Shopify, WooCommerce, etc.)

**Built-in Analytics:**

- Limited to basic metrics
- Can't easily track [custom events](/features/custom-events)
- Limited filtering and segmentation
- No performance metrics
- No data portability

**Rybbit:**

- Comprehensive yet simple analytics
- Custom event tracking
- Powerful filtering and segmentation
- Built-in performance monitoring
- Self-hostable with full data control

## Advanced E-commerce Tracking with Rybbit

For more detailed tracking beyond page views, refer to the [Rybbit event tracking documentation](/docs/track-events) for implementation guidance on:

### Customer Lifetime Value (CLV) Tracking

Track repeat purchases to understand customer lifetime value. The documentation covers best practices for tracking first-time purchases and repeat customer behavior.

### Cart Abandonment Analysis

Track the checkout flow to identify where customers are abandoning carts. By measuring events at key steps (add to cart, checkout started, purchase completed), you can calculate your cart abandonment rate and work to reduce it.

### Product Category Performance

Track which product categories perform best by recording category and product information when users interact with products. This helps you understand which categories drive the most engagement and sales.

## Getting Started with Rybbit for Your E-commerce Store

1. **Sign up or self-host** - Choose between the hosted version for convenience or self-hosting for complete data control

2. **Add the script** - Install the Rybbit tracking script on your e-commerce site (works with any platform)

3. **Set up event tracking** - Add events for your key conversion points (add to cart, checkout, purchase)

4. **Define your metrics** - Decide what success looks like for your business and set up your Rybbit dashboard accordingly

5. **Start analyzing** - Use the filtering and segmentation features to understand your customer behavior

6. **Optimize and iterate** - Use insights from Rybbit to make data-driven decisions about your store

## Best Practices for E-commerce Analytics with Rybbit

**Track What Matters** - Don't track everything, focus on metrics that drive business decisions

**Segment Your Analysis** - Use filters to understand different customer segments, devices, and channels

**Compare Time Periods** - Always look at trends over time, not just individual snapshots

**Combine with Other Data** - Rybbit analytics work best when combined with your e-commerce platform's sales data

**Act on Insights** - Analytics are only valuable if you use them to make changes

**Monitor Performance** - Keep an eye on page performance metrics - they directly impact conversion

**Review Regularly** - Set aside time weekly or monthly to review your analytics and spot trends

## Conclusion

Running a successful e-commerce business requires understanding your customers, your traffic, and your conversion funnel. Rybbit Analytics provides all the tools you need to make data-driven decisions without the complexity, privacy concerns, or performance impact of traditional analytics platforms.

Whether you're just starting out with a small online store or scaling a multi-million dollar e-commerce business, Rybbit gives you the insights you need to grow - in a simple, privacy-respecting package that your customers will appreciate.

The combination of Rybbit's clean interface, powerful filtering capabilities, performance monitoring, and privacy-first approach makes it the ideal analytics solution for modern e-commerce businesses. And with both hosted and self-hosted options, you can choose the setup that best fits your needs.

Ready to get better insights into your e-commerce business? Give Rybbit a try and see how much clearer your data becomes when you focus on what actually matters.

---

**Related Reading:**
- [What Is Bounce Rate? How to Calculate and Reduce It](/blog/bounce-rate)
- [Retention Analysis: How to Measure and Improve User Retention](/blog/retention-analysis)
- [Why Did My Website Traffic Drop?](/blog/website-traffic-drop)
- [Funnel Analysis: How to Find Where Users Drop Off](/blog/funnel-analysis)
- [What Is Web Analytics? A Complete Guide](/blog/what-is-web-analytics)
- [Behavior Analytics - Sessions](/docs/feature-guides/sessions)
- [Behavior Analytics - Events](/docs/feature-guides/events)
- [Behavior Analytics - Users](/docs/feature-guides/users)
- [Track Custom Events](/docs/track-events)
- [Product Analytics - Funnels](/docs/funnels)
- [Getting Started with Rybbit](/docs)

---

### Content Blog Fullstory Alternatives (`docs/content/blog/fullstory-alternatives.mdx`)

---
title: "Best FullStory Alternatives in 2026: 11 Tools by Use Case"
description: "FullStory alternatives for enterprise, developer and product teams: 11 tools compared on replay, heatmaps, analytics, self-hosting and verified prices."
keyword: "fullstory alternatives"
date: 2026-09-27
tags: ["session replay", "comparisons", "product analytics"]
author: Rybbit Team
---

FullStory is an enterprise digital experience platform: session replay, heatmaps, frustration signals and product analytics, bought through a sales conversation because none of its paid plans has a public price. Which of the FullStory alternatives fits depends on why you're leaving. For the same enterprise depth from another vendor, look at Contentsquare and Quantum Metric. If your engineers mainly use replay to debug, look at LogRocket, Sentry and OpenReplay. Product teams that want analytics with replay attached should compare PostHog, Amplitude and Heap. Microsoft Clarity and Mouseflow cover small budgets, and OpenReplay and Rybbit let you keep recordings on your own servers.

Disclosure up front: we build [Rybbit](https://rybbit.com), one of the eleven tools below. Every claim about FullStory and the other ten tools comes from that vendor's own pricing or documentation pages, linked inline and checked in September 2026. Where a vendor doesn't publish a number, we say so rather than guess.

## FullStory alternatives at a glance

| Tool | Session replay | Heatmaps | Product analytics | Dev tools | Entry pricing (Sept 2026) | Self-host |
| --- | --- | --- | --- | --- | --- | --- |
| FullStory (for reference) | Web; mobile is an add-on | Yes | Yes | Console, network | Free: 30k sessions/mo; paid plans by quote | No |
| [Contentsquare](#contentsquare) | Web and apps; fixed replay allowance | Yes | Separate app for now | JS errors, network waterfall | Free: 200k sessions; Growth from $49/mo | No |
| [Quantum Metric](#quantum-metric) | Web and native apps | Yes | Yes | Errors, performance | Quote only | No |
| [LogRocket](#logrocket) | Web and mobile | Yes | Funnels, paths | Console, network, errors, performance | Free: 1k sessions; $176/mo for 25k | Enterprise plan |
| [Sentry Session Replay](#sentry-session-replay) | Web and mobile | — | — | Errors, traces, console, network | Free: 50 replays, 1 user; Team $26/mo billed annually | Yes (FSL) |
| [OpenReplay](#openreplay) | Web and mobile | Yes | Funnels, journeys | Console, network, performance, state | Self-host free; cloud from $199/mo | Yes (AGPL-3.0 core) |
| [PostHog](#posthog) | Web and mobile; 5k/mo free | Yes | Yes | Console, network | Free tier, then per recording | Hobby deploy, unsupported |
| [Amplitude](#amplitude) | Web and mobile; 10k/mo free | Plus plan and up | Yes | — | Free: 2M events; Plus usage-based | No |
| [Heap](#heap) | Add-on (Pro, Premier) | Add-on (Pro, Premier) | Yes | Add-on (Pro, Premier) | Free: 10k sessions; paid by estimate or quote | No |
| [Microsoft Clarity](#microsoft-clarity) | Web and mobile; no traffic limits | Yes | Funnels | — | Free | No |
| [Mouseflow](#mouseflow) | Yes | Yes | Funnels, journeys, forms | JS error detection | Free: 500 sessions; $25/mo for 5k | No |
| [Rybbit](#rybbit) | Web, on the Pro plan | No | Funnels, journeys, retention | Error tracking, outside replay | Pro from $39/mo; 7-day trial | Yes, free (AGPL-3.0) |

"No" in the self-host column means the vendor's pricing and documentation pages we checked list no self-hosted or on-premises option. A dash in the other columns means those pages don't list the feature. If you only need replay, not FullStory's analytics, our [session replay tools comparison](/blog/best-session-replay-tools) goes deeper on replay pricing, masking defaults and retention, and [what is session replay](/blog/what-is-session-replay) covers the basics.

## What FullStory includes and what it costs

FullStory's [pricing page](https://www.fullstory.com/pricing/) lists three analytics plans, Business, Advanced and Enterprise, and each one says "Request pricing & demo". None has a public price.

The published part is the free plan. FullstoryFree is "a permanent version of our platform designed for individuals and small teams": 30,000 sessions a month, 12 months of data retention, "Session Replay, basic analytics, and debugging tools for up to 10 users", and no credit card. Dashboards, StoryAI (FullStory's AI agents) and anything above 30,000 sessions need a paid plan. The paid plans' comparison table lists heatmaps, click and scroll maps, funnels, journeys, retention analysis, sentiment signals, co-browsing and retroactive data history, with mobile analytics and Guides and Surveys as add-ons on all three.

Three things make FullStory hard to replace one for one:

- **Sentiment signals.** FullStory flags rage clicks, error clicks, dead clicks and thrashed cursors ([developer docs](https://developer.fullstory.com/browser/rage-clicks/)) and says these signals "are automatically prioritized by business impact" ([sentiment signals](https://www.fullstory.com/platform/sentiment-signals/)).
- **Dev Tools in the player.** A [Console and a Network view](https://help.fullstory.com/hc/en-us/articles/360020828313-Guide-to-Dev-Tools) open beside any replay, on every plan including Free.
- **Mobile coverage.** The [mobile SDKs](https://www.fullstory.com/platform/mobile-apps/) cover iOS, Android, Flutter, React Native and Cordova/Capacitor.

If 30,000 sessions a month covers you, FullstoryFree may be all you need.

## Why teams look for alternatives to FullStory

- **Price opacity and sales-led buying.** Past 30,000 sessions, every route runs through a demo and a quote. Teams that budget from a price list, or pay by card, need a vendor that publishes one.
- **Where the data lives.** [Private by Default](https://help.fullstory.com/hc/en-us/articles/360044349073-FullStory-Private-by-Default) is strict ("no text is captured or sent outside the user's browser unless it is explicitly allowlisted as safe to capture"), but you turn it on by asking FullStory's support team. [Fullstory Relay](https://help.fullstory.com/hc/en-us/articles/360046112593-How-to-send-captured-traffic-to-your-First-Party-Domain-using-Fullstory-Relay) routes capture through your own domain, yet the traffic is still "relayed immediately to the Fullstory servers for processing". Recordings that must stay on your infrastructure need a self-hosted tool.
- **Debugging depth.** Teams that mostly use replay to fix bugs want it inside the tool that already holds their errors, traces and logs.
- **Paying for more than you use.** Watching replays and running a few funnels doesn't need an enterprise contract.

## Enterprise digital experience analytics

These two cover most of what FullStory does, and both sell their top tiers through sales, as FullStory does.

### Contentsquare

[Contentsquare](https://contentsquare.com) is the closest like-for-like replacement: replay, heatmaps, funnels, journey analysis, JavaScript error and performance monitoring, surveys and an AI assistant called Sense, for "sites and mobile apps". Unlike FullStory, its lower tiers are self-serve. From the [pricing page](https://contentsquare.com/pricing/):

- **Free:** 200,000 monthly sessions, replays for 5% of them (up to 10,000), 1 project.
- **Growth:** from $49/month for 7,000 sessions to $409/month at 100,000, with 13 months of session data, 2 months of replays, 3 projects and zone-based heatmaps.
- **Pro** (from 1 million monthly sessions) and **Enterprise:** no published price.

Replay volume is the limit to understand. The replay allowance is "a fixed amount per month — it can't be increased", and on Growth every session is replayable up to 20,000 a month; above that, 15% of sessions are, with a floor of 20,000. Growth has a 15-day trial without payment details and 20% off when paid annually by card. Product analytics is still "a separate application for the next few months", and Contentsquare also owns Heap, which it [acquired](https://contentsquare.com/press/contentsquare-completes-acquisition-heap/) in December 2023.

**Pick it if** you want FullStory-level experience analytics with a free tier and public prices up to 200,000 sessions. **Skip it if** you need any session on demand: past 20,000 a month, most replays are sampled.

Contentsquare also runs Hotjar now; if Hotjar is what you're replacing, see our [Hotjar alternatives](/blog/hotjar-alternatives) guide.

### Quantum Metric

[Quantum Metric](https://www.quantummetric.com) is the other enterprise platform in this category, and FullStory's own [alternatives article](https://www.fullstory.com/blog/fullstory-alternatives/) groups it with Contentsquare. There's no self-serve tier: the [pricing page](https://www.quantummetric.com/pricing) offers "Enterprise plans built around your business", priced on "your annual session volume, the digital properties where we are deployed, product add-ons and other things like the amount of professional services you're looking for".

Every plan includes the Felix AI analyst, analytics with anomaly detection and Core Web Vitals monitoring, journeys, and replay with AI summaries, plus implementation and PII remediation; contact center, data streaming and voice of customer are among the add-ons. Replay covers "web or native mobile applications", [interaction heatmaps](https://www.quantummetric.com/platform/session-replay) show "page-level clicks, scrolls, and attention", and Quantum Metric says "Everything is encrypted at the client device and exposed personal information is never sent across the network."

**Pick it if** you're a large company replacing FullStory with a peer and want onboarding and PII cleanup in the contract. **Skip it if** sales-led pricing is why you're leaving: the buying process is the same.

## Developer debugging: replay tied to errors, console and network

These three suit teams whose main use of FullStory is Dev Tools: reproducing bugs from real sessions.

### LogRocket

[LogRocket](https://logrocket.com) pairs replay with debugging data. Each session carries "logs, network requests, JavaScript exceptions and performance data, as well as user and device metadata" ([session replay for developers](https://logrocket.com/products/session-replay-developers)), and the [mobile SDKs](https://logrocket.com/products/mobile) run on Android, iOS, React Native, Flutter, Ionic and Expo. Core also includes clickmaps, heatmaps, rage-click maps, path analysis, funnels and surveys.

The [pricing page](https://logrocket.com/pricing) charges for sessions; analytics, error and log events are unlimited. Its calculator runs from a free tier at 1,000 sessions a month to $83/month for 10,000, $176/month for 25,000 and $717/month for 100,000 on Core, and the page notes that the final price also depends on seats, retention and add-ons. Pro adds Galileo AI (session summaries, issue detection) and is "included free above 100K sessions / mo"; Enterprise adds unlimited seats and a self-hosted option. The trial is 14 days with no card.

**Pick it if** engineers own replay and you need mobile apps covered. **Skip it if** you want a flat price: costs climb quickly past 25,000 sessions.

### Sentry Session Replay

If you already use [Sentry](https://sentry.io) for errors, its replay covers the debugging half of FullStory. "Errors, traces, network requests, and console logs are all connected to the replay timeline", and rage clicks and dead clicks are linked to replays too, per the [Session Replay docs](https://docs.sentry.io/product/explore/session-replay/). It records the web plus Android, iOS, React Native and Flutter.

The [pricing page](https://sentry.io/pricing/) lists Developer free for one user, Team at $26/month and Business at $80/month billed annually, each with 50 replays a month; new users get 5,000 free replays a month for their first 3 months. Extra replays start at $0.00375 each pay-as-you-go and get cheaper with volume ([pricing docs](https://docs.sentry.io/pricing/)), so 100,000 replays a month on Team comes to about $383 including the plan. Self-hosted Sentry is "Fair Source under the FSL (Functional Source License)" and needs 4 CPU cores and 16 GB of RAM plus 16 GB of swap ([self-hosted docs](https://develop.sentry.dev/self-hosted/)).

**Pick it if** you want replays of the sessions that broke, next to the stack trace. **Skip it if** UX research is the main job: Sentry is an error-monitoring tool first.

### OpenReplay

[OpenReplay](https://openreplay.com) is open-source, developer-first replay. Its [DevTools](https://openreplay.com/product/feature/developer-tools) show "JavaScript errors with full stack traces and sourcemaps", let you "Inspect API calls, track failing requests, and analyze response payloads with sensitive data sanitized", and track CPU, memory and application state through a session. The [pricing page](https://openreplay.com/pricing.html) also lists co-browsing, heatmaps, funnels, journeys, product analytics and mobile.

The code defaults to AGPL-3.0, with the `ee/` directory under its own license ([LICENSE](https://github.com/openreplay/openreplay/blob/main/LICENSE)). "Open-Source is free — you host it on your own infrastructure", on at least 2 vCPUs, 8 GB of RAM and 50 GB of storage ([Docker guide](https://docs.openreplay.com/en/deployment/deploy-docker/)). The managed Dedicated plan starts at $199/month, billed hourly: a dedicated VM "with no limits on users, recordings, or retention", with a 7-day trial and no card. The page also lists a usage-based Serverless option and a self-hosted Enterprise tier.

**Pick it if** recordings must stay on your servers and developers are the main users. **Skip it if** you'd rather not run the infrastructure and $199/month is too much for a managed instance.

## FullStory alternatives for product teams

These three put product analytics first and attach replay to it, which suits teams that use FullStory mostly for funnels, retention and cohort questions.

### PostHog

[PostHog](https://posthog.com) is the broadest product-team option: product analytics, replay, heatmaps, feature flags, experiments, surveys and error tracking on one usage-based bill. [Rybbit vs PostHog](/compare/posthog) compares it with our product in detail.

Each month the [pricing page](https://posthog.com/pricing) gives 1 million analytics events, 5,000 recordings, 1 million feature-flag requests, 100,000 exceptions and 1,500 survey responses free, with no card. [Replay pricing](https://posthog.com/docs/session-replay/pricing) then runs from $0.0050 per web recording down to $0.0015 above 500,000, so 100,000 web recordings a month cost $272.50; mobile gets 2,500 free, then $0.0100 each. Replays can [include console logs and the network waterfall](https://posthog.com/docs/session-replay), and recording works in React Native, iOS, Android and Flutter apps as well as on the web. [Heatmap data](https://posthog.com/docs/toolbar/heatmaps), per PostHog, "is captured along with other events, so it doesn't contribute to your bill".

PostHog offers "a free Docker Compose deployment under an MIT license", but its [self-host page](https://posthog.com/docs/self-host) calls self-hosted deployments "officially unsupported" and says it has no guarantees for self-hosted instances.

**Pick it if** your product team wants flags, experiments and replay in one tool. **Skip it if** you need supported self-hosting or a narrower tool.

### Amplitude

[Amplitude](https://amplitude.com) is product analytics first, with replay, heatmaps, experimentation, and guides and surveys in one platform; its [pricing page](https://amplitude.com/pricing) says "Every plan includes the full platform and unlimited seats". Free gives 2 million events and 10,000 session replays a month with no card. Plus "Starts at $0", keeps the first 2 million events free, scales to 70 million, and adds heatmaps and 2-year data retention; it needs a card. Growth and Enterprise are custom and include 20,000 and 50,000 monthly replays. Replays cover web and mobile but are kept for 1 month on every plan, with more retention sold as an add-on on Growth and Enterprise.

**Pick it if** analytics is the main job and replay is for spot checks. **Skip it if** you watch replays at volume or need more than a month of them.

### Heap

[Heap](https://www.heap.io) is autocapture product analytics, owned by Contentsquare since December 2023. On its [pricing page](https://www.heap.io/pricing), Free covers up to 10,000 monthly sessions with 6 months of data history and SSO. Growth adds Contentsquare's Sense AI assistant, unlimited users and reports and 12 months of history, priced by estimate once you install the snippet; Pro and Premier have "Custom Session Pricing". Session replay, heatmaps and error analysis aren't available on Free or Growth; they're add-ons on Pro and Premier.

**Pick it if** you want autocapture analytics and already buy from Contentsquare. **Skip it if** replay is why you're shopping: here it's an add-on to an enterprise plan.

## Free and low-cost options

### Microsoft Clarity

[Clarity](https://clarity.microsoft.com) is the zero-cost answer. Microsoft's [FAQ](https://learn.microsoft.com/en-us/clarity/faq) says "Clarity is a free service forever. You never encounter traffic limits or are compelled to upgrade to a paid version", with no sampling. You get recordings, heatmaps and insights, and [mobile SDKs](https://learn.microsoft.com/en-us/clarity/mobile-sdk/) cover Android, iOS, React Native, Flutter and Cordova/Ionic.

The trade-offs are retention and control. Data "is retained for the webmaster's consumption up to 30 days from the time of recording", the FAQ states that Microsoft has access to it, and for visitors in the EEA, UK and Switzerland you must pass a consent signal; without valid consent, Clarity "limits data collection in those regions".

**Pick it if** the budget is zero and 30 days of history is enough. **Skip it if** you need longer retention, or a large European audience would leave you with thin data.

### Mouseflow

[Mouseflow](https://mouseflow.com) publishes its prices and bundles session replay; click, scroll and geo heatmaps; friction and JavaScript error detection; funnels; journey and form analytics; and feedback surveys. Per the [pricing page](https://mouseflow.com/pricing/), Free covers 500 sessions a month with 1 month of retention. Essential is $25/month for 5,000 sessions and 3 months, Advanced $109/month for 25,000 and 6 months, and Premium $319/month for 100,000 and 12 months, adding API access, an MCP connection and SSO. Every plan has unlimited users, annual billing saves up to 35%, and paid plans start with a 14-day trial.

**Pick it if** you want heatmaps and 12 months of history at a published price. **Skip it if** you need product analytics or debugging depth: Mouseflow is built for UX and conversion work.

## Privacy-first and self-hosted options

OpenReplay, above, is the self-hosted pick for developer replay, and PostHog and Sentry self-host with the caveats in their sections. Rybbit fits if you want replay inside privacy-first web analytics.

### Rybbit

[Rybbit](https://rybbit.com) is open-source web and product analytics with [session replay](/features/session-replay) on the Pro plan. The player's timeline marks page navigations, clicks, typing and rage clicks, so you can jump to where a visitor got stuck. Around it sit [funnels](/features/funnels), [user journeys](/features/user-journeys), [goals](/features/goals), [retention](/features/retention), [user profiles](/features/user-profiles), [Web Vitals](/features/web-vitals) and [error tracking](/features/error-tracking), which groups uncaught JavaScript errors and unhandled promise rejections with stack traces and affected-session counts.

Input values are masked by default, and [script attributes](/docs/script) mask or block elements by class or selector. Analytics tracking is cookieless and needs no consent banner. The cloud is hosted in the EU, and you can [self-host](/docs/self-hosting) everything, replay included, free under AGPL-3.0.

Standard starts at $19/month for 100,000 events with funnels, journeys, error tracking and Web Vitals, but no replay. Pro starts at $39/month for 100,000 events and adds replay, unlimited sites and team members, and 5-year data retention; billed annually, it's $26/month ([pricing](/pricing)). Both start with a 7-day trial, and a card is collected at signup. Replays are stored for 30 days.

Where it falls short of FullStory:

- No heatmaps, click maps or scroll maps.
- Rage clicks are marked in each replay, but there are no aggregated frustration reports and no error-click, dead-click or thrashed-cursor signals.
- Replays don't capture console output or network requests; errors live in the separate error tracking view.
- Web only: the [React Native SDK](/docs/sdks/react-native) tracks screens, events and errors, but session replay doesn't apply to mobile apps.
- No surveys, no AI session summaries and no FullStory importer; [data import](/docs/data-import) covers Plausible, Umami and Simple Analytics only.

**Pick it if** you want replay next to privacy-first analytics, at a published price, with the option to self-host. **Skip it if** heatmaps, mobile replay, frustration dashboards or console and network capture are why you use FullStory; LogRocket, Contentsquare or PostHog fit those better.

## Switching from FullStory: what to plan for

- **History doesn't move.** Rybbit has no FullStory importer, and none of the pages we checked for this article advertise one. Run both tools until the new one has enough history for your reports.
- **Replay retention varies.** FullstoryFree keeps 12 months of data. Clarity keeps 30 days, Amplitude 1 month, Contentsquare Growth 2 months of replays, Mouseflow 1 to 12 months by plan and Rybbit 30 days; OpenReplay's Dedicated plan sets no limit.
- **Know what's capped.** Contentsquare and Amplitude cap replays separately from sessions or events. Sentry, PostHog and LogRocket bill per replay or session past the free allowance.
- **Rebuild privacy rules.** FullStory's `.fs-exclude`, `.fs-mask` and `.fs-unmask` classes are FullStory's own. Map each to the new tool's equivalent before you record; in Rybbit, that's the block and mask options in the [script docs](/docs/script).
- **Mobile narrows the list.** Contentsquare, Quantum Metric, LogRocket, Sentry, OpenReplay, PostHog, Amplitude and Clarity document mobile app replay; Rybbit records the web only.

## Which FullStory alternative should you choose?

- **Same enterprise depth, different vendor:** Contentsquare for a free tier and a price list to start with; Quantum Metric for implementation and PII remediation in the contract.
- **Engineers debugging production issues:** LogRocket for web and mobile; Sentry Session Replay if you already run Sentry; OpenReplay if recordings must stay on your servers.
- **Product teams:** PostHog for analytics, flags, experiments and replay together; Amplitude if analytics comes first; Heap if you already buy from Contentsquare.
- **No budget:** Microsoft Clarity, if 30 days of data and an EU consent signal are acceptable, or FullstoryFree, if 30,000 sessions a month is enough.
- **Heatmaps and a year of history at a published price:** Mouseflow Premium, $319/month for 100,000 sessions.
- **Privacy-first analytics with replay and a self-hosted option:** Rybbit, unless heatmaps or mobile replay are requirements.

## FAQ

**What is the best free FullStory alternative?**
Microsoft Clarity is free with no traffic limits, but keeps data for 30 days. PostHog (5,000 recordings and 1 million events a month), Amplitude (10,000 replays and 2 million events) and Contentsquare (200,000 sessions, up to 10,000 replays) also have free plans, and OpenReplay and Rybbit are free to self-host.

**How much does FullStory cost?**
FullstoryFree costs nothing for 30,000 sessions a month, 12 months of data and up to 10 users. Business, Advanced and Enterprise have no public price; the pricing page says "Request pricing & demo" for each.

**Who are FullStory's main competitors?**
Contentsquare and Quantum Metric in enterprise experience analytics; LogRocket, Sentry and OpenReplay in developer-focused replay; Amplitude, Heap and PostHog in product analytics. FullStory's own alternatives article also names Adobe Analytics, Mixpanel, Pendo, Datadog and Dynatrace.

**Is there an open-source alternative to FullStory?**
Yes. OpenReplay (AGPL-3.0 by default, with a separately licensed `ee/` directory) and Rybbit (AGPL-3.0) self-host with replay included. PostHog's Docker Compose deployment is MIT-licensed but officially unsupported, and Sentry self-hosts under a license it calls Fair Source rather than open source.

**Which FullStory alternatives have heatmaps?**
Contentsquare, Quantum Metric, LogRocket, OpenReplay, PostHog, Microsoft Clarity and Mouseflow; Amplitude from its Plus plan; Heap as an add-on on Pro and Premier. Rybbit has none, and Sentry's pages we checked don't list them.

---

### Content Blog Funnel Analysis (`docs/content/blog/funnel-analysis.mdx`)

---
title: "Funnel Analysis: How to Find Where Users Drop Off"
description: "Funnel analysis shows where people abandon a multi-step flow. Learn how to define steps, read drop-off rates, segment by source and device, and fix the leaks."
keyword: "funnel analysis"
date: 2025-11-26
updated: 2026-09-28
tags: ["analytics", "funnels", "conversion", "optimization"]
author: Rybbit Team
---

Your site gets thousands of visitors a month. How many of them do the thing you built the site for?

Funnel analysis is how you answer that. You define the sequence of steps a user takes to reach a goal, count how many sessions reach each step, and find the step where most of them leave. Traffic numbers tell you how many people arrived. The funnel tells you where you lost them.

This guide covers what a conversion funnel is, the metrics that matter, how to set up and read a funnel, how to segment it by source and device, the patterns that point at specific problems, and how to fix them. Examples use [Rybbit's funnels](/features/funnels), but the method applies to any tool.

## What is a conversion funnel?

A conversion funnel is the path a visitor follows from arriving on your site to completing a goal: a purchase, a signup, a demo request, a download. It is called a funnel because it narrows. Many people enter at the top and a few come out the bottom.

Marketers often describe the same thing as a **sales funnel** with stages such as awareness, consideration, decision, and retention. In web analytics the stages are concrete: pages viewed and events fired, in order.

Some typical funnels:

- **Ecommerce:** product page → add to cart → checkout → payment complete
- **SaaS:** landing page → signup form → email verified → first key action
- **Lead generation:** landing page → form submitted → thank-you page
- **Content:** blog post → CTA click → form → download confirmed

Not everyone who enters completes the funnel, and that is normal. If 1,000 people view a product page, 300 add it to the cart, and 150 pay, the overall conversion rate is 15%. The opportunity is in the steps: lifting checkout completion from 50% to 60% yields 30 more orders from the same 300 carts, without buying a single extra visitor.

## The funnel metrics that matter

For every step you want four numbers:

- **Sessions at the step.** How many sessions reached it. The absolute count tells you how much is at stake at each stage.
- **Step conversion rate.** The share of sessions from the previous step that reached this one. This is the efficiency of the step.
- **Drop-off rate.** The inverse: the share that completed the previous step but not this one. The step with the highest drop-off and the most traffic is your first priority.
- **Overall conversion rate.** The share of sessions that entered the funnel and completed it. This is the number that connects the funnel to revenue.

Two more are worth knowing. **Time to convert** shows whether a step is slow as well as leaky. **Conversion by segment** (source, device, country, new vs returning) shows whether the funnel has one problem or several.

## How to run a funnel analysis

**1. Start from the goal.** Pick the single most important action on the site. If you cannot name one, you are not ready to build a funnel.

**2. Map three to five steps backward from it.** Every extra step adds noise. Start with the steps a user *must* pass through, and add optional pages later if they turn out to matter.

**3. Decide how strict the order is.** A strict funnel counts only sessions that hit the steps in sequence. Looser definitions count any session that eventually reached each step. Strict funnels are easier to act on; loose ones are more forgiving of real user behaviour. Know which one your tool uses.

**4. Pick the time window.** A checkout funnel should complete within a session. A B2B signup-to-activation funnel might take two weeks. If the window is too short you undercount conversions; too long and you blur the signal.

**5. Segment before you conclude.** A 40% drop-off at checkout might be 20% on desktop and 70% on mobile. Split by device, traffic channel, country, and landing page before deciding what is broken.

**6. Compare to a previous period.** The absolute drop-off matters less than whether it got better or worse after the change you shipped.

## Funnel analysis in Rybbit

Rybbit's funnel builder lets you define steps from page paths, custom events, or autocaptured interactions (outbound link clicks, button clicks, form submissions, and copied text), then shows sessions, conversion rate, dropped sessions, and drop-off rate for every step.



**Define the steps.** Path steps match pageviews by URL, with `*` wildcards for things like `/products/*`. Event steps match [custom events](/features/custom-events) such as `add_to_cart`. Each step can carry property filters, so "purchase where plan = pro" is one step, not a separate funnel.

**Read the result.** The visualization updates as you edit. For each step you see how many sessions arrived, the percentage of first-step sessions that made it this far, and how many were lost since the previous step. The biggest bar-to-bar drop is where to look first.

**Drill into the sessions.** Every step links to the sessions behind it, so when a number looks wrong you can open [session replay](/features/session-replay) and watch what people did at that step instead of guessing.

**Track single-step conversions as goals.** Not every conversion is multi-step. [Goals](/features/goals) track one action, such as a thank-you page view, a form submission, or a button click, and report conversions, conversion rate, and performance by channel and device.



**Find the pages that feed the funnel.** The Pages report shows traffic, bounce rate, and time on page for every URL. Click into a page to see only the sessions that visited it and check whether they convert more or less than average. This is how you learn which product pages produce cart additions and which blog posts produce signups.



**Segment by channel and device.** Any filter applies to the funnel. Filter by channel to compare organic search, paid, email, social, and referral; filter by device to compare mobile and desktop.





A common finding: two channels send the same traffic, but one converts twice as well. Another: mobile brings half the sessions and a tenth of the conversions, which makes mobile checkout the highest-value fix on the site.

**Check the paths you did not define.** A funnel assumes a sequence. Real users skip steps, loop back, and leave and return. The [Journeys report](/features/user-journeys) shows the actual paths through the site, so you can work backward from a conversion to see what successful users did, or forward from a landing page to see where people actually go.



## Reading the patterns

**A steep drop at one step.** Something specific is wrong there: a confusing form, a bug, a slow page, an unexpected cost, a trust gap at payment, or a broken mobile layout. Watch replays of sessions that dropped at that step; the cause is usually visible within ten recordings.

**Gradual erosion at every step.** No single step looks terrible, but 30–40% leaks at each one add up. This usually means the value proposition is unclear or the traffic is poorly matched to the offer. Fix the message and the targeting before the UI.

**Segments that disagree.** Organic converts and paid does not; new visitors convert and returning ones do not; one country lags the rest. Each of these points at a specific audience or campaign rather than the funnel itself.

**A drop that appeared on a date.** Line the funnel up against your deploys and campaigns. A step that lost 20% overnight is a regression, not a design problem.

## Fixing the leaks

- **Reduce friction.** Every field, step, and second of load time costs conversions. Remove fields you do not need, merge steps, and measure page speed at the leaky step.
- **Build trust where money changes hands.** Security badges, transparent pricing, a clear privacy policy, and recognizable payment options matter most at checkout.
- **Say what happens next.** Progress indicators and a preview of the following step reduce abandonment from uncertainty.
- **Fix mobile first if mobile is worse.** It is usually where the largest gap is.
- **Address the objection at the step where it appears.** High price: show value. Long commitment: offer a trial. Confusing process: add one sentence of explanation.
- **Change one thing and re-measure.** Compare the funnel before and after, on the same segment, over the same window. If you change three things at once you will not know which one worked.

## Typical funnel conversion rates

Ranges vary enormously by industry and traffic quality, so use them to sanity-check, not to grade yourself:

| Funnel type | Typical path | Typical overall conversion | Usual weak point |
|---|---|---|---|
| Ecommerce | product → cart → checkout → payment | 1–5% | payment step, mobile checkout |
| SaaS signup | landing → form → verify email → first action | 5–30% | form abandonment, email verification |
| Lead generation | landing → form → thank-you | 10–40% | forms that ask too much |
| Content download | post → CTA → form → confirmation | 5–20% | unclear value of the download |

For ecommerce specifically, the [ecommerce analytics guide](/blog/ecommerce-analytics) goes deeper on the metrics behind each step.

## Funnel analysis tools

Most analytics products can build a funnel; they differ in how much setup they need and what you can do with a leaky step once you find it.

- **Rybbit** defines steps from paths, events, or autocaptured clicks and forms, filters by any dimension, and links each step to session replays. Open source and self-hostable.
- **Google Analytics 4** builds funnels in Explorations. Powerful, but every step must be an event you configured in advance, and the interface is the hardest of the group to learn.
- **PostHog, Mixpanel, and Amplitude** are product-analytics suites with deep funnel tooling, including strict ordering and conversion-time analysis. They assume an instrumented product and event schema.
- **Matomo** offers funnels as a paid plugin on self-hosted installs and as part of its cloud plans.

If you already track pageviews and a handful of events, you can build your first funnel in any of these in an afternoon. Pick the one whose data you already have.

## Getting started

1. Define the one conversion that matters most.
2. Map three to five steps backward from it.
3. Build the funnel and note the overall conversion rate and the biggest drop.
4. Segment by device and channel to see whether the drop is universal.
5. Watch a few replays at the leaky step.
6. Change one thing, wait a week or two, and compare.
7. Repeat.

Most sites do not need more traffic to grow. They need to convert more of the traffic they already have, and funnel analysis is how you find out where that traffic is going.

---

**Related reading:**
- [Funnels documentation](/docs/funnels), [Goals](/docs/goals), and [Journeys](/docs/feature-guides/journeys)
- [Retention Analysis: How to Measure and Improve User Retention](/blog/retention-analysis)
- [What Is Bounce Rate? How to Calculate and Reduce It](/blog/bounce-rate)
- [What Is Multivariate Testing?](/blog/multivariate-testing)
- [Best Marketing Attribution Tools](/blog/marketing-attribution-tools)

---

### Content Blog Geographic Segmentation (`docs/content/blog/geographic-segmentation.mdx`)

---
title: "Geographic Segmentation: How to Use Location Data in Analytics"
description: "How marketers use geographic segmentation: what location data reveals, how to segment campaigns by country and region, and how to act on the results."
keyword: "geographic segmentation"
date: 2025-12-03
updated: 2026-09-28
tags: ['marketing', 'analytics', 'segmentation', 'targeting', 'geographic']
author: Rybbit Team
---

Geographic segmentation divides your audience by location—country, region, city, or neighborhood. For marketers, it's one of the most practical segmentation strategies because location data reveals real behavioral differences across markets. Whether you're optimizing landing pages, adjusting ad spend, or localizing content, geographic insights drive better decisions.

## What is Geographic Segmentation?

Geographic segmentation groups your visitors, customers, or prospects based on where they're located. It's straightforward in concept but powerful in application.

Simple examples:
- **US visitors** might see pricing in USD with US shipping costs
- **European visitors** might see GDPR-compliant messaging
- **Mobile-heavy regions** might receive mobile-optimized experiences
- **High-traffic regions** might warrant dedicated marketing budgets

The core idea: Different locations have different needs, behaviors, and preferences. When you recognize and respond to those differences, your marketing becomes more effective.

## Why Geographic Segmentation Matters

Geographic segmentation isn't just about translation or currency conversion. It addresses fundamental marketing questions:

### Market Penetration
Some geographic markets are more mature than others. Your product might dominate in Germany but be relatively unknown in Brazil. Segmentation helps you identify which regions are underperforming and deserve more attention or different positioning.

### Regional Preferences
Consumer behavior varies by region. Payment methods differ, holidays differ, language preferences differ, seasonal patterns differ. What sells in summer in the Northern Hemisphere sells in winter in the Southern Hemisphere.

### Resource Allocation
Marketing budgets are finite. Geographic data shows you where your customer acquisition is most efficient. Instead of spreading spend evenly, you concentrate resources on high-performing regions and test carefully in emerging markets.

### Regulatory Compliance
Different regions have different rules. GDPR applies to Europe. Privacy laws differ in California, Canada, Australia. Geographic segmentation helps you understand where compliance requirements apply.

### Competitive Positioning
Competition isn't uniform globally. You might face different competitors in different regions. Knowing where competitors are strong helps you position strategically.

## Common Applications of Geographic Segmentation

### Localized Marketing Campaigns
Tailoring campaigns by region increases relevance. A campaign that resonates in New York might miss entirely in rural Montana. Geographic segmentation lets you test different messages in different places.

### Pricing Strategy
International pricing often varies based on local purchasing power, competition, and regulations. Geographic segmentation reveals whether your pricing strategy is competitive in each region.

### Content Localization
Beyond translation, content localization means adjusting tone, examples, references, and focus for different audiences. A tech product aimed at decision-makers in Japan might emphasize different benefits than the same product for German audiences.

### Regional Product Focus
Some product features resonate more in certain regions. Geographic segmentation shows you which features drive engagement where, informing your regional roadmap.

### Event and Campaign Timing
Geographic data reveals whether events and campaigns land better at certain times for certain regions. Holiday campaigns, seasonal promotions, and product launches can be timed regionally.

### Paid Advertising Optimization
With geographic performance data, you adjust ad spend per region, pause campaigns in underperforming areas, and double down on high-ROI locations.

## How to Implement Geographic Segmentation in Your Analytics

Analytics platforms that support geographic filtering let you slice your data by location. This reveals actual visitor behavior by region rather than guessing.

### Accessing Geographic Data in Your Analytics

Modern analytics tools display visitor locations automatically. This typically comes from IP geolocation data. Once you can see how your visitors distribute geographically, you can start segmenting your analysis.



The geographic filter lets you focus on specific regions. Rather than analyzing all visitors together, you can examine Germany separately from the US, which often reveals completely different behavior patterns.

### Comparing Geographic Segments

Once you can filter by location, comparison becomes possible. You can ask questions like:

- **Which country has the highest bounce rate?** If visitors from Brazil bounce 2x more than visitors from Canada, that signals either a targeting problem or a product-market fit issue in Brazil.

- **Where do users spend the most time?** Some regions might have significantly longer session durations, suggesting higher engagement.

- **Which regions convert best?** Conversion rates often vary dramatically by geography.

- **Where do users engage most with specific features?** Geographic data combined with [event tracking](/docs/track-events) shows which regions engage with which parts of your product.



### Combining Geographic Filters with Other Dimensions

Geographic data becomes even more powerful when combined with other segmentation dimensions:

- **Geography + Traffic Source:** Do visitors from organic search in Germany behave differently than paid visitors from Germany?
- **Geography + Device Type:** Is your mobile experience performing worse in specific regions?
- **Geography + Traffic Channel:** Which channels work best in which regions?



By layering filters, you discover specific insights that single-dimension analysis misses.

## Challenges in Geographic Segmentation

Geographic segmentation, while powerful, comes with practical challenges:

### Data Quality
IP geolocation isn't perfect. It's usually accurate to the country level, sometimes to the city level, but not to the street address. VPNs and proxies distort location data. Mobile networks sometimes mis-report location.

### Sample Size
If you're optimizing for 50 different countries, some might have tiny visitor volumes. Statistical significance becomes harder to achieve. You need enough traffic per region to draw reliable conclusions.

### Cultural Complexity
Just because two people live in the same country doesn't mean they're the same audience. Large countries like the US, India, and Brazil have significant cultural variation within their borders.

### Implementation Complexity
Localizing campaigns for 20 different regions requires 20x more marketing effort. It's powerful but expensive. Most companies focus on their top 5-10 regions rather than trying to personalize for everyone.

### Privacy Considerations
IP-based geolocation relies on collecting location data. Depending on your audience and jurisdiction, you may face privacy restrictions. Privacy-first analytics approaches need to balance geolocation with visitor privacy.

## Real-World Example: Using Geographic Data to Optimize

Here's how geographic segmentation might work in practice:

**Your situation:** You run a B2B SaaS tool. You notice overall bounce rates are 40%. That seems high.

**Geographic breakdown reveals:**
- US visitors: 35% bounce rate
- German visitors: 45% bounce rate
- Brazilian visitors: 65% bounce rate

**This tells you:**
- US is working fine
- Germany needs attention
- Brazil has a serious problem

**Next steps:**
- For Germany: Maybe pricing is unclear, or the mobile experience is poor. You dig deeper.
- For Brazil: Currency conversion, payment methods, or messaging might be off. Or maybe you're attracting wrong audience through ads.

By segmenting geographically first, you avoid the trap of "our bounce rate is too high" and instead ask "our bounce rate is too high in specific regions, and here's what I'll do about it."

## Segmentation and Analytics Tools

Not all analytics platforms handle geographic segmentation equally. Some key questions to ask:

- Can you filter your data by country and region in real time?
- Can you combine geographic filters with other dimensions (traffic source, device, custom properties)?
- How accurate is the location data?
- Can you set up comparisons between geographic segments?
- Does the tool support historical trend analysis by region?

Having geographic segmentation built into your analytics workflow means you can answer location-based questions quickly without exporting data or running custom reports.

## Moving Beyond Basic Segmentation

Once you master basic geographic segmentation (country-level filtering), more advanced approaches become possible:

### Regional Cohort Analysis
Track specific cohorts by region. Do monthly active users in Japan have different [retention curves](/features/retention) than monthly active users in the UK?

### Regional Funnels
Build funnels for specific regions. Does your checkout funnel drop off at different steps in different countries?

### Regional Benchmarking
As you expand into new markets, compare early performance against established markets. This reveals whether you're on track.

### Geographic Performance Trends
Watch how performance changes over time in specific regions. Is Germany growing or declining? When did that shift happen?

## Common Mistakes in Geographic Segmentation

### Mistake 1: Over-Segmentation
Trying to customize for 100 different locations dilutes your effort. Start with your top 5-10 regions.

### Mistake 2: Ignoring Within-Country Variation
Treating "Germany" as homogeneous misses important local differences. Large countries need sub-regional analysis.

### Mistake 3: No Action on Geographic Insights
Discovering that bounce rates differ by region only matters if you then act on it. Insights without action are just interesting facts.

### Mistake 4: Assuming Causation
If US visitors engage more than Brazil visitors, that could be many things—product-market fit, targeting, product maturity, time of day when they visit. Don't assume one cause.

### Mistake 5: Static Segmentation
Geographic markets change. Competition shifts, regulations evolve, audience preferences shift. Regular review of geographic performance prevents stale strategies.

## Geographic Segmentation and Broader Analytics Strategy

Geographic segmentation doesn't live in isolation. It connects to your broader [web analytics](/blog/what-is-web-analytics) approach.

Understanding [traffic sources](/blog/marketing-attribution-tools) by geography reveals which channels work best where. Tracking [bounce rates](/blog/bounce-rate) by region shows you where engagement is weakest. Analyzing [session behavior](/docs/track-events) by location reveals usage patterns.

For [e-commerce businesses](/for-ecommerce), geographic data combines with [product-level analytics](/blog/ecommerce-analytics) to show which products sell where. For content creators, geography reveals which content resonates in different markets.

## Getting Started with Geographic Segmentation

If you're new to geographic segmentation, start simple:

**Week 1:** Look at your visitor distribution. Which countries/regions send you the most traffic? Which are you surprised by?

**Week 2:** Compare performance metrics by region. Bounce rate, session duration, conversion rate—which regions stand out?

**Week 3:** Layer in traffic source. Which channels work best in which regions?

**Week 4:** Make one small change based on geographic insights and measure the result.

This four-week approach turns geographic data from "interesting dashboard numbers" into actionable insights.

## Privacy-Conscious Geographic Segmentation

If you're using Rybbit or another privacy-first analytics platform, geographic segmentation works through IP-based location data without tracking individual users across sites. You see regional patterns without collecting personally identifiable information.

This matters because geographic segmentation becomes both more useful (you get real data) and more ethical (you're not invading privacy to get it).

## Final Thoughts

Geographic segmentation is one of the most practical marketing optimizations available. It doesn't require expensive tools or complex infrastructure. It simply requires answering: How do different locations behave differently?

The answer often surprises you. What you assume is true for "your audience" might only be true for your largest geographic segment. What you think is a product problem might actually be a regional messaging problem.

By implementing geographic segmentation in your analytics, you move from "one-size-fits-all" marketing to location-aware marketing. You spend more where it works, test carefully where it doesn't, and optimize regionally rather than globally.

Start by exploring your geographic data. Segment by country. Compare performance. Act on what you learn.

---

**Additional Resources:**

- [Best Marketing Attribution Tools](/blog/marketing-attribution-tools)
- [What Is Bounce Rate? How to Calculate and Reduce It](/blog/bounce-rate)
- [Ecommerce Analytics: The Metrics and Reports That Matter](/blog/ecommerce-analytics)
- [What Is Web Analytics? A Complete Guide](/blog/what-is-web-analytics)
- [Tracking Events and Custom Dimensions](/docs/track-events)
- [Getting Started with Rybbit](/docs)

---

### Content Blog Google Analytics Mcp (`docs/content/blog/google-analytics-mcp.mdx`)

---
title: "Google Analytics MCP Server: Setup Guide and Alternatives"
description: "How to set up Google's official Google Analytics MCP server in Claude Code, Claude Desktop or Gemini CLI, plus its 9 tools, common errors and alternatives."
keyword: "google analytics mcp"
date: 2026-09-27
tags: ["google analytics", "mcp", "ai"]
author: Rybbit Team
---

The Google Analytics MCP server is Google's official, open-source bridge between GA4 and AI assistants. It runs on your own computer, reads your properties through the Google Analytics Admin and Data APIs, and lets Claude, Gemini and other MCP clients answer questions like "how many users did I have yesterday?" It is free, read-only and still labeled experimental. Setup takes Python and pipx, the gcloud CLI, a Google Cloud project with two APIs enabled, an OAuth client and one entry in your AI client's config.

This guide covers that setup in Claude Code, Claude Desktop and Gemini CLI using Google's documented configuration, then the nine tools (the README names seven), fixes for common errors, the server's limits, and hosted alternatives.

Disclosure: we build [Rybbit](/features/web-analytics), a privacy-first analytics tool with its own hosted MCP server, which appears in the alternatives. Every claim about Google's server, Stape and Zapier comes from that vendor's own documentation, repository or pricing page, linked inline and checked in September 2026.

## Google Analytics MCP at a glance

| Detail | Google Analytics MCP server |
| --- | --- |
| Publisher | Google, in the [googleanalytics GitHub organization](https://github.com/googleanalytics/google-analytics-mcp); Apache-2.0 license |
| Package | [`analytics-mcp` on PyPI](https://pypi.org/project/analytics-mcp/), version 0.7.0 (29 July 2026) |
| Status | "Experimental" in the README title; support through GitHub issues and a Discord channel |
| Where it runs | On your machine, as a local process your AI client starts. Google doesn't host it |
| Access | Read-only: it "can't edit your Google Analytics configuration or settings" |
| Tools | 9: account and property info, annotations, custom definitions, and core, realtime, funnel and conversions reports |
| Documented clients | Gemini CLI, Gemini Code Assist, Claude Code |
| Price | Free; requests count against GA's [Data API quotas](https://developers.google.com/analytics/devguides/reporting/data/v1/quotas) |

## What the Google Analytics MCP server is

MCP (Model Context Protocol) lets an AI application call tools on a server. Google's server wraps the [Google Analytics Admin API](https://developers.google.com/analytics/devguides/config/admin/v1) and [Data API](https://developers.google.com/analytics/devguides/reporting/data/v1) as tools, so an assistant can find your property, pull a report and explain it in one conversation.

It is official: the Google Analytics team maintains it, the [Analytics developer docs](https://developers.google.com/analytics/devguides/MCP) link to it, and Google's [list of official MCP servers](https://github.com/google/mcp) includes it among the open-source servers you run yourself. Two design choices shape everything below:

- **It is read-only.** Google's developer page says: "The MCP server is available for read requests only. It can't edit your Google Analytics configuration or settings."
- **It runs locally.** Your AI client launches it as a subprocess. Google hosts [remote MCP servers](https://docs.cloud.google.com/mcp/supported-products) for BigQuery, Cloud SQL and dozens of other products, but that list (updated 25 September 2026) has no Analytics entry, and Anthropic's [connector docs](https://support.claude.com/en/articles/11175166-getting-started-with-custom-connectors-using-remote-mcp) say local servers aren't available on claude.ai or in Cowork, only in Claude Desktop.

One naming trap: Google's package is `analytics-mcp`. The similarly named [`google-analytics-mcp`](https://pypi.org/project/google-analytics-mcp/) on PyPI is a separate community project by a different author.

## What you need before you start

- **A Google account with access to the GA4 properties** you want to query. The server sees exactly what that account can see.
- **Python 3.10 or newer**, the package's [minimum](https://github.com/googleanalytics/google-analytics-mcp/blob/main/pyproject.toml), and [pipx](https://pipx.pypa.io/stable/how-to/install-pipx.html), which runs the server in an isolated environment.
- **The [Google Cloud CLI](https://docs.cloud.google.com/sdk/docs/install-sdk)** (`gcloud`), used to create credentials.
- **A Google Cloud project** where you can enable APIs and create an OAuth client. It hosts nothing; it owns the OAuth client and the API quota.

## How to set up the Google Analytics MCP server

Steps 1 to 5 are the same for every AI client. They follow Google's [README](https://github.com/googleanalytics/google-analytics-mcp), plus the Google Cloud pages it links to.

### Step 1: Install pipx

On macOS with Homebrew:

```bash
brew install pipx
pipx ensurepath
```

On Ubuntu or Debian use `sudo apt install pipx`, and on Windows `scoop install pipx`, then run `pipx ensurepath` and open a new terminal ([pipx docs](https://pipx.pypa.io/stable/how-to/install-pipx.html)). If you use uv, a Google maintainer [confirmed](https://github.com/googleanalytics/google-analytics-mcp/issues/165) that `uvx analytics-mcp` works too.

### Step 2: Enable the Admin and Data APIs

In your Google Cloud project, enable the [Google Analytics Admin API](https://console.cloud.google.com/apis/library/analyticsadmin.googleapis.com) and the [Google Analytics Data API](https://console.cloud.google.com/apis/library/analyticsdata.googleapis.com), or do both from the terminal with [`gcloud services enable`](https://docs.cloud.google.com/sdk/gcloud/reference/services/enable):

```bash
gcloud services enable analyticsadmin.googleapis.com analyticsdata.googleapis.com --project=YOUR_PROJECT_ID
```

### Step 3: Create an OAuth client

The login in step 4 needs an OAuth client of your own: Google's [ADC troubleshooting page](https://docs.cloud.google.com/docs/authentication/troubleshoot-adc) explains that gcloud's default login can block scopes for services outside Google Cloud, and that you add them with your own client ID.

1. Open the Google Auth Platform **Clients** page in the same project ([Google's guide](https://support.google.com/cloud/answer/15549257)). If the project isn't registered with Google Auth yet, you'll be asked to do that first.
2. Click **Create client**, pick the **Desktop app** type, and create it.
3. Download the client's JSON file.

If you chose an External audience, the app starts in **Testing**, which only accepts listed test users, so add your own Google account under **Audience**. Google also [expires test users' authorizations seven days after consent](https://support.google.com/cloud/answer/15549945), refresh tokens included, so a setup left in Testing stops working after a week until you log in again. Publishing the app removes that expiry.

### Step 4: Log in with Application Default Credentials

Run Google's documented command, pointing `--client-id-file` at the JSON from step 3:

```bash
gcloud auth application-default login \
  --scopes https://www.googleapis.com/auth/analytics.readonly,https://www.googleapis.com/auth/cloud-platform \
  --client-id-file=YOUR_CLIENT_JSON_FILE
```

Sign in with the Google account that can see your GA4 properties. gcloud then prints `Credentials saved to file: [PATH_TO_CREDENTIALS_JSON]`. Copy that path: it is the file the server reads, not the client JSON you downloaded. By default it is `$HOME/.config/gcloud/application_default_credentials.json` on macOS and Linux and `%APPDATA%\gcloud\application_default_credentials.json` on Windows ([Google Cloud docs](https://docs.cloud.google.com/docs/authentication/application-default-credentials)).

Then give the credentials a quota project, the Google Cloud project that API usage from user credentials counts against ([troubleshooting page](https://docs.cloud.google.com/docs/authentication/troubleshoot-adc)). The APIs from step 2 must be enabled there, and your account needs the `serviceusage.services.use` permission, which the Service Usage Consumer role includes:

```bash
gcloud auth application-default set-quota-project YOUR_PROJECT_ID
```

The README's other route is impersonating a service account, but Google [notes](https://docs.cloud.google.com/docs/authentication/troubleshoot-adc) that locally impersonated credentials aren't supported by every authentication library, so the OAuth route is simpler for a personal setup.

### Step 5: Test the credentials with one API call

Before involving an AI client, check that the credentials, APIs and property access work with a call a Google maintainer [suggests in the issue tracker](https://github.com/googleanalytics/google-analytics-mcp/issues/31) (macOS and Linux shells). Your numeric property ID is under **Admin > Property Settings** in GA ([Google's docs](https://developers.google.com/analytics/devguides/reporting/data/v1/property-id)):

```bash
curl \
  -H "x-goog-user-project: YOUR_CLOUD_PROJECT_ID" \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  https://analyticsdata.googleapis.com/v1beta/properties/YOUR_PROPERTY_ID/metadata
```

A JSON list of dimensions and metrics means you're ready. Any error here would also break the server inside Claude or Gemini, where it's harder to read; the [errors section](#common-errors-and-fixes) maps the usual ones to fixes.

## Add the GA4 MCP server to Claude Code, Claude Desktop or Gemini CLI

Every client needs the same three things: the command `pipx run analytics-mcp`, the full path to your credentials file, and your project ID. On Windows, JSON strings need doubled backslashes (`C:\\Users\\...`).

### Claude Code

Google's README gives a one-command setup:

```bash
claude mcp add analytics-mcp \
  --scope user \
  -e "GOOGLE_APPLICATION_CREDENTIALS=PATH_TO_CREDENTIALS_JSON" \
  -e "GOOGLE_PROJECT_ID=YOUR_PROJECT_ID" \
  -- pipx run analytics-mcp
```

`--scope user` makes the server available in all your projects, not just the current one ([Claude Code docs](https://code.claude.com/docs/en/mcp)). Run `claude mcp list`, or `/mcp` inside a session, to confirm it connected.

### Claude Desktop

The README doesn't cover Claude Desktop, but Google's maintainers say they [tested it with Claude](https://github.com/googleanalytics/google-analytics-mcp/issues/40), including on Windows. Open **Settings > Developer > Edit Config** from the Claude menu, which opens `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS or `%APPDATA%\Claude\claude_desktop_config.json` on Windows ([MCP docs](https://modelcontextprotocol.io/docs/develop/connect-local-servers)), and add Google's server block:

```json
{
  "mcpServers": {
    "analytics-mcp": {
      "command": "pipx",
      "args": ["run", "analytics-mcp"],
      "env": {
        "GOOGLE_APPLICATION_CREDENTIALS": "PATH_TO_CREDENTIALS_JSON",
        "GOOGLE_PROJECT_ID": "YOUR_PROJECT_ID"
      }
    }
  }
}
```

Quit Claude Desktop completely and reopen it (on Windows, quit from the system tray, the maintainers note). `analytics-mcp` then appears in the chat box's connectors menu. If it doesn't, its log says why: `~/Library/Logs/Claude/mcp-server-analytics-mcp.log` on macOS, or the same file name under `%APPDATA%\Claude\logs` on Windows.

### Gemini CLI and Gemini Code Assist

This is the path Google documents first. Add the same `analytics-mcp` block to `~/.gemini/settings.json`, merged into any existing `mcpServers` object, then start Gemini and type `/mcp`; `analytics-mcp` should be listed with its tools.

### Cursor, VS Code, Codex and other clients

Any client that launches local servers takes the same command, arguments and environment variables. Asked about Google's Antigravity IDE, a maintainer [answered](https://github.com/googleanalytics/google-analytics-mcp/issues/171): "The config is the same as in the setup in the readme." Clients that only connect to remote URLs need a [hosted alternative](#alternatives-to-googles-server).

## The 9 Google Analytics MCP tools

The README lists seven tools, but release 0.7.0 registers nine in its [tool list](https://github.com/googleanalytics/google-analytics-mcp/blob/main/analytics_mcp/coordinator.py), adding `list_property_annotations` and `run_conversions_report`.

| Tool | What it returns |
| --- | --- |
| `get_account_summaries` | Every Google Analytics account and property your credentials can access |
| `get_property_details` | Details of one property |
| `list_google_ads_links` | Google Ads accounts linked to a property |
| `list_property_annotations` | The property's annotations: notes on dates or periods such as releases or campaign launches |
| `get_custom_dimensions_and_metrics` | The property's custom dimensions and metrics |
| `run_report` | A Data API report: dimensions, metrics, date ranges, filters and sort order, [10,000 rows by default and up to 250,000](https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/properties/runReport) |
| `run_realtime_report` | Activity from the last 30 minutes; realtime reports can't use custom metrics |
| `run_funnel_report` | A funnel from event or filter steps, with optional breakdowns, next actions and segments |
| `run_conversions_report` | Attributed conversions, ad clicks, ad cost and return on ad spend, with data-driven or last-click attribution |

The realtime tool doesn't expose minute ranges, so it always gets the API's default window of the last 30 minutes ([realtime docs](https://developers.google.com/analytics/devguides/reporting/data/v1/realtime-basics)). Funnel and conversions reports use the Data API's alpha version: Google warns that funnel reporting may see breaking changes and that conversion reporting "may not be available to your Google Analytics property" ([funnel docs](https://developers.google.com/analytics/devguides/reporting/data/v1/funnels), [conversions docs](https://developers.google.com/analytics/devguides/reporting/data/v1/conversions-api-basics)).

### Questions to ask it

Google's developer page and README suggest prompts like "How many users did I have yesterday?", "What were my top selling products yesterday?" and "Were most of my users in the last 6 months logged in?" The newer tools open up a few more:

- "Build a funnel from `view_item` to `add_to_cart` to `purchase` for last month, broken down by device category." (`run_funnel_report`)
- "Traffic dropped in the second week of August. Are there annotations for that week?" (`list_property_annotations`, then `run_report`)
- "Compare return on ad spend by campaign under last-click and data-driven attribution for Q3." (`run_conversions_report`)

Start by asking for your account summary, so the assistant picks up the numeric property IDs every other tool needs.

## Common errors and fixes

Each fix comes from Google's documentation or a Google maintainer's answer in the repository's issue tracker.

**"This app is blocked" or "Access blocked: Authorization Error" at login.** Usually the Analytics scope went through gcloud's default client. Create your own OAuth client (step 3) and pass it with `--client-id-file` ([ADC troubleshooting](https://docs.cloud.google.com/docs/authentication/troubleshoot-adc)).

**"Access blocked: [app] has not completed the Google verification process."** The OAuth app is in Testing and your account isn't a test user. Add it under **Audience** ([audience docs](https://support.google.com/cloud/answer/15549945)).

**"Unrecognized credential type."** `GOOGLE_APPLICATION_CREDENTIALS` points at the OAuth client JSON you downloaded; Google says "Client ID files are not supported to provide credentials for ADC." Point it at the file gcloud wrote in step 4.

**Errors about a missing quota project, an API that isn't enabled, or project 764086051850.** Your credentials have no quota project (764086051850 is gcloud's own project). Run `gcloud auth application-default set-quota-project YOUR_PROJECT_ID`, or set `GOOGLE_CLOUD_QUOTA_PROJECT` in the server's `env`, which Google's auth library [reads](https://googleapis.dev/python/google-auth/latest/reference/google.auth.environment_vars.html) as the quota project.

**Permission errors on one property while others work.** The account behind the credentials has no access to it. The README requires "a user with access to your Google Analytics accounts or properties", so grant access in GA's Admin settings or log in as a different account.

**It worked for a week, then stopped authenticating.** The OAuth app is still in Testing, where authorizations expire after seven days. Rerun the step 4 login, or publish the app.

**The server is missing in Claude Desktop, or tool calls hang.** A maintainer's [checklist](https://github.com/googleanalytics/google-analytics-mcp/issues/40): use the full path in `GOOGLE_APPLICATION_CREDENTIALS`, set the project variable, quit Claude completely and reopen it, and check that pipx works with `pipx run cowsay -t mooooo`. If the log says `pipx` can't be found, use its full path (from `which pipx`) as the command. Release 0.4.0 fixed tool-call deadlocks and 0.6.0 a Windows-specific one (both May 2026), so make sure you're on a current version.

**Gemini CLI seems to hang after your question.** It is probably waiting on its "Allow execution of MCP tool" prompt; a maintainer [notes](https://github.com/googleanalytics/google-analytics-mcp/issues/31) that Gemini won't proceed until you pick an option. Run `gemini --debug` for detail.

**Answers cut off, or Claude Code warns about output size.** Claude Code warns above 10,000 tokens of tool output and caps it at 25,000 by default ([Claude Code docs](https://code.claude.com/docs/en/mcp)). A `run_report` with no limit returns up to 10,000 rows, so ask for the top 20 or 50, or raise `MAX_MCP_OUTPUT_TOKENS`.

## Limitations of Google's GA4 MCP server

- **Read-only by design.** It can't create key events, custom dimensions, audiences or annotations, or change any setting.
- **Local only.** Everyone who wants it installs Python, the gcloud CLI and their own credentials. There's no shared endpoint for a team, and claude.ai, Claude's mobile apps and other remote-only clients can't use it.
- **Experimental, and moving.** Eight releases shipped between August 2025 and July 2026 ([PyPI history](https://pypi.org/project/analytics-mcp/#history)), and the annotations, funnel and conversions tools call alpha APIs.
- **Broader credentials than it needs.** gcloud's login requires the `cloud-platform` scope alongside the Analytics one. The server asks for read-only Analytics access, but a Google maintainer [explains](https://github.com/googleanalytics/google-analytics-mcp/issues/187) that this narrowing only applies to service account credentials; user credentials keep every scope they were created with. Treat the credentials file like a password.
- **GA's quotas apply.** A standard property gets [200,000 core tokens a day](https://developers.google.com/analytics/devguides/reporting/data/v1/quotas), 40,000 an hour (at most 14,000 from any one Cloud project) and 10 concurrent requests, with separate, equal buckets for realtime and funnel requests. Most requests cost 10 tokens or fewer; long date ranges and high-cardinality dimensions such as `pagePath` cost more, and every reporting tool accepts `return_property_quota` to show the cost. Analytics 360 gets ten times the token quota (our [Google Analytics pricing guide](/blog/google-analytics-pricing) covers what 360 costs).
- **GA's data caveats come along.** Results can be sampled, thresholded or folded into an "(other)" row, and each response flags these in its [metadata](https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/ResponseMetaData) (`samplingMetadatas`, `subjectToThresholding`, `dataLossFromOtherRow`). Ask the assistant to check before quoting a number.
- **Consent gaps stay in the data.** GA4 identifies visitors with a `_ga` cookie that lasts two years ([cookie docs](https://support.google.com/analytics/answer/11397207)). Where you ask for consent, visitors who decline are missing from every report, unless your property qualifies for Consent Mode's [behavioral modeling](https://support.google.com/analytics/answer/11161109), which requires, among other things, at least 1,000 denied events and 1,000 consenting users a day.
- **Analytics only.** None of the nine tools reads Google Search Console. For search data, see our [Google Search Console MCP guide](/blog/google-search-console-mcp).

## Alternatives to Google's server

| | Google's server | Stape | Zapier MCP | Rybbit MCP |
| --- | --- | --- | --- | --- |
| Where it runs | Your machine | Stape's servers | Zapier's servers | Rybbit Cloud, or your own server if you self-host |
| Login | gcloud credentials plus your own OAuth client | Google OAuth in the browser | Zapier account with Google Analytics connected | OAuth, or an API key in any client |
| Data it reads | Your GA4 properties | Your GA4 properties | Your GA4 properties | Rybbit's own tracking, plus Search Console |
| Can it write? | No | No | Some GA4 actions | Goals, funnels, sites, members, teams, user traits |
| Tools | 9 | The same 9 | 6 GA4 actions | 44 |
| Price | Free | Free | Included in Zapier plans; 2 tasks per tool call | API access on Standard (from $19/month) and Pro; free to self-host |

### Stape: Google's server, hosted for you

[Stape](https://stape.io/solutions/mcp-server-ga4), a server-side tagging company, hosts a version at `https://mcp-google-analytics.stape.io/mcp`. Its [repository](https://github.com/stape-io/google-analytics-mcp) describes a fork of Google's server under the same Apache-2.0 license, with the same nine tools, read-only through the `analytics.readonly` scope. You log in with Google OAuth in the browser, so there's no Google Cloud project, gcloud or pipx, and Stape says it is "available for free". It works wherever remote servers do, including as a custom connector on claude.ai, and in Claude Code:

```bash
claude mcp add --transport http ga4-mcp-server https://mcp-google-analytics.stape.io/mcp
```

The trade-off is spelled out in Stape's own README: with the hosted server, your data passes through `mcp-google-analytics.stape.io`; with a local install, it goes straight from your machine to Google.

### Zapier MCP: a few GA4 write actions

Zapier's [Google Analytics 4 MCP](https://zapier.com/mcp/google-analytics-4) exposes six actions: Create Conversion Event for a Property, Create Measurement (a measurement secret), Run Report for a Property, Send Measurement Events for an Application, Find Conversion, and a beta raw API Request. It's the only option here that can change something in GA4, though reporting is a single action rather than nine specialized tools.

[Zapier MCP](https://zapier.com/mcp) is included in every Zapier plan, and "each tool call uses two tasks" from your plan's quota. Zapier's [pricing page](https://zapier.com/pricing) lists a Free plan with 100 tasks a month, about 50 tool calls, and paid plans from $19.99 a month billed yearly.

### Ask Advisor, inside Google Analytics

For quick questions about one property, Google's [Ask Advisor](https://support.google.com/analytics/answer/16675569) is a Gemini-powered assistant built into GA, with nothing to install. It's in beta, currently limited to properties whose language is English, and it doesn't feed GA data into Claude or your own agents.

### Rybbit: a hosted MCP server for cookieless analytics

Rybbit's [MCP server](/features/mcp) isn't a Google Analytics connector: it reads what Rybbit, the analytics tool we build, collects with its own script. It suits people who'd rather not run a local server with Google Cloud credentials, and people wondering whether GA's consent-banner gaps are worth keeping.

- **Hosted, with OAuth.** Claude Code, Claude Desktop (as a custom connector, which also works on claude.ai and mobile), Codex and opencode log in with OAuth; other clients use an API key, optionally scoped to `resource:action` permissions such as analytics read-only ([MCP docs](/docs/mcp)). In Claude Code: `claude mcp add --transport http rybbit https://app.rybbit.io/api/mcp`.
- **44 tools that read and write.** Overviews, breakdowns, [funnels](/features/funnels), retention, journeys, Web Vitals, errors, sessions and user profiles; write tools for goals, funnels, sites, members, teams and user traits; and `run_query`, read-only ClickHouse SQL. Session replay isn't available over MCP.
- **Search Console in the same chat.** On Rybbit Cloud, once a site admin connects Search Console under Site settings > Integrations, `get_search_console_status` and `get_search_console_data` return clicks, impressions, CTR and position by query, page, country, device or date, with filters, up to 25,000 rows per call. No URL Inspection, sitemaps or indexing requests.
- **Cookieless tracking.** Rybbit is cookieless by default, so its own tracking needs no consent banner, and the cloud is EU-hosted.

The limits, plainly. Rybbit can't import GA history: its [importers](/docs/data-import) cover Plausible, Umami and Simple Analytics only, so switching means running both side by side for a while. If your reporting leans on GA's Google Ads data, keep GA. [Standard](/pricing) starts at $19 a month for 100k events and Pro at $39, after a 7-day trial with the card collected at signup. Rybbit is open source under AGPL-3.0, and self-hosted installs include the MCP endpoint (the Search Console tools are Cloud-only). Our [Google Analytics comparison](/compare/google-analytics) goes feature by feature, and the [best Google Analytics alternatives](/blog/best-google-analytics-alternatives) roundup covers the rest of the field.

## Which option should you use?

For servers beyond GA4 (PostHog, Mixpanel, Amplitude and others), see our comparison of the [best analytics MCP servers](/blog/best-analytics-mcp-servers).

- **Google's server** if you're comfortable with gcloud, want requests to go straight from your machine to Google, and only need to read.
- **Stape** for the same nine tools on claude.ai or without a local install, if a third party in the data path is acceptable.
- **Zapier** if you already pay for it, or need a GA4 write action such as creating a conversion event.
- **Ask Advisor** for quick questions inside GA.
- **Rybbit** if you're open to changing the analytics tool itself and want a hosted MCP server with write tools and Search Console. Not if you need your GA history or Google Ads data in one place.

## FAQ

**Is there an official Google Analytics MCP server?**
Yes. Google's Analytics team publishes it on GitHub as `googleanalytics/google-analytics-mcp` and on PyPI as `analytics-mcp`, and Google lists it among its official MCP servers. The README labels it experimental.

**Is the Google Analytics MCP server free?**
Yes. It's open source under Apache-2.0. Requests count against the Data API's token quotas: 200,000 core tokens a day on a standard property.

**Can the GA4 MCP server change my Google Analytics settings?**
No. Google says it is "available for read requests only" and "can't edit your Google Analytics configuration or settings." Of the options above, only Zapier's GA4 MCP has write actions.

**How do I use the GA4 MCP server with Claude?**
In Claude Code, run Google's `claude mcp add analytics-mcp` command with your credentials path and project ID. In Claude Desktop, add the `analytics-mcp` block to `claude_desktop_config.json` and restart. Claude on the web and mobile can't run local servers, and Google doesn't host one, so use a hosted server such as Stape's there.

**Does it work with Universal Analytics?**
No. It uses the Data API, which Google says doesn't support Universal Analytics properties.

---

*Want an MCP server with no Google Cloud project to set up? [Start a Rybbit trial](https://app.rybbit.io/signup), connect it to Claude with one command, or read the [MCP docs](/docs/mcp) first.*

---

### Content Blog Google Analytics Pricing (`docs/content/blog/google-analytics-pricing.mdx`)

---
title: "How Much Does Google Analytics Cost? GA4 vs GA 360 Pricing"
description: "Google Analytics 4 is free, but Google Analytics 360 is sold by quote. What each tier costs, where the free limits stop, and what paid alternatives charge."
keyword: "google analytics pricing"
date: 2026-09-13
tags: ["google analytics", "pricing", "comparisons"]
author: Rybbit Team
---

Google Analytics 4, the standard version, is free. Google's product page says it gives you the tools ["free of charge"](https://marketingplatform.google.com/about/analytics/), and there is no usage fee at any traffic level. Google Analytics 360 is the paid enterprise tier: higher limits, SLAs and multi-property features, sold by quote through Google sales and authorized resellers at a reported starting price of $50,000 per year. The real cost of the free version sits elsewhere: the visitors you lose to consent banners, the retention and sampling limits, the BigQuery bill once you export, and the hours it takes to set up and learn.

One disclosure up front: we build [Rybbit](/features/web-analytics), which appears in the alternatives table at the end. All prices are as of September 2026.

## Is Google Analytics free?

Yes. Standard GA4 has no subscription, no traffic-based fee and no paid upgrade inside the product; the only paid version is Analytics 360, a separate enterprise contract. It is the same product for a hobby blog and a company, and Google doesn't restrict commercial use.

What "free" doesn't mean is unlimited. Standard properties get lower limits on retention, sampling, exports, custom dimensions and API quotas, and those limits are why large companies upgrade.

## What the free version includes and where it stops

Every row below comes from Google's own help pages, checked in September 2026.

| Limit | GA4 standard (free) | Google Analytics 360 |
| --- | --- | --- |
| [Event data retention](https://support.google.com/analytics/answer/7667196) (explorations and funnel reports) | 2 or 14 months | 2, 14, 26, 38 or 50 months |
| [Sampling threshold](https://support.google.com/analytics/answer/13331292) per query | 10 million events | 100 million by default, up to 1 billion with "More detailed results"; [unsampled explorations](https://support.google.com/analytics/answer/10896953) are 360-only |
| [BigQuery daily export](https://support.google.com/analytics/answer/9823238) | 1 million events per day (streaming export has no limit) | "Billions of events" per day |
| [Event parameters per event](https://support.google.com/analytics/answer/11202874) | 25 | 100 |
| [Event-scoped custom dimensions / metrics](https://support.google.com/analytics/answer/11202874) | 50 / 50 | 125 / 125 |
| [Key events (conversions)](https://support.google.com/analytics/answer/11202874) | 30 | 50 |
| [Audiences](https://support.google.com/analytics/answer/11202874) | 100 | 400 |
| [Data API quota](https://developers.google.com/analytics/devguides/reporting/data/v1/quotas) | 200,000 tokens per property per day, 40,000 per hour, 10 concurrent requests | 2,000,000 per day, 400,000 per hour, 50 concurrent requests |
| [Intraday data freshness](https://support.google.com/analytics/answer/11198161) | 2–6 hours | About 1 hour |
| [Expanded data sets](https://support.google.com/analytics/answer/12867885) for the "(other)" row | Not available | Expand up to 2 million rows, 100 per property |
| [Subproperties](https://support.google.com/analytics/answer/11525732) and [roll-up properties](https://support.google.com/analytics/answer/11526039) | Not available | 360 only (roll-up events billed separately) |
| [Service level agreements](https://marketingplatform.google.com/about/analytics-360/) | None | SLAs "from data collection to attribution", including the BigQuery daily export |

Three of these deserve a closer look.

**Retention.** The setting only controls event-level data used in explorations and funnel reports; Google states it does not affect standard aggregated reports. Standard traffic reports keep working past 14 months, but you can't build an exploration over two years of data on a free property. New properties [default to 2 months](https://support.google.com/analytics/answer/9327811), so change the setting the day you create one.

**Sampling.** Google says sampling "may occur when the number of events used to create a report, exploration, or request exceeds the quota limit", which is 10 million events per query on a standard property. In practice it bites in explorations and API pulls over long date ranges on busy sites.

**What isn't limited.** Web data streams have [no limit on distinctly named events](https://support.google.com/analytics/answer/9267744), and there is no monthly event cap. High-traffic sites can stay on the free tier; they live with sampling, the export cap and the "(other)" row instead.

## Google Analytics 360 pricing

Google does not publish a price list for Analytics 360. The [Analytics 360 page](https://marketingplatform.google.com/about/analytics-360/) has no prices and one call to action: "Talk to sales". It is sold by Google's sales team and by Google Marketing Platform sales partners such as [InfoTrust](https://infotrust.com/products/google-analytics-360-reseller/) and [Cardinal Path](https://www.cardinalpath.com/gmp-full-stack-reseller), which describe themselves as official resellers.

Because there is no price list, the numbers that circulate are reported ones:

- Cardinal Path, a GA 360 reseller, wrote in [August 2022](https://www.cardinalpath.com/blog/ua-360-vs-ga4-360-pricing-model) that Google "suggests for the cost of Google Analytics 4 360 to start at a retail price of USD $50,000/year, which entitles customers to 25 million events per month", with usage-based pricing above that volume.
- Vision Labs, a Google Cloud partner, publishes a [GA 360 cost calculator](https://visionlabs.com/calculator/ga360/) (undated, checked September 2026) with a base of $4,200 per month for 25 million events plus $17.50 per additional million, and says organizations typically invest "anywhere from $50,000 to over $150,000 annually".
- Under the older Universal Analytics model, InfoTrust quoted [$150,000 per year for up to 500 million hits per month](https://infotrust.com/articles/10-common-questions-from-companies-considering-google-analytics-360/) (April 2020), plus a suggested $30,000–$50,000 for implementation.

Treat these as reported figures. Your quote depends on event volume, property count and support level, and contracts are typically annual. Two line items come on top: [roll-up property events are billed at half the rate of the source property](https://support.google.com/analytics/answer/11539651), and implementation is usually a separate agency engagement.

What the money buys, per Google's 360 documentation: retention up to 50 months, unsampled explorations, BigQuery exports of billions of events per day, 100 event parameters and 125 custom dimensions, subproperties for access control, roll-ups for multi-brand reporting, intraday data in about an hour, and SLAs from collection to the BigQuery export. For a large company already on the Google Marketing Platform, that list is the product.

## The hidden costs of free GA4

None of this appears on an invoice from Google, which is why it's easy to miss.

### Consent banners and lost data

GA4 sets [first-party cookies](https://support.google.com/analytics/answer/11397207) that last two years to distinguish users and sessions. France's regulator CNIL is blunt about what that means: operators must obtain consent before setting trackers, and ["most large audience measurement offerings do not fall within the scope of the exemption"](https://www.cnil.fr/en/sheet-ndeg16-use-analytics-your-websites-and-applications). Every visitor who ignores or declines the banner is invisible to your reports.

How many? It varies widely by country, industry and banner design. Didomi's consent benchmark, built on hundreds of millions of banner interactions collected throughout 2025, puts [opt-in rates](https://www.didomi.io/blog/benchmark-average-consent-rate-europe) (opt-ins divided by banners displayed) between 55.7% in Western Europe and 67.6% in Eastern Europe. For a Western European audience, roughly four in ten banner impressions never become a tracked visitor. That gap costs you twice: in the campaign you can't attribute, and in the tool you buy to fill it.

### Setup and agency time

GA4 is an event model with a reporting layer on top. Someone has to design the event taxonomy, register custom dimensions within the 50-dimension cap, mark key events, fix the retention setting, link Google Ads and Search Console, and build explorations for whatever the standard reports don't answer. Small teams do this in evenings; larger ones pay an agency. The InfoTrust figure above ($30,000–$50,000 for an enterprise implementation) marks the top end.

### BigQuery storage and query costs

Exporting raw events to BigQuery is the standard fix for sampling and retention limits, and the export is free on the Analytics side. BigQuery is not. The [free tier](https://docs.cloud.google.com/free/docs/free-cloud-features) covers 1 TiB of queries and 10 GiB of storage per month; the [sandbox](https://docs.cloud.google.com/bigquery/docs/sandbox) caps storage at 10 GiB for life, expires every table after 60 days and doesn't support streaming, so the unlimited streaming export needs billing enabled. Google's [export documentation](https://support.google.com/analytics/answer/9823238) says you need a valid form of payment on file and that BigQuery charges for storage and query processing. A busy site keeping years of raw events with dashboards on top will see a monthly Cloud bill, and someone has to write the SQL.

### Training and complexity

The standard reports answer a fixed set of questions. Anything else means learning explorations, segments, the "(other)" row, and which reports are sampled. That curve is paid in hours, and for a marketing team it's often the difference between using the tool and glancing at it.

### Privacy and legal exposure

This is a factual summary, not legal advice. In 2022, three EU data protection authorities found that sending Google Analytics data to the US violated the GDPR's transfer rules: [Austria's DSB on 13 January](https://noyb.eu/en/austrian-dsb-eu-us-data-transfers-google-analytics-illegal), [France's CNIL on 10 February](https://www.wilmerhale.com/en/insights/blogs/wilmerhale-privacy-and-cybersecurity-law/20220216-the-french-data-protection-authority-joins-the-austrian-data-protection-authority-in-ruling-that-the-use-of-google-analytics-violates-the-gdpr), which gave the website operator one month to comply, and [Italy's Garante in June](https://www.insideprivacy.com/data-privacy/italian-garante-bans-use-of-google-analytics/), which gave 90 days. The ground shifted on [10 July 2023](https://commission.europa.eu/law/law-topic/data-protection/international-dimension-data-protection/eu-us-data-transfers_en), when the European Commission adopted the EU–US Data Privacy Framework adequacy decision, and again on 3 September 2025, when the [EU General Court dismissed a challenge to it](https://curia.europa.eu/site/upload/docs/application/pdf/2025-09/cp250106en.pdf). Transfers to certified US companies currently rest on that framework; cookie consent is a separate rule and hasn't changed.

The cost is compliance work: a consent management platform, a data processing agreement, a privacy policy that describes the transfer, and legal time whenever the framework is challenged again.

## When free GA4 is enough

Plenty of teams should stay on the free tier:

- **You run on Google Ads.** The Ads integration, audiences and key-event import are the strongest reasons to use GA4, and no alternative replicates them fully.
- **Your traffic is under the sampling line.** If your explorations rarely touch 10 million events, sampling and the 1-million-event export cap don't affect you.
- **You have an analyst comfortable in BigQuery.** The export plus SQL turns GA4 into a capable warehouse, and the bill for a mid-size site is often small.
- **You track a website and a mobile app together.** GA4's shared event model across web and Firebase is hard to match.
- **You already have consent infrastructure** and have accepted the data loss that comes with it.

If none of those apply, compare the free tier's real price with what paid tools charge.

## What paid alternatives cost

Prices below come from each vendor's pricing page as of September 2026. Rybbit is our product; the other numbers are the vendors' own.

| Tool | Starting price | What it covers | Trial or free tier |
| --- | --- | --- | --- |
| [Rybbit](/pricing) | $19/month (Standard) | 100k events/month, 5 sites, 3 team members, 3-year retention; Pro from $39/month adds session replay, unlimited sites and members, 5-year retention; 4 months free on annual billing | 7-day free trial; a card is collected at signup and nothing is charged until the trial ends |
| [Plausible](https://plausible.io/) | $9/month (Starter) | 10k pageviews/month; Growth $14, Business $19 | 30-day trial, no card |
| [Fathom](https://usefathom.com/pricing) | $15/month | 100k pageviews/month, rising to $470/month at 25M | 7-day trial; no free plan |
| [Matomo Cloud](https://matomo.org/pricing/) | €29/month | 50k hits/month | Free trial, no card |
| [Simple Analytics](https://www.simpleanalytics.com/pricing) | $20/month | Slider from 100k pageviews; 1 user, $20 per extra user | 14-day trial, no card; free plan with 1 user, 5 sites and 1 month of history |
| [PostHog](https://posthog.com/product-analytics/pricing) | Usage-based | 1M events/month free, then $0.00005/event for 1–2M, falling to $0.000009/event above 250M | Free tier, no card |

Self-hosting is free for [Rybbit](/pricing), Plausible's [Community Edition](https://plausible.io/self-hosted-web-analytics), Matomo's On-Premise edition and PostHog's [open-source build](https://posthog.com/docs/self-host), which PostHog labels officially unsupported.

The entry price of a paid tool is $9–$29 per month, less than an hour of agency time, and what you're paying for is mostly the removal of the hidden costs: a cookieless tool like Rybbit sets no tracking cookies, so the traffic a GA4 banner hides shows up, there is no sampling line, and Standard plans keep three years of data. Our [Google Analytics comparison](/compare/google-analytics) goes feature by feature, and if you're weighing several tools, see the [best Google Analytics alternatives](/blog/best-google-analytics-alternatives) and [best web analytics tools](/blog/best-web-analytics-tools) roundups.

## FAQ

**Is Google Analytics free?**
Yes. The standard version of Google Analytics 4 is free with no usage fees. The paid version is Google Analytics 360, an enterprise contract sold by quote.

**How much is Google Analytics 360?**
Google doesn't publish a price. A GA 360 reseller reported a starting retail price of $50,000 per year for 25 million events per month in 2022, and a partner calculator puts typical contracts between $50,000 and more than $150,000 per year.

**Does GA4 have a paid version?**
Yes. Google Analytics 360 runs on GA4 properties with higher limits (50-month retention, a 1-billion-event sampling threshold, billions of BigQuery events per day, 125 custom dimensions), subproperties, roll-ups, expanded data sets and SLAs.

**Is Google Analytics free for business?**
Yes. There is no separate business license; companies of any size use the free tier. Businesses pay in other ways: consent-banner data loss, BigQuery costs if they export, setup and training time, and compliance work.

**What are the limits of free Google Analytics?**
The ones that matter most: 14 months of event data for explorations (2 months by default), sampling above 10 million events per query, a BigQuery export of 1 million events per day, 25 event parameters per event, 50 custom dimensions, 30 key events, 100 audiences and 200,000 Data API tokens per day. Standard reports are not affected by the retention setting.

---

*Want to see what the banner has been hiding? [Start a free Rybbit trial](https://app.rybbit.io/signup) or open the [live demo](https://demo.rybbit.com/81). Setup is one script tag.*

---

### Content Blog Google Search Console Mcp (`docs/content/blog/google-search-console-mcp.mdx`)

---
title: "Google Search Console MCP: Setup Guide and 5 Options Compared"
description: "Google Search Console MCP servers compared: self-run and hosted options for Claude, Cursor and ChatGPT, with setup steps, example prompts and limits."
keyword: "google search console mcp"
date: 2026-09-27
tags: ["mcp", "seo", "ai", "comparisons"]
author: Rybbit Team
---

A Google Search Console MCP server lets an AI assistant such as Claude, Cursor or ChatGPT read your Search Console performance data (clicks, impressions, click-through rate and average position by query, page, country, device and date) by calling Google's Search Console API when you ask it a question. As of September 2026 Google doesn't publish one, so every option is third-party, and they come in three kinds: open-source servers you run on your own machine with your own Google Cloud credentials, hosted connectors you sign in to, and analytics tools that include Search Console in their own MCP server. This guide compares five of them on setup effort, capabilities and cost, then walks through a setup and the prompts that work.

Disclosure: we build [Rybbit](https://rybbit.com), one of the five. Every claim about the other four comes from that project's own README or the vendor's own documentation and pricing pages, linked inline and checked in September 2026.

## Quick comparison

| Option | Type | What you set up | Beyond performance data | Other data in the same MCP | Price (Sept 2026) |
| --- | --- | --- | --- | --- | --- |
| [mcp-gsc](#mcp-gsc) | Self-run, open source (MIT) | Google Cloud project, OAuth client or service account, local config | URL Inspection, sitemaps | None | Free |
| [suganthan-gsc-mcp](#suganthan-gsc-mcp) | Self-run, open source (Apache 2.0) | Google Cloud OAuth client, then a setup wizard | URL Inspection, sitemaps, Indexing API submissions | None | Free |
| [Windsor.ai](#windsorai) | Hosted connector | Windsor account, Search Console connection, connector with OAuth | Sitemap status; computed fields such as branded vs non-branded | 350+ sources, including GA4 and Google Ads | Free plan (1 source, 30 days of history); Basic $23/mo |
| [Coupler.io](#couplerio) | Hosted connector | Coupler account, a Search Console data flow, connector with a token | URL Inspection and index coverage data | 400+ sources | Free plan (100 rows per run); Starter $32/mo |
| [Rybbit](#rybbit) | Web analytics with a hosted MCP server | Connect Search Console in site settings, add one URL to your client | None | Rybbit's own analytics: sessions, goals, funnels, errors | From $19/mo, 7-day trial |

## What a Google Search Console MCP server can answer

The Model Context Protocol (MCP) is an open standard for connecting AI assistants to outside tools. An MCP server publishes tools with descriptions; the assistant picks one, fills in the arguments and reads the result. A Search Console MCP server wraps Google's Search Console API, which has three parts that matter here:

- **Search Analytics**, the data behind the Performance report: clicks, impressions, CTR and average position, grouped by query, page, country, device, date, search appearance or hour, filtered by exact, substring or regex matches, across web, image, video, news, Discover and Google News. One request returns up to 25,000 rows ([API reference](https://developers.google.com/webmaster-tools/v1/searchanalytics/query)).
- **URL Inspection**: Google's indexing status for one URL, capped at 2,000 inspections per property per day ([usage limits](https://developers.google.com/webmaster-tools/limits)).
- **Sitemaps**: list, submit and delete.

Google says all use of the API is [free of charge](https://developers.google.com/webmaster-tools/pricing) within those limits. Requesting indexing is a separate API, the Indexing API.

That covers questions like "which queries does this page rank for", "which pages lost clicks since last month" or "is this URL indexed". It won't give you search volume for keywords you don't appear for, backlinks, or competitors' rankings; Search Console only knows about your own property.

## Is there an official Google Search Console MCP server?

No, not as of September 2026. Google's [list of official MCP servers](https://github.com/google/mcp) covers Google Analytics, BigQuery, Maps, Firebase and a long list of Google Cloud products, but not Search Console, and the [Search Console API documentation](https://developers.google.com/webmaster-tools) doesn't mention one.

What Google does publish is a [Google Analytics MCP server](https://developers.google.com/analytics/devguides/MCP), a read-only server built for GA4 data; we cover it in our [Google Analytics MCP guide](/blog/google-analytics-mcp) and compare it with other vendors' servers in [best analytics MCP servers](/blog/best-analytics-mcp-servers). (If GA4 itself is what you're weighing, see [Rybbit vs Google Analytics](/compare/google-analytics).) Every Search Console MCP server below is third-party code calling Google's public API, so the real choice is who holds your Google credentials and how much setup you do.

## Google Search Console MCP servers compared

### mcp-gsc

[mcp-gsc](https://github.com/AminForou/mcp-gsc) is an MIT-licensed Python server that runs on your computer with your own Google credentials. Its README counts 20 tools: search analytics tools such as `compare_search_periods` and `get_advanced_search_analytics` (filters, sorting, and paging up to 25,000 rows per call); URL inspection with `inspect_url_enhanced` and `batch_url_inspection` (up to 10 URLs at once); `check_indexing_issues`; and tools to list, submit and delete sitemaps. Tools that add or delete properties or delete sitemaps stay off unless you set `GSC_ALLOW_DESTRUCTIVE`.

Setup starts in Google Cloud: create a project, enable the Search Console API, then create either an OAuth client ID of type Desktop app (you sign in through a browser on first use) or a service account that you add as a user on your Search Console property. Then install uv, add an entry that runs `uvx mcp-search-console` to your client's config with the credentials path as an environment variable, and restart the client. Because the server runs locally, the README notes it works in the Claude Desktop app but not on claude.ai in a browser; an SSE mode and a Dockerfile are there if you want to host it. The [server code](https://github.com/AminForou/mcp-gsc/blob/main/gsc_server.py) asks Google for the full Search Console scope (`webmasters`), not the read-only one.

- **Pros:** free; URL inspection and sitemap tools; a ready-made period comparison
- **Cons:** needs a Google Cloud project and credentials; local by default; Search Console data only; full-access Google scope
- **Price (as of September 2026):** free and open source (MIT). Source: [mcp-gsc README](https://github.com/AminForou/mcp-gsc)

### suganthan-gsc-mcp

[Suganthan's GSC MCP](https://github.com/Suganthan-Mohanadasan/Suganthans-GSC-MCP) (`suganthan-gsc-mcp` on npm, Apache 2.0) is a Node server whose 29 tools lean toward finished analysis rather than raw rows: `quick_wins` (queries at positions 4 to 15 with high impressions), `cannibalization_check`, `content_decay`, `traffic_drops`, an eight-tool image-search suite, and `genai_conversation_queries`, which hunts for AI Mode follow-ups that Google logs as ordinary queries. It has `inspect_url` and sitemap tools, and it is the only one of the five that submits URLs to Google's Indexing API (`submit_url`, and `submit_batch` for up to 200 a day). Google restricts that API to pages with `JobPosting` or `BroadcastEvent` structured data ([Indexing API docs](https://developers.google.com/search/apis/indexing-api/v3/quickstart)), so for most sites it isn't a general "request indexing" button.

`npx -y suganthan-gsc-mcp setup` runs a wizard that signs you in, lets you pick a property and writes the config for Claude Desktop and Claude Code. You still need your own OAuth client JSON from a Google Cloud project, or a service account. The wizard defaults to read-only access and asks for full access only if you want the submission tools, and your data goes from your machine straight to Google.

- **Pros:** free; ready-made SEO analyses; indexing and sitemap tools; read-only by default through the wizard
- **Cons:** Google Cloud credentials still required; local only; Search Console data only
- **Price (as of September 2026):** free and open source (Apache 2.0). Source: [suganthan-gsc-mcp README](https://github.com/Suganthan-Mohanadasan/Suganthans-GSC-MCP)

### Windsor.ai

[Windsor.ai](https://windsor.ai/connect/google-search-console-mcp/) is a data-connector service whose hosted MCP server (`https://mcp.windsor.ai/`) reaches 350+ sources, Search Console among them, alongside GA4, Google Ads and Meta Ads. You connect Search Console to a Windsor account, then add the server to your client. The [server's README](https://github.com/windsor-ai/windsor_mcp) lists OAuth 2.0 sign-in, one-click listings in Claude's connector directory and as a ChatGPT app, and API keys for clients without OAuth. The tools are generic (`get_connectors`, `get_fields`, `get_data`). For Search Console, Windsor adds computed fields the raw API lacks: branded vs non-branded queries, words per query, the results page a query landed on, and URL path levels for rolling up by directory. Its [Claude setup guide](https://windsor.ai/how-to-send-google-search-console-data-to-claude/) says it pulls Search Analytics and sitemap status (submitted vs indexed counts, errors and warnings) but not the URL Inspection API.

[Pricing](https://windsor.ai/pricing/) is per data source. The free plan starts as a 30-day trial, then keeps 1 user, 1 data source and 1 account, and on it "you can access and retrieve data from the past 30 days only", a real limit when Search Console holds 16 months. Basic is $23/month, or $19/month billed annually, for 3 data sources; Standard is $118/month. Every plan includes the MCP server.

- **Pros:** no Google Cloud setup; OAuth in Claude and ChatGPT; Search Console next to GA4 and ad platforms
- **Cons:** free plan reaches back 30 days; no URL Inspection; generic tools; priced per data source
- **Price (as of September 2026):** free plan; Basic $23/month ($19/month billed annually). Source: [windsor.ai/pricing](https://windsor.ai/pricing/)

### Coupler.io

[Coupler.io](https://www.coupler.io/mcp/google-search-console) syncs 400+ sources into spreadsheets, BI tools and AI clients, and its MCP sits on top of those syncs. You build a data flow from Search Console with an AI tool as the destination, then connect Claude, ChatGPT, Cursor, Gemini, Copilot or Perplexity, authenticated with a personal access token. Coupler's [post on its MCP endpoint](https://blog.coupler.io/custom-mcp-server/) says the endpoint exposes your data flow and that you "automate data refresh on a schedule", so the assistant queries the data your flow last pulled rather than asking Google on demand. The Search Console MCP page lists search performance, performance by search appearance, Discover and Google News data, URL Inspection data and page-indexing coverage.

Coupler's pages disagree on the plan you need. The Search Console MCP page says "at least the Professional plan"; the [Claude integration page](https://www.coupler.io/claude-integrations) says any active plan, "starting from free-forever"; and the [pricing page](https://www.coupler.io/pricing) has no Professional tier. It lists Free (1 account, manual refresh, 100 rows per run), Starter at $32/month or $24/month billed annually (daily refresh, 5,000 rows per run), Active at $132/month and Pro at $259/month (hourly refresh). New accounts get 7 days of full access.

- **Pros:** no Google Cloud setup; URL Inspection and index coverage data; many other sources
- **Cons:** scheduled snapshots, not live queries; row caps on Free and Starter; conflicting plan requirements
- **Price (as of September 2026):** free plan; Starter $32/month ($24/month billed annually). Source: [coupler.io/pricing](https://www.coupler.io/pricing)

### Rybbit

[Rybbit](/features/mcp) is privacy-first web analytics with a hosted MCP server of 44 tools, two of which read Search Console. A site admin connects Search Console once under **Site settings > Integrations** on Rybbit Cloud, signing in with Google and picking the property. Rybbit asks Google for read-only Search Console access and keeps that grant for the site, so any teammate with access to the site can query the property from the dashboard or their own assistant, with no Google Cloud project, service account or Search Console access of their own.

`get_search_console_status` reports whether a property is connected and which one. `get_search_console_data` returns clicks, impressions, CTR and average position grouped by up to three of `query`, `page`, `country`, `device` and `date`, with up to 10 filters on query, page, country or device (`equals`, `notEquals`, `contains`, `notContains`, `includingRegex`, `excludingRegex`) across the `web`, `image`, `video`, `news`, `discover` and `googleNews` search types. It returns 100 rows by default and up to 25,000 per call, paged with `start_row`, and `data_state: "all"` adds fresh, still-changing numbers.

The difference from the other four is what sits next to those two tools: [sessions](/features/sessions), [goals](/features/goals), [funnels](/features/funnels), [user journeys](/features/user-journeys), [errors](/features/error-tracking) and [Web Vitals](/features/web-vitals) for the visitors Google sent, plus read-only SQL over your raw events. "Which Google landing pages turn clicks into signups" becomes one conversation instead of two tools and a spreadsheet. OAuth sign-in works in Claude Code, Claude Desktop and claude.ai, Codex and opencode; other clients, including Cursor and VS Code, use an API key, which you can limit to scopes such as Search Console read and Analytics read. Rybbit is open source (AGPL-3.0) and self-hostable, but the Search Console connection is a Rybbit Cloud feature.

- **Pros:** no Google Cloud project; read-only Google access; Search Console and on-site conversions in one assistant; one connection serves the whole team
- **Cons:** a paid analytics product rather than a standalone connector; no URL Inspection, sitemaps or indexing; one property per site; no built-in period comparison (the assistant makes two calls)
- **Price (as of September 2026):** Standard from $19/month for 100k events, Pro from $39/month; 7-day free trial with a card collected at signup. Source: [rybbit.com/pricing](/pricing)

## How to set up a Search Console MCP with Rybbit

You need a site on Rybbit Cloud with the tracking script installed, and a Google account that can see the site's Search Console property.

### 1. Connect Search Console to your site

1. Open your site's **Site settings**, go to **Integrations** and click **Connect Google Search Console**. This needs admin access to the site.
2. Sign in with Google and approve read-only access to Search Console.
3. Pick the property, either a domain property (`sc-domain:example.com`) or a URL-prefix property, and click **Connect Property**.

Rybbit sends you back to the dashboard, where a Search Console card now shows top queries, pages, countries and devices.

### 2. Add the MCP endpoint to your client

The endpoint for Rybbit Cloud is `https://app.rybbit.io/api/mcp`. In **Claude Code**, add it:

```bash
claude mcp add --transport http rybbit https://app.rybbit.io/api/mcp
```

Then run `/mcp`, select **rybbit** and choose **Authenticate**. A browser window opens for you to log in to Rybbit, and the connection acts with your user's role.

In **Claude Desktop or claude.ai**, open **Settings > Connectors**, click **Add custom connector**, name it Rybbit, paste the URL and click **Connect**. Anthropic offers custom connectors on every Claude plan, with Free users [limited to one](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp).

In **Codex**:

```bash
codex mcp add rybbit --url https://app.rybbit.io/api/mcp
codex mcp login rybbit
```

In **Cursor**, create an API key in Rybbit under **Settings > Account > Personal API Keys**, then add it to `~/.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "rybbit": {
      "url": "https://app.rybbit.io/api/mcp",
      "headers": {
        "Authorization": "Bearer <RYBBIT_API_KEY>"
      }
    }
  }
}
```

Any other client that speaks Streamable HTTP takes the same URL and header. The [MCP docs](/docs/mcp) have walkthroughs for [Claude Code](/docs/mcp/claude-code), [Claude Desktop](/docs/mcp/claude-desktop), [Cursor](/docs/mcp/cursor), [Codex](/docs/mcp/codex), VS Code and opencode.

### 3. Check the connection

Ask: "Is Search Console connected for my Rybbit site, and which property?" The assistant calls `list_sites` to find the site ID, then `get_search_console_status`. If a later call returns a 404 saying GSC isn't connected, step 1 didn't finish.

## Example prompts that work

Each prompt below maps to calls the tools can make. Give the assistant a date range, and remember Search Console only covers the last 16 months.

1. **"Which queries does /pricing rank for over the last 28 days, with clicks, impressions and average position?"** One call grouped by query, filtered to pages containing `/pricing`.
2. **"Find pages with more than 1,000 impressions and a CTR under 1% in the last three months, and suggest new titles for the worst five."** One call grouped by page. Google sorts rows by clicks, so ask for a few thousand rows; low-CTR pages sit further down the list.
3. **"Compare clicks by query for the last 28 days against the 28 days before. Which queries lost the most?"** Two calls, one per date range; the assistant does the diff.
4. **"Show daily clicks and impressions for the last 90 days, and flag any drop that lines up with our annotations."** One call grouped by date, plus `get_annotations` for the launches and deploys your team pinned to the traffic chart.
5. **"For our top 20 queries in the US, compare CTR and position on mobile and desktop."** One call grouped by query and device, filtered to country `US`.
6. **"How is the post we published on Monday doing in Google? Include fresh data."** A page filter with `data_state: "all"`, which adds the recent days Google hasn't finalized yet.
7. **"Which of our top Google landing pages bring organic-search visitors who complete the Signup goal?"** Search Console grouped by page, then `get_goals` filtered to the Organic Search channel and to each landing page.
8. **"Build a funnel for organic-search visitors: any blog post, then /pricing, then /signup, over the last 30 days. Where do they drop off?"** `analyze_funnel` with a channel filter; page steps accept wildcards, so `/blog/**` matches every post.

For the first prompt, the call the assistant makes looks like this:

```json
{
  "site_id": 1,
  "start_date": "2026-08-27",
  "end_date": "2026-09-23",
  "dimensions": ["query"],
  "filters": [{ "dimension": "page", "operator": "contains", "expression": "/pricing" }],
  "row_limit": 100
}
```

## Limitations

### Limits that come from Google

Every server on this list reads the same API, so these apply to all of them.

- **16 months of history.** Google suggests pulling anything you want to keep longer through the API or the bulk data export and [storing it yourself](https://developers.google.com/search/docs/monitor-debug/debugging-search-traffic-drops).
- **Two to three days of lag.** Google says data is [typically available after 2–3 days](https://developers.google.com/webmaster-tools/v1/how-tos/all-your-data). Fresher, still-changing numbers need the `all` data state, which mcp-gsc and suganthan-gsc-mcp use by default and Rybbit exposes as `data_state: "all"`.
- **Missing rows.** Rare queries are [anonymized and left out](https://support.google.com/webmasters/answer/17011259) of query tables, and the API returns [top rows rather than every row](https://developers.google.com/webmaster-tools/v1/searchanalytics/query), [at most 50,000](https://developers.google.com/webmaster-tools/v1/how-tos/all-your-data) per day per search type. Query rows won't add up to the property total.
- **Pacific Time.** API dates are in [Pacific Time](https://developers.google.com/webmaster-tools/v1/searchanalytics/query), so daily figures won't line up exactly with analytics that report in your time zone.
- **AI Overviews and AI Mode are inside web search.** Google [reports them](https://developers.google.com/search/docs/appearance/ai-features) within the Performance report's Web search type, and the API reference has no search type for the separate Generative AI report added in June 2026, so no MCP server can read that report's numbers. Visits from ChatGPT, Perplexity or Claude never appear in Search Console at all; see [how to track AI referral traffic](/blog/track-chatgpt-traffic).

### Limits of Rybbit's Search Console tools

- **Search Analytics only.** No URL Inspection, sitemaps or indexing requests. For URL Inspection and sitemap management use mcp-gsc; to submit URLs to the Indexing API, suganthan-gsc-mcp.
- **One property per site.** Each Rybbit site connects to one Search Console property, and the assistant can't browse other properties in your Google account.
- **A subset of the API.** Five dimensions (no search appearance or hour), final or all data (no hourly data), and no comparison tool, so the assistant calls once per date range. Discover and Google News don't support the query dimension.
- **Clicks aren't sessions.** Search Console counts clicks on Google results; Rybbit's Organic Search channel counts sessions from every search engine, Bing and DuckDuckGo included. Expect the numbers to differ, and filter Rybbit by a Google referrer for a closer match.
- **Queries join through landing pages.** Search Console reports queries in aggregate, not per visit, so the assistant connects queries to conversions through the pages they land on. Treat query-level conversion figures as estimates.
- **Not an SEO suite.** No rank tracking beyond Search Console's average position, no keyword volumes, no backlinks.

## Which GSC MCP server to pick

- **You want it free and can set up a Google Cloud project:** mcp-gsc for raw access plus URL Inspection and sitemaps, or suganthan-gsc-mcp for ready-made SEO analyses and Indexing API submissions.
- **You want Search Console in claude.ai or ChatGPT without running a server, next to GA4 and ad platforms:** Windsor.ai, keeping in mind its free plan reaches back only 30 days.
- **You already move data into sheets and dashboards with Coupler:** Coupler.io, knowing the assistant reads scheduled snapshots.
- **You want to connect search data to what visitors did after the click, and give teammates access without Google Cloud credentials:** Rybbit.
- **You need URL Inspection, sitemap management or indexing requests:** not Rybbit. Use mcp-gsc or suganthan-gsc-mcp.

## FAQ

**What is a Google Search Console MCP server?**
A program that exposes Search Console data to AI assistants through the Model Context Protocol. The assistant calls the server's tools, and the server queries Google's Search Console API with credentials you authorized.

**Is there an official Google Search Console MCP server?**
Not as of September 2026. Google's list of official MCP servers includes one for Google Analytics but none for Search Console.

**Is there a free Google Search Console MCP?**
Yes. mcp-gsc and suganthan-gsc-mcp are free and open source, and Google doesn't charge for the Search Console API; you supply the Google Cloud project and credentials. Windsor.ai's free plan also includes its MCP server, with 30 days of history.

**Which GSC MCP server is easiest to set up?**
The hosted ones. Windsor.ai, Coupler.io and Rybbit need no Google Cloud project: you sign in with Google, connect the property and add a URL or token to your client (Coupler also has you build a data flow first). Among self-run servers, suganthan-gsc-mcp's wizard is the shortest path, but it still needs an OAuth client JSON from Google Cloud.

**Does a Google Search Console MCP work with Claude?**
Yes. Remote servers such as Rybbit's are added as custom connectors, which Anthropic offers on every Claude plan (Free users get one), and Windsor.ai is listed in Claude's connector directory. Local servers such as mcp-gsc run through the Claude Desktop app's config file, not claude.ai in a browser.

**Can ChatGPT use a Search Console MCP server?**
Yes, in [developer mode](https://developers.openai.com/api/docs/guides/developer-mode), which is available to Plus, Pro, Business, Enterprise and Education accounts on the web and connects to remote servers over SSE or streaming HTTP. Local servers have to be hosted somewhere reachable first. Windsor.ai publishes a ChatGPT app. Rybbit's endpoint uses OAuth, which developer mode supports, but Rybbit's docs don't have a ChatGPT walkthrough yet, so treat that path as untested.

**Can a Search Console MCP server request indexing?**
Only servers that call Google's Indexing API, such as suganthan-gsc-mcp, and Google limits that API to pages with JobPosting or BroadcastEvent structured data. Rybbit's Search Console tools are read-only.

---

### Content Blog Hotjar Alternatives (`docs/content/blog/hotjar-alternatives.mdx`)

---
title: "10 Hotjar Alternatives in 2026: Heatmaps, Replay, Surveys"
description: "Compare 10 Hotjar alternatives by what they replace: heatmaps, session recordings, surveys and feedback. Free, open-source and paid picks, with verified prices."
keyword: "hotjar alternatives"
date: 2026-09-27
tags: ["hotjar", "comparisons", "session replay", "heatmaps"]
author: Rybbit Team
---

Hotjar alternatives are worth a fresh look in 2026 because Hotjar has merged into Contentsquare, and new users can no longer create a Hotjar account. Hotjar's core toolkit covers four jobs: heatmaps, session recordings, on-site surveys and feedback widgets. This guide sorts the best Hotjar alternatives by which of those jobs they cover, with prices and limits taken from each vendor's own pages.

The short version: [Microsoft Clarity](#microsoft-clarity) replaces heatmaps and recordings for free but has no surveys. [Mouseflow](#mouseflow) and [Lucky Orange](#lucky-orange) cover all four jobs in one paid tool. [PostHog](#posthog) adds product analytics and surveys with a monthly free tier. [OpenReplay](#openreplay) and [Matomo](#matomo) are the self-hosted options with heatmaps. [Rybbit](#rybbit), which we build, suits teams that mostly watched recordings and want them next to funnels and conversion data; it has no heatmaps or surveys.

Disclosure: we build [Rybbit](https://rybbit.com). Every claim about the other tools comes from that vendor's own pricing or documentation pages, linked inline and checked in September 2026. Prices are the US-dollar figures those pages show; some vendors show euros to European visitors.

## Hotjar is now Contentsquare

[Contentsquare's Hotjar page](https://contentsquare.com/hotjar/) puts it plainly: "Hotjar is now part of Contentsquare. The two platforms have merged into a single, more powerful experience intelligence platform," and "it's no longer possible to create a new account on Hotjar." Existing users can still log in, and hotjar.com/pricing redirects to [Contentsquare's pricing page](https://contentsquare.com/pricing/), where Hotjar's toolkit is now split across products:

- **Experience Analytics** (heatmaps, replays, funnels, error monitoring). Free covers 200,000 sessions a month for the whole account, 10,000 replays and 1 project, with one month of data according to Contentsquare's [free plan announcement](https://contentsquare.com/blog/contentsquare-free-plan/). Growth starts at $49/month ($39 billed yearly) for 7,000 sessions and 7,000 replays, and costs $409/month at 100,000 sessions, where replays stay at 20,000. Replays are "a fixed amount per month — it can't be increased." Growth keeps session data for 13 months and replays for 2 months.
- **Voice of Customer** (surveys and feedback widgets). Free includes 100 responses a month, with 3 surveys or widgets live at a time. Growth starts at $99/month ($79 billed yearly) for 500 responses.
- **Interviews and user tests** (Hotjar Engage). These are "no longer available for purchase by new customers," per [Hotjar's help center](https://help.hotjar.com/hc/en-us/articles/36820003581841-Getting-Started-with-Engage).

Contentsquare also sells Product Analytics as a third product, free up to 10,000 monthly sessions. Staying is reasonable if the free quotas fit and you want heatmaps, recordings and surveys behind one login. The reasons to look elsewhere are specific: replays stop scaling with sessions on paid plans (20,000 replays at 100,000 sessions), surveys are billed as a separate product, and the pricing page lists no self-hosted option.

## Hotjar alternatives and competitors at a glance

| Tool | Heatmaps | Recordings | Surveys and feedback | Free plan | Paid from (Sept 2026) | Self-host |
| --- | --- | --- | --- | --- | --- | --- |
| [Hotjar (Contentsquare)](#hotjar-is-now-contentsquare) | Yes | Yes, 10k/mo on Free | Yes, separate product | Yes, 200k sessions | $49/mo | No |
| [Microsoft Clarity](#microsoft-clarity) | Yes | Yes | No | Yes, no traffic limits | No paid plan | No |
| [Mouseflow](#mouseflow) | Yes | Yes | Advanced plan and up | Yes, 500 sessions | $39/mo | No |
| [Lucky Orange](#lucky-orange) | Yes | Yes | Yes, unlimited | No, 7-day trial | $39/mo | No |
| [Crazy Egg](#crazy-egg) | Yes | Paid plans | Yes | Yes, heatmaps and surveys | $29/mo, billed yearly | No |
| [FullStory](#fullstory) | Yes | Yes | Paid add-on | Yes, 30k sessions | Quote only | No |
| [PostHog](#posthog) | Yes | Yes | Yes | Yes, 5k recordings | Usage-based | Free features only |
| [LogRocket](#logrocket) | Yes | Yes | Yes | No, 14-day trial | $176/mo at 25k sessions | Enterprise only |
| [Rybbit](#rybbit) | No | Pro plan, 10k/mo at 100k events | No | No, 7-day trial | $39/mo (Pro) | Yes, free |
| [OpenReplay](#openreplay) | Yes | Yes | No | Self-hosted only | $199/mo (cloud) | Yes, free |
| [Matomo](#matomo) | Plugin or Cloud | Plugin or Cloud | No | Self-hosted core | USD 26/mo (Cloud) | Yes |

"No" under self-host means the vendor's pricing page lists only hosted plans. PostHog's self-hosted build includes only its free-plan features and is officially unsupported. Matomo's heatmaps and recordings are a paid plugin when you self-host and are included in Matomo Cloud.

## Alternatives to Hotjar by the feature you use

- **Heatmaps.** Clarity gives you click and scroll maps for free with no traffic limits. Mouseflow, Lucky Orange and Crazy Egg are heatmap-first tools, and PostHog, FullStory, LogRocket, OpenReplay and Matomo include heatmaps next to their other features. Rybbit has none.
- **Recordings.** Every tool here records sessions, which most vendors call [session replay](/blog/what-is-session-replay); Crazy Egg's free plan doesn't list them. Free allowances differ the most: Clarity is unlimited, Contentsquare Free has 10,000 replays a month, FullStory's free plan covers 30,000 sessions, PostHog 5,000 recordings and Mouseflow 500 sessions. If recordings are all you need from Hotjar, our [session replay tools comparison](/blog/best-session-replay-tools) goes deeper on masking, sampling and retention.
- **Surveys and feedback widgets.** Lucky Orange includes unlimited surveys on every plan, and Crazy Egg includes them even on its free plan. PostHog gives 1,500 responses a month free, LogRocket lists surveys on its Core plan, Mouseflow starts at its Advanced plan, and FullStory sells them as an add-on. Clarity, OpenReplay, Matomo and Rybbit have no surveys.
- **Funnels and product analytics.** Clarity, Mouseflow, Lucky Orange and Crazy Egg include conversion funnels. PostHog, FullStory, LogRocket, OpenReplay and Rybbit add event-based product analytics on top.
- **Interviews and user tests.** Engage is no longer sold to new customers. PostHog's [surveys](https://posthog.com/surveys) can book user interviews from in-app templates; for recruiting participants and running tests, you'll need a dedicated research tool.

## The best free Hotjar alternative

### Microsoft Clarity

[Microsoft Clarity](https://clarity.microsoft.com) is the usual answer to "free Hotjar alternative". Its [FAQ](https://learn.microsoft.com/en-us/clarity/faq) says: "Clarity is a free service forever. You never encounter traffic limits or are compelled to upgrade to a paid version." There's no sampling and no limit on the number of sites. The [feature list](https://learn.microsoft.com/en-us/clarity/setup-and-installation/about-clarity) covers session recordings, heatmaps (click and scroll maps), event and funnel tracking, and Copilot summaries, and there are SDKs for Android, iOS, Flutter and React Native apps.

The limits are retention and consent. Recordings are kept for 30 days (favorites and a random sample for up to 9 months), heatmaps stay available for 9 months, and each heatmap draws on up to 100,000 page views. For visitors in the EEA, UK and Switzerland, Clarity expects a consent signal; without one, the FAQ says, sessions are fragmented and "full session replays and user journey continuity are unavailable."

- **Covers:** heatmaps, recordings, funnels, AI summaries
- **Missing or limited:** no surveys or feedback widgets; 30-day recording retention; consent signal needed for European visitors
- **Pricing (September 2026):** free, with no paid plan. Source: [Clarity FAQ](https://learn.microsoft.com/en-us/clarity/faq)

## Closest like-for-like Hotjar replacements

[Hotjar's help center](https://help.hotjar.com/hc/en-us/articles/36820019634961-What-is-Hotjar) groups its product into Observe tools (heatmaps and recordings) and Ask tools (surveys and feedback). These three cover both in a single product, at published prices.

### Mouseflow

[Mouseflow](https://mouseflow.com) lines up most closely with Hotjar's feature set. Every plan, Free included, gets session replays, conversion funnels, friction detection, unlimited users, and click, scroll and geo heatmaps. Feedback surveys, with NPS scoring and branch logic, start at the Advanced plan, along with attention and movement heatmaps. Form analytics start at Premium and journey analytics at Enterprise, per the [pricing page](https://mouseflow.com/pricing/).

Plans are sized by monthly sessions. Free covers 500 sessions and 1 month of data. Essential is $39/month ($25 billed yearly) for 5,000 sessions and 3 months; Advanced is $149/month ($109 yearly) for 25,000 sessions and 6 months; Premium is $399/month ($319 yearly) for 100,000 sessions and 12 months. The 14-day trial needs no credit card.

- **Covers:** heatmaps, recordings, funnels, friction detection; surveys and feedback from Advanced
- **Missing or limited:** no surveys on Free or Essential; form analytics only from Premium
- **Pricing (September 2026):** free for 500 sessions a month; paid from $39/month, or $25/month billed yearly. Source: [mouseflow.com/pricing](https://mouseflow.com/pricing/)

### Lucky Orange

[Lucky Orange](https://www.luckyorange.com) keeps packaging simple: every plan has every feature. The [pricing page](https://www.luckyorange.com/pricing) lists dynamic heatmaps, session recordings, unlimited surveys and announcements, live chat, a live visitor view, conversion funnels, and page and form analytics on all plans, with unlimited team members. Its Discovery AI is an add-on.

You pay by sessions. Build is $39/month ($32 billed yearly) for 3,500 sessions and Grow is $72/month billed yearly for 10,000. Expand is $199/month billed yearly ($249 monthly) from 50,000, and Scale is $839/month billed yearly ($1,049 monthly) from 300,000. Data is kept for 60 days by default, with 90, 180 or 365-day upgrades, and each extra site costs $5. There's no free plan, only a 7-day trial.

- **Covers:** heatmaps, recordings, surveys, announcements, funnels, form analytics, plus live chat
- **Missing or limited:** no free plan; 60-day default retention
- **Pricing (September 2026):** from $39/month, or $32/month billed yearly, for 3,500 sessions. Source: [luckyorange.com/pricing](https://www.luckyorange.com/pricing)

### Crazy Egg

[Crazy Egg](https://www.crazyegg.com) is heatmap-first and prices by tracked pageviews, with a separate, smaller recording allowance. According to its [pricing page](https://www.crazyegg.com/pricing), the free plan needs no credit card and includes web analytics, instant heatmaps, conversion analytics and on-site surveys. Paid plans are billed annually. Starter is $29/month for 5,000 tracked pageviews, 5 heatmap reports and 50 recordings a month. Plus is $99/month for 150,000 pageviews, 75 heatmap reports and 1,000 recordings, and adds A/B testing and error tracking. Pro is $249/month for 500,000 pageviews and 5,000 recordings.

Every paid plan includes unlimited surveys and responses, unlimited domains and team members, and a 30-day free trial. Heatmaps track 100% of pageviews on the pages you target unless you set a lower sample rate.

- **Covers:** heatmaps, surveys, funnels, A/B testing (Plus and up); recordings in small volumes
- **Missing or limited:** 50 recordings a month on Starter and 1,000 on Plus; no monthly billing
- **Pricing (September 2026):** free plan with heatmaps and surveys; paid from $29/month billed annually. Source: [crazyegg.com/pricing](https://www.crazyegg.com/pricing)

## Hotjar alternatives with product analytics

These tools are built around event data (funnels, retention, user paths) and attach recordings to it, which fits questions about conversion and retention across sessions better than questions about one page's layout.

### PostHog

[PostHog](https://posthog.com) is the broadest single replacement on this list: heatmaps, session replay, surveys and product analytics in one product, each with a free monthly allowance. The [pricing page](https://posthog.com/pricing) lists 1 million analytics events, 5,000 recordings and 1,500 survey responses free every month, with the same allowance whether or not a card is on file. Beyond that, web recordings cost $0.005 each and fall to $0.0015 above 500,000 ([replay pricing](https://posthog.com/session-replay/pricing)), and survey responses start at $0.10 each ([survey pricing](https://posthog.com/surveys/pricing)). Surveys can run as [popovers, behind a feedback button, or as hosted links](https://posthog.com/docs/surveys/creating-surveys).

[Heatmaps](https://posthog.com/docs/toolbar/heatmaps) show clicks, dead clicks, rage clicks, mouse movement and scroll depth, and PostHog says heatmap data "doesn't contribute to your bill". Recordings are kept for 1 month on the free plan and 3 months on pay-as-you-go.

Self-hosting is limited. The MIT-licensed Docker deployment is "officially unsupported", wants roughly a 4 vCPU, 16 GB RAM machine, and "All paid-plan features are Cloud-only", per the [self-hosting docs](https://posthog.com/docs/self-host). We compare the two products in [Rybbit vs PostHog](/compare/posthog).

- **Covers:** heatmaps, recordings, surveys and a feedback button, funnels and product analytics
- **Missing or limited:** per-recording and per-response billing past the free tier; self-hosting unsupported
- **Pricing (September 2026):** free tier renewed monthly, then usage-based. Source: [posthog.com/pricing](https://posthog.com/pricing)

### FullStory

[FullStory](https://www.fullstory.com) is the enterprise option. FullstoryFree, a permanent free plan with no credit card, includes "30,000 sessions per month with 12 months of data retention" for up to 10 users, covering session replay, basic analytics and debugging tools, per the [plans page](https://www.fullstory.com/plans/). The paid Business, Advanced and Enterprise plans are sold through "Request pricing & demo", and the plan table lists click and scroll maps, funnels, dashboards, journeys and retention analysis. Surveys come with the Guides and Surveys add-on, and mobile analytics is an add-on too.

- **Covers:** recordings, heatmaps, funnels and product analytics; surveys as an add-on
- **Missing or limited:** no published prices above the free plan
- **Pricing (September 2026):** free for 30,000 sessions a month; paid plans by quote. Source: [fullstory.com/plans](https://www.fullstory.com/plans/)

If FullStory itself is what you're replacing, see our [FullStory alternatives](/blog/fullstory-alternatives) guide.

### LogRocket

[LogRocket](https://logrocket.com) records sessions along with "console and network logs, errors, warnings, crashes and performance data", per its [docs](https://docs.logrocket.com/docs/introduction), and adds product analytics on top. Its [pricing page](https://logrocket.com/pricing) charges only for sessions captured; analytics events, errors, logs and network data are unlimited. The Core plan lists click maps, heatmaps and rage-click maps, path analysis, conversion funnels, alerting, SSO and surveys. Pro adds the Galileo AI layer and is "included free above 100K sessions / mo".

The pricing calculator shows $176/month at 25,000 sessions. There's no free plan, only a 14-day trial that needs no credit card, and self-hosting is an Enterprise option.

- **Covers:** recordings with console and network data, heatmaps, surveys, funnels, error tracking
- **Missing or limited:** no free plan; self-hosting only on Enterprise
- **Pricing (September 2026):** $176/month at 25,000 sessions. Source: [logrocket.com/pricing](https://logrocket.com/pricing)

### Rybbit

[Rybbit](https://rybbit.com) is the only tool on this list without heatmaps: no click or scroll maps, and no surveys or feedback widgets either. What it does have is [session replay](/features/session-replay) inside a full web and product analytics tool: [funnels](/features/funnels), [user journeys](/features/user-journeys), [goals](/features/goals), [error tracking](/features/error-tracking), [Web Vitals](/features/web-vitals), [retention](/features/retention) and [user profiles](/features/user-profiles). The replay timeline marks clicks, page navigations, typing and rage clicks, so you can jump straight to the moment a visitor got stuck. Input values are masked by default, the analytics are cookieless, and Rybbit Cloud is hosted in the EU.

It fits teams that mostly watched recordings in Hotjar and read them against conversion numbers. If heatmaps are why you open Hotjar, Clarity or Mouseflow will serve you better. If it's surveys, stay on Contentsquare's Voice of Customer or use Mouseflow's feedback surveys.

On Rybbit Cloud, replay is a Pro feature: from $39/month for 100k events, or $26/month billed yearly. Pro's replay quota is 10% of its event allowance, so 10,000 replays a month on the 100k tier. Standard, from $19/month, includes the analytics but not replay. Both start with a 7-day trial, and the card is collected at signup. Cloud replays are kept for 30 days. Rybbit is open source under AGPL-3.0, and [self-hosting](/docs/self-hosting) is free with replay included. There's no Hotjar importer; [data import](/docs/data-import) covers Plausible, Umami and Simple Analytics.

- **Covers:** recordings with rage-click markers, funnels, journeys, goals, error tracking, Web Vitals
- **Missing or limited:** no click or scroll heatmaps, surveys or feedback widgets; replay needs the Pro plan on cloud, with a monthly replay quota
- **Pricing (September 2026):** Pro, with replay, from $39/month; self-hosting free. Source: [rybbit.com/pricing](/pricing)

## Open-source and self-hosted Hotjar alternatives

If recordings can't leave your servers, these two self-host with heatmaps included. PostHog's self-hosted build (above) covers its free-plan features, and Rybbit self-hosts replay and analytics but has no heatmaps.

### OpenReplay

[OpenReplay](https://openreplay.com) is an open-source session replay and product analytics suite; most of the code is AGPL-3.0, with a separately licensed `ee/` directory, per the [license file](https://github.com/openreplay/openreplay/blob/main/LICENSE). Its [pricing page](https://openreplay.com/pricing/) marks session replay, developer tools, product analytics, co-browsing and bug reports as included in the free self-hosted edition, and the [heatmaps](https://docs.openreplay.com/en/product-analytics/heatmaps/) show clicks and rage clicks per page. The managed option is a Dedicated instance from $199/month, billed hourly at $0.276, "with no limits on users, recordings, or retention", with a 7-day trial and no credit card.

- **Covers:** recordings with developer tools, heatmaps, funnels and journeys, co-browsing
- **Missing or limited:** no surveys or feedback widgets; you run the servers
- **Pricing (September 2026):** self-hosted free; Dedicated cloud from $199/month. Source: [openreplay.com/pricing](https://openreplay.com/pricing/)

### Matomo

[Matomo](https://matomo.org) is a Google Analytics alternative first, with Hotjar-style features on top. Its [Hotjar comparison page](https://matomo.org/hotjar-alternative/) says self-hosted users can buy heatmaps, session recordings, funnels and form analytics from the Matomo Marketplace, and that "all these features are included in the Matomo Cloud Business plan." It is just as clear that Matomo "doesn't currently offer feedback or surveys tools."

On [Matomo's pricing page](https://matomo.org/pricing/), Cloud is priced by traffic: the 50,000-hit tier is USD 26/month (excluding tax) and includes 1,500 heatmap pageviews and 150 session recordings a month, with data stored in Frankfurt. The self-hosted core is free, and the heatmap and recording plugin has a 30-day trial. See how it compares with Rybbit in [Rybbit vs Matomo](/compare/matomo).

- **Covers:** heatmaps, recordings, funnels, form analytics, full web analytics
- **Missing or limited:** no surveys or feedback; small recording quotas on entry Cloud tiers; heatmaps are a paid plugin when self-hosted
- **Pricing (September 2026):** self-hosted core free; Cloud from USD 26/month for 50,000 hits. Source: [matomo.org/pricing](https://matomo.org/pricing/)

## Before you switch: what you can export from Hotjar

Heatmaps and recordings are built from each tool's own tracking script, so your history stays behind and the new tool starts from zero. According to Hotjar's [export guide](https://help.hotjar.com/hc/en-us/articles/36820033667729-How-to-Share-and-Export-Data), you can take out:

- **Survey responses** as CSV or XLSX. Exports over 10,000 responses arrive as an emailed link.
- **A list of recordings with metadata** (pages, duration, device, country, rage-click flag and more) as CSV or XLSX, up to 100,000 recordings per export. The recordings themselves stay in Hotjar, though Highlights download as video files.
- **Heatmaps** as a JPG image, or as a CSV of clicks per element and scroll depth.

Then run both scripts side by side until the new tool has enough sessions to answer the questions you used Hotjar for, rebuild your surveys, and remove the Hotjar snippet last.

## Which Hotjar alternative should you pick?

- **Heatmaps and recordings for free:** Microsoft Clarity, if 30-day recordings and sending consent signals for European visitors are acceptable.
- **All of Hotjar in one paid tool with public prices:** Mouseflow (surveys from Advanced) or Lucky Orange (every feature on every plan).
- **Heatmaps and surveys matter more than recordings:** Crazy Egg.
- **Product analytics, heatmaps, replay and surveys with a free tier:** PostHog.
- **Engineers who debug from recordings:** LogRocket, or OpenReplay if it has to be self-hosted.
- **Recordings next to funnels and conversion data, cookieless and open source:** Rybbit, with the caveat that it has no heatmaps or surveys.
- **Heatmaps, recordings and surveys behind one login, and the free quotas fit:** stay on Contentsquare.

One to skip: Smartlook. Its [homepage](https://www.smartlook.com/) says end of sale took effect on 31 May 2026, renewals stopped on 31 August 2026, and the platform will be decommissioned on 30 September 2027.

## FAQ

**What is the best free alternative to Hotjar?**
Microsoft Clarity. It's free with no traffic limits and includes heatmaps, recordings and funnels, but it has no surveys, and recordings are kept for 30 days. If you also need free surveys, PostHog's free tier includes 1,500 responses a month and Crazy Egg's free plan includes on-site surveys.

**Who are Hotjar's main competitors?**
Contentsquare now owns Hotjar. The direct competitors in heatmaps and recordings are Microsoft Clarity, Mouseflow, Lucky Orange, Crazy Egg and FullStory. PostHog, LogRocket, OpenReplay, Matomo and Rybbit compete from the product analytics, debugging or self-hosting side.

**Can you still sign up for Hotjar?**
No. Contentsquare says "it's no longer possible to create a new account on Hotjar." New users sign up for Contentsquare instead, whose Free plan includes 200,000 monthly sessions, 10,000 replays, heatmaps and basic surveys. Existing Hotjar users can still log in.

**Is there a Hotjar alternative with product analytics?**
Yes. PostHog, FullStory, LogRocket, OpenReplay and Rybbit pair recordings with event-based product analytics. PostHog and LogRocket also include heatmaps and surveys; Rybbit has neither. Contentsquare sells Product Analytics as a separate product with its own free tier.

**Is there an open-source Hotjar alternative?**
OpenReplay (AGPL-3.0 core) self-hosts recordings and heatmaps for free. PostHog's MIT-licensed build self-hosts its free-plan features without official support. Matomo is open source too, but heatmaps and recordings are a paid plugin when self-hosted. Rybbit (AGPL-3.0) self-hosts replay and analytics, without heatmaps.

**Does Rybbit have heatmaps or surveys?**
No. Rybbit has session replay on the Pro plan, with rage clicks marked on the timeline, plus web and product analytics. It has no click or scroll heatmaps, surveys or feedback widgets. For heatmaps, use Clarity or Mouseflow; for surveys, use Contentsquare, Lucky Orange or Mouseflow.

---

### Content Blog Marketing Attribution Tools (`docs/content/blog/marketing-attribution-tools.mdx`)

---
title: "Best Marketing Attribution Tools in 2026 (Compared)"
description: "The top marketing attribution tools compared: how each tracks customer journeys, which attribution models they support, and what they cost."
keyword: "marketing attribution tools"
date: 2025-11-23
updated: 2026-09-28
tags: ['marketing', 'attribution', 'analytics', 'channels', 'ROI']
author: Rybbit Team
---


You've spent $50,000 on marketing this month. Your revenue jumped by $100,000. Great, right?

But here's the uncomfortable question: Which $50,000 actually drove that $100,000?

Was it the Google Ads campaign? The content marketing from two months ago? The viral tweet? The demo request from your sales rep who knows everyone?

If you can't answer that question with confidence, you're flying blind.

That's where marketing attribution comes in. It's the practice of assigning credit to the marketing touchpoints that led to a conversion. And if you're not using an attribution tool, you're leaving money on the table - lots of it.

In this guide, we'll cover what attribution is, why it matters, the different models available, and the best tools to implement it in your business.

## What is Marketing Attribution?

Marketing attribution is the process of determining which marketing channels, campaigns, and touchpoints deserve credit for a customer conversion.

Think of it like this: A customer's journey to your product is usually a series of touches, similar to how customers progress through a [sales funnel](/blog/funnel-analysis):
1. They see your Google Ads (touch 1)
2. They read a blog post you published (touch 2)
3. They watch a YouTube video about your industry (touch 3)
4. They click your email newsletter (touch 4)
5. They sign up for a free trial (conversion)

So which one deserves credit? All of them? Just the last one? The first one?

That's what attribution tries to answer.

**Why does it matter?** 

Because if you think only the email newsletter drove the conversion, you'll gut your content and YouTube budgets. You'll reallocate money away from the channels that actually built awareness and intent. Your revenue will eventually drop because you killed the early-stage awareness that makes your demand gen work.

Attribution prevents this disaster. It shows you the full picture of how customers find you.

## The Attribution Problem: Why Most Companies Get It Wrong

Here's the dirty secret: Most companies don't do attribution at all. They use what's called "last-click attribution."

**Last-click attribution** means you give 100% credit to the last touchpoint before conversion. So in our example above, the email newsletter gets 100% credit. Everything else that happened? Ignored.

This is insanely common because it's easy to implement (Google Analytics does it by default). But it's also insanely misleading.

Here's why last-click attribution breaks down:

### Problem 1: It Undervalues Awareness & Consideration

Your blog post and YouTube video didn't convert anyone immediately. They created awareness. They made people know you exist. But last-click attribution gives them zero credit.

So marketing leaders think: "Blog posts don't drive conversions. Cut the budget."

Then 6-12 months later, brand awareness drops, and conversion rates plummet. Oops.

This is why understanding [content marketing funnels](/blog/funnel-analysis) is crucial—early-stage content drives long-term value that last-click attribution completely misses.

### Problem 2: It Ignores the Middle of the Funnel

Most sales cycles have multiple stages:
- Awareness (someone learns you exist)
- Consideration (they're comparing you to alternatives)
- Decision (they choose you or a competitor)
- Purchase (they convert)

Last-click attribution only credits the decision/purchase stage. The consideration stage - which often takes weeks or months - gets ignored.

### Problem 3: It Creates Channel Bias

Channels that are "close" to conversion (retargeting ads, sales calls, email) will always win in last-click. But they're often the last link in a chain that started with organic search or content.

You end up over-investing in bottom-funnel channels and starving top-funnel channels. Your funnel becomes leaky because you never put enough demand in the top.

### Problem 4: It Doesn't Reflect Reality

In reality, conversions are the result of multiple touches. No single touchpoint is responsible. But last-click makes you think one channel is a hero and others are villains. It's false.

## Attribution Models: Which One Should You Use?

There are several different attribution models. Each one allocates credit differently.

### 1. Last-Click Attribution

**How it works:** The final touchpoint before conversion gets 100% credit.

**Why to use it:**
- Easiest to implement
- Works for bottom-funnel optimization
- Default in Google Analytics

**Why not to use it:**
- Ignores all the awareness and consideration work that feeds those conversions
- Creates misleading ROI pictures
- Leads to poor budget allocation decisions

**When it's useful:** If you're only optimizing the last step before purchase (like retargeting), this is fine. But as a company-wide model, it's dangerous.

### 2. First-Click Attribution

**How it works:** The first touchpoint (the one that started the journey) gets 100% credit.

**Why to use it:**
- Highlights the importance of awareness
- Prevents you from over-investing in retargeting
- Good for understanding which channels bring in new customers

**Why not to use it:**
- Ignores all the middle touches that actually built intent
- Might give credit to accidental clicks or random impressions
- Incomplete picture

**When it's useful:** For companies focused on brand building and awareness. But again, only part of the story.

### 3. Linear Attribution

**How it works:** All touchpoints get equal credit.

If a customer has 4 touchpoints before converting, each gets 25% credit.

**Why to use it:**
- Simple to understand
- Acknowledges all touchpoints matter
- More balanced than first/last click

**Why not to use it:**
- Assumes all touches are equally important (they usually aren't)
- Doesn't reflect different funnel stages
- Can still mislead about which channels drive value

**Example:** Touchpoint 1 (organic search) might be massively important. Touchpoint 4 (remarketing ad) might be just a reminder. But they'd get equal credit in a linear model.

### 4. Time-Decay Attribution

**How it works:** Touchpoints closer to conversion get more credit.

Typical setup: The touchpoint right before conversion gets 40% credit, the second-to-last gets 30%, the third-to-last gets 20%, the first gets 10%.

**Why to use it:**
- Reflects that recent touches are usually more important
- Gives some credit to earlier touches (unlike last-click)
- Works well for shorter sales cycles

**Why not to use it:**
- Arbitrary weighting - why 40/30/20/10? Why not 50/30/15/5?
- Still doesn't account for different funnel stages
- Can underweight important top-of-funnel touches

**When it's useful:** For most B2B [SaaS companies](/for-saas), this is a good middle ground. It acknowledges that the last touches matter more, but doesn't completely ignore the journey.

### 5. Position-Based Attribution (U-Shaped)

**How it works:** The first and last touchpoints get the most credit (typically 40% each), and middle touches split the remaining 20%.

**Why to use it:**
- Balances awareness and conversion
- Reflects that first and last touches matter most
- Better than simple linear

**Why not to use it:**
- Still somewhat arbitrary
- Might miss important mid-funnel touches
- Doesn't account for your actual sales cycle length

**When it's useful:** When you want to balance top-of-funnel and bottom-of-funnel investments.

### 6. Multi-Touch (Custom) Attribution

**How it works:** You define the rules for how credit is allocated based on your actual sales cycle and business model.

Example: For a B2B SaaS with a 3-month sales cycle, you might say:
- First touch: 30% (created awareness)
- Middle touches: 40% (built consideration and intent)
- Last touch: 30% (facilitated decision)

Or you might weight based on channel. If you know organic search drives qualified leads:
- Organic search: 35%
- Content: 20%
- Retargeting: 25%
- Direct: 10%
- Referral: 10%

**Why to use it:**
- Reflects your actual business model
- Can be very accurate
- Gives you strategic control over attribution

**Why not to use it:**
- Requires more data and analysis to set up
- Needs ongoing refinement
- Easy to bias toward channels you prefer

**When it's useful:** When you have the sophistication to track your sales cycle and customer journey data. This is what advanced teams use.

## The Real Truth About Attribution

Here's what most attribution experts won't tell you: The model matters less than consistency.

It's better to use a simple, consistent model that you understand than to use a sophisticated model that confuses everyone.

Pick a model (time-decay or position-based are good defaults), implement it, understand it, and stick with it. Use it to make decisions. Review and refine it quarterly.

What you're really doing with attribution is creating a consistent framework for how you evaluate marketing performance. The specific percentages matter less than the discipline of looking at the full customer journey instead of just the last click.

## The Best Marketing Attribution Tools for 2025

Now that you understand attribution, which tools should you use to implement it?

### Top Tier: Best Overall Tools

#### 1. **Rybbit** - Best for Comprehensive Customer Analytics

Rybbit is our top recommendation because it's purpose-built for understanding [customer journeys](/features/user-journeys) and behavior.



**What makes Rybbit great for attribution:**

- **Full user journey tracking**: See every touchpoint a user had before converting. Not just last-click. The entire history.
- **[Custom event](/features/custom-events) tracking**: Define exactly what "conversion" means for your business. Not just form submissions - actual valuable outcomes.
- **Session tracking**: See when users engaged, what they did, how long they spent. This context is crucial for attribution.
- **Behavioral segmentation**: Group users by how they converted. Did demo-request converters have different journeys than free-trial converters? Rybbit shows you.
- **Source tracking**: Automatically capture [UTM parameters](/tools/utm-builder), referral source, and organic keywords to understand where traffic came from.
- **Cohort analysis**: Compare conversion patterns across different user cohorts (by traffic source, geographic region, device type, etc.).

**Real example of Rybbit for attribution:**

You can see:
1. User lands on content marketing blog post (utm_source=organic)
2. User browses 5 pages over 3 days
3. User clicks email newsletter link (utm_source=email)
4. User watches demo video
5. User requests a demo
6. User becomes customer

With Rybbit, you can see that entire journey. You can then report: "Of users who came from organic search who also received an email, 25% converted. Of users who came from email alone, only 8% converted."



This immediately shows that organic traffic + email combination is powerful. You shouldn't cut either one.

**Best for**: Any company that wants to understand the full customer journey, not just last-click. Especially good for companies with multiple touchpoints and longer sales cycles.

#### 2. **Mixpanel** - Best for Product Analytics Attribution

Mixpanel is excellent if your attribution needs are product-focused (in-app features, free trials, etc.).

**What makes Mixpanel great for attribution:**

- **Comprehensive event tracking**: Track everything that happens in your product
- **Funnels**: See where users drop off in your [conversion funnel](/features/funnels)
- **User segmentation**: Break down funnels by user properties
- **Cohort analysis**: Compare different user cohorts
- **Attribution reporting**: See the top sources/campaigns driving conversions

**Limitations**: Mixpanel is primarily a product analytics tool. It's not as strong for multi-channel marketing attribution (email, ads, content) as tools specialized for that.

**Best for**: Companies with strong in-product conversion flows who want to understand how product usage leads to revenue.

#### 3. **Segment (now part of Twilio Engage)** - Best for Data Collection & Unification

Segment is excellent for collecting data from all your marketing and product tools and sending it to a data warehouse.

**What makes Segment great for attribution:**

- **Data collection**: Pull data from Google Analytics, email platforms, ads platforms, CRM, product analytics - everything
- **Data warehouse**: Send all data to Snowflake, BigQuery, Redshift for custom analysis
- **Standardization**: Put all data in the same format so you can analyze across sources
- **Data governance**: Ensure data quality and compliance

**Real example**: You can use Segment to pull user behavior from your product (Mixpanel), ad interactions (Google Ads), email clicks (Mailchimp), and website visits (Google Analytics) into one unified data warehouse. Then you can build custom attribution models.

**Limitations**: Segment is a data pipe, not an attribution tool. It gets the data to the right place, but you still need to analyze it.

**Best for**: Large companies with technical data teams who want to build custom attribution models.

#### 4. **HubSpot** - Best for B2B Sales-Driven Attribution

HubSpot offers attribution modeling specifically designed for B2B sales teams.

**What makes HubSpot great for attribution:**

- **Contact lifecycle tracking**: Track every touchpoint with a lead (email, web visit, call, etc.)
- **Deal tracking**: Tie touches to actual revenue deals
- **Attribution modeling**: Built-in multi-touch attribution
- **Lead scoring**: Automatically score leads based on engagement
- **Integration with sales**: Your sales team sees attribution data, not just marketing

**Limitations**: HubSpot's attribution works best if you're using HubSpot for CRM and email. If you're using separate tools, it's less effective.

**Best for**: B2B companies using HubSpot as their CRM. Great for small-to-medium companies.

#### 5. **Google Analytics 4 (GA4)** - Best for Free Option (with limitations)

GA4 now offers multi-touch attribution modeling.

**What makes GA4 great for attribution:**

- **Free** (with limitations)
- **Multiple attribution models**: Offers time-decay, first-click, last-click, etc.
- **Data-driven attribution**: Machine learning model that learns from your data
- **Cross-platform tracking**: See journeys across web and app
- **Integration with ads**: Direct integration with Google Ads

**Limitations**: 
- Data-driven attribution requires significant traffic volume to work
- Limited to Google-owned sources (Google Ads, Search, YouTube)
- Doesn't track email or other channels as well
- Complexity has increased significantly

**Best for**: Small companies just starting with attribution. It's free and decent for web analytics. But you'll outgrow it.

### Mid-Tier: Specialized Tools

#### **Littledata** - Best for Ecommerce Attribution

Excellent if you're an ecommerce company. Connects your Shopify store to GA4 with proper multi-touch attribution.

#### **AppsFlyer** - Best for Mobile & App Attribution

If you're a mobile app company, AppsFlyer is the standard for attribution.

#### **Improvado** - Best for Marketing Agencies

Great for [agencies](/for-agencies) managing multiple client accounts and needing consolidated reporting.

## How to Implement Attribution (Step by Step)

Implementing attribution isn't as hard as it sounds. Here's the practical path:

### Step 1: Choose Your Attribution Model

Start with time-decay or position-based. Don't overthink it.

### Step 2: Pick Your Tool

Start with Rybbit or GA4. Don't go complex until you need to.

### Step 3: Implement Tracking

Make sure you're capturing:
- Where traffic comes from (UTM parameters, referral source)
- What users do (events, page views, time on page)
- When conversions happen (form submissions, purchases, sign-ups)
- User properties (device, location, company size)

### Step 4: Wait for Data

You need at least 100-300 conversions to see patterns. This usually takes 4-12 weeks depending on your volume.

### Step 5: Analyze Results

Look at your top conversion sources. Are they what you expected? Are there surprises?

### Step 6: Take Action

Increase investment in high-performing sources. Investigate underperforming sources. Test improvements.

You can use [multivariate testing](/blog/multivariate-testing) to optimize your marketing channels and see which combinations drive the best results.

### Step 7: Review Quarterly

Attribution models should be revisited every quarter. If you're seeing patterns that don't make sense, refine your model.

## Using Rybbit for Marketing Attribution: Implementation Guide

Since we recommend Rybbit, here's how to use it for attribution:



The Journey dashboard in Rybbit lets you visualize the complete customer journey, showing exactly how users moved through your funnel and which touchpoints led to conversion.

### Setting Up Conversion Tracking

First, define what a "conversion" is for your business (signup, customer acquisition, purchase, etc.). Then track this conversion event in Rybbit. Refer to the [event tracking documentation](/docs/track-events) for implementation details.

### Capturing Traffic Source

Rybbit automatically captures UTM parameters from your URLs. You can also manually enhance tracking by recording campaign details in your events. See the [tracking documentation](/docs/track-events) for specifics.

### Tracking the Full Journey

As users interact with your site and marketing, track key behavioral events:
- Content engagement (blog reads, video views, etc.)
- Email interactions
- Ad interactions
- Product feature usage
- Trial signups

The key is consistency: track the same events across all channels so you can compare how different sources interact and contribute to conversions.

1. **In the Users tab:**
   - Filter by users who converted
   - Look at their "First Visit" to see acquisition source
   - See total sessions, pages viewed, time spent
   - Compare converters vs. non-converters

2. **In the Sessions tab:**
   - See session frequency for users on different journeys
   - Compare session duration across different traffic sources
   - Identify patterns in converted vs. non-converted users

3. **In the Events tab:**
   - See which events most frequently appear before conversion
   - Filter by conversion source to see typical journey
   - Compare event sequences across different acquisition channels

4. **Using Filtering and Segmentation:**
   - Create segments like "Came from organic + opened email" 
   - Create segments like "Came from ads + used feature X"
   - Compare conversion rates between segments
   - See which combinations drive the most conversions

### Real Example: Multi-Channel Attribution Analysis

When analyzing multi-channel journeys, you can identify whether different traffic sources are working together or cannibalizing each other. Rybbit lets you compare conversion rates across different source combinations to understand channel interactions.

By comparing users who saw only one source versus multiple sources, you can determine whether to invest in multiple channels simultaneously or focus on one primary channel.

### Creating Custom Attribution Reports

You can export Rybbit data to build custom attribution models based on your specific business needs. The key steps are to identify all user touchpoints, apply your custom weighting model based on your sales cycle, and calculate total conversions attributed to each channel.

## Common Attribution Mistakes to Avoid

### Mistake 1: Over-Optimizing Last-Click

Last-click is easy to understand but deeply flawed. Don't base all your decisions on it.

**Fix:** Use multi-touch attribution. See the full journey.

### Mistake 2: Not Accounting for Seasonality

Your acquisition sources shift seasonally. Google searches spike in Q4. Email performance varies. Don't compare January to December.

**Fix:** Always compare same time periods year-over-year. Use rolling averages.

### Mistake 3: Confusing Correlation with Causation

If users who convert have 10 touchpoints and users who don't convert have 2, don't assume the extra touches caused the conversion.

Maybe high-intent users naturally engage more. The touches are a signal, not the cause.

**Fix:** A/B test to validate. If you double email touches for one group and see different conversion rates, that's causation.

### Mistake 4: Ignoring Offline Touchpoints

Your sales team talks to customers. Your brand building happens in podcasts. Your CEO speaks at conferences. None of these show up in your digital analytics.

**Fix:** For B2B, track offline touches manually. For B2C, understand that brand and word-of-mouth matter.

### Mistake 5: Not Refreshing Your Model

Your business changes. Sales cycle changes. New channels emerge. Your attribution model should evolve too.

**Fix:** Review attribution quarterly. If something looks wrong, investigate.

## The Future of Attribution: What's Changing

### Privacy Changes

Apple's App Tracking Transparency is killing third-party cookies. This makes attribution harder because you have less data about users across the web.

This actually increases the importance of first-party data collection. Tools like Rybbit that use server-side tracking and your own data become more valuable.

### First-Party Data Focused

Privacy regulations mean the future is first-party data. You'll track your own customer data more, and rely less on third-party pixels.

This is good for smaller companies - you can build attribution without relying on ad platform data.

### AI and Machine Learning

GA4's data-driven attribution uses machine learning. Expect more AI-powered attribution that learns from your actual conversion patterns.

Instead of using a fixed model, the AI determines which touchpoints matter most for your specific business.

## The Bottom Line

Marketing attribution answers one simple question: Which channels and campaigns are actually driving your revenue?

The answer matters enormously. It determines where you invest, where you cut, and what you optimize.

Most companies use last-click attribution because it's easy. But it's also dangerously misleading. It over-credits bottom-funnel channels and under-credits the awareness work that feeds those channels.

**Here's what you should do:**

1. **Implement multi-touch attribution.** Use time-decay or position-based models.
2. **Pick a tool that tracks the full journey.** Rybbit is excellent for this. GA4 works too.
3. **Capture everything.** Traffic source, user behavior, events, conversions.
4. **Wait for data.** Attribution needs volume to be reliable.
5. **Analyze patterns.** Which sources appear most in successful customer journeys?
6. **Adjust spending.** Increase channels that lead to conversions. Decrease or optimize those that don't.
7. **Test and refine.** Attribution is not a set-it-and-forget-it system.

The companies that do attribution well gain a massive advantage. They know which $1 of marketing spend drives $3 of revenue. They can confidently increase spending on high-ROI channels. They avoid the trap of over-investing in bottom-funnel while starving top-funnel.

Don't be left guessing. Implement attribution today.

---

**Related Reading:**
- [Understanding User Behavior with Rybbit](/docs)
- [Setting Up Event Tracking](/docs/track-events)
- [Behavior Analytics - Sessions](/docs/feature-guides/sessions)
- [Behavior Analytics - Events](/docs/feature-guides/events)
- [Behavior Analytics - Users](/docs/feature-guides/users)
- [Retention Analysis: How to Measure and Improve User Retention](/blog/retention-analysis)
- [Why Did My Website Traffic Drop?](/blog/website-traffic-drop)
- [Ecommerce Analytics: The Metrics and Reports That Matter](/blog/ecommerce-analytics)
- [Funnel Analysis: How to Find Where Users Drop Off](/blog/funnel-analysis)
- [What Is Web Analytics? A Complete Guide](/blog/what-is-web-analytics)

---

### Content Blog Mixpanel Alternatives (`docs/content/blog/mixpanel-alternatives.mdx`)

---
title: "9 Best Mixpanel Alternatives in 2026, by Reason for Switching"
description: "Mixpanel alternatives matched to why you're leaving: cost, complexity, self-hosting, privacy. 9 tools compared on verified prices, free tiers and licenses."
keyword: "mixpanel alternatives"
date: 2026-09-27
tags: ["product analytics", "comparisons", "open source"]
author: Rybbit Team
---

Most teams shopping for Mixpanel alternatives are leaving for one of five reasons: the bill grows faster than the product, the setup is more than a small team can maintain, they need open source or self-hosting, they want analytics that doesn't depend on a consent banner, or they pay for Mixpanel plus a separate web analytics tool. Each reason points to a different replacement, so this guide starts with the reasons, then compares nine tools on price, free allowance, license and the Mixpanel reports you keep.

Disclosure: we build [Rybbit](https://rybbit.com), one of the nine. Every claim about the other tools comes from that vendor's own pricing or documentation pages, linked inline and checked in September 2026. Where a vendor doesn't publish a number, we say so.

## Quick comparison

| Tool | Best for | Free allowance | Paid plans from | Open source / self-host |
| --- | --- | --- | --- | --- |
| [Rybbit](#1-rybbit) | Web and product analytics in one cookieless tool | None (7-day trial) | $19/mo for 100k events | Yes, AGPL-3.0 |
| [PostHog](#2-posthog) | Mixpanel's breadth plus flags, experiments, replay | 1M events, 5K recordings/mo | $0.00005/event after 1M | MIT core; self-host unsupported |
| [Amplitude](#3-amplitude) | Closest like-for-like replacement | 2M events/mo | Plus "starts at $0"; rate not published | No |
| [Heap](#4-heap) | Autocapture, events defined later | 10k sessions/mo | Estimate after install | No |
| [OpenPanel](#5-openpanel) | Cheapest published entry price | 30-day trial | $2.50/mo for 5K events | Yes, AGPL-3.0 |
| [Countly](#6-countly) | Mobile apps on private or own servers | Lite edition, self-hosted | Flex from $175/mo | Lite: AGPL-3.0 with branding terms |
| [TelemetryDeck](#7-telemetrydeck) | Privacy-first app analytics | 50K events/mo | €9/mo | SDKs only |
| [Matomo](#8-matomo) | Self-hosted web analytics with funnels | On-Premise edition | Cloud €29/mo for 50k hits | Yes, GPL v3 |
| [Google Analytics 4](#9-google-analytics-4) | Free funnels and cohorts for marketing sites | Free | Analytics 360 via sales | No |
| Mixpanel, for reference | | 1M events/mo | Growth: 1M free, then $0.00028/event on the base plan | No |

## Why teams look for Mixpanel alternatives

Mixpanel is a capable product analytics tool with a real free plan: 1M events a month, unlimited seats and 10K session replays, per its [pricing page](https://mixpanel.com/pricing/); our [Mixpanel pricing breakdown](/blog/mixpanel-pricing) lists every tier. The reason you're leaving decides the replacement.

### The bill grows faster than the product

On the Growth plan the first 1M events a month are free; after that, the base plan's overage rate is $0.00028 per event, or $280 per extra million, according to Mixpanel's [pricing docs](https://docs.mixpanel.com/docs/pricing). Larger prepaid plans get a lower rate that only Mixpanel's calculator shows. Group Analytics adds 40% and Data Pipelines 20% on overage, and the MTU-based Enterprise plan starts at $20,000 a year. The Free plan never bills overages; past 1M monthly events it blocks your reports until you upgrade or the month rolls over.

Published prices at two volumes (tools count events differently, so treat this as a rough guide):

| Monthly events | 1M | 10M |
| --- | --- | --- |
| Mixpanel Growth | $0 | $2,520 at the base plan's overage rate; less on a larger prepaid plan (not published) |
| Amplitude Plus | $0 (first 2M free) | Not published |
| PostHog | $0 | About $324 |
| OpenPanel | $90 | $350 |
| Rybbit Standard / Pro | $69 / $139 | $249 / $499 |
| Heap | Priced by sessions, not published | Not published |

Best fits: [PostHog](#2-posthog) keeps the breadth for less, [OpenPanel](#5-openpanel) and [Rybbit](#1-rybbit) have flat published tiers, and the [open-source options](#open-source-mixpanel-alternatives) cost only the server.

### It's more tool than a small team needs

Mixpanel works best with a [tracking plan](https://docs.mixpanel.com/docs/tracking-best-practices/tracking-plan), which its docs call "a centralized document that should serve as the source of truth on your Mixpanel implementation". That pays off for a product team with an analyst. For a three-person startup it's a lot of planning before the first useful chart, and the free plan caps saved reports at 5 per user.

Best fits: [Heap](#4-heap) captures interactions first and lets you define events later; in [Rybbit](#1-rybbit), traffic, [sessions](/features/sessions), page [funnels](/features/funnels), [journeys](/features/user-journeys) and [retention](/features/retention) work from the script alone; [TelemetryDeck](#7-telemetrydeck) ships pre-made dashboards for apps.

### You need open source or self-hosting

Mixpanel keeps your data on its own servers: in the US on Google Cloud by default, or in the EU if you choose EU residency when you create a project, and it [can't move an existing project](https://docs.mixpanel.com/docs/privacy/eu-residency) that landed in the wrong region. If data must stay on infrastructure you control, or you want to read and change the code, skip to the [open-source Mixpanel alternatives](#open-source-mixpanel-alternatives).

### Privacy and consent

Mixpanel's JavaScript SDK keeps each user's distinct ID and super properties in a first-party cookie that lasts 365 days by default, or in localStorage if you configure it ([SDK docs](https://docs.mixpanel.com/docs/tracking-methods/sdks/javascript)). In the EU that generally needs consent: France's CNIL applies the consent rule to audience-measurement trackers outside a narrow exemption and says "[most large audience measurement offerings do not fall within the scope of the exemption](https://www.cnil.fr/en/sheet-ndeg16-use-analytics-your-websites-and-applications), regardless of their configuration." Visitors who decline the banner never reach your reports. This is a summary, not legal advice.

Best fits: [Rybbit](#1-rybbit) (cookieless by default, EU-hosted cloud), [OpenPanel](#5-openpanel) (cookieless, EU-hosted), [TelemetryDeck](#7-telemetrydeck) (anonymizes on the device), or PostHog's opt-in cookieless mode.

### You want web analytics and product analytics in one tool

Mixpanel has closed some gaps: its free plan includes 10K session replays a month, up to 10 active feature flags and a small experiments allowance, so check your plan if those were the reason. What many teams still pay for alongside it is a web analytics tool for traffic sources, campaigns, SEO and page speed.

Best fits: [Rybbit](#1-rybbit), [PostHog](#2-posthog) and [Matomo](#8-matomo).

## The 9 best Mixpanel alternatives

### 1. Rybbit

**Best for:** [web analytics](/features/web-analytics) and Mixpanel's core reports in one cookieless tool, or a Mixpanel open-source alternative that runs on a small server.

[Rybbit](https://rybbit.com) covers the marketing site and the product in one dashboard. Pageviews, sources, campaigns and sessions come from the script, and [custom events](/features/custom-events) with properties are one function call. Standard includes [funnels](/features/funnels) from pages or events, [user journeys](/features/user-journeys), cohort [retention](/features/retention), [goals](/features/goals), [Web Vitals](/features/web-vitals) and [error tracking](/features/error-tracking). [User profiles](/features/user-profiles) come from `identify()`, which links a visitor's earlier anonymous activity to their account and stores traits like plan or company ([docs](/docs/identify-users)). [Session replay](/features/session-replay) is on Pro.

Tracking is cookieless by default, so Rybbit's own tracking needs no consent banner (identifying logged-in users is opt-in and needs its own lawful basis), and the cloud is hosted in the EU. An [MCP server](/features/mcp) with 44 tools lets Claude, Codex and other MCP clients query your data, including read-only SQL, and create goals and funnels.

- **Pricing (as of September 2026):** Standard from $19/month for 100k events, with 5 websites, 3 team members and 3-year retention; Pro from $39/month adds session replay (10,000 replays a month on the 100k-event tier), unlimited websites and members, and 5-year retention. 7-day trial, card collected at signup. Source: [rybbit.com/pricing](/pricing)
- **Open source / self-host:** AGPL-3.0, free to [self-host](/docs/self-hosting), replay included
- **Gaps vs Mixpanel:** no feature flags or experiments; no Mixpanel importer ([imports](/docs/data-import) cover Plausible, Umami and Simple Analytics), so you run both tools side by side for a while; retention groups users by first visit rather than an event you choose, with no behavioral cohorts or group analytics; for apps there is a [React Native SDK](/docs/sdks/react-native) that tracks screens, events, errors and users in an App Analytics dashboard mode, but no native iOS or Android SDK and no replay in apps

Pick PostHog or Amplitude instead if you need flags and experiments in your analytics tool, build a native iOS or Android app, or run on behavioral cohorts.

### 2. PostHog

**Best for:** product teams that want Mixpanel's breadth plus flags, experiments, replay and surveys, with a large free allowance.

[PostHog](https://posthog.com) is the widest platform here: product and web analytics, session replay, feature flags, experiments, surveys, error tracking and a data warehouse, each on its own meter. The free allowance resets monthly to 1M analytics events, 5K recordings, 1M flag requests, 100K exceptions and 1,500 survey responses, no card required ([pricing](https://posthog.com/pricing)). Events past 1M cost $0.00005 each up to 2M, falling in steps to $0.000009 above 250M ([product analytics pricing](https://posthog.com/product-analytics/pricing)), so 10M a month comes to about $324. Retention is 1 year on free and 7 years on paid. A [managed migration](https://posthog.com/docs/migrate/mixpanel) exports your Mixpanel data, converts it to PostHog's schema and imports it, and the [web analytics](https://posthog.com/web-analytics) dashboard adds referrers, UTMs and Core Web Vitals.

- **Pricing:** free within the allowances, then usage-based with volume tiers
- **Open source / self-host:** MIT core; self-hosting is "officially unsupported" ([details](#open-source-mixpanel-alternatives))
- **Drawbacks:** several meters on one bill; cookieless tracking is an [opt-in mode](https://posthog.com/tutorials/cookieless-tracking) that needs a project setting, and in its strictest form you shouldn't call `identify()`

See [Rybbit vs PostHog](/compare/posthog).

### 3. Amplitude

**Best for:** the closest like-for-like replacement, with a bigger free allowance.

[Amplitude](https://amplitude.com) covers the same ground as Mixpanel (event analytics, session replay, feature flags, experiments), so switching is mostly remapping events and rebuilding reports. Its [pricing page](https://amplitude.com/pricing) lists a bigger free plan than Mixpanel's or PostHog's: 2M events a month "forever", 10K session replays, unlimited flags and seats, and 1 year of retention, with no card. Plus "starts at $0" with 2M free events, scales to 70M, and adds 2-year retention and one active experiment, but the rate past the free events isn't published. Growth and Enterprise are custom-priced, with unlimited retention. The [Migrations panel](https://amplitude.com/docs/migration/migrate-from-mixpanel) imports Mixpanel events through a guided UI; user profiles go through the Batch Event Upload API instead.

- **Pricing:** Free (2M events/month); Plus from $0 at an unpublished rate; Growth and Enterprise by quote
- **Open source / self-host:** no
- **Drawbacks:** no public price past 2M events; active experiments capped at 1 on Plus and 2 on higher plans unless you buy an add-on

If you're comparing Amplitude against other tools too, see our [Amplitude alternatives](/blog/amplitude-alternatives) roundup.

### 4. Heap

**Best for:** teams whose Mixpanel data has gaps because events were never instrumented.

[Heap](https://www.heap.io), now part of Contentsquare, inverts the tracking plan: its [autocapture](https://www.heap.io/platform/autocapture) collects "every click, swipe, tap, pageview, and fill" from install, and you name events later with the history already there. The free plan covers 10k monthly sessions, 6 months of history and one project, including funnels, journeys and retention ([pricing](https://www.heap.io/pricing)). Growth adds 12 months of history, unlimited users and CSV exports, priced by an estimate once you install the snippet. Pro and Premier use custom session pricing, with replay and heatmaps as add-ons.

- **Pricing:** Free (10k sessions/month); no published prices above it
- **Open source / self-host:** no
- **Drawbacks:** no public prices; billed by sessions; replay only as an add-on on the top two tiers

### 5. OpenPanel

**Best for:** small teams that want Mixpanel-style event analytics at the lowest published price, or an AGPL tool to self-host.

[OpenPanel](https://openpanel.dev) bills itself as an open-source alternative to Mixpanel, with the most granular [pricing](https://openpanel.dev/pricing) here: $2.50 a month for 5K events, $20 for 100K, $90 for 1M, $350 for 10M and $900 for 50M, all with unlimited sites, users, dashboards and profiles, and a 30-day trial without a card. Self-hosting is free with unlimited events. The [README](https://github.com/Openpanel-dev/openpanel) lists funnels, cohorts, user profiles, session replay and A/B test breakdowns, and the [SDKs](https://openpanel.dev/docs/sdks) cover web frameworks, React Native, Swift, Kotlin and several server languages. Tracking is cookieless (IP and user agent hashed daily) and the cloud is EU-hosted, per the [homepage](https://openpanel.dev/).

- **Pricing:** from $2.50/month for 5K events
- **Open source / self-host:** AGPL-3.0, free to self-host
- **Drawbacks:** Mixpanel data comes over by replaying your export through the server-side SDK ([guide](https://openpanel.dev/guides/migrate-from-mixpanel)), not a one-click import; the self-hosting docs give no hardware requirements

### 6. Countly

**Best for:** mobile-heavy products that need analytics on a dedicated instance or their own servers.

[Countly](https://countly.com) maintains SDKs for iOS, Android, React Native, Flutter and web ([GitHub](https://github.com/Countly)) and sells three editions ([pricing](https://countly.com/pricing)). [Flex](https://countly.com/getflex) is a dedicated cloud instance ("No shared compute") from $175 a month, priced by monthly active users and data points, with funnels, retention, flows, user profiles and crash reporting; push, A/B testing and journeys are an add-on, and the 14-day trial requires a card. Enterprise is self-hosted and custom-priced. The free Countly Lite isn't a Mixpanel replacement by itself: Countly's [blog](https://countly.com/blog/the-product-led-story-of-open-source-saas-and-enterprise) says user profiles, cohorts, funnels, formulas and drill rely on a database the Community edition lacks.

- **Pricing:** Flex from $175/month; Enterprise by quote; Lite free
- **Open source / self-host:** Lite is AGPL-3.0 with branding terms ([below](#open-source-mixpanel-alternatives)); Enterprise runs on your servers
- **Drawbacks:** the highest published entry price among the hosted tools here; the free edition lacks funnels and profiles

### 7. TelemetryDeck

**Best for:** app developers who want product analytics without personal data.

[TelemetryDeck](https://telemetrydeck.com) is built for apps, with SDKs for Swift (iOS, macOS, visionOS), Kotlin, React Native, Flutter and JavaScript. Its [homepage](https://telemetrydeck.com/) says it anonymizes on the device, uses no cookies and needs no consent banner; funnels, retention and pre-made dashboards are included. New accounts get 50,000 free events a month (accounts from before 1 July 2026 keep 100,000, per its [pricing update](https://telemetrydeck.com/blog/pricing-update-2026/)), and paid plans start at €9 a month according to its [Mixpanel comparison](https://telemetrydeck.com/comparison/mixpanel-alternative/).

- **Pricing:** 50K events/month free; paid from €9/month
- **Open source / self-host:** SDKs under a modified MIT license ([FAQ](https://telemetrydeck.com/faq/)); the service is hosted
- **Drawbacks:** it avoids identifying users, so it isn't built for looking up what one customer did; the full price list sits in a calculator inside its dashboard

### 8. Matomo

**Best for:** organizations that want self-hosted web analytics with funnels and can pay for plugins.

[Matomo](https://matomo.org) is web analytics first, with premium features that reach many of Mixpanel's reports. [Matomo Cloud](https://matomo.org/pricing/) starts at €29 a month for 50,000 hits on the Business plan, which includes funnels, users flow, heatmaps, session recording and form analytics; data is stored in Frankfurt. The self-hosted Community edition is free with unlimited users and hits, but there those features are paid: the Team bundle (from €230 a month, 4 users, 5M hits) covers funnels, users flow and recordings, cohorts need the Business bundle (from €1,209 a month), and A/B testing the Enterprise bundle (from €2,834 a month). One line of code turns cookies off, which Matomo's [FAQ](https://matomo.org/faq/general/faq_157/) says can reduce returning-visitor accuracy.

- **Pricing:** Cloud from €29/month; On-Premise free, plugins extra
- **Open source / self-host:** GPL v3 or later
- **Drawbacks:** self-hosted product-analytics features cost extra, and cohorts sit in the €1,209/month bundle

See [Rybbit vs Matomo](/compare/matomo).

### 9. Google Analytics 4

**Best for:** marketing-led teams that want free funnels and cohorts and already use Google's ad stack.

Google provides GA4 ["free of charge"](https://marketingplatform.google.com/about/analytics/), and Explorations cover a fair share of Mixpanel's reports: funnel, path and cohort exploration, segment overlap, user explorer and user lifetime ([Google's list](https://support.google.com/analytics/answer/7579450)). The limits appear on product questions: standard properties keep exploration data for at most [14 months](https://support.google.com/analytics/answer/7667196), queries over 10 million events [get sampled](https://support.google.com/analytics/answer/13331292), and the two-year `_ga` [cookies](https://support.google.com/analytics/answer/11397207) mean a consent banner for most European traffic. Raw events can go to BigQuery, with a 1-million-event daily export cap on standard properties, uncapped streaming, and BigQuery [charges](https://support.google.com/analytics/answer/9823238) for storage and queries.

- **Pricing:** free; [Analytics 360](https://marketingplatform.google.com/about/analytics-360/) is sold through "Talk to Sales"
- **Open source / self-host:** no
- **Drawbacks:** 14-month exploration window; sampling; cookie consent; built for marketing attribution more than product analysis

See [Rybbit vs Google Analytics](/compare/google-analytics) and our roundup of the [best web analytics tools](/blog/best-web-analytics-tools).

## Open-source Mixpanel alternatives

If you want to run or read the server code yourself, these six publish it. What separates them is the license, the self-hosting effort, and which Mixpanel reports survive the move.

| Tool | License | Self-hosting | Mixpanel-style reports you keep |
| --- | --- | --- | --- |
| PostHog | MIT, except the `ee/` directory | Docker Compose "hobby" install; 4 vCPU, 16 GB RAM, 30 GB+ disk | Free-plan features: funnels, retention, profiles, replay, flags, experiments |
| OpenPanel | AGPL-3.0 | `./setup` script on a VPS, tested on Ubuntu 24.04 | Funnels, retention, cohorts, profiles, replay |
| Rybbit | AGPL-3.0 | `./setup.sh` and Docker Compose on a VPS with 2 GB+ RAM | Funnels, journeys, retention, profiles, replay |
| Matomo | GPL v3 or later | PHP 8.1+, MySQL 8.0+ or MariaDB 10.6+; 2 CPU and 2 GB RAM under 100k pageviews/month, 4 CPU and 8 GB up to 1M | Funnels, flows, cohorts, recordings, A/B tests as paid plugins |
| Countly Lite | AGPL-3.0 plus branding terms | One-line script on Ubuntu, CentOS or RHEL, or Docker | Events, sessions, crash reports, push, remote config; no funnels or profiles |
| Umami | MIT | Node.js 18.18+ and PostgreSQL 12.14+, or Docker | Funnels, retention, journeys, goals, distinct IDs, replays |

**PostHog** is the closest feature match, and code outside its `ee/` directory is [MIT-licensed](https://github.com/PostHog/posthog/blob/master/LICENSE). But its [self-hosting page](https://posthog.com/docs/self-host) calls self-hosted deployments "officially unsupported", states that "All paid-plan features are Cloud-only", and steers you to Cloud if you expect more than 300k events, 1k recordings or 300k flag API calls.

**OpenPanel** and **Rybbit** are AGPL-3.0: free to run and modify for any purpose, including commercially, provided that if you offer a modified version to users over a network, you make its source available to them. OpenPanel's [self-hosting docs](https://openpanel.dev/docs/self-hosting/self-hosting) ask for a VPS and a setup script. Rybbit's [setup](/docs/self-hosting) runs ClickHouse, Postgres, Redis and Caddy in Docker Compose on a 2 GB VPS, with session replay included.

**Matomo** is the most mature option, with published [server requirements](https://matomo.org/faq/on-premise/matomo-requirements/) for every traffic level; the GPL covers the core, and the product-analytics features are the paid plugins in the table.

**Countly Lite** needs a careful read: the [license](https://github.com/Countly/countly-server/blob/master/LICENSE.md) adds a Section 7 term that forbids removing Countly's logos and branding, and the [README](https://github.com/Countly/countly-server/blob/master/README.md) calls Lite "free to use under an open-source, non-commercial license". Check Countly's licensing FAQ before building a commercial product on it.

**Umami** is [MIT-licensed](https://github.com/umami-software/umami) and needs only Node.js and PostgreSQL. Its [reports](https://docs.umami.is/docs) include funnels, retention, journeys, goals and attribution, and [session replays](https://docs.umami.is/docs/replays) arrived in v3.1.0, kept for 30 days. See [Rybbit vs Umami](/compare/umami).

## Switching from Mixpanel

Mixpanel lets you take your data: the Raw Event Export API returns events as JSONL, and the Engage API and the Users tab export profiles ([export methods](https://docs.mixpanel.com/docs/export-methods)).

PostHog's managed migration does the export, conversion and import for you. Amplitude's Migrations panel brings in events, and profiles need a separate upload. OpenPanel has you replay the export through its server-side SDK. Rybbit has no Mixpanel importer, so run both tools in parallel until Rybbit has enough history, and keep the Mixpanel export as an archive.

Whichever you pick, run it beside Mixpanel for a few weeks, compare daily event and user counts, and rebuild only the reports your team actually opens.

## Which Mixpanel alternative should you pick

- **The bill is the problem and you want the same breadth:** PostHog (about $324 at 10M events) or OpenPanel ($350).
- **The closest like-for-like switch, with a bigger free plan:** Amplitude.
- **Your team never kept a tracking plan:** Heap.
- **Web and product analytics in one cookieless tool, or self-hosting on a 2 GB server:** Rybbit.
- **A native app:** TelemetryDeck for privacy-first analytics, Countly for private hosting, or PostHog and Amplitude.
- **Data must stay on your servers:** the [open-source options](#open-source-mixpanel-alternatives).
- **No budget, mostly marketing traffic:** Google Analytics 4.

Staying on Mixpanel is reasonable if you rely on Group Analytics, experiments or a mature tracking plan and the bill works. Startups under five years old with up to $8M in funding can get their first year free on Mixpanel's Startup Plan ([pricing](https://mixpanel.com/pricing/)).

## FAQ

**What is the best open-source Mixpanel alternative?**
PostHog has the most features, but self-hosting it is officially unsupported. OpenPanel and Rybbit, both AGPL-3.0, install on one VPS with a setup script. Matomo is the most mature, but self-hosted it sells funnels and cohorts as plugins.

**Who are Mixpanel's main competitors?**
The most direct alternatives to Mixpanel are Amplitude and Heap in hosted product analytics and PostHog in open-source product analytics. Google Analytics 4 competes for teams that mostly need marketing analytics.

**Is there a free Mixpanel alternative?**
Yes. Amplitude includes 2M events a month at no cost, PostHog 1M events and 5K recordings, Heap 10k sessions and TelemetryDeck 50K events, and GA4 is free. The open-source tools cost only the server when you self-host.

**Which Mixpanel alternatives have affordable pricing?**
OpenPanel starts at $2.50 a month for 5K events and charges $90 at 1M. Rybbit Standard is $19 a month for 100k events and $69 at 1M. TelemetryDeck starts at €9 and Matomo Cloud at €29, and PostHog is free until 1M events.

**Can I import my Mixpanel data into another tool?**
Into PostHog, Amplitude or OpenPanel, yes. Rybbit has no Mixpanel importer; it imports from Plausible, Umami and Simple Analytics.

---

### Content Blog Mixpanel Pricing (`docs/content/blog/mixpanel-pricing.mdx`)

---
title: "Mixpanel Pricing 2026: Plans, Calculator Prices and Add-ons"
description: "Mixpanel pricing for 2026: Free up to 1M events, Growth from $0 to $2,289/month at 20M events, Enterprise by quote. Add-ons, overages and examples."
keyword: "mixpanel pricing"
date: 2026-09-27
tags: ["mixpanel", "pricing", "product analytics", "comparisons"]
author: Rybbit Team
---

Mixpanel pricing comes in three plans. **Free** costs $0 for up to 1 million events a month. **Growth** also starts at $0, because the first 1 million events each month are free; above that, Mixpanel's own calculator prices it from $140 a month for 1.5 million events to $2,289 a month for 20 million on monthly billing, or $120 to $1,635 a month billed annually. **Enterprise** is quoted by sales, and the user-based (MTU) version starts at $20,000 a year. Seats are unlimited on every plan, and add-ons such as Group Analytics and Data Pipelines have no public price.

Disclosure up front: we build [Rybbit](/features/web-analytics), which appears in the comparison at the end. Every Mixpanel number here comes from Mixpanel's own [pricing page](https://mixpanel.com/pricing/) and its calculator, its [pricing documentation](https://docs.mixpanel.com/docs/pricing) and its [changelog](https://docs.mixpanel.com/changelogs). Figures for other tools come from those vendors' own pricing pages, linked inline. Prices are as of September 2026, when we checked every source.

## Mixpanel pricing at a glance

| | Free | Growth | Enterprise |
| --- | --- | --- | --- |
| Price | $0 | $0 for the first 1M events a month, then by tier: $140–$2,289/month billed monthly, $120–$1,635/month billed annually | Custom quote; MTU-based plans from $20,000/year |
| Monthly events | Up to 1M | Up to 20M online, more through sales | Up to 1 trillion |
| Seats | Unlimited | Unlimited | Unlimited |
| Session replays included | 10k a month | 20k a month; buy up to 500k | 20k a month; custom volumes |
| Saved reports | 5 per user | Unlimited | Unlimited |
| Experiments | 1k monthly experiment users (MEU) | 5k MEU; buy up to 100k | Custom |
| Active feature flags | 10 | 50 | Custom |
| Support | Email, 24/5 | Email, 24/5 | 24/7, dedicated success team |
| Payment | No card | Card required | Custom terms through sales |

Sources: [mixpanel.com/pricing](https://mixpanel.com/pricing/), the [Session Replay](https://docs.mixpanel.com/docs/session-replay) and [Experiments](https://docs.mixpanel.com/docs/experiments) docs, and Mixpanel's [February 2025 pricing announcement](https://mixpanel.com/blog/mixpanel-pricing-1m-free-events-autocapture/) for the card requirement on Growth.

## How Mixpanel pricing works

By default, Mixpanel bills on events, not on users or seats. An event is one tracked action (a page view, a sign-up, a purchase), and the [pricing docs](https://docs.mixpanel.com/docs/pricing) count them "across all projects in your organization". A handful of system events are excluded: `$identify`, `$create_alias`, `$merge`, `$opt_in`, session recording checkpoints, and user profile creation or updates.

Three rules decide how large that count gets:

- **Autocapture counts.** Mixpanel's [Autocapture](https://docs.mixpanel.com/docs/tracking-methods/autocapture) records page views, scrolls, form interactions, element clicks, dead clicks and rage clicks by default, and the [heatmap docs](https://docs.mixpanel.com/docs/session-replay/heatmaps) state that autocaptured clicks are billable. Page views and heatmap clicks recorded while a session replay is running are exempt.
- **Imports count when they arrive.** Since September 1, 2025, Mixpanel bills by ingestion time (some older accounts still bill by event timestamp), so backfilling last year's data counts toward this month's total.
- **Warehouse syncs count updates.** With Mirror-type warehouse syncs, every update or deletion of an existing event counts as one event.

Pricing by monthly tracked users (MTUs) still exists, but only as an Enterprise contract. Mixpanel's [MTU guide](https://docs.mixpanel.com/docs/pricing/legacy-mtu-billing) says MTU plans "start at $20,000 per year", can't be bought online, and are worth considering above 100k users or 5M monthly events. Anonymous visitors count as MTUs.

## The Mixpanel free plan: what's included and where it stops

The Free plan is "free forever" and needs no card. It includes unlimited seats, up to 1M events a month, 10k session replays, funnels and retention reports, 1k MEU for experiments, and up to 10 active feature flags.

The limits, from the [pricing docs](https://docs.mixpanel.com/docs/pricing) and the plan comparison table on the pricing page:

- **5 saved reports per user.** You can build cohorts but not save them.
- **Missing features.** No custom or borrowed properties, multi-touch attribution, Query API, warehouse connectors or alerts, and lookup tables are limited.
- **The 1M cap is hard.** Mixpanel keeps collecting data, but once you reach 1M events it blocks your reports until you upgrade or the month resets. There are no overage charges.
- **Replays expire after 30 days**, and once the 10k allowance is used you can't view more replays until you upgrade.
- **Data history isn't unlimited.** Mixpanel's [free-plan page](https://mixpanel.com/free-plan/) advertises "unlimited data history", but its [data retention policy](https://docs.mixpanel.com/docs/privacy/gdpr-compliance) deletes events after 2 years for projects created since September 1, 2025. Older projects keep 5 years until their plan changes or moves to Free. Only Enterprise lists a customizable retention policy.

## Mixpanel Growth plan pricing, step by step

Growth's first 1M events each month are free; above that you choose a monthly event tier. These are the prices the [calculator on Mixpanel's pricing page](https://mixpanel.com/pricing/) showed at every slider step on September 27, 2026:

| Monthly events | Billed monthly | Billed annually (per month) | Annual plan, per year |
| --- | --- | --- | --- |
| 1M | $0 | Not offered | Not offered |
| 1.5M | $140 | $120* | $1,440 |
| 2M | $259 | $185 | $2,220 |
| 3M | $378 | $270 | $3,240 |
| 4M | $504 | $360 | $4,320 |
| 5M | $613 | $438 | $5,256 |
| 6.5M | $785 | $561 | $6,732 |
| 8M | $952 | $680 | $8,160 |
| 10M | $1,176 | $840 | $10,080 |
| 12.5M | $1,466 | $1,047 | $12,564 |
| 15M | $1,722 | $1,230 | $14,760 |
| 17.5M | $2,003 | $1,431 | $17,172 |
| 20M | $2,289 | $1,635 | $19,620 |
| Over 20M | Contact sales | Contact sales | Contact sales |

\*At 1.5M the calculator shows $140 and $120 but labels the saving "$480/yr", while the two prices differ by $240 a year. Confirm the figure at checkout.

How to read the table:

- **Annual billing is about 29% cheaper** from 2M events up: at every step from 2M to 20M, the monthly price is about 1.4 times the annual one. Annual plans require a commitment above 1M events a month, and the calculator shows their allowance as a yearly total (120M events a year at the 10M tier).
- **The only published per-event rate is $0.00028**, which is what overage costs on the 1M plan ($0.28 per 1,000 events). Committing to a tier lowers the effective rate: on monthly billing the 10M tier works out to $1,176 ÷ 9M billable events, about $0.13 per 1,000.
- **The calculator prices events only.** Extra replays, extra experiment users and add-ons are billed separately.

What Growth adds over Free: unlimited saved reports and cohorts, custom and borrowed properties, multi-touch attribution, up to 5 alerts per project, Query API access, data warehouse connectors and, since February 24, 2026, SSO, Data Views and sensitive data classification. The pricing docs describe the typical Growth customer as a team with "fewer than 50 employees".

## What happens when you go over your plan

On Growth, Mixpanel keeps collecting. Overage is billed at the start of the next month at your "À La Carte" rate, which is $0.00028 per event on the 1M plan and lower on bigger tiers. If you have add-ons, the [pricing docs](https://docs.mixpanel.com/docs/pricing) add 20% of the overage for Data Pipelines and 40% for Group Analytics.

There is no spending cap; the docs say "it's not possible to set a billing limit at this moment". Owners and billing admins get emails at 85%, 100%, 110%, 120%, 200%, 300% and 400% of plan volume.

The other meters work differently. Experiment users beyond your allowance are billed at an overage rate Mixpanel doesn't publish. Replays beyond the included allowance can't be viewed unless you've bought the replay add-on; with it, replays beyond your purchased amount are billed at your per-unit rate. And if you decide to drop back to Free, the downgrade takes effect at the next billing date, not immediately.

## Mixpanel add-ons and their prices

Mixpanel's public pricing page marks add-ons without prices. Here is what the page and the docs do say:

| Add-on | Plans | Published price | Notes |
| --- | --- | --- | --- |
| [Group Analytics](https://docs.mixpanel.com/docs/data-structure/advanced/group-analytics) | Growth, Enterprise | None | Analysis at the company or account level for B2B products; can't be applied retroactively to events already ingested; adds 40% to overage; required for group cohort targeting in feature flags |
| [Data Pipelines](https://docs.mixpanel.com/docs/data-pipelines) | Growth, Enterprise | None; 30-day free trial | Exports events to your warehouse or a storage bucket; adds 20% to overage; the trial allows daily exports only and one pipeline per data source |
| [Session Replay volume](https://docs.mixpanel.com/docs/session-replay) | Growth (up to 500k a month or 6M a year online), Enterprise | Shown in the in-app plan builder | Required for the in-app sampling controls; Enterprise customers can customize replay retention (the pricing page lists 7–365 days) |
| [Experiments volume](https://docs.mixpanel.com/docs/experiments) | Growth (10k–100k MEU a month), Enterprise | None; bought in-app under Plan Details & Billing | On Growth, purchased volume also raises the limit to 500 active [feature flags](https://docs.mixpanel.com/docs/featureflags) |
| [Metric Trees](https://docs.mixpanel.com/docs/metric_tree) | Enterprise only | None | Maps input metrics to business outcomes |
| Slack shared channel, faster response SLAs | Enterprise only | None | Support add-ons |

Budget for these separately: the calculator figure covers event volume only.

## Mixpanel Enterprise pricing

Enterprise has no list price; the pricing page says "Let's chat". It raises the event ceiling to 1 trillion a month and adds what Growth lacks: anomaly detection, root cause analysis, signal correlation, cross-product analytics, data standards, event owners and approval, data quality monitoring, verified data and cohorts, HIPAA compliance tools, first-party tracking domains, a customizable data retention policy, 24/7 support and a dedicated account manager. Metric Trees and the Slack and SLA support options are Enterprise add-ons. The MTU-based Enterprise plan is the only one with a published floor: $20,000 a year.

## Mixpanel startup program

The [startup program](https://docs.mixpanel.com/docs/pricing/startup-program) is open to companies incorporated less than five years ago that have raised no more than $8 million, haven't redeemed another Mixpanel offer, and have never been on a paid plan. They get the first year free on a Startup Plan with 1 billion events a year, 500,000 session replays, Experiments with 1.2 million MEU a year, 50 feature flags, and the Data Pipelines, Group Analytics, Warehouse Connectors and Session Replay add-ons. You must start sending data within 90 days, and there's no second free year. The FAQ on the pricing page puts the replay allowance at 1 million, so confirm which figure applies when you apply.

## Mixpanel pricing changes in 2025 and 2026

Dates come from Mixpanel's [changelog](https://docs.mixpanel.com/changelogs) and docs:

| Date | Change |
| --- | --- |
| December 2, 2024 | Session Replay added to Free (10k replays a month) and Growth (20k) |
| February 2025 | Growth starts including the first 1M events a month free. Per Mixpanel's [announcement](https://mixpanel.com/blog/mixpanel-pricing-1m-free-events-autocapture/), Growth previously "started at $24 a month", and it now requires a credit card |
| August 18, 2025 | Growth customers can buy extra replays online, up to 500k a month or 6M a year |
| September 1, 2025 | Billing moves to ingestion time; default event retention drops to 2 years |
| February 24, 2026 | SSO, Data Views and sensitive data classification move into Growth |
| August 24, 2026 | Experiments and Feature Flags added to Free (1k MEU, 10 active flags) and Growth (5k MEU and 50 flags, expandable to 100k MEU and 500 flags) |

## Three worked examples

### A new app under 1M events

4,000 monthly active users × 120 events each = 480,000 events a month.

- Free plan: **$0**, with room to double before the 1M cap.
- If the team needs saved cohorts, alerts or more than 5 saved reports per person, Growth is also **$0** at this volume. It needs a card, and any month over 1M is billed at $0.00028 per extra event.

### A consumer app at 4M events

25,000 monthly active users × 160 events each = 4,000,000 events a month.

- Growth, billed monthly: $504 × 12 = **$6,048 a year**.
- Growth, billed annually: $360 × 12 = **$4,320 a year**.
- Annual billing saves $1,728 a year, which matches the saving the calculator shows. Recording more than 20k sessions a month would add the replay add-on on top.

### A B2B SaaS that never resized its plan

The team signed up for the 1M-event Growth plan ($0) with Group Analytics and now sends 3M events a month.

- Event overage: 2,000,000 × $0.00028 = $560.
- Group Analytics surcharge: 40% × $560 = $224.
- Total: **$784 a month**, plus the Group Analytics fee itself.
- The 3M tier lists at **$378 a month** billed monthly ($270 billed annually), $182 less than the event overage alone. Mixpanel doesn't publish how the Group Analytics fee changes between tiers, so ask for both totals before switching.

## Mixpanel vs alternatives at the same volume

For the full field, organized by why teams switch, see our guide to [Mixpanel alternatives](/blog/mixpanel-alternatives).

### Event counts aren't interchangeable

All three tools below bill per event, but they don't count the same things:

- **Mixpanel** counts every tracked event, including everything Autocapture records (page views, scrolls, clicks, form interactions), across all projects.
- **[PostHog](/compare/posthog)** bills product analytics "on events captured – pageviews, clicks, custom events, and everything autocapture picks up", per its [pricing page](https://posthog.com/product-analytics/pricing). Our [PostHog pricing breakdown](/blog/posthog-pricing) covers its other products.
- **Rybbit** counts pageviews and outbound-link clicks (both tracked by default) plus the [custom events](/features/custom-events) you send. [Web Vitals](/features/web-vitals), button clicks, copies and form interactions count only if you switch them on.

The same traffic therefore produces different totals. A site that tracks only page views sends about one event per view to each tool; turn on autocapture in Mixpanel or PostHog and every captured click adds to the count. Estimate your volume per tool before comparing prices.

### Price at the same event volume

| Monthly events | Mixpanel Growth (monthly / annual billing) | PostHog product analytics | Rybbit Standard | Rybbit Pro |
| --- | --- | --- | --- | --- |
| 1M | $0 / not offered | $0 | $69 | $139 |
| 5M | $613 / $438 | $152.90 | $149 | $299 |
| 10M | $1,176 / $840 | $324.40 | $249 | $499 |
| 20M | $2,289 / $1,635 | $643.40 | $399 | $799 |

PostHog's figures apply its graduated rates as its calculator shows them: the first 1M events free, then $0.00005 per event up to 2M, $0.0000343 from 2M to 15M, and $0.0000295 from 15M to 50M. Rybbit's figures are monthly billing from our [pricing page](/pricing); annual billing gives 4 months free.

What the table leaves out:

- **Session replay.** Mixpanel Growth includes 20k replays a month. PostHog includes [5k recordings a month](https://posthog.com/pricing) free and bills the rest separately. Rybbit includes [session replay](/features/session-replay) on Pro only, with a monthly quota of 10% of the event tier: 10,000 replays at 100k events, 100,000 at 1M and 1 million at 10M. Once the quota is used, new sessions aren't recorded until the next calendar month.
- **Data retention.** Mixpanel keeps events for 2 years (projects created since September 2025). PostHog keeps them 1 year without a card and 7 years once you add one for pay-as-you-go. Rybbit keeps them 3 years on Standard and 5 on Pro.
- **Seats.** Mixpanel has unlimited seats on every plan, and PostHog doesn't charge per seat. Rybbit Standard allows 3 team members and 5 websites; Pro makes both unlimited.
- **Under 1M events, free wins.** Mixpanel and PostHog cost nothing there. Rybbit Cloud is paid after a 7-day trial, with a card collected at signup, from $19 a month for 100k events. Self-hosting Rybbit is free under the AGPL-3.0 license.

### Who should stay on Mixpanel

- **You run experiments and feature flags from your analytics tool.** Since August 2026 both are included on Free and Growth, with CUPED, sequential testing and sample-ratio-mismatch monitoring.
- **You sell to businesses.** Group Analytics answers questions at the account level, such as which companies convert through a funnel.
- **Your data team depends on governance.** Mixpanel has a data dictionary on every plan, warehouse connectors from Growth up, and event approval, data standards and verified cohorts on Enterprise.
- **You're under 1M events a month.** Mixpanel Free costs $0 at that volume and includes 10k replays a month.
- **You qualify for the startup program**, which covers the first year.

### Where Rybbit fits

Rybbit combines web analytics and product analytics: traffic sources, pages and Web Vitals alongside [funnels](/features/funnels), [retention](/features/retention), [user journeys](/features/user-journeys) and [user profiles](/features/user-profiles). It's cookieless by default, so its own tracking needs no consent banner, and the cloud version is hosted in the EU. It suits teams whose product lives on the web and who want marketing and product questions answered in one tool, at a fixed price per event tier.

It doesn't match Mixpanel's depth in behavioral cohorts, experimentation or data governance. There's also no Mixpanel importer, so switching means running both tools side by side until Rybbit has built up enough history. For a wider view of the market, see our [roundup of web analytics tools](/blog/best-web-analytics-tools).

## FAQ

**How much does Mixpanel cost?**
Mixpanel Free costs $0 up to 1M events a month. Growth is $0 for the first 1M events, then $140 to $2,289 a month on monthly billing (1.5M to 20M events), or $120 to $1,635 a month billed annually. Enterprise is quoted by sales, with MTU-based contracts from $20,000 a year. Prices are as of September 2026.

**Is Mixpanel free?**
Yes. The Free plan is free forever with no card: 1M events a month, 10k session replays, unlimited seats and 5 saved reports per user. Growth also costs $0 up to 1M events but requires a card.

**What are Mixpanel's pricing plans?**
Free, Growth and Enterprise, all billed on monthly events. Growth and Enterprise can add Group Analytics, Data Pipelines, extra session replays and extra experiment volume; Enterprise also offers Metric Trees and an MTU-based contract.

**Is there a Mixpanel pricing calculator?**
Yes, on [mixpanel.com/pricing](https://mixpanel.com/pricing/). It covers Growth event volumes from 1M to 20M a month, with monthly and annual prices; the full table is above. It doesn't price add-ons or extra replays.

**How much does Mixpanel charge per event?**
The published rate is $0.00028 per event above 1M on the 1M Growth plan, or $0.28 per 1,000 events. Larger tiers are cheaper per event: the 10M tier works out to about $0.13 per 1,000 billable events on monthly billing.

**Does Mixpanel charge per user?**
Not by default. Seats are unlimited on every plan, and standard pricing is per event. Pricing by monthly tracked users is available only on Enterprise contracts, from $20,000 a year.

**Is Mixpanel session replay free?**
Up to a limit: 10k replays a month on Free and 20k on Growth and Enterprise, kept for 30 days. Growth customers can buy up to 500k a month, with the price shown in the in-app plan builder.

**What changed in Mixpanel pricing in 2025 and 2026?**
In February 2025, Growth started including 1M free events a month (it previously started at $24 a month). In September 2025, billing moved to ingestion time and default retention dropped to 2 years. SSO came to Growth in February 2026, and Experiments and Feature Flags were added to Free and Growth in August 2026.

---

*See what your own traffic looks like in Rybbit: [start a 7-day trial](https://app.rybbit.io/signup) or open the [live demo](https://demo.rybbit.com/81).*

---

### Content Blog Multivariate Testing (`docs/content/blog/multivariate-testing.mdx`)

---
title: "What Is Multivariate Testing? Examples and How to Run One"
date: 2025-11-22
updated: 2026-09-28
description: "Multivariate testing explained: how it differs from A/B testing, when to use it, real examples, and how to measure the results with your analytics."
keyword: "multivariate testing"
tags: ["testing", "experimentation", "optimization", "analytics"]
author: Rybbit Team
---

You have a checkout page that isn't converting as well as you'd like. The problem is obvious: something needs to change. But what?

The button color? The copy? The form layout? The trust badge position? All of the above?

If you test each change one at a time (button color this week, copy next week), you're looking at months of experimentation. By then, you've lost potential customers and revenue.

This is where multivariate testing comes in. Instead of testing one variable at a time, you test multiple variables simultaneously. It's faster, cheaper, and gives you deeper insights into what actually drives conversions.

Let's walk through what multivariate testing is, how it works, when to use it, and how to implement it with Rybbit Analytics.

## What is Multivariate Testing?

Multivariate testing (MVT) is an experimentation method where you test multiple variables at the same time to see how they work together and individually.

**Example:** Instead of testing:
- Just the button color (red vs blue)

You test:
- Button color (red vs blue) × Button copy ("Buy Now" vs "Add to Cart") × Form fields (3 fields vs 5 fields)

This creates multiple combinations (variations) that users are randomly exposed to. Then you measure which combination performs best.

**Key distinction:** Multivariate testing is different from A/B testing:
- **A/B Testing**: Test one variable (button color)
- **Multivariate Testing**: Test multiple variables simultaneously (button color + copy + layout)

Think of multivariate testing as A/B testing on steroids. You're not just comparing two options. You're exploring a matrix of possibilities.

## How Multivariate Testing Works

Let's say you're running a multivariate test on your landing page with three variables:

1. **Headline**: Option A ("Save Time") vs Option B ("Save Money")
2. **CTA Button**: Option A ("Start Free") vs Option B ("See Demo")
3. **Image**: Option A (Product screenshot) vs Option B (Customer testimonial)

That creates 2 × 2 × 2 = **8 possible combinations** (variations).

This is similar to testing different stages in your [content marketing funnel](/blog/funnel-analysis), but focused specifically on optimization within a single page or experience.

| Variation | Headline | Button | Image |
|-----------|----------|--------|-------|
| 1 | Save Time | Start Free | Screenshot |
| 2 | Save Time | Start Free | Testimonial |
| 3 | Save Time | See Demo | Screenshot |
| 4 | Save Time | See Demo | Testimonial |
| 5 | Save Money | Start Free | Screenshot |
| 6 | Save Money | Start Free | Testimonial |
| 7 | Save Money | See Demo | Screenshot |
| 8 | Save Money | See Demo | Testimonial |

Each visitor randomly sees one of these 8 combinations. You then measure which combination converts the best.

### The Math Behind Multivariate Testing

Multivariate testing uses **factorial design** - a statistical method that efficiently tests multiple variables and their interactions.

Here's the key insight: A multivariate test gives you more data than running separate A/B tests.

**Example:** If you want to test 3 variables with 2 options each:
- **Separate A/B tests**: 3 tests × 2 weeks each = 6 weeks to get all the data
- **One multivariate test**: 2 weeks to test all 8 combinations simultaneously

But there's a catch: You need enough traffic to detect winners across all combinations.

**Rule of thumb:** Each variation needs at least 100-200 conversions to be statistically significant. So if you have 8 variations, you need 800-1,600 conversions total.

If your traffic is lower, you might not have enough data to reach statistical significance. That's when A/B testing individual changes becomes smarter.

## Multivariate Testing vs A/B Testing: When to Use Each

**Use A/B Testing when:**
- You have lower traffic (< 100 conversions/week)
- You're testing one specific hypothesis (Does red button convert better than blue?)
- You need results fast and traffic is limited
- You're optimizing a single element (headline, CTA, etc.)

**Use Multivariate Testing when:**
- You have higher traffic (> 500 conversions/week)
- You want to understand how multiple elements interact
- You have time to gather data (2-4 weeks)
- You're trying to optimize an entire page experience
- You want to find unexpected winner combinations

**Real example:** Slack probably uses multivariate testing on their signup page because they have massive traffic and want to optimize multiple elements (headline, CTA, form fields, imagery). A small SaaS with 50 signups/week would be better off with A/B testing one element at a time.

## Real-World Multivariate Testing Examples

### Example 1: E-commerce Product Page

You're running an online bookstore and want to increase purchases. For more details on e-commerce optimization, see our [e-commerce analytics guide](/blog/ecommerce-analytics).

**Variables:**
- **Price display**: Show as "$15.99" vs Show as "Only 1 left - $15.99" (scarcity)
- **Social proof**: Show 1,200 reviews vs Show 4.8★ rating
- **Shipping info**: "FREE shipping" vs "FREE 2-day shipping"

**Result:** The combination of scarcity + star rating + specific shipping info converts 23% better than the baseline.

**Finding:** Customers care more about shipping specificity than just the word "free."

### Example 2: SaaS Pricing Page

You're testing what makes people upgrade.

**Variables:**
- **Pricing comparison**: Monthly vs Annual prominent
- **Social proof**: "Used by 500+ companies" vs "10,000+ users"
- **CTA color**: Blue vs Green

**Result:** Annual pricing prominent + user count + green button wins, converting 18% higher.

**Finding:** B2C appeals more to user quantity; the specific button color matters (small but consistent winner).

### Example 3: Mobile App Onboarding

You want users to complete onboarding.

**Variables:**
- **Progress indicator**: Shows steps (Step 1/4) vs No progress indicator
- **Skip option**: Visible skip button vs Hidden skip option
- **Visuals**: Animation + icons vs Static images

**Result:** Progress indicator + no visible skip + animations convert 31% better.

**Finding:** Psychological commitment (progress bar) + removing friction (hidden skip) + engagement (animation) work together better than individually.

## The Real Power: Interaction Effects

Here's why multivariate testing is powerful: **variables don't exist in a vacuum**.

In the onboarding example above, the progress indicator might only improve completion by 5% on its own. But combined with animations and a hidden skip button, it jumps to 31%.

This is called an **interaction effect** - when variables work better together than they would individually.

This is nearly impossible to discover with A/B testing alone. You'd test progress indicator vs no progress indicator and see a small 5% lift. You might even conclude it's not worth it. But in combination with other changes, it's powerful.

Multivariate testing reveals these hidden synergies.

## How to Run a Multivariate Test with Rybbit

Rybbit Analytics lets you set up and analyze multivariate tests by tracking variations and comparing performance metrics.



The Rybbit dashboard provides detailed metrics that let you compare performance across your different test variations and identify winning combinations.

### Step 1: Define Your Variables and Create Variations

First, identify what you want to test. Let's say you're optimizing your signup page:

**Variables:**
- Headline: "Start Free" vs "See How It Works"
- Button color: Blue vs Green
- Form fields: 2 fields vs 4 fields

**Result:** 2 × 2 × 2 = 8 variations to test

### Step 2: Implement Variation Tracking with Rybbit

When you expose users to different variations, track which variation each user sees. Use Rybbit's event tracking to record variation assignments. Refer to the [event tracking documentation](/docs/track-events) for specific implementation details on how to properly record variation assignments.

### Step 3: Track Conversion Events

Track when users complete the desired action (signup, purchase, etc.). See the [event tracking documentation](/docs/track-events) for details on how to properly record conversion events in Rybbit.

### Step 4: Analyze Results in Rybbit

Use Rybbit's filtering and segmentation to compare conversion rates across variations:



The Funnels section in Rybbit allows you to create custom [conversion funnels](/features/funnels) and view performance across your different test variations, making it simple to see which combination is winning.

1. Go to **Product Analytics → Funnels**
2. Create a funnel from "Signup_Variation_Shown" to "Signup_Completed"
3. Use the "Breakdown by" option in the Funnels interface to select your variation property and compare which combination wins



Use the Pages dashboard to analyze which page variations are performing best, showing traffic and engagement metrics for each variation.

**What you'll see:**
- Conversion rate for each variation
- Which combinations perform best
- Statistical significance (are differences real or random?)
- How long it takes to reach statistical significance

### Step 5: Analyze Interaction Effects

To understand which variable combinations matter most, create additional analysis by tracking the individual variables within your events. This allows you to create custom cohorts by each variable in Rybbit to see conversion rates for each combination.

### Step 6: Determine Statistical Significance

This is crucial: **Don't stop the test too early just because one variation is winning.**

Rybbit doesn't automatically calculate statistical significance (you'll need to use an external tool like [this calculator](https://www.optimizely.com/sample-size-calculator/)), but you can estimate:

**Rule of thumb:** Run the test until each variation has at least 100-200 conversions.

If you have 8 variations, that's 800-1,600 total conversions needed. At 500 conversions/week, that's about 2-3 weeks.

### Step 7: Document Learnings

After your test concludes, document what you learned about which variables and combinations drove better results. This builds institutional knowledge for future tests and helps you understand what types of changes are worth pursuing.

## Multivariate Testing Best Practices

### 1. Start Simple

Don't test 5 variables with 3 options each on your first attempt. That's 3^5 = 243 variations. You'll never reach statistical significance.

Start with 2-3 variables, 2 options each. That's 4-8 variations - manageable and testable.

### 2. Test Independent Variables

Make sure your variables don't overlap. Testing both "button color" and "button style" at the same time confuses results.

Good: Button color + Headline + Form length
Bad: Button color + Button size (these affect each other)

### 3. Have Enough Traffic

This is non-negotiable. Low-traffic sites should do A/B testing, not multivariate testing.

**Traffic check:** (Traffic per variation per week) × (Number of variations) = Do you have enough?

If variation 1 gets 50 visitors/week and you have 8 variations, each gets about 6-7 visitors/week. That's not enough to measure conversions accurately.

### 4. Run Tests Long Enough

Don't stop after 1 week. Run for at least 2 full weeks to account for day-of-week effects (Monday traffic might differ from Friday traffic).

### 5. Test the Right Metrics

Don't just track clicks. Track conversions - the action that matters to your business.

- E-commerce: Track purchases, not just add-to-cart
- SaaS: Track signups, not just clicks
- Content: Track time-on-page, not just visits

### 6. Understand Diminishing Returns

Each additional variable you test increases complexity exponentially. 2 variables = 4 combinations. 3 variables = 8 combinations. 4 variables = 16 combinations.

At some point, the added complexity outweighs the benefit. Usually 3-4 variables is the sweet spot.

### 7. Have a Hypothesis

Don't test random combinations. Have a theory about what will work and why.

**Good hypothesis:** "We think shortening the form (2 fields vs 4) combined with a specific CTA ('Start Free' vs 'See Demo') will increase conversions because it reduces friction while being clear on value."

**Bad hypothesis:** "Let's test random stuff and see what sticks."

## Common Multivariate Testing Mistakes

**Mistake 1: Not having enough traffic**
- You can't trust results with only 10-20 conversions per variation
- Minimum 100-200 conversions per variation

**Mistake 2: Too many variables**
- Testing 5 variables = 32 combinations at minimum
- You need massive traffic to reach statistical significance

**Mistake 3: Stopping too early**
- "We have a winner after 3 days!" - No, you need 2-3 weeks minimum
- Variance will make random variation look like a winner early on

**Mistake 4: Not tracking properly**
- If you don't know which variation each user saw, you can't analyze results
- Always track variation assignment with every event

**Mistake 5: Testing insignificant changes**
- Testing button color shade (navy vs dark blue) is a waste of time
- Test changes that could meaningfully impact behavior

**Mistake 6: Not planning for "no winner"**
- Sometimes all variations perform similarly
- That's still valuable data - it means this area isn't a lever for improvement

## When Multivariate Testing Isn't the Right Choice

**Don't use multivariate testing if:**

1. **You have low traffic** - Stick to A/B testing one element at a time
2. **You need results fast** - Multivariate tests take 2-4 weeks
3. **Changes are risky** - If one variation could break the experience, test one thing at a time
4. **Variables are intertwined** - If changing one necessarily changes the other, you can't isolate effects
5. **You're testing radical redesigns** - This is better suited to A/B testing or user research

**Do use multivariate testing if:**

1. ✅ You have 500+ conversions per week
2. ✅ You want to optimize multiple elements together
3. ✅ You're willing to wait 2-3 weeks for results
4. ✅ You want to discover interaction effects
5. ✅ You're testing independent variables

## Multivariate Testing with Rybbit: Real Example

Let's walk through a complete multivariate testing approach.

### Scenario: E-commerce Checkout Page

You run an online store and want to increase purchase completion.

**Current state:** 1,000 visitors/week to checkout page.

**What you want to test:**
- Payment options prominence: Show all options vs Highlight most popular
- Trust badge position: Top of form vs Bottom of form
- Shipping messaging: "Ships in 1-2 days" vs "Free shipping"

**That's 2 × 2 × 2 = 8 variations to test**

### Week 1-3: Run the Test

Implement variation tracking using Rybbit's event system. Record when each user sees a variation and when they complete (or abandon) the checkout.

### Week 4: Analyze in Rybbit

Use Rybbit's filtering and segmentation features to compare conversion rates across your variations.

Look for:
- Which variation has the highest conversion rate
- Whether certain variable combinations outperform others
- Patterns in which users from different sources convert better with specific variations

## The Bottom Line

Multivariate testing is powerful because it's efficient. Instead of spending months testing one variable at a time, you test multiple variables simultaneously and discover how they interact.

The key requirements are:
1. **Enough traffic** - At least 500+ conversions per week
2. **Clear variables** - Independent factors you want to test
3. **Time** - 2-4 weeks to gather data
4. **Proper tracking** - Know which variation each user saw
5. **Statistical rigor** - Wait for real significance, not random luck

With Rybbit's event tracking and analytics, you can set up and measure multivariate tests without complex testing platforms. You get the data, you make the decisions.

The companies that win are the ones that test relentlessly and act on data. Multivariate testing is how you scale optimization from "one variable at a time" to "multiple hypotheses, simultaneously."

---

**Related Reading:**
- [Behavior Analytics - Events Tab](/docs/feature-guides/events)
- [Behavior Analytics - Users Tab](/docs/feature-guides/users)
- [Track Custom Events](/docs/track-events)
- [Product Analytics - Funnels Guide](/docs/funnels)
- [Product Analytics - Journeys Dashboard](/docs/feature-guides/journeys)
- [Retention Analysis: How to Measure and Improve User Retention](/blog/retention-analysis)
- [Ecommerce Analytics: The Metrics and Reports That Matter](/blog/ecommerce-analytics)
- [Funnel Analysis: How to Find Where Users Drop Off](/blog/funnel-analysis)
- [Why Did My Website Traffic Drop?](/blog/website-traffic-drop)
- [What Is Web Analytics? A Complete Guide](/blog/what-is-web-analytics)

---

### Content Blog Posthog Pricing (`docs/content/blog/posthog-pricing.mdx`)

---
title: "PostHog Pricing 2026: Free Tier, Rates and Worked Examples"
description: "PostHog pricing explained: each product's free tier, per-unit rates by volume, identified vs anonymous events, and four real bills worked out step by step."
keyword: "posthog pricing"
date: 2026-09-27
tags: ["posthog", "pricing", "comparisons"]
author: Rybbit Team
---

PostHog pricing is usage-based and billed separately for each product. Every product has a free allowance that resets monthly (1 million analytics events, 5,000 session recordings, 1 million feature flag requests, 100,000 exceptions and 1,500 survey responses, among others), and you don't need a card to use it. Past the allowance you pay per unit on a sliding scale: analytics events start at $0.00005 each ($50 per million) and fall to $0.000009, web recordings start at $0.005 and fall to $0.0015, and flag requests start at $0.0001. There are no seats, minimums or contracts, and you can cap each product with a billing limit. Two things push real bills above the headline rate: events from identified users carry an extra person-profile charge, and each product you switch on is metered on its own.

The free tier is genuinely generous. PostHog's [official pricing page](https://posthog.com/pricing) says 97% of companies using it pay nothing, and a small SaaS can run analytics, replay, feature flags and error tracking without a bill (see worked example 1).

Disclosure: we build [Rybbit](/features/web-analytics), an open-source analytics tool that appears in the comparison near the end; [Rybbit vs PostHog](/compare/posthog) covers features. Every PostHog number here comes from posthog.com (the pricing page and its calculator, the per-product pricing pages, PostHog's docs and its public handbook), linked inline and checked in September 2026. Prices are as of September 2026, in US dollars, excluding tax.

## PostHog pricing at a glance

| Product | Billed on | Free every month | Then from |
| --- | --- | --- | --- |
| Product and web analytics | Events | 1,000,000 | $0.00005 per event |
| Person profiles | Identified events | 1,000,000 | +$0.000198 per event |
| Session replay | Web / mobile recordings | 5,000 / 2,500 | $0.005 / $0.01 per recording |
| Feature flags and experiments | Flag requests | 1,000,000 | $0.0001 per request |
| Error tracking | Exceptions | 100,000 | $0.00037 per exception |
| Surveys | Responses | 1,500 | $0.10 per response |
| AI observability and evaluations | Events | 100,000 | $0.00035 per event |
| Data warehouse | Rows synced | 1,000,000 (historical syncs free) | $0.000015 per row |
| Data pipelines | Trigger events / exported rows | 10,000 / 1,000,000 | $0.0005 / $0.000015 |
| Logs and tracing | GB ingested | 10 GB | $0.25 per GB |
| Workflows | Emails / push and other dispatches | 10,000 each | $0.003 / $0.00075 |
| PostHog AI, Replay Vision, PostHog Desktop | Credits | 500 / 2,500 / 2,000 | $0.01 per credit |
| Inbox (beta) | Pull requests | 3 | $15 per PR |

Sources: the [pricing page](https://posthog.com/pricing) and its calculator, plus each product's pricing page linked below. Experiments are billed with feature flags and evaluations with AI observability. Platform packages (Boost $250/month, Scale $750/month, Enterprise custom) come on top.

## How PostHog's usage-based pricing works

- **Graduated tiers.** Each band's rate applies only to the units inside it. Five million events cost $0 for the first million, $50 for the second and $102.90 for the next three ($0.0000343 each), not the lowest rate on all five million.
- **Allowances reset monthly** and stay the same whether or not you have a card on file. Unused allowance doesn't roll over.
- **You pay on ingestion only.** PostHog charges when data is first received and has no storage fees.
- **No seats, minimums or commitments.** Billing is monthly on your sign-up date, the first charge comes a month after sign-up, and cards are charged through Stripe. Paying by invoice requires buying credits up front.

## Free plan vs pay-as-you-go

Both self-serve plans get the same free allowances. The difference is what happens past them.

| | Free | Pay-as-you-go |
| --- | --- | --- |
| Cost | $0, no card | $0 until you pass an allowance, then per unit |
| Projects | 1 | 6 |
| Event data retention | 1 year | 7 years |
| Recording retention | 30 days | 90 days |
| Team members | Unlimited | No per-seat charges |
| Support | Community | Email (Slack above $2,000/month) |
| Past the allowance | Extra events are dropped; flags return a quota-limited response | Billed per unit, up to any billing limit you set |

PostHog recommends pay-as-you-go for "nearly everyone": it unlocks all features and still costs nothing within the allowances, which a billing limit can enforce.

## Product analytics pricing: events

Product and web analytics share one meter: PostHog bills "events captured – pageviews, clicks, custom events, and everything autocapture picks up" ([product analytics pricing](https://posthog.com/product-analytics/pricing)), and [web analytics](https://posthog.com/web-analytics/pricing) uses the same events. Heatmaps don't add to the count: PostHog's [autocapture docs](https://posthog.com/docs/product-analytics/autocapture) say heatmap capture creates no additional events.

### Identified vs anonymous events

Every event pays the base rate. Events from identified users (or users with custom person properties) also pay a **person profiles** rate, with its own tiers and its own first million free. From the breakdown in PostHog's calculator:

| Monthly volume | Base rate (all events) | Person profiles (identified events only) |
| --- | --- | --- |
| First 1M | Free | Free |
| 1M–2M | $0.0000500 | $0.0001980 |
| 2M–15M | $0.0000343 | $0.0000697 |
| 15M–50M | $0.0000295 | $0.0000360 |
| 50M–100M | $0.0000218 | $0.0000146 |
| 100M–250M | $0.0000150 | $0.0000037 |
| 250M+ | $0.0000090 | $0.0000010 |

Base tiers count all events; person-profile tiers count identified events only. In the 1M–2M band an identified event costs $0.000248 against $0.00005 for an anonymous one, about five times as much; from 2M to 15M it's about three times, and above 100M only 1.1 to 1.25 times. PostHog's own pages round this differently ("up to 4x" in the docs, "10x" on the web analytics page); the rates above are the ones its calculator uses.

The JavaScript SDK defaults to `person_profiles: 'identified_only'`, so visitors stay anonymous until you call `identify()`, after which all of that user's events are identified. Backend SDKs and the API capture identified events by default ([anonymous vs identified events](https://posthog.com/docs/data/anonymous-vs-identified-events)). A marketing site with no logins can run entirely on the base rate; a logged-in SaaS app mostly can't.

**Group analytics**, a paid add-on for account-level B2B analysis, is billed on every identified event in the project once you subscribe: first 1M free, then $0.000071 per event, falling to $0.0000029 above 250M ([group analytics pricing](https://posthog.com/group-analytics/pricing)).

## Session replay pricing: recordings

| Recordings per month | Web | Mobile |
| --- | --- | --- |
| Free allowance | First 5,000 | First 2,500 |
| Up to 15,000 | $0.0050 | $0.0100 |
| 15,000–50,000 | $0.0035 | $0.0070 |
| 50,000–150,000 | $0.0020 | $0.0040 |
| 150,000–500,000 | $0.0017 | $0.0034 |
| 500,000+ | $0.0015 | $0.0030 |

Web and mobile are billed separately, each with its own allowance ([session replay pricing](https://posthog.com/session-replay/pricing)). Recordings are kept up to 30 days on the free plan, 90 days on pay-as-you-go, up to 1 year with the Boost or Scale package and up to 5 years with Enterprise ([recording retention](https://posthog.com/docs/session-replay/recording-retention)). [Replay Vision](https://posthog.com/docs/replay-vision), which uses AI to watch recordings and turn what it sees into queryable data, is billed separately in credits.

## Feature flags and experiments pricing: requests

| Flag requests per month | Price per request |
| --- | --- |
| First 1M | Free |
| 1M–2M | $0.000100 |
| 2M–10M | $0.000045 |
| 10M–50M | $0.000025 |
| 50M+ | $0.000010 |

A request is a call to PostHog's `/flags` endpoint, not each flag check in your code ([feature flags pricing](https://posthog.com/feature-flags/pricing)). In the browser, the SDK makes one when it initializes, when a user is identified or their properties change, when you reload flags and when you create a group. On a server, each evaluation call is a request unless you use local evaluation, where every poll for flag definitions counts as 10 requests; at the default 30-second interval that's 864,000 requests a month per server ([estimating usage and costs](https://posthog.com/docs/billing/estimating-usage-costs)). Surveys evaluate flags for targeting, so they generate requests too.

## Other products: error tracking, surveys, AI observability and more

Each band's price applies to usage up to the volume shown, after the free allowance.

| Product (unit) | Free every month | Price per unit by volume |
| --- | --- | --- |
| [Error tracking](https://posthog.com/error-tracking/pricing) (exception) | 100,000 | $0.00037 to 325k · $0.00014 to 10M · $0.000115 above |
| [Surveys](https://posthog.com/surveys/pricing) (response) | 1,500 | $0.10 to 2k · $0.035 to 10k · $0.015 to 20k · $0.01 above |
| [AI observability](https://posthog.com/ai-observability/pricing) (event) | 100,000 | $0.00035 to 150k · $0.000175 to 250k · $0.0001 to 500k · $0.00008 to 1M · $0.00007 to 10M · $0.000065 to 50M · $0.00006 above |
| Data warehouse (row synced) | 1,000,000 | $0.000015 to 10M · $0.00001 to 25M · $0.000008 to 50M · $0.000005 to 100M · $0.000002 to 1B · $0.000001 above |
| Realtime destinations (trigger event) | 10,000 | $0.0005 to 50k · $0.0003 to 100k · $0.00015 to 1M · $0.0001 to 10M · $0.00005 to 100M · $0.000025 above |
| Batch exports (row) | 1,000,000 | $0.000015 to 10M · $0.000005 to 50M · $0.0000025 to 100M · $0.00000125 above |
| [Logs and tracing](https://posthog.com/logs/pricing) (GB) | 10 GB | $0.25 to 300 GB · $0.15 above |
| [Workflow emails](https://posthog.com/workflows/pricing) (email) | 10,000 | $0.003 to 50k · $0.0018 to 100k · $0.001 to 1M · $0.0005 to 10M · $0.000375 to 100M · $0.00025 above |
| Workflow push and other destinations (dispatch) | 10,000 | $0.00075 to 50k · $0.00045 to 100k · $0.000225 to 1M · $0.00015 to 10M · $0.000075 to 100M · $0.0000375 above |

Data warehouse and pipeline rates come from the calculator on the [pricing page](https://posthog.com/pricing). Surveys don't limit how many surveys or questions you create, AI observability adds no markup on model tokens, a pipeline trigger event that fans out to five destinations is billed once, and logs are kept 14 days unless you pay $0.05 per GB per month for longer retention.

## Platform packages and other extras

| Package | Price | What it adds |
| --- | --- | --- |
| Boost | $250/month | Unlimited projects, white labeling, HIPAA BAA, SSO enforcement, collaboration features; recordings kept up to 1 year |
| Scale | $750/month | Everything in Boost, plus priority support, SAML and more organization controls |
| Enterprise | Custom | Everything in Scale, plus role-based access control, dedicated support and training; recordings kept up to 5 years |

Packages are a flat monthly fee on top of usage ([platform packages](https://posthog.com/platform-packages)). PostHog also sells a 30-minute onboarding call for $80.

## Billing limits, alerts and what happens when you go over

On pay-as-you-go, usage past the allowance is billed until you cap it. The controls, per PostHog's [billing limits](https://posthog.com/docs/billing/limits-alerts) and [spike detection](https://posthog.com/docs/billing/spike-detection) docs:

- **Billing limits per product.** At the limit PostHog stops ingesting that product's data, and the extra data is lost for good. Feature flags return a `quota_limited` response, and SDKs fall back to default values.
- **Alert emails** go to organization owners at 80% and 100% of each limit and of each free allowance.
- **Spike detection** runs daily and emails self-serve customers about unusual jumps or drops, at most once a week.
- **A billable-usage dashboard template** breaks usage down by event, SDK and product.

A hard limit means a traffic spike leaves a gap in your data until the billing period resets or you raise the limit, so set limits with headroom. PostHog's pricing page also says that if you get an unexpected bill, "we'll pretty much always refund it".

## Discounts: startups, non-profits and prepaid credits

- **Startups:** $50,000 in credits for 12 months if your company is under 2 years old, has raised less than $5 million, signed up on or after January 1, 2023, and applies from a company email domain. Since September 14, 2026, credits no longer cover PostHog's AI tools (PostHog Desktop, the Slack app, Replay Vision, PostHog AI and Inbox). Y Combinator companies get a separate deal ([startup program](https://posthog.com/startups)).
- **Non-profits:** the pricing FAQ says there's a discount "in most cases" once you contact sales. PostHog's public sales handbook sets it at 15% on credit purchases under $25,000 and an extra 5% on purchases of $25,000–$100,000 ([contract rules](https://posthog.com/handbook/growth/sales/contract-rules)).
- **Prepaid credits:** monthly pay-as-you-go gets no discount beyond the built-in volume tiers. Buying credits up front, usually for 12 months, does: 20% off at $25,000–$59,000 a year, 25% at $60,000–$99,000, 30% at $100,000–$249,000, 35% at $250,000–$499,000 and 40% at $500,000–$999,000. Self-serve accounts with at least three paid invoices averaging $280 or more can buy credits in the app at 10% off. Half of any unused credit rolls over if you renew at equal or higher spend ([pre-paid plans](https://posthog.com/docs/billing/pre-paid-plans)).

## Worked examples: what PostHog costs at four sizes

All four use pay-as-you-go rates. We ran each through the calculator on PostHog's pricing page, and the usage totals match to the dollar (the calculator rounds).

### 1. Early-stage SaaS: 500k events and 5k recordings

Usage: 500,000 events (400,000 from logged-in users), 5,000 web recordings, 300,000 flag requests and 20,000 exceptions.

| Line item | Usage vs free allowance | Cost |
| --- | --- | --- |
| Events, base rate | 500,000 of 1,000,000 | $0 |
| Person profiles | 400,000 of 1,000,000 | $0 |
| Session replay | 5,000 of 5,000 | $0 |
| Feature flags | 300,000 of 1,000,000 | $0 |
| Error tracking | 20,000 of 100,000 | $0 |
| **Total** | | **$0/month** |

On the free plan that means 1 project, a year of event history and 30 days of recordings. Pay-as-you-go with billing limits at $0 keeps the bill at $0 and unlocks 6 projects, 7-year retention and 90-day recordings. Replay has the least headroom: at 6,000 recordings the extra 1,000 would cost $5.

### 2. Growth-stage product: 5M events, 50k recordings, 2M flag requests

Assume 2M of the 5M events come from identified users.

| Line item | Calculation | Cost |
| --- | --- | --- |
| Events, base rate | 1M free; 1M × $0.00005 = $50.00; 3M × $0.0000343 = $102.90 | $152.90 |
| Person profiles | 2M identified: 1M free; 1M × $0.000198 | $198.00 |
| Session replay | 5k free; 10k × $0.005 = $50.00; 35k × $0.0035 = $122.50 | $172.50 |
| Feature flags | 1M free; 1M × $0.0001 | $100.00 |
| **Total** | | **$623.40/month** |

The identified share swings this bill. With all 5M events anonymous, the total would be $425.40. With all 5M identified, the person-profile line would be $407.10 ($198 + 3M × $0.0000697) and the total $832.50.

### 3. High-traffic content site: 20M anonymous events and 20k recordings

A publisher with no logins and roughly 10M pageviews a month, which becomes about 20M events because the JavaScript SDK sends a `$pageview` and a `$pageleave` by default (click autocapture is off here). It also records 20,000 sessions.

| Line item | Calculation | Cost |
| --- | --- | --- |
| Events, base rate | 1M free; 1M × $0.00005 = $50.00; 13M × $0.0000343 = $445.90; 5M × $0.0000295 = $147.50 | $643.40 |
| Person profiles | No identified events | $0 |
| Session replay | 5k free; 10k × $0.005 = $50.00; 5k × $0.0035 = $17.50 | $67.50 |
| **Total** | | **$710.90/month** |

Turning off automatic `$pageleave` capture would bring events down to about 10M: analytics falls to $324.40 ($50 + 8M × $0.0000343) and the bill to $391.90. PostHog documents the setting, with the caveat that bounce rate may stop working.

### 4. Scale-up on five products plus Boost

Usage: 10M events, all identified; 100,000 web recordings; 10M flag requests; 300,000 exceptions; 5,000 survey responses; and the Boost package for SSO enforcement.

| Line item | Calculation | Cost |
| --- | --- | --- |
| Events, base rate | 1M free; 1M × $0.00005 = $50.00; 8M × $0.0000343 = $274.40 | $324.40 |
| Person profiles | 1M free; 1M × $0.000198 = $198.00; 8M × $0.0000697 = $557.60 | $755.60 |
| Session replay | 5k free; 10k × $0.005 = $50.00; 35k × $0.0035 = $122.50; 50k × $0.002 = $100.00 | $272.50 |
| Feature flags | 1M free; 1M × $0.0001 = $100.00; 8M × $0.000045 = $360.00 | $460.00 |
| Error tracking | 100k free; 200k × $0.00037 | $74.00 |
| Surveys | 1,500 free; 500 × $0.10 = $50.00; 3,000 × $0.035 = $105.00 | $155.00 |
| Boost package | Flat fee | $250.00 |
| **Total** | | **$2,291.50/month** |

That's about $27,500 a year, inside the $25,000–$59,000 band where PostHog's handbook offers 20% off prepaid credits: roughly $5,500 a year if usage holds. The person-profile line is the largest item; if half of those events could be sent anonymously, it would drop to $407.10.

## How to keep your PostHog bill down

From PostHog's own cost-cutting docs:

1. **Keep events anonymous unless you need person data.** Leave the JavaScript SDK on `person_profiles: 'identified_only'`, and call `identify()` and `group()` once per session. On backend SDKs, set `$process_person_profile` to `false` for events that don't need a profile ([product analytics costs](https://posthog.com/docs/product-analytics/cutting-costs)).
2. **Trim autocapture** with an allow or ignore list, or turn it off and send custom events for what you actually analyze.
3. **Drop automatic events you don't use.** You can disable automatic `$pageview` and `$pageleave` capture and send them manually where they matter; bounce rate may break.
4. **Record fewer sessions.** Replay trigger groups let you sample a percentage of sessions, start recording on a URL or event, gate recording behind a feature flag, and set a minimum duration so quick bounces aren't recorded ([recording controls](https://posthog.com/docs/session-replay/how-to-control-which-sessions-you-record)).
5. **Cut flag requests.** Skip the first-load request (`advanced_disable_feature_flags_on_first_load`), bootstrap flags, poll for local evaluation every 5 minutes instead of 30 seconds, avoid local evaluation in edge or Lambda functions, disable flags in test and CI, and find forgotten environments still calling `/flags` ([feature flag costs](https://posthog.com/docs/feature-flags/cutting-costs)).
6. **Set a billing limit on every product** and build the billable-usage dashboard from PostHog's template to see which events and SDKs drive the bill.
7. **Prepay once spend is stable.** Bills above about $2,100 a month cross the $25,000-a-year prepaid threshold; smaller accounts can use the 10% self-serve credit purchase.

## PostHog vs alternatives at the same volume

Per-unit prices only compare cleanly if the units match, and events don't.

- **PostHog** bills every event its SDKs capture. By default the JavaScript SDK sends a `$pageview` and a `$pageleave` for each page, plus autocapture events for clicks, form changes and copies. Identified events pay the person-profile rate on top.
- **Rybbit** bills by events too, but counts fewer of them: pageviews, custom events and outbound-link clicks by default, plus Web Vitals, button clicks, copies and form interactions only if you switch those on. Error events, session replays and identify calls don't count, and logged-in users cost the same as anonymous ones. [Rybbit's pricing](/pricing) is a set of flat tiers: Standard from $19/month for 100k events, and Pro from $39/month, which adds [session replay](/features/session-replay) with a monthly quota of 10% of your event allowance (10,000 replays on the $39 tier).

The table assumes two PostHog events per pageview (click autocapture off) and one Rybbit event per pageview, at monthly list prices. The cheaper option in each row is in bold.

| Monthly traffic | PostHog | Rybbit |
| --- | --- | --- |
| 250k pageviews, anonymous | 500k events: **$0** | Standard 250k: $29 |
| 1M pageviews, anonymous | 2M events: **$50** | Standard 1M: $69 |
| 1M pageviews, logged-in users, 20k replays | 2M identified events ($248) plus 20k recordings ($67.50): $315.50 | Pro 1M: **$139** |
| 10M pageviews, anonymous | 20M events: $643.40 | Standard 10M: **$249** |
| 10M pageviews, anonymous, 20k replays | $710.90 (worked example 3) | Pro 10M: **$499** |

**Where PostHog is cheaper.** Small sites: below about 500k pageviews a month PostHog costs nothing, while Rybbit Cloud is paid after a 7-day trial (the card is collected at signup). Under these assumptions PostHog stays cheaper for anonymous traffic up to about 1.7M pageviews a month, and Rybbit Standard is cheaper at every tier above about 2.5M; in between, it depends on where you land in Rybbit's tiers. Turning off `$pageleave` roughly halves PostHog's count and moves both points higher. PostHog also sells what Rybbit doesn't: surveys, a data warehouse, AI observability, logs and workflows, plus heatmaps that create no extra events (Rybbit has no click or scroll heatmaps). If you'd use several of those, PostHog puts them on one bill.

**Where Rybbit is easier to predict.** Logged-in traffic costs the same as anonymous traffic, replays are included in Pro up to its quota (100,000 a month on the 1M plan), and the price only changes when you change tiers. Rybbit doesn't bill overage: past your tier it stops recording new events until you upgrade or the next month starts, much like a PostHog billing limit. On retention, Rybbit Standard keeps data 3 years and Pro 5, against 7 on PostHog's paid plan; Rybbit keeps replays 30 days, PostHog's pay-as-you-go 90.

Both can be self-hosted without license fees: Rybbit under AGPL-3.0, and PostHog as an MIT-licensed Docker Compose deployment that PostHog calls "officially unsupported" and steers away from once you expect more than 300k events, 1k recordings or 300k flag requests a month ([self-host docs](https://posthog.com/docs/self-host)).

For a feature-by-feature view, see [Rybbit vs PostHog](/compare/posthog). For more replay tools with verified prices, see [the best session replay tools](/blog/best-session-replay-tools).

## FAQ

**How much does PostHog cost per month?**
Nothing until you pass a product's free allowance. After that it depends on volume and mix: 2 million anonymous events cost $50 a month, and 5 million events (2 million identified) plus 50,000 recordings and 2 million flag requests cost $623.40. The [calculator](https://posthog.com/pricing) on PostHog's pricing page estimates your own mix.

**What does the PostHog free tier include in 2026?**
Every month: 1M analytics events, 5,000 web and 2,500 mobile recordings, 1M flag requests, 100,000 exceptions, 1,500 survey responses, 1M warehouse rows, 100,000 AI observability events, 10 GB of logs, plus pipeline, workflow and AI-credit allowances. The free plan adds 1 project, 1-year data retention, unlimited team members and community support. It doesn't expire, and no card is needed.

**Does PostHog's free allowance reset every month?**
Yes. Allowances reset monthly, stay the same after you add a card, and don't roll over.

**Why do identified events cost more?**
PostHog creates a person profile for identified users, and querying their events needs more expensive table joins. So identified events pay a person-profile rate on top of the base rate: $0.000198 extra per event between 1M and 2M, falling to $0.000001 above 250M, with the first 1M identified events exempt.

**Can I cap my PostHog bill?**
Yes, with a billing limit on each product. At the limit PostHog stops ingesting that product's data (it's dropped, not queued) and feature flags return default values.

**Does PostHog offer startup or non-profit discounts?**
Yes. Startups under 2 years old that have raised less than $5 million can get $50,000 in credits for 12 months. Non-profits get a discount through sales; PostHog's handbook puts it at 15% on credit purchases under $25,000.

**Is PostHog cheaper than FullStory?**
FullStory doesn't publish paid prices. Its [plans page](https://www.fullstory.com/plans/) lists a free plan with 30,000 sessions a month, 12 months of data retention and up to 10 users, and quotes everything above that. On PostHog, the first 5,000 recordings each month are free and 30,000 recordings cost $102.50 on pay-as-you-go (10,000 × $0.005 plus 15,000 × $0.0035). For other replay options, see our [FullStory alternatives](/blog/fullstory-alternatives) guide.

---

*Want analytics and session replay on a flat monthly price? [Start a 7-day Rybbit trial](https://app.rybbit.io/signup) or open the [live demo](https://demo.rybbit.com/81).*

---

### Content Blog Retention Analysis (`docs/content/blog/retention-analysis.mdx`)

---
title: "Retention Analysis: How to Measure and Improve User Retention"
description: "A practical guide to retention analysis: cohort retention curves, churn rate vs retention rate, the metrics that matter, and how to act on what you find."
keyword: "retention analysis"
date: 2025-11-22
updated: 2026-09-28
tags: ['analytics', 'metrics', 'product', 'retention']
author: Rybbit Team
---

Retention analysis answers one question: of the people who showed up, how many came back? It is the most honest measure of whether a product delivers value, because acquisition can be bought and retention cannot.

This guide covers what retention analysis is, how retention rate and churn rate relate, the three ways to measure retention (and when to use each), what a healthy retention curve looks like, and how to run the analysis in [Rybbit](/features/retention).

## What is retention analysis?

Retention analysis groups users by when they first arrived, then tracks what share of each group is still active after a day, a week, a month, and so on. The output is a retention curve: a line that starts at 100% and falls over time.

Two things make it more useful than a single "active users" number:

- **It separates new users from old ones.** A flat total can hide a product that loses most new signups while a loyal base holds the line.
- **It is comparable.** Users who joined in March can be compared with users who joined in June at the same point in their lifecycle, which is how you find out whether a change actually helped.

## Retention rate vs churn rate

Retention rate and churn rate describe the same thing from opposite sides.

```
Churn rate     = (customers lost during period / customers at start of period) × 100
Retention rate = (customers at end of period   / customers at start of period) × 100
Retention rate = 100 − churn rate
```

If you start a month with 100 customers and 5 leave, churn is 5% and retention is 95%. Same data, different story:

- **Churn focuses on loss.** Finance teams care about it because it sits in the denominator of lifetime value, payback period, and every growth projection. Moving monthly churn from 5% to 4% makes the business roughly 25% more valuable.
- **Retention focuses on success.** Product teams care about it because it is the leading indicator of product-market fit. If retention climbs, users are finding value.

The trap is that retention *sounds* better. "90% monthly retention" and "10% monthly churn" are the same number, and both mean about 72% of your customers are gone within a year, because the loss compounds:

- 5% monthly churn ≈ 46% annual churn
- 3% monthly churn ≈ 31% annual churn
- 1% monthly churn ≈ 11% annual churn

Track both. Use churn when you are talking about sustainability and use retention when you are talking about product decisions.

## Three ways to measure retention

### 1. Period retention

The simplest version: what share of the customers you had at the start of a period were still there at the end. It is easy to explain to stakeholders and fine for month-over-month reporting, but it mixes brand-new customers with ten-year veterans, so it tells you little about *why* the number moved.

### 2. Cohort retention

The standard for product work. Group users by their first visit or signup date, then measure what percentage of each cohort is still active after 1, 7, 30, and 90 days.

| Cohort   | Day 1 | Day 7 | Day 30 | Day 90 |
|----------|-------|-------|--------|--------|
| January  | 95%   | 85%   | 75%    | 60%    |
| February | 92%   | 82%   | 72%    | 58%    |
| March    | 94%   | 84%   | 74%    | 62%    |
| April    | 93%   | 83%   | 73%    | 61%    |

This table says far more than "our retention is 75%". Day-1 retention is stable, so onboarding is not the problem. Day-30 is stable, so the product delivers enough value to keep people a month. Day-90 is creeping up, so the recent changes are working for the long tail.

### 3. Rolling-window retention

For subscription businesses that want a live signal instead of a month-end report: what share of the customers who were active 30 days ago are still active today. If 100 customers were paying 30 days ago and 90 still are, rolling 30-day retention is 90%.

## Decide what "retained" means before you measure

Retention analysis is only as good as its definition of activity. Pick one per product and keep it stable:

- **A return visit** works for content sites and most websites. It is what a web analytics tool measures by default.
- **A key action** (created a project, sent a message, placed a second order) works better for products where a visit without the action is not really retention.
- **A renewal** is the right definition for subscriptions, but it lags by a billing cycle, so pair it with an engagement signal that moves faster.

Also decide whether a user counts as retained on Day 30 only if they were active *on* day 30 (bounded) or if they were active at any point *after* day 30 (unbounded). Bounded curves are lower and noisier; unbounded curves are smoother and more common in product analytics tools. Either is fine as long as you compare like with like.

## What a good retention rate looks like

Benchmarks vary widely by business model. Treat these as rough ranges, not targets:

**B2B SaaS.** Monthly churn of 3–5% is acceptable, 1–3% is good, under 1% is great. Most healthy B2B products aim for monthly churn under 2%.

**B2C and free-to-paid.** Monthly churn of 5–10% is acceptable, 2–5% is good, under 2% is great. Consumers try more and cancel more; that is normal.

**Freemium.** The number that matters is whether activated users are being retained, not whether every free signup is. Free users who never reach the core action will always churn at very high rates.

The more useful benchmark is your own history. A retention curve that flattens is the signal to look for: it means some share of each cohort has found a durable reason to stay. A curve that keeps sliding toward zero means the product is being tried, not adopted.

## How to read a retention curve

Three shapes cover most cases:

- **Cliff then flat.** A sharp drop in the first few days followed by a plateau. Onboarding loses people, but the ones who get through stay. Fix time-to-value.
- **Steady slide.** No plateau; every cohort drifts toward zero. The product is not delivering lasting value, or the wrong users are being acquired. Fix the core loop before spending on acquisition.
- **Curves that separate by cohort.** Newer cohorts sit above older ones at the same age: your changes are working. Newer cohorts sit below: something regressed, or a marketing push brought in poorer-fit users.

Then segment. Retention by acquisition channel, device, country, and plan almost always reveals one group that is dragging the average down, and that group is the cheapest place to start.

## Running retention analysis in Rybbit

Rybbit builds cohorts automatically from each user's first visit, so there is nothing to configure before you can look at the data.



**Read the cohort table.** The [Retention report](/features/retention) lists one row per cohort (grouped by first visit date) and one column per period. Each cell shows the percentage of the cohort that came back, with color coding so the plateau, or the cliff, is visible at a glance.

**Segment the curve.** Every filter in Rybbit applies to retention too. Compare organic search against paid social, mobile against desktop, or one country against another. Save the comparisons you keep coming back to as [segments](/docs/segments) so the whole team looks at the same slices.

**Define retention by action, not just by visit.** Track the moments that matter as [custom events](/features/custom-events): first project created, first report shared, subscription renewed. Then look at which cohorts reach those milestones and how their retention differs from cohorts that did not.

**Find out what retained users did differently.** The [Journeys report](/features/user-journeys) shows the paths users take through your site. Work backward from your key action to see which sequences precede it, and forward from your landing pages to see where new users go instead.



**Check the onboarding leak.** If day-1 retention is the problem, build the signup-to-first-value flow as a [funnel](/features/funnels) and see which step loses people. The [funnel analysis guide](/blog/funnel-analysis) walks through it.

**Watch individual users when the numbers do not explain themselves.** [User profiles](/features/user-profiles) and [session replay](/features/session-replay) show what a churned user actually did in their last few sessions, which is often faster than another dashboard.

## How to improve retention

Knowing the rate is half the job. These are the changes that move it, roughly in order of payoff:

1. **Shorten time-to-value.** Most churn happens in the first 30 days, and most of that happens before the user reaches the core action. Cut every step between signup and that action, and measure how long it takes.
2. **Segment at-risk users.** Users whose sessions get shorter and less frequent are telling you something before they leave. Give them a different email, a support check-in, or a walkthrough of the feature they have not found.
3. **Talk to churned users.** The data says *that* they left. A two-question cancellation survey or five interviews will tell you *why*, and the answer is often a messaging problem that costs nothing to fix.
4. **Invest in the features retained users rely on.** Compare feature usage between users who stayed and users who churned. Ship more of what the first group uses and less of everything else.
5. **Answer support faster.** Response time correlates with retention in almost every product. Measure whether users who got help within an hour retain better than users who waited a day.
6. **Fix pricing that attracts the wrong customers.** Churn by plan tier usually shows one tier bleeding. Either improve fit for that tier or reprice so it attracts more committed buyers.
7. **Set a target and a cadence.** "Cut monthly churn from 5% to 3% this year" produces different decisions than a dashboard nobody owns. Review the cohort table monthly, change one thing, and check whether the newest cohort moved.

### The retention funnel

Retention has stages, and each fails for a different reason:

1. **Day 1:** did they experience value? (onboarding)
2. **Day 7:** did they come back? (habit)
3. **Day 30:** are they using the core features? (product fit)
4. **Day 90:** are they engaged enough to upgrade or recommend you? (expansion)

If day-1 retention is 50%, fix onboarding. If day-1 is 90% but day-30 is 40%, the product is not delivering lasting value. If day-30 is 80% but day-90 is 60%, you are losing long-term engagement. Measure each stage and fix the biggest leak first.

## The bottom line

Churn rate and retention rate are two views of the same number. Use churn to talk about sustainability and retention to talk about product decisions, and track both by cohort rather than as a single monthly figure. Segment by channel, device, and plan to find the group that is dragging the curve down, change one thing, and check the next cohort.

A business does not scale on acquisition alone. It scales by finding the right users, delivering value quickly, and keeping them. Everything else is pouring water into a leaking bucket.

---

**Related reading:**
- [Retention report guide](/docs/feature-guides/retention)
- [Funnel Analysis: How to Find Where Users Drop Off](/blog/funnel-analysis)
- [What Is Bounce Rate? How to Calculate and Reduce It](/blog/bounce-rate)
- [Ecommerce Analytics: The Metrics and Reports That Matter](/blog/ecommerce-analytics)
- [What Is Web Analytics? A Complete Guide](/blog/what-is-web-analytics)

---

### Content Blog Track Chatgpt Traffic (`docs/content/blog/track-chatgpt-traffic.mdx`)

---
title: "How to Track ChatGPT and AI Referral Traffic to Your Site"
description: "AI traffic is two things: crawlers reading your pages and people clicking through from ChatGPT, Perplexity or Claude. How to see both in GA4 and in Rybbit."
keyword: "track chatgpt traffic"
date: 2026-09-13
tags: ["ai", "bot detection", "traffic"]
author: Rybbit Team
---

"AI traffic" is two different things that share a name. One is machines: crawlers and agents that fetch your pages to train a model, build an answer index, or answer one person's question on the spot. The other is people: visitors who click a link inside ChatGPT, Perplexity, Claude, or Gemini and land on your site. The first mostly never runs JavaScript, so a browser-based tracker never sees it; the second often arrives with no referrer and gets filed under Direct. They need different tracking, and this post covers both.

## Why AI traffic is hard to see

Referral attribution rests on one HTTP header: when someone clicks a link, the browser sends a `Referer` naming the page they came from, and your analytics tool turns it into a source. AI traffic breaks that chain in several places.

**Many assistant clicks carry no referrer.** A link marked `rel="noreferrer"` tells the browser to omit the `Referer` header ([MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/noreferrer)). A link opened from a native app reaches the browser as a fresh navigation, with no referring page. Both match GA4's definition of Direct: source `(direct)` and medium `(none)` or `(not set)` ([GA4 channel definitions](https://support.google.com/analytics/answer/9756891)). When Google's AI Mode launched in May 2025, Search Engine Land reported that its links were marked `noreferrer`, so clicks showed as Direct or Unknown until Google fixed it a week later ([Search Engine Land](https://searchengineland.com/googles-ai-mode-traffic-untrackable-455883)).

**The referrer you do get is trimmed.** The browser's default policy, `strict-origin-when-cross-origin`, sends only the origin on cross-origin navigations ([MDN Referrer-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy)). You see `https://chatgpt.com/`, never the conversation.

**Some AI clicks look like ordinary search.** Google reports AI Overviews and AI Mode clicks inside the normal Web search type in Search Console ([Google Search Central](https://developers.google.com/search/docs/appearance/ai-features)), and GA4 counts them as Organic Search ([GA4 channel definitions](https://support.google.com/analytics/answer/9756891)). Nothing in the referrer separates them from a classic blue-link click.

**UTM parameters exist, from one source.** OpenAI states that ChatGPT automatically includes `utm_source=chatgpt.com` in referral URLs from ChatGPT search results ([OpenAI Publishers and Developers FAQ](https://help.openai.com/en/articles/12627856-publishers-and-developers-faq)). A UTM survives an app hand-off where the referrer does not, but it covers one assistant and only its search results, so treat it as a bonus rather than a baseline.

**Crawlers never fire a browser tracker.** A crawler fetches your HTML and, in most cases, executes nothing, so Rybbit's script, GA4's tag, and every other browser-side tracker never run; those fetches exist only in server or CDN logs. Some agents do render pages, which is why browser-based tracking sees part of the picture.

## The referrer domains to watch

Rybbit keeps one registry that maps each AI operator to the domains its assistant sends people from and the user agents its crawlers and agents announce, so crawls and referrals can be compared per company. This is what it maps today.

| Operator | Assistant referrer domains | Crawler and agent user agents (purpose) |
| --- | --- | --- |
| OpenAI | chatgpt.com, chat.openai.com | GPTBot (training), OAI-SearchBot (search), ChatGPT-User (agent) |
| Anthropic | claude.ai | ClaudeBot (training), Claude-SearchBot (search), Claude-User (agent), Claude Code (agent) |
| Google | gemini.google.com | Google-Extended (training), Google-Agent (agent) |
| Microsoft | copilot.microsoft.com | No AI-specific crawler mapped |
| Perplexity | perplexity.ai | PerplexityBot (search), Perplexity-User (agent) |
| Meta | meta.ai | Meta-ExternalAgent (training), Meta-ExternalFetcher (agent) |
| Mistral | chat.mistral.ai, mistral.ai | MistralAI-User (agent) |
| xAI | grok.com | GrokBot (search) |
| You.com | you.com | YouChat (search) |
| Cursor | cursor.com | Cursor (agent) |
| Cohere | coral.cohere.com | cohere-ai (training) |

The purposes matter more than the names:

- **Training** crawlers (GPTBot, ClaudeBot, CCBot, Google-Extended, Bytespider) collect pages to train a model and send no readers back.
- **Search** crawlers, which Rybbit labels answer engines (OAI-SearchBot, Claude-SearchBot, PerplexityBot), index pages so an assistant can cite them, and can send readers back.
- **Agents** (ChatGPT-User, Claude-User, Perplexity-User, Google-Agent) fetch a page because a person asked an assistant to open it, right then.

Some operators only crawl: Common Crawl (CCBot), Apple (Applebot-Extended), ByteDance (Bytespider), Amazon (Amazonbot), Diffbot, and Webz.io (Omgilibot) train; DuckDuckGo (DuckAssistBot) indexes for search; Manus and Firecrawl run agents. Some assistants only refer: deepseek.com, poe.com, pi.ai, character.ai, qwen.ai, and phind.com count as AI referrals with no crawl side. A click from duckduckgo.com is deliberately left as organic search, not an AI hand-off.

## How to track AI referrals in GA4

GA4 now does part of this on its own. Its default channel group has a channel called **AI Assistant**: GA4 sets the medium to `ai-assistant` and the campaign to `(ai-assistant)` when the referrer matches its list of AI assistants, which Google describes as sources like ChatGPT, Gemini, DeepSeek, Copilot, and Grok ([GA4 default channel group](https://support.google.com/analytics/answer/9756891)). Google's own AI Overviews and AI Mode are excluded and stay in Organic Search. Start with that row in your acquisition reports; a referrer Google's list does not recognize still falls into Referral, and a click without a referrer stays in Direct.

To catch the rest, build a custom channel group:

1. Go to **Admin**, then **Data display**, then **Channel groups**, and create a new group. Standard properties get two custom groups of up to 50 channels each ([Google's guide](https://support.google.com/analytics/answer/13051316)).
2. Add a channel named AI with the condition **Source** *matches regex*; Google's help page has an AI-assistants example built the same way. A pattern from the table above:

```text
.*(chatgpt\.com|chat\.openai\.com|perplexity\.ai|claude\.ai|gemini\.google\.com|copilot\.microsoft\.com|meta\.ai|mistral\.ai|grok\.com|you\.com|deepseek\.com|poe\.com).*
```

3. Save. Google states that custom channel groups apply to reports retroactively, so there is no waiting for new data.

For a one-off look, open **Explore**, start a **Free form** exploration, add the session source dimension as a row, and add a filter for the assistant domains in the Filters section at the bottom of the Settings panel; expressions are case sensitive ([explorations](https://support.google.com/analytics/answer/9327972), [filters](https://support.google.com/analytics/answer/9328518)).

Two limits remain whatever you build: GA4 cannot see crawlers, because they never load its tag, and the referrer-less, untagged remainder is Direct for good.

## How Rybbit does it

[Web analytics](/features/web-analytics) classifies each session into an acquisition channel, and one channel is named **AI**. A session lands there when its referrer is one of the domains in the table, when `utm_source` names an assistant (chatgpt, openai, claude, perplexity, gemini, copilot, deepseek and others), or when `utm_medium` is one of `ai`, `ai-chat`, `chatbot`, `llm`, `ai-assistant`, `gen-ai`, or `ai-search`. Channel is also a filter, so "Channel is AI" works anywhere the filter bar does.

The crawler half comes from [bot detection](/features/bot-detection). Enable **Block Bot Traffic** in Site Settings and every tracking request runs through five detection layers; the user-agent layer carries the AI patterns, so a matched bot is stored with a `bot_name`, `bot_operator`, and `bot_purpose` (`ai_training`, `ai_search`, or `ai_agent`). Detected bots stay out of your analytics and your bill, and are kept separately for inspection ([bot detection docs](/docs/bot-detection)).

The Bots page opens on the **AI & agents** tab (the other is **All bots**). Here is the [public demo](https://demo.rybbit.com/81/bots) over the last 30 days:



Top to bottom:

- Four counts: **AI requests**, **AI agents** (a person asked an assistant to open a page), **AI crawlers** (background crawling for training or an answer index), and **Visits sent back** (people who arrived from an AI product).
- A coverage note, the most important sentence on the page: "These counts cover bots that execute JavaScript. Crawlers that fetch your HTML and run nothing — which includes most training crawlers — never reach Rybbit unless your server reports them through the tracking API."
- A chart of **Agents** against **Crawlers** over time.
- The **AI operators** table: per operator, **Agent**, **Answer engine**, **Training**, and **Total reads** on one side, **Visits back** on the other, and **Reads per visit**, the exchange rate. Reads are requests whose user agent names the operator; visits back are sessions that arrived from that company's product.
- **AI bots** by bot, by purpose (**AI training crawler**, **AI answer engine**, **AI agent**), and by operator.
- **Pages read by AI**, with an **Agents only** tab for **Pages agents opened**, which track live intent rather than a crawler's sweep.

The same rows come from the API as [`GET /api/sites/:site/bots/ai-summary`](/docs/api/bots/ai-summary).

The coverage note is the honest limit of browser-based tracking, and it has a fix: report the non-JavaScript crawlers from the edge. Middleware, a worker, or a reverse proxy forwards the request to the tracking API with an API key and the real `user_agent`, and a forwarded GPTBot user agent lands in your bot events with full identity rather than in your analytics as a phantom visitor ([how to report crawlers that never run JavaScript](/docs/bot-detection#reporting-crawlers-that-never-run-javascript)).

## What to do with the data

**Segment the AI-referred sessions.** Open the **Filter** button, switch to the **Segments** tab, click **New segment**, and define it as Channel is AI ([segments docs](/docs/segments)). Apply it to pages, sessions, journeys, funnels, and retention to see what these visitors read and where they go. To compare AI with other channels, use one segment per channel on the same report; our [marketing attribution guide](/blog/marketing-attribution-tools) covers the models behind that comparison.

**Set a goal and check whether they convert.** [Goals](/features/goals) track a conversion rate against sessions for a page visit or a custom event. Filter the Goals page to the AI channel and you have the conversion rate of AI-referred visitors next to everyone else's, which is the number that decides whether AI referrals deserve attention.

**Compare pages read with pages that earn visits.** Put **Pages read by AI** next to the pages your AI-channel sessions land on. Pages crawled heavily but never referred are feeding training, or an index that does not cite you. Pages that agents open are what people are actively asking about, and often deserve a clearer answer at the top.

**Decide your crawler policy by purpose, not by name.** Every major operator documents its crawlers and how to block them, and the details differ:

- OpenAI keeps GPTBot (training) and OAI-SearchBot (search) separate: "a webmaster can allow OAI-SearchBot in order to appear in search results while disallowing GPTBot", and sites that opt out of OAI-SearchBot "will not be shown in ChatGPT search answers". ChatGPT-User acts on user requests, so OpenAI notes robots.txt rules may not apply to it ([OpenAI crawler docs](https://developers.openai.com/api/docs/bots)).
- Anthropic documents ClaudeBot (training), Claude-SearchBot (search), and Claude-User (user requests). All three honor robots.txt and support `Crawl-delay` ([Anthropic](https://support.claude.com/en/articles/8896518-does-anthropic-crawl-data-from-the-web-and-how-can-site-owners-block-the-crawler)).
- Perplexity's PerplexityBot surfaces and links sites in Perplexity results and respects robots.txt; Perplexity-User fetches on a user's behalf and "generally ignores robots.txt rules", so Perplexity publishes its IP ranges for firewall blocking ([Perplexity](https://docs.perplexity.ai/guides/bots)).
- Google-Extended controls whether your content trains future Gemini models or grounds Gemini Apps and Vertex AI; Google says it "does not impact a site's inclusion in Google Search nor is it used as a ranking signal in Google Search" ([Google crawlers](https://developers.google.com/search/docs/crawling-indexing/google-common-crawlers)). AI Overviews follow the ordinary Googlebot rules and snippet controls instead ([Google AI features](https://developers.google.com/search/docs/appearance/ai-features)).

The pattern: blocking a training crawler costs you no referrals, blocking a search crawler removes you from the answers that send them, and agents are people asking about you. The Bots report shows, per operator, which of the three you are getting, so the robots.txt decision can rest on numbers. If a traffic change follows a policy change, the [traffic drop checklist](/blog/website-traffic-drop) still applies: check the channel breakdown before assuming a cause.

## FAQ

### Does ChatGPT send referral traffic?

Yes. A click on a link in a ChatGPT answer arrives with a `chatgpt.com` referrer when the browser sends one, and OpenAI says ChatGPT adds `utm_source=chatgpt.com` to referral URLs from ChatGPT search results. A click that reaches you with neither shows as Direct.

### Why does AI traffic show as Direct?

Because Direct is where every visit without a referrer and without campaign parameters goes, in GA4 and in Rybbit alike. Links marked `rel="noreferrer"` send no `Referer` header, and links opened from a native app start a fresh navigation with no referring page.

### How do I see AI traffic in Google Analytics?

Start with the AI Assistant channel in GA4's default channel group, which covers referrers on Google's list, then add a custom channel group with a Source regex for the assistant domains you care about; it applies retroactively. Google's AI Overviews and AI Mode clicks count as Organic Search and cannot be separated in GA4 or Search Console.

### Can I block AI crawlers without losing AI referrals?

Mostly, if you block by purpose. OpenAI, Anthropic, and Perplexity each document separate user agents for training, search, and user-triggered fetches, and OpenAI states that a site can allow OAI-SearchBot while disallowing GPTBot. Block the training crawlers, leave the rest alone, then watch the Visits back column to confirm nothing changed. Google-Extended only affects Gemini training and grounding; AI Overviews follow the normal Googlebot rules.

### Why does Rybbit show zero crawls for an operator I see in my server logs?

Because Rybbit's script runs in the browser, and a crawler that fetches HTML without executing JavaScript never triggers it. Forward those requests from your server or edge to the tracking API with the original user agent and they appear in the report with full identity.

---

### Content Blog Website Traffic Drop (`docs/content/blog/website-traffic-drop.mdx`)

---
title: "Why Did My Website Traffic Drop? Causes and How to Fix It"
date: 2025-06-08
updated: 2026-09-28
description: "Diagnose a website traffic drop step by step: tracking issues, Google updates, seasonality, lost referrers, and how to confirm each cause in your analytics."
keyword: "website traffic drop"
tags: ['traffic', 'analytics']
author: Rybbit Team
---

If you are reading this article right now you are probably panicking about a recent drop in traffic, while this is a reasonable response, it is possible to reverse engineer why your traffic has dropped and to apply the correct measures to remediate the issue.

Don't panic just yet – traffic drops happen to literally every website owner at some point. Maybe you got hit by a Google algorithm update, something technical went wrong, your site is responding to seasonal trends, or maybe you just need to create better content. Whatever it is, we can figure it out.

To properly investigate what's going on, you'll need a solid analytics tool with [web analytics capabilities](/blog/what-is-web-analytics) to understand traffic patterns and [bounce rate analysis](/blog/bounce-rate) to identify engagement issues. We obviously recommend Rybbit (and we'll show you exactly how to use it), plus you'll want access to an SEO tool and Google Search Console. By walking through Rybbit's specific features, we'll show you how to diagnose what's actually causing your traffic to tank.

## **What Usually Causes Traffic Drops**

Before we dive into the detective work, let's talk about the usual suspects:

**Google Algorithm Updates** Google loves to shake things up with algorithm updates. Sometimes these hit like a truck – one day you're ranking great, the next day you've disappeared from page one. These updates can target specific types of content, keywords, or entire industries.

**Technical Website Problems** Maybe your site is loading slow, or maybe your ad network decided to put ads everywhere. Could be your server is having issues, or something broke during a recent update. Technical problems are actually pretty common reasons for traffic drops.

**Manual Penalties from Google** If you've been playing fast and loose with SEO – buying sketchy backlinks, keyword stuffing, or copying content – Google's manual review team might have slapped you with a penalty.

**Negative SEO Attacks** Sometimes competitors will try to hurt your rankings by pointing spammy backlinks at your site or other nasty tactics. It's not super common, but it happens.

## **Step 1: Is it really a traffic drop or just a trend?**

First things first – let's figure out if this is actually a sudden drop or if traffic has been slowly declining over time.

###### *Main:*

First, take a look at the amount of traffic that has dropped. You can use the main tab on Rybbit to find this information: 

Head to your [main tab](https://demo.rybbit.com/81/main) in Rybbit and start looking at different time periods:

* Compare the last 7 days to the week before  
* Look at the past 30 days versus the 30 days before that  
* If you have the data, check year-over-year numbers



What you're looking for here is patterns. Is this a sudden cliff where traffic just fell off overnight? Or has it been a gradual slide? If it's sudden, definitely check Google's Search Status page to see if they were having crawling or indexing issues around the same time.

## **Step 2: Which pages are getting affected?**

You should first ascertain which pages have seen drops in traffic, it's quite likely that not all of your pages have dropped in traffic. You can easily do this by clicking the pages tab on the left hand panel and you will see a list of pages on your website, you can view traffic over time using the graph that is embedded in the row, you can see any drops or any dips throughout a 24 hour period up until the current time.

If your [sales funnel](/features/funnels) pages are seeing drops, check our [sales funnel guide](/blog/funnel-analysis) to understand the impact on conversions.

**What to look for:**

* **Sitewide drops** – If every page's traffic trend is going down, you're probably dealing with a technical issue or algorithm update that hits your whole site.  
* **Specific page problems** – If only certain pages are hurting, focus on those. Maybe the content is outdated, or there are page-specific technical issues.  
* **Pattern recognition** – Are all your blog posts down? Product pages? Homepage traffic? The pattern tells you where to focus your efforts.

The embedded graphs make it super easy to spot which pages used to get steady traffic but suddenly tanked. Those are your priority pages to investigate further.



## **Step 3: Assessing Technical Performance**

If this is really the reason why your website dropped in traffic, then count yourself lucky. Oftentimes this is an easy problem to fix, your website might be slower than usual, or a component might not be working properly and this is causing your bounce rate to be higher than usual or is causing people to drop from your website due to poor performance. 

#### Using Rybbit to investigate:

Rybbit has several great features that allow you to investigate potential technical issues that could be causing your website to drop in traffic:

###### *Performance:*



If you notice that one of your pages has tanked in performance, you can use the performance tab to find the LCP (LCP measures loading performance. LCP marks the time when the largest content element becomes visible in the viewport.), if a page has a really high LCP, it's likely that people are simply deciding not to go on the page.

Once you recognize that a certain page has performance issues, you can begin the process of fixing the issue.

## **Step 4: Which channels have drops in traffic**

### **Identify Which Traffic Sources Are Affected**

Understanding which channels are contributing to your traffic decline is crucial for focusing your investigation efforts. This is similar to analyzing attribution across different [marketing channels](/blog/marketing-attribution-tools). You can use the filter function on each channel to look for trends:



(Example: I set the filter to use Channel is Organic Search to look for any drops, the graph shows that traffic has actually grown over the past 14 days).

Pay special attention to which sources show the steepest declines. Organic search drops are typically the most impactful and require the most thorough investigation.

### Filtering on referrers:

You can likely find even more actionable information if you filter on referrers as well, as you can see here we set the filter to only look for events that have Google as a referrer.  


## **Step 5: Is the drop in certain geographies or devices or timeframes?**

Once you have identified if the drop is truly a drop and pages have been affected, you should start looking for any patterns in geographics or devices, use Rybbit's filter function to filter on device type as well as country:


(Using Rybbit to filter for a specific country)



(Using Rybbit to filter for a specific device type).  
You can also use set time filter on custom range to compare different time frames:  


## **Step 5: Digging Into Specific Scenarios**

## Google Algorithm Updates

Google rolls out algorithm updates silently many times a year, you probably won't notice most of them as they don't necessarily target all keywords or all topics. However, you might get unlucky and get hit by a drop in the SERPs due to an update that hits your particular topic or website particularly harshly.

You can monitor core updates in the [Google Search Status Dashboard](https://status.search.google.com/products/rGHU1u87FJnkP6W2GwMi/history) and see if it correlates with your website's drop in traffic, it might not be one to one, but it should be fairly close to the time of the traffic drop. You should read [Google core updates](https://developers.google.com/search/updates/core-updates) to understand better what a core update is and assess whether you think your traffic has dropped due to a core update or not.

#### How would you go about fixing this?

The simple truth is that fixing your website and content for a Google algorithm update is not going to have a one size fits all solution, in most cases you are probably going to notice that not all pages have had any drops in traffic. You will probably notice that traffic drops are a few pages that might be outdated or perhaps are slow or have too many ads, in that case it would be quite easy to fix. In other cases it will be more difficult to understand why certain pages are losing traffic or why you lost rankings in certain keywords you were trying to rank for.

##### Check using an SEO Tool of your choice

SEO tools can be a valuable resource to find which keywords in particular you have lost rankings in. You can use Ahrefs or a tool like SemRush to find keywords that have lost ranking.

## Website technical issues

If technical problems are behind your traffic drop, count yourself lucky. These are usually fixable.

**Common technical problems:**

* Site loading too slowly  
* Pages returning error codes  
* Mobile site not working properly  
* Recent changes that broke something

**Using Rybbit to spot technical issues:**

* Performance tab shows you which pages are loading slowly  
* Pages tab shows you if the drop happened right after you made changes to your site  
* Main dashboard can show you if mobile vs desktop traffic patterns are different

## Manual Penalty

A manual penalty on Google is a manual action that reduces or demotes your website in Google Search Rankings (SERPs). These are pretty hard to get, and most if not all gray hat actions that people take such as buying backlinks or spamming won't cause a manual penalty at first. So you must have really screwed up to get to this point.

###### *What could be the cause (Be honest with yourself):*

- Backlink purchasing \- If you have ever bought a backlink from a shady website or Fiverr?  
- Have you ever published a bunch of pages worth of spam?  
- Keyword stuffing to rank for a keyword?  
- Used a tool to spam the internet to increase the amount of backlinks?  
- Stole content from another website?

If so, you could be facing a manual penalty.

###### *How do I go about fixing this?*

First, calm down and breathe.

Second, go ahead and understand what the cause is:

- Go to your Search console and open the manual action report to see what the issue is exactly.  
- Find which pages the penalty applies to.  
- Fix every single problem that Google has detected on your website:  
  - Remove any suspicious backlinks  
  - Remove stolen content.  
  - Get rid of keywords you stuffed in the page.  
  - Get rid of any spam.

- Next, click "Request review" on the console and spell out exactly what you did to fix the problem and be patient.

Google is strict, but fair, and most likely if you fix the issues correctly and do not engage in any Blackhat SEO again Google will give you a second chance. Be sure not to buy any more links on Fiverr or anything stupid again, or else Google might just permanently send your site to the bottom of the SERPs forever.

## Negative SEO

A competitor may be conducting a negative SEO campaign against you, usually by:

###### *Spamming bad backlinks towards your domain:*

If you noticed a huge increase in backlinks from random spammy domains, or perhaps see a huge surge in backlink count despite you not having done anything to get them. It's possible you might have been targeted by negative SEO.

**Warning signs:**

* Sudden spike in backlinks from sketchy sites  
* Your link profile shows tons of links you never built  
* The timing aligns with when a competitor might be motivated to hurt you

**What to do:**

* Use Google's Disavow Tool to tell Google to ignore the bad links  
* Document everything in case you need to file a reconsideration request  
* Be patient – it can take months for Google to process disavowed links

## **Making Sense of Your Traffic Numbers**

Not every traffic drop is worth panicking over. Here's how to tell if you should be worried:

**Small sites (under 10,000 monthly visitors):** A 10% drop might only mean 50-100 fewer visitors. That could just be normal fluctuation.

**Medium sites (10,000-100,000 monthly visitors):** Focus more on percentage changes and which pages are affected. A 20% drop here is worth investigating.

**Large sites (100,000+ monthly visitors):** Even small percentage drops represent thousands of visitors, so take any consistent decline seriously.

**Industry context matters too:** News sites naturally have more volatile traffic than, say, a local plumber's website. [E-commerce sites](/for-ecommerce) have seasonal patterns. [SaaS companies](/for-saas) usually expect steady growth, so any drop is concerning.

## **Red Flags That Need Immediate Attention**

Drop everything and investigate immediately if you see:

* **More than 50% traffic drop** (unless you know there's a seasonal reason)  
* **Complete disappearance** from Google for your main keywords  
* **Week-over-week declines** for more than a month  
* **Technical errors** that are affecting how your site works

## **Conclusion**

Traffic drops are a natural part of website ownership, but they don't have to be devastating. With the right investigative approach using tools like Rybbit, you can quickly identify the causes and take corrective action.

Remember that Rybbit's strength lies in its detailed page-level analytics and performance monitoring capabilities. For longer-term health, also track [retention and churn metrics](/blog/retention-analysis) to ensure recovered traffic actually converts to engaged users.

The combination of traffic analysis through the Main dashboard, page-specific investigation through the Pages tab, and technical performance review through the Performance tab gives you a comprehensive toolkit for understanding and addressing traffic declines.

The key is to approach the investigation systematically:

1. **Don't panic** \- Traffic fluctuations are normal  
2. **Use data** \- Let Rybbit's analytics guide your investigation rather than making assumptions  
3. **Focus on the biggest impacts** \- Address the most significant issues first  
4. **Monitor recovery** \- Track the effectiveness of your fixes  
5. **Learn for the future** \- Use the experience to build better monitoring and prevention strategies

Most importantly, remember that while traffic drops can be concerning, they often provide valuable insights into how to improve your website's performance, content quality, and user experience. With Rybbit's detailed analytics at your disposal, you're well-equipped to not just recover from traffic drops, but to build a stronger, more resilient web presence.

---

**Related Reading:**
- [What Is Bounce Rate? How to Calculate and Reduce It](/blog/bounce-rate)
- [Retention Analysis: How to Measure and Improve User Retention](/blog/retention-analysis)
- [Ecommerce Analytics: The Metrics and Reports That Matter](/blog/ecommerce-analytics)
- [Funnel Analysis: How to Find Where Users Drop Off](/blog/funnel-analysis)
- [What Is Web Analytics? A Complete Guide](/blog/what-is-web-analytics)
- [Behavior Analytics - Sessions](/docs/feature-guides/sessions)
- [Behavior Analytics - Users](/docs/feature-guides/users)
- [Behavior Analytics - Errors](/docs/feature-guides/errors)
- [Track Custom Events](/docs/track-events)
- [Troubleshooting Guide](/docs/troubleshooting)

---

### Content Blog What Is Session Replay (`docs/content/blog/what-is-session-replay.mdx`)

---
title: "What Is Session Replay? How It Works and When to Use It"
description: "Session replay rebuilds what a visitor did from DOM events, not video. How it works, what it can't capture, privacy rules, and setup in Rybbit."
keyword: "what is session replay"
date: 2026-09-13
tags: ["session replay", "analytics", "ux"]
author: Rybbit Team
---

Session replay is a way to reconstruct what an individual visitor did on your website: the pages they opened, where they clicked, how far they scrolled, what they typed (masked), and where they gave up. It works by recording changes to the page and the visitor's interactions as a stream of events, then playing that stream back in a player that looks like a video but isn't one.

This guide is for product managers, developers, and support teams who have the numbers from [web analytics](/blog/what-is-web-analytics) but not the story behind them. It covers how session replay works, what it can't capture, how it compares to heatmaps and analytics, privacy rules, performance cost, and setup in Rybbit.

## How does session replay work?

### It records the DOM, not pixels

A session replay tool doesn't capture the screen. When a page loads, the recorder walks the DOM (the tree of elements the browser built from your HTML) and serializes it into a full snapshot. From then on it records only the differences, so a session is one snapshot followed by a long list of small updates.

Many tools, including Rybbit, build on [rrweb](https://github.com/rrweb-io/rrweb), an open-source library whose name stands for "record and replay the web"; its README lists PostHog, Sentry, Datadog, and Mixpanel among its users. Rybbit's tracker loads the rrweb recorder in the visitor's browser, and the Rybbit dashboard plays sessions back with rrweb-player.

Per rrweb's [observer documentation](https://github.com/rrweb-io/rrweb/blob/master/docs/observer.md), the recorder captures DOM changes (nodes added and removed, attribute and text changes), mouse movement and interaction, scrolling, window resizes, and input. DOM changes come from the browser's MutationObserver API, which delivers a batch of mutation records after each round of changes.

### Playback rebuilds the page in a sandbox

The player takes the initial snapshot, rebuilds the page inside a sandboxed iframe, and applies each recorded event at the right moment. rrweb's [sandbox notes](https://github.com/rrweb-io/rrweb/blob/master/docs/sandbox.md) state that the replayer "will not execute any JavaScript in the recorded page during replay" and that script tags are rewritten as noscript tags, so a recording can't run code, submit forms, or open pop-ups for the person watching.

Since every event depends on the state before it, recorders also take periodic full snapshots (rrweb calls them checkouts) so the player can jump ahead without processing everything before it. Rybbit's recorder takes one every 60 seconds or every 500 events.

### What a replay captures

- Page structure and every change to it, including route changes in single-page apps
- Clicks, focus and blur, and scroll position (Rybbit samples scroll every 500 ms)
- Input values, masked by default (see the privacy section)
- Viewport size, so the player shows what fit on the visitor's screen
- Metadata such as country, browser, operating system, and device type, which Rybbit's [replay list API](/docs/api/session-replay/list) returns with each recording

### What a replay can't capture

- **Canvas and WebGL.** Anything drawn on a canvas element (charts, maps, games) is pixels, not DOM. rrweb's `recordCanvas` option is off by default, and Rybbit keeps it off to limit storage, so canvases appear empty in playback.
- **Cross-origin iframes.** Payment forms, videos, and chat widgets served from another domain live in their own document. rrweb's [guide](https://github.com/rrweb-io/rrweb/blob/master/guide.md) notes that recording them is off by default and that "rrweb has to be injected in each child iframe for this to work." Rybbit doesn't do that, so those frames stay blank.
- **Mouse movement, unless you ask for it.** Rybbit's default sampling turns mouse-move recording off to cut data volume; clicks are still recorded.
- **Anything outside the page.** Browser chrome, other tabs, native dialogs, and operating system menus never enter the DOM.

## What you can learn from session replay

Analytics tells you a number moved. Replay tells you what a person did.

**Debugging user-reported problems.** A customer says "the checkout button does nothing". The replay shows they clicked it four times while a validation message rendered below the fold. Pair recordings with [error tracking](/features/error-tracking) and you can watch the interaction that preceded an exception on the customer's own browser and device instead of reproducing it from a vague description.

**Finding UX friction.** Some patterns are obvious the moment you watch them: repeated clicks on a heading that looks like a link, a form that clears a field when the user tabs past it, a mobile menu that covers the call to action. None of these show up as a metric; all of them are visible in a two-minute recording.

**Explaining conversion drop-offs.** A [funnel](/features/funnels) tells you that 40% of visitors leave on the shipping-options step. Filter replays to sessions that reached that step and watch ten of them. If eight scroll up and down looking for a delivery date that isn't there, you have your answer. The same approach turns a [bounce rate](/blog/bounce-rate) into something concrete: did single-page visitors read for a minute and leave satisfied, or leave in three seconds because the page looked broken?

**Faster support.** When a recording is tied to an identified user, a support agent can open the session behind a ticket and skip the "can you describe what you did?" round trip.

## Session replay vs heatmaps vs traditional analytics

| | Session replay | Heatmaps | Traditional analytics |
| --- | --- | --- | --- |
| What it shows | One visitor's session, step by step | Where many visitors clicked or scrolled on one page | Counts and rates across all visitors |
| Best question | Why did this happen? | Which parts of this page get attention? | What happened, and how much? |
| Sample size | One session can be useful | Needs many sessions per page | Works at any scale |
| Time to insight | Minutes per session | Fast for one page | Fast for trends |
| Privacy exposure | Highest: individual behavior | Aggregated, low | Aggregated, low |
| Data cost | Highest per session | Moderate | Lowest |

Work top down: analytics finds where the problem is, replay finds out what it is. Heatmaps sit in between, summarizing one page. Rybbit doesn't have heatmaps; if you need them, a standalone tool such as [Microsoft Clarity](https://clarity.microsoft.com/) bundles heatmaps with session recordings and is free.

## Privacy and compliance

Replay records far more than aggregate analytics does. This section describes what the tools do and what the rules say; it isn't legal advice.

### What a recording can expose

A replay of an account page, a checkout form, or a support chat contains information about a specific person. Under the GDPR's [Article 4(1)](https://gdpr-info.eu/art-4-gdpr/), personal data is "any information relating to an identified or identifiable natural person", and its list of identifiers includes names, location data, and online identifiers. Assume recordings are personal data and configure masking before you turn replay on.

### Masking and blocking

Rybbit's recorder has three levels of protection, documented on the [tracking script page](/docs/script):

- **Mask.** All input values are replaced with asterisks by default (`data-replay-mask-all-inputs` is `true`), and password and email fields stay masked even if you turn that off. Add the `rr-mask` class to any element, or list selectors in `data-replay-mask-text-selectors`, to mask visible text such as names and addresses.
- **Ignore.** Elements with the `rr-ignore` class (or matched by `data-replay-ignore-selector`) are shown, but interactions with them aren't recorded.
- **Block.** Elements with the `rr-block` class (or matched by `data-replay-block-selector`) are left out entirely and replay as a placeholder. Use this for admin panels, payment modals, and third-party widgets.

The npm SDK exposes the same options through `replayPrivacyConfig`, described in the [web SDK docs](/docs/sdks/web).

### Consent

In the EU, Article 5(3) of the [ePrivacy Directive](https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:02002L0058-20091219) allows storing information on, or reading information from, a user's device only with consent, unless it's strictly necessary for a service the user asked for. Whether a given replay setup falls inside that rule depends on how it's implemented and where your users are; ask your counsel.

In the US, session replay has drawn lawsuits under the California Invasion of Privacy Act. In *Javier v. Assurance IQ* (May 31, 2022), the Ninth Circuit held that the statute requires the prior consent of all parties, so consent collected after recording had begun, via a privacy policy accepted at the end of a form, was not enough ([DLA Piper's summary](https://www.dlapiper.com/en/insights/publications/2022/06/javier-v-assurance-iq-llc-et-al)).

Rybbit's tracker has `startSessionReplay()` and `stopSessionReplay()` methods. Recording starts on its own once the site setting is on, so they're better suited to pausing recording on a sensitive page than to holding it until consent; test the timing before relying on them for that.

### Data residency and retention

Recordings live wherever your analytics backend runs. On Rybbit Cloud, replay data is kept for 30 days. If you self-host, recordings stay on infrastructure in the region you choose, and you control retention. Individual recordings can be deleted from the dashboard or through the [API](/docs/api/session-replay/delete).

## Cost and performance

### Script weight

Rybbit's base tracking script (`script.js`) is about 11 KB compressed. The rrweb recorder is a separate file of roughly 80 KB compressed, fetched only when replay is enabled and the session has been chosen for recording. The tracker injects it at runtime from the same origin, so it isn't in your HTML and doesn't block parsing. That doesn't make it free: on pages with heavy DOM churn, serializing mutations is real CPU work in the visitor's browser.

### Data volume and sampling

Volume is driven by how busy the page is, not how long the visit lasts. Rybbit's defaults keep it down: no mouse-move events, scroll sampled every 500 ms, only the final value of each input rather than every keystroke, and a slimmed DOM snapshot that drops comments and most head metadata. Events are buffered and sent in batches of 250 or every five seconds.

You can also record only a share of sessions. The script accepts a `data-replay-sample-rate` attribute from 0 to 100, and the decision sticks for the whole browser session. On a busy site, 10 to 20% is usually enough to catch recurring problems and cuts storage in proportion.

### Retention and pricing

Session replay is included in the Pro and Enterprise plans: Pro starts at $39 per month for 100,000 events, while the Standard plan ($19 per month) does not include replays. Every cloud plan has a 7-day free trial, with a card collected at signup and no charges until the trial ends; see the [pricing page](/pricing). Self-hosting is free and includes session replay.

## How to set up session replay with Rybbit

1. **Install the [tracking script](/docs/script)** or npm SDK if you haven't already; replay uses the same tracker as everything else.
2. **Turn it on in site settings.** Find Session Replay under Analytics Features in your site's settings and enable it. The script picks up the setting automatically; nothing changes in your script tag.
3. **Decide what to mask.** Inputs are masked by default. Add `rr-mask`, `rr-ignore`, or `rr-block` classes to sensitive elements, or set the `data-replay-*` attributes on the script tag.
4. **Set a sample rate** with `data-replay-sample-rate` if you have significant traffic.
5. **Watch sessions.** The Replay page lists recordings on the left and plays them in the middle, with a breadcrumb timeline of pages, clicks, and custom events on the right. Filter by minimum duration, browser, device, country, referrer, channel, or user ID, and delete any recording you don't want to keep.



The [session replay feature guide](/docs/feature-guides/session-replay) covers the dashboard in more detail, and the [feature page](/features/session-replay) summarizes what's included.

## FAQ

### Is session replay legal?

In most places, yes, with appropriate notice, masking, and consent, but the rules differ by jurisdiction. EU rules require consent for storing or accessing information on a user's device unless strictly necessary, and under California law *Javier v. Assurance IQ* makes clear that consent has to come before recording starts. Configure masking, update your privacy policy, and ask a lawyer about your setup.

### Does session replay slow down my site?

It adds a script (about 80 KB compressed for the rrweb recorder in Rybbit) and some CPU work as the page changes. Rybbit loads the recorder after the main tracker, only for sampled sessions, and skips mouse-move events by default. Most visitors won't notice, but pages with constant DOM churn spend more time serializing changes.

### What's the difference between session replay and session recording?

Nothing meaningful. "Session recording" describes the capture step and "session replay" the playback, and vendors use the terms interchangeably. Both refer to the DOM-event recording described above, not screen video.

### Can session replay see passwords or credit card numbers?

With Rybbit's defaults, no. Input values are masked in the browser before they're sent, so the recording contains asterisks rather than the typed text. Payment forms in a third-party iframe aren't recorded at all, since cross-origin iframes are outside the recording's reach.

### How long should I keep recordings?

Long enough to investigate the problems you care about. A session from three months ago shows a version of your site that may no longer exist. Rybbit Cloud keeps recordings for 30 days; if you self-host, you set your own retention.

---

### Content Blog What Is Web Analytics (`docs/content/blog/what-is-web-analytics.mdx`)

---
title: "What Is Web Analytics? A Complete Guide for 2026"
date: 2025-11-23
updated: 2026-09-28
description: "What web analytics is, which metrics matter, how tracking works without cookies, and how to pick a tool that answers real questions about your visitors."
keyword: "what is web analytics"
tags: ['analytics', 'web-analytics', 'privacy']
author: Rybbit Team
---


## Introduction

Understanding how visitors interact with your website is crucial for success. But with legacy analytics tools collecting excessive data, compromising privacy, and overwhelming you with complexity, many businesses struggle to get actionable insights. That's where modern web analytics comes in—and **Rybbit is leading the charge** as the open-source, privacy-first alternative that makes analytics simple, powerful, and honest.

In this guide, we'll explore what web analytics is, why it matters, and why Rybbit stands out as the best choice for businesses serious about understanding their users without sacrificing privacy.

---

## What is Web Analytics?

Web analytics is the collection, measurement, and analysis of data about visitors and their behavior on your website. It answers fundamental questions: Where do your visitors come from? What pages do they view? How long do they stay? Do they complete your goals? Are they returning or new?

At its core, web analytics helps you understand the customer journey. Instead of guessing what works, you can make data-driven decisions backed by real user behavior patterns.

### Traditional Web Analytics vs. Modern Analytics

**Traditional web analytics** (like Google Analytics) has been the industry standard for years. It's free, comprehensive, and widely adopted. However, it comes with significant tradeoffs:

- **Privacy concerns**: GA4 sends data to Google's servers in the US, raising GDPR, CCPA, and other regulatory compliance questions
- **Cookie-dependent**: Relies on cookies that are increasingly blocked by browsers and user consent requirements
- **Overly complex**: The interface is confusing for non-technical users; many teams struggle to use the available features effectively
- **Vendor lock-in**: Your data lives in Google's ecosystem
- **Opaque algorithms**: You don't know exactly how your data is processed

**Modern web analytics** (represented by tools like Rybbit, Plausible, and Fathom) flips the script:

- **Privacy-first**: Cookie-free tracking, GDPR-compliant out of the box, data stays in your control
- **Simplicity**: Clean dashboards that show what matters, not feature overload
- **Transparency**: You can see exactly how your data is collected and processed
- **Data ownership**: Your analytics data belongs to you, not a third party

---

## Why Web Analytics Matters

### 1. **Understand Your Audience**

Web analytics reveals who your visitors are and where they come from. With Rybbit's advanced location tracking spanning country, region, and city levels, combined with sophisticated filtering across 15+ dimensions, you can create detailed visitor profiles. You'll know if your audience is concentrated geographically, what devices they use, and how different segments behave differently.

### 2. **Optimize Conversion Rates**

The average website leaves money on the table. Rybbit's [funnel analysis](/features/funnels) and [session replay](/features/session-replay) capabilities let you see exactly where visitors drop off in your conversion process. Understanding your [sales funnel](/blog/funnel-analysis) helps you identify which steps are causing the most drop-off. Watch a session replay to see if users are confused by your checkout flow or abandoning your signup form at a specific step. Then optimize based on real behavior, not assumptions.

### 3. **Track Goal Progress**

Whether your goal is generating leads, selling products, or building community, Rybbit lets you define custom goals and track them across all your pages. See which traffic sources, devices, and user segments drive your most valuable conversions. This transforms scattered activity into a coherent picture of what actually drives revenue.

### 4. **Monitor Real-Time Activity**

Rybbit's real-time dashboard lets you see visitor activity as it happens. A new campaign launching? Watch the traffic spike in real time. Deploy a new feature? Immediately see how users interact with it. This immediate feedback loop enables rapid iteration and quick problem detection.



### 5. **Improve User Retention**

One-time visitors are nice, but returning customers drive sustainable growth. Understanding [churn rate vs retention rate](/blog/retention-analysis) is critical for long-term business health. Rybbit's [retention analysis](/features/retention) shows you which cohorts of users keep coming back and which ones don't. [User journeys](/features/user-journeys) reveal the path successful users take, helping you replicate that experience for others.



---

## Core Web Analytics Metrics Explained

When you open Rybbit's dashboard, here are the essential metrics you'll encounter:

### **Sessions**
A session is a collection of user interactions on your website. If someone visits, browses three pages, and leaves, that's one session. If they return the next day, that's a new session. Sessions help you understand visit frequency and user engagement patterns.



### **Unique Users**
The actual number of distinct people visiting your site. This differs from sessions because one person can create multiple sessions over time. Tracking unique users helps you understand audience growth.

### **Pageviews**
How many times pages were viewed. High pageviews with low unique users suggests power users diving deep. High unique users with low pageviews might indicate visitors aren't finding what they need.

### **Bounce Rate**
The percentage of visitors who leave without taking action (no additional pageviews, events, or interactions). A high [bounce rate](/blog/bounce-rate) on your homepage might signal unclear messaging or poor traffic quality. A high bounce rate on a specific article? That might be perfectly normal—they got what they came for.



### **Session Duration**
Average time spent on your site per session. Longer duration typically indicates engagement, though context matters. A blog post reader spending 10 minutes is engaged; a checkout abandoner staying 12 minutes might be frustrated.



### **Custom Events**
Beyond pageviews, Rybbit lets you track [custom events](/features/custom-events) with rich JSON properties. Track video plays, button clicks, form submissions, or any behavior that matters to your business. This transforms analytics from passive observation to active insight capture.



---

## Why Rybbit is the Best Web Analytics Solution Available

We could throw marketing terms at you, but let's be direct: **Rybbit is the best because it solves the actual problems businesses face with modern analytics**. Here's why:

### **1. Complete Data Ownership (Open Source + Self-Hosting)**

Rybbit is open source under AGPL-3.0. You can inspect every line of code. Deploy it on your own VPS and your analytics data never leaves your infrastructure. Want to know exactly how tracking works? Read the source code. Need custom modifications? Fork and build.

**Compare this to:**
- GA4: Your data goes to Google. You have no visibility. They collect whatever they want.
- Plausible: Some features require their cloud. Limited self-hosting options.
- Cloudflare: Proprietary, limited customization.

With Rybbit, your analytics are truly yours.

### **2. Privacy-First Architecture**

Rybbit is **cookieless and GDPR-compliant by design**. No consent banners needed. No data processors in foreign jurisdictions. No regulations violations hiding in your terms of service.

How does Rybbit track without cookies? It uses a combination of advanced fingerprinting techniques and IP-based session tracking, all processed on your own infrastructure. Your data never gets sold or shared with third parties because there are no third parties—the system runs on your servers.

**Real impact**: Some European SaaS companies have raised concerns about GA4's compliance status. Rybbit's design addresses these concerns through its cookieless, privacy-first approach.

### **3. Superior Session Replay Technology**

Rybbit isn't just metrics—it's understanding. Session replay shows actual user behavior. See how someone navigates your site, where they click, what frustrates them, what delights them.

Watch a customer abandon your checkout and see exactly why. Was it confusing? Too many steps? A broken button? Session replay answers these questions that metrics alone cannot.

### **4. Advanced Filtering & Segmentation Across 15+ Dimensions**

Most analytics tools offer basic filters. Rybbit's advanced filtering system lets you drill deep:

- Show me mobile users from Germany who visited the pricing page but didn't convert
- Which dark mode users spend the most time on our site?
- Filter by custom event properties: "users who played video AND visited checkout"
- Combine multiple conditions with AND/OR logic

GA4 calls this "limited" segmentation for a reason—building complex segments is painful. Rybbit makes sophisticated analysis effortless.



### **5. Powerful Visualization Tools**

Rybbit's advanced map visualization is stunning and informative. See visitor distribution across countries, regions, and cities with intuitive heat maps. Understand geographic patterns at a glance.

Meanwhile, GA4's basic country-level maps look like they're from 2005.

### **6. User Journeys & Path Analysis**

Want to understand the actual path users take through your site? User journeys show how visitors move between pages and where they drop off. Identify your most successful user flows and replicate them. Spot problematic paths causing abandonment.



This is standard with an affordable plan in Rybbit ([see pricing](/pricing)). GA4 makes it difficult. Plausible doesn't offer it at all.

### **7. Funnel Analysis & Conversion Tracking**

Define custom funnels tracking users through your conversion process:
- Signup flow: landing page → signup form → email verification
- Purchase funnel: product browse → add to cart → checkout → payment
- Content funnel: discover content → read → comment → share



See exactly where users abandon. Identify bottlenecks. Optimize each step. Rybbit makes this visual and actionable.

### **8. Retention Analysis for Growth Focus**

Not all analytics tools understand retention. Rybbit's retention dashboard shows you which cohorts of users return and which don't. Segment by acquisition date, traffic source, or custom properties. See if new users from paid ads have better or worse retention than organic users.



This is how you identify growth channels that actually build sustainable businesses, not just vanity metrics.

### **9. Error Tracking Built-In**

Website errors damage user experience and conversions. Rybbit's [error tracking](/features/error-tracking) captures JavaScript errors and exceptions automatically. See your most common errors, affected users, and error trends over time.



GA4 requires separate error tracking. Plausible doesn't offer it. Rybbit includes it.

### **10. Organizations & Team Collaboration**

Managing multiple sites as a team? Rybbit supports unlimited organizations, team members, and sites. Assign roles, control permissions, and collaborate effectively. Whether you're an agency managing 50 client sites or a company with multiple products, Rybbit scales.

### **11. Real-Time Dashboard**

See activity as it happens. Launch a campaign? Watch traffic spike live. Deploy a new feature? Immediately see user interaction. A campaign is unexpectedly tanking? Spot it in seconds, not hours.



Rybbit's real-time dashboard is smooth, responsive, and genuinely useful for operational decisions.

### **12. Pricing That Scales With Usage**

GA4 is "free" but you're the product. Your data funds Google's ML engines.

Every Rybbit Cloud plan starts with a 7-day free trial, and pricing is based on the number of events you track, so you only pay for what you use. Session replay and unlimited sites come with the Pro plan, and self-hosting is free if you'd rather run it yourself. See [pricing](/pricing) for current tiers.

### **13. Transparent, Privacy-Respecting Pricing**

Rybbit's pricing is designed to be straightforward. Refer to [rybbit.io/pricing](/pricing) for current pricing details. Self-hosting options are also available for users who want more control over their deployment costs.

---

## The Real-World Impact: Why This Matters

Consider a typical e-commerce or SaaS company using analytics tools. With GA4:

- You're dependent on Google's infrastructure
- You face potential GDPR compliance risks if serving EU customers
- Your data may be used for Google's AI training
- You're frustrated by complexity in the interface
- You cannot see exactly how your tracking works

With Rybbit:

- Your analytics run on your own servers—completely under your control
- GDPR compliance is built into the design
- Your customer data is yours and yours alone
- The interface shows you what matters: are users converting or not?
- You can read, audit, and modify the code yourself
- Session replay shows actual user behavior leading to concrete improvements
- You understand retention, not just vanity metrics

That freedom and clarity is valuable for growing companies.

---

## Common Web Analytics Questions Answered

### **How long should I wait before analyzing data?**

With Rybbit's real-time dashboard, you can see initial trends immediately. However, wait at least 2 weeks before drawing conclusions—this accounts for weekly patterns (weekends vs. weekdays). For major decisions, wait 4+ weeks of data.

### **What's a good bounce rate?**

It depends entirely on context. A 70% bounce rate on a specific article is normal. A 70% bounce rate on your homepage suggests a problem. Compare your bounce rate to your historical average and segment by traffic source—paid traffic often has higher bounce rates than organic.

### **How often should I check my analytics?**

Daily checks for operational monitoring (campaigns, errors) are useful. But obsessive daily metric-watching leads to premature decisions based on noise. Weekly and monthly reviews are more actionable. Quarterly reviews show true trends.

### **Why should I care about session duration if I have conversion data?**

Session duration correlates with engagement and satisfaction. Long sessions with high conversions indicate happy, engaged customers. Short sessions with conversions might indicate urgency-driven behavior (sales pressure). Long sessions with no conversions suggests lost or confused users. Context matters.

### **When should I use custom events vs. goals?**

Goals track high-level conversions (completed purchase, newsletter signup). Custom events capture detailed interactions (video watched, file downloaded, section scrolled). Use both: goals for business outcomes, events for understanding behavior.

---

## Making Analytics Actionable

Having data isn't enough. Here's how to extract real value from Rybbit:

### **1. Define Metrics That Matter**

Not everything is important. Define 3-5 key metrics directly tied to business goals:
- E-commerce: conversion rate, average order value, repeat customer rate
- SaaS: signup conversion rate, feature adoption rate, churn rate
- Media: pageviews per session, return visitor rate, premium conversion
- Nonprofits: donation conversion rate, volunteer signup rate, retention rate

### **2. Set Benchmarks and Watch Trends**

Once you have baseline metrics, track how they change week-to-week and month-to-month. A 2% dip isn't concerning. A 20% dip warrants investigation. Trends matter more than absolute numbers.

### **3. Segment and Investigate**

When metrics dip, use Rybbit's advanced filtering to investigate. Is conversion down across all traffic sources or just one? Is it device-specific? Did something break on mobile? Segmentation transforms "our conversions are down" into "our mobile conversions are down for Google Ads traffic from Germany"—a solvable problem.



### **4. Watch Session Replays**

When you spot a problematic pattern in metrics, watch actual sessions. See what users do. This transforms abstract metrics into human behavior you can understand and fix.



### **5. Build Retention-Focused Habits**

Don't optimize for vanity—optimize for sustainable growth. An acquisition channel generating lots of one-time visitors is less valuable than one generating loyal repeat users. Use Rybbit's retention analysis to identify quality traffic sources.

---

## Conclusion: Choose Clarity Over Complexity

Web analytics is fundamentally about understanding your users and making better decisions. But the tool you choose shapes everything: your compliance obligations, your data privacy, your ability to customize, your dependency risk, and the insights you actually get.

**Rybbit wins because it aligns with modern realities:**

- Privacy laws (GDPR, CCPA) are real constraints, not marketing buzzwords
- Transparency is increasingly demanded by customers and regulators
- Open source communities outpace proprietary vendors in innovation
- Simplicity beats feature bloat
- Data ownership is a competitive advantage

You don't need to choose between powerful analytics and privacy-respecting practices. Rybbit proves you can have both—plus data ownership, GDPR compliance, beautiful visualizations, session replay, error tracking, and retention analysis.

Whether you're running a startup, managing a portfolio of sites, or leading analytics for an enterprise, **Rybbit delivers clarity without compromise**. Your analytics should serve your business and respect your users. Rybbit does both.

**Ready to see the difference?** Start a [free trial](/pricing), check out the [live demo](https://demo.rybbit.com/81), or dive into the [documentation](/docs).

Your future self—the one with better insights and fewer compliance headaches—will thank you.

---

## Quick Reference: Rybbit vs. GA4 vs. Plausible vs. Cloudflare

| Feature | Rybbit | GA4 | Plausible | Cloudflare |
|---------|--------|-----|-----------|-----------|
| **Open Source** | ✅ | ❌ | ✅ | ❌ |
| **Self-Hosting** | ✅ | ❌ | ⚠️ Limited | ❌ |
| **Cookieless & Privacy-First** | ✅ | ❌ | ✅ | ✅ |
| **GDPR Compliant** | ✅ | ⚠️ Debated | ✅ | ✅ |
| **Session Replay** | ✅ | ❌ | ❌ | ❌ |
| **Advanced Filters** | ✅ | ⚠️ Limited | ✅ | ❌ |
| **Advanced Maps** | ✅ | ❌ | ❌ | ❌ |
| **User Journeys** | ✅ | ✅ | ❌ | ⚠️ |
| **Retention Analysis** | ✅ | ✅ | ❌ | ❌ |
| **Error Tracking** | ✅ | ❌ | ❌ | ❌ |
| **Real-Time Dashboard** | ✅ | ✅ | ✅ | ✅ |
| **Custom Events (JSON)** | ✅ | ✅ | ⚠️ Limited | ⚠️ |
| **Funnel Analysis** | ✅ | ✅ | ✅ | ❌ |
| **Public Dashboards** | ✅ | ❌ | ✅ | ❌ |
| **Transparent Pricing** | ✅ | ❌ | ✅ | ✅ |
| **Data Ownership** | ✅ (yours) | ❌ (Google's) | ⚠️ (theirs) | ❌ (theirs) |

**Notes:**
- ⚠️ = Feature available with limitations, requires setup/integration, or may need paid tier
- This comparison table is accurate as of November 2025; features and availability may change in future versions. For the most current details, please refer to each product's official documentation.
- Plausible Advanced Filters: Supports multi-criteria filtering, saved segments, and subdomain/hostname filtering
- Cloudflare User Journeys: Available through Sequence Analytics feature
- Cloudflare Custom Events: Requires Zaraz integration setup (zaraz.track(), zaraz.ecommerce(), zaraz.set())

---

**Want to learn more?** Explore the [Rybbit documentation](/docs), join the [Discord community](https://discord.gg/DEhGb4hYBj), or contribute to the [open source project](https://github.com/rybbit-io/rybbit).

**Related Blog Posts:**
- [What Is Bounce Rate? How to Calculate and Reduce It](/blog/bounce-rate)
- [Retention Analysis: How to Measure and Improve User Retention](/blog/retention-analysis)
- [Ecommerce Analytics: The Metrics and Reports That Matter](/blog/ecommerce-analytics)
- [Funnel Analysis: How to Find Where Users Drop Off](/blog/funnel-analysis)
- [Why Did My Website Traffic Drop?](/blog/website-traffic-drop)
- [What Is Multivariate Testing?](/blog/multivariate-testing)
- [Best Marketing Attribution Tools](/blog/marketing-attribution-tools)

---

### Content Docs (Docs) Account Settings (`docs/content/docs/(docs)/account-settings.mdx`)

---
title: Account settings
description: Manage your personal account settings including profile, security, and API keys
---

import { Callout } from 'fumadocs-ui/components/callout';

Access your account settings by clicking on "Account" or going to [app.rybbit.io/settings/account](https://app.rybbit.io/settings/account).



## Account

### Name

Update your display name shown across the platform. This name is visible to other members in your organization.

### Email

Update your email address for account notifications and login. This is the primary email associated with your account.

### Weekly Email Reports <span className="text-green-500 text-sm">(Cloud)</span>

Enable or disable automatic weekly email reports. When enabled, you'll receive a summary of your analytics performance delivered to your inbox each week.

### Sign Out

Sign out of your current session.

## Security

### Change Password

Update your password to keep your account secure.

### Delete Account

Permanently delete your account and all associated data.

<Callout type="warn">
  This action cannot be undone. If you have an active subscription, you must cancel it before deleting your account.
</Callout>

## API Keys

Generate API keys to access Rybbit's analytics endpoints from your applications.

### Creating an API Key

1. Enter a name for your API key (e.g., "Production App", "Development")
2. Optionally enable **Restrict permissions** to limit the key to specific resources. Leave it off for a full-access key that can do everything you can.
3. Click "Create"
4. Copy and save the generated key immediately

<Callout type="warn">
  On Rybbit Cloud, API keys require a Standard or Pro plan. On a free plan you'll see an upgrade prompt in place of the creation form.
</Callout>

<Callout type="warn">
  API keys are only shown once when created. Store your key securely as you won't be able to view it again.
</Callout>

### Managing API Keys

Your existing API keys are displayed in a table showing:
- **Name** - The name you assigned to the key
- **Key** - A preview of the key (first few characters)
- **Permissions** - Whether the key has full access or is restricted to specific scopes
- **Created** - When the key was created
- **Last used** - When the key last made a request

You can delete API keys that are no longer needed. Deleting a key immediately revokes access for any applications using it.

### Using API Keys

Include your API key in requests to Rybbit's API endpoints. See the [API documentation](/docs/api) for available endpoints and usage examples.

---

### Content Docs (Docs) Autocapture (`docs/content/docs/(docs)/autocapture.mdx`)

---
title: Autocapture
description: Automatically track user interactions without writing code
---

Rybbit can automatically capture common user interactions without requiring any code changes. These features can be enabled in your [Site Settings](/docs/site-settings) under the "Auto Capture" section.

## Available Autocapture Features

| Feature | Description | Event Type |
|---------|-------------|------------|
| Outbound Links | Track clicks on external links | `outbound` |
| Error Tracking | Capture JavaScript errors and exceptions | `error` |
| Button Clicks | Track clicks on buttons | `button_click` |
| Copy Events | Track when users copy text | `copy` |
| Form Interactions | Track form submissions and input changes | `form_submit`, `input_change` |

## Outbound Links

When enabled, Rybbit automatically tracks clicks on links that navigate to external domains.

**Tracked properties:**
- `url` - The destination URL
- `text` - The link text
- `target` - The link target attribute (e.g., `_blank`, `_self`)

## Error Tracking

Captures uncaught JavaScript errors and unhandled promise rejections from your site. Only errors originating from your domain are tracked (third-party script errors are filtered out).

**Tracked properties:**
- `message` - The error message
- `stack` - Stack trace (truncated to 2000 characters)
- `fileName` - Source file where the error occurred
- `lineNumber` - Line number
- `columnNumber` - Column number

## Button Clicks

Automatically tracks clicks on:
- `<button>` elements
- Elements with `role="button"`
- `<input type="submit">` and `<input type="button">`

**Tracked properties:**
- `text` - The button's text content (falls back to `aria-label`, then `value` for input buttons, then `title`)

### Adding Custom Properties

You can add custom properties to auto-tracked button clicks using `data-rybbit-prop-*` attributes:

```html
<button
  data-rybbit-prop-category="pricing"
  data-rybbit-prop-variant="blue"
>
  Buy Now
</button>
```

This tracks: `{ text: "Buy Now", category: "pricing", variant: "blue" }`

### Skipping Autocapture for Specific Buttons

If you want to track a button with a custom event instead of the automatic `button_click`, add the `data-rybbit-event` attribute:

```html

<button data-rybbit-event="signup" data-rybbit-prop-plan="pro">
  Sign Up
</button>
```

See [Track Events](/docs/track-events) for more details on custom event tracking.

## Copy Events

Tracks when users copy text from your site using Ctrl+C, Cmd+C, or the context menu.

**Tracked properties:**
- `text` - The copied text (truncated to 500 characters)
- `textLength` - Original length of the copied text (only included if text was truncated)
- `sourceElement` - The HTML tag where the text was copied from (e.g., `p`, `div`, `span`)

## Form Interactions

Tracks two types of form events:

### Form Submissions (`form_submit`)

Triggered when a form is submitted.

**Tracked properties:**
- `formId` - The form's `id` attribute
- `formName` - The form's `name` attribute
- `formAction` - The form's `action` URL
- `method` - HTTP method (GET, POST, etc.)
- `fieldCount` - Number of form fields
- `ariaLabel` - The form's `aria-label` attribute (if present)

You can add custom properties using `data-rybbit-prop-*` on the form element:

```html
<form
  id="signup"
  data-rybbit-prop-form-type="registration"
  data-rybbit-prop-variant="a"
>
  ...
</form>
```

### Input Changes (`input_change`)

Triggered when a user changes the value of an input, select, or textarea field. Disabled inputs are automatically excluded from tracking.

**Tracked properties:**
- `element` - The element type (`input`, `select`, or `textarea`)
- `inputType` - For inputs: the type attribute (e.g., `text`, `email`, `checkbox`)
- `inputName` - The field's `name`, `id`, `aria-label`, or `placeholder` attribute (first available)
- `formId` - The parent form's `id` (if within a form)
- `formName` - The parent form's `name` (if within a form)

You can add custom properties using `data-rybbit-prop-*` on the input element:

```html
<input
  name="email"
  data-rybbit-prop-field-category="contact"
/>

<select
  name="country"
  data-rybbit-prop-section="demographics"
>
  ...
</select>
```

**Privacy note:** Hidden inputs, password fields, and disabled inputs are automatically excluded from tracking.

## Enabling Autocapture

1. Go to your site's **Settings**
2. Navigate to the **Auto Capture** section
3. Toggle on the features you want to enable

Changes take effect immediately for new visitors. Existing sessions may need to refresh to pick up the new configuration.

---

**Related Documentation:**
- [Track Events](/docs/track-events) - Manual event tracking
- [Site Settings](/docs/site-settings) - Configure your site
- [Behavior Analytics - Events](/docs/behavior-analytics/events) - View captured events

---

### Content Docs (Docs) Billing Settings (`docs/content/docs/(docs)/billing-settings.mdx`)

---
title: Billing
description: Manage your subscription, billing, and usage for Rybbit Cloud
---

import { Callout } from 'fumadocs-ui/components/callout';

Access billing settings by hovering over the sidebar and clicking "Billing".

Subscriptions are scoped to a single organization. If you have multiple organizations, each one requires its own subscription. Switching organizations will show that organization's subscription status.

---

### Content Docs (Docs) Bot Detection (`docs/content/docs/(docs)/bot-detection.mdx`)

---
title: Bot detection
description: How Rybbit identifies and filters bot traffic from your analytics
---

import { Callout } from 'fumadocs-ui/components/callout';
import { Steps, Step } from 'fumadocs-ui/components/steps';

Rybbit can filter bot traffic before it reaches your normal analytics. When **Block Bot Traffic** is enabled for a site, each incoming tracking request is checked by several detection layers. If any layer identifies the request as bot traffic, the request is excluded from the normal analytics tables and stored separately for bot traffic inspection.

## Enabling Bot Detection

Bot blocking is configured per site:

1. Open your site in the Rybbit dashboard
2. Go to **Site Settings**
3. Enable **Block Bot Traffic**

When this setting is off, Rybbit does not block requests based on bot detection.

## What Happens to Detected Bots

Detected bot requests are not added to your normal analytics data. This keeps dashboard totals, reports, journeys, funnels, session lists, and usage metrics focused on human traffic.

Detected bot visits also do not count toward billable analytics usage. If bot blocking filters a request, you are not charged for that bot visit.

Rybbit still stores a compact bot event record so you can inspect what was filtered. Bot event records include route, device, location, ASN, and which detection layers matched.

## Detection Layers

Rybbit runs all detection layers before making a final decision. A request can match multiple layers, and the bot event records every layer that matched.

<Steps>

<Step>

### User-Agent Patterns

The `ua_pattern` layer checks the request user-agent against known bot, crawler, AI agent, SEO tool, monitoring, social preview, framework, and headless browser patterns.

Examples include:

- Search engine crawlers
- Headless browser user-agents
- AI crawler and agent user-agents
- SEO and monitoring tools
- Script or framework HTTP clients

</Step>

<Step>

### Header Heuristics

The `header_heuristics` layer scores request headers for browser consistency.

It looks for signals such as:

- Missing browser headers
- Suspicious fetch metadata
- Inconsistent browser claims
- Headless or automation-looking headers
- Stale or unusual Chrome versions
- Script/framework-style requests that do not look like normal browser traffic

This layer is useful because many bots use a browser-like user-agent but do not send the full set of headers a real browser normally sends.

</Step>

<Step>

### Client Signals

The `client_signals` layer uses lightweight browser-side signals collected by the tracking script.

Signals include:

- Automation APIs
- Zero or impossible window dimensions
- Default automation viewport sizes such as `800x600` and `1024x768`
- Suspicious outer window dimensions
- Missing browser APIs
- Missing Chrome globals
- SwiftShader renderer signals
- Empty plugin lists

Rybbit combines these into a weighted score. Strong signals can identify bot traffic on their own, while weaker signals contribute supporting evidence.

</Step>

<Step>

### ASN and Network Signals

The `bot_asn` layer uses ASN metadata from the resolved IP address.

There are two kinds of ASN matches:

- **Curated bot provider ASNs**: Known AI, scanner, and internet measurement providers can trigger bot detection directly.
- **Generic hosting/datacenter ASNs**: Hosting ASNs are treated as supporting evidence. They are recorded when another bot layer also matches, but generic hosting ASN alone is not enough to block a request.

This avoids filtering every legitimate visitor who happens to browse through a cloud, CDN, VPN, corporate gateway, or first-party proxy, while still preserving ASN context when other bot evidence exists.

</Step>

<Step>

### Rate and Anomaly Detection

The `rate_anomaly` layer watches for bursty or crawl-shaped behavior.

It tracks short rolling windows for patterns such as:

- Too many events from the same IP and user-agent
- Too many events from the same IP
- Too many distinct paths visited quickly
- Too many different user-agents from one IP
- Too many hostnames from one IP
- High site-wide volume from one user-agent
- Large volumes of requests missing client-side bot scores

This layer is designed to catch fast crawlers, floods, and replayed tracking requests that may not have obvious user-agent or browser fingerprint signals.

</Step>

</Steps>

## Bot Identity

Every detected bot carries the layers that caught it. Bots matched by one of Rybbit's curated user-agent patterns carry three more fields, which is what the Bots dashboard groups by:

| Field | Example | Meaning |
| --- | --- | --- |
| `bot_name` | `GPTBot` | The name the bot publishes in its user agent |
| `bot_operator` | `OpenAI` | Who runs it |
| `bot_purpose` | `ai_training` | What the fetch is for |

A bot matched only by a broad pattern (`crawl`, `spider`) is still filtered, but has no published identity — those rows read as **Unnamed**, which is more useful than a guess.

### Purposes

`bot_purpose` answers a different question from `bot_category`. Category groups bots into families; purpose says what the operator does with the page, which is the distinction that matters for AI traffic:

| Purpose | What it means |
| --- | --- |
| `ai_training` | Collecting pages to train a model. Sends no readers back. Examples: GPTBot, ClaudeBot, CCBot, Google-Extended, Bytespider |
| `ai_search` | Indexing pages so an assistant can cite them. Can send readers back. Examples: OAI-SearchBot, Claude-SearchBot, PerplexityBot |
| `ai_agent` | A person asked an assistant to open the page, and it fetched it there and then. Examples: ChatGPT-User, Claude-User, Perplexity-User, Google-Agent, Claude Code |
| `search` | Classic search engine indexing |
| `social_preview` | Link unfurling |
| `seo` | Backlink, rank, and site-audit crawlers |
| `monitoring` | Uptime, synthetic, and performance checks |
| `security` | Scanners |
| `scripted` | HTTP libraries and CLI clients |
| `headless` | Browser automation |

Splitting the AI family in three is the point: a training crawler reading 4,000 pages and a person's assistant opening one page are both `category: ai`, but they mean opposite things for your site.

<Callout>
These fields were added in a later release. Bot events recorded before then have empty identity fields and appear as **Unnamed** / **Unclassified**; nothing is backfilled.
</Callout>

## How Decisions Are Made

Rybbit does not stop at the first matching layer. It runs every layer, records all matches, and then makes one final decision.

A request is marked as bot traffic when at least one blocking layer matches. The resulting bot event includes boolean fields for each layer:

- User-agent pattern
- Header heuristics
- Client signals
- Bot ASN
- Rate anomaly

Because multiple layers can match the same request, per-layer bot counts can add up to more than the total number of bot requests.

## Server-Side Tracking

Requests sent to `/api/track` with `Authorization: Bearer <api key>` and a valid API key are treated as trusted server-side ingestion. Four of the five layers are skipped: the headers, the client signals, the network address, and the request rate all belong to your server, not to the visitor it is reporting, so scoring them would convict your own backend.

The user-agent layer still runs, on the `user_agent` you send. Nothing changes for ordinary backend events — a request reporting a real browser's user agent is tracked normally.

### Reporting crawlers that never run JavaScript

Rybbit's tracker is JavaScript, so a crawler that fetches your HTML and executes nothing never reaches Rybbit at all. Most AI training crawlers work exactly that way, which is why the Bots dashboard carries a note about its own coverage.

If you want to see them, report them from the edge — middleware, a worker, or a reverse proxy — forwarding the real request's `user_agent`:

```json
{
  "site_id": "1",
  "hostname": "example.com",
  "pathname": "/pricing",
  "user_agent": "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.2; +https://openai.com/gptbot"
}
```

A forwarded crawler user agent is classified and lands in your bot events with full identity, rather than in your analytics as a phantom visitor.

<Callout type="warn">
Do not expose API keys in browser JavaScript. API keys are only for server-side requests.
</Callout>

## Proxies and CDN Setups

If you proxy Rybbit through Cloudflare Workers, AWS CloudFront, Nginx, Caddy, or another reverse proxy, forward the original visitor IP:

```txt
X-Forwarded-For: <visitor-ip>
X-Real-IP: <visitor-ip>
```

Also preserve the original `User-Agent`, `Referer`, and `Accept-Language` headers where possible.

If the proxy IP is sent instead of the visitor IP, traffic may be geolocated to the proxy location and may inherit the proxy provider's ASN.

See the [proxy troubleshooting guide](/docs/proxy-guide/troubleshooting) for examples.

## What Bot Detection Does Not Guarantee

Bot detection improves analytics quality, but no bot filter is perfect.

Some sophisticated bots can look like normal browsers. Some legitimate users may browse through unusual network paths or constrained browser environments. Rybbit uses multiple layers to reduce both misses and false positives, but you should still interpret bot counts as an operational signal rather than an exact measurement of all automation on your site.

---

### Content Docs (Docs) Comparison (`docs/content/docs/(docs)/comparison.mdx`)

---
title: Comparison
description: Comparison of Rybbit with other analytics solutions
---

import { Check, Minus } from 'lucide-react';


| Feature                  | Rybbit  | GA4        | Plausible  | Cloudflare |
| ------------------------ | ------- | ---------- | ---------- | ---------- |
| **Open Source**          | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> |
| **Self-Hosting**         | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} />\* | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> |
| **Cookieless & Privacy friendly** | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> |
| **Advanced Maps**        | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> |
| **Advanced Filters**     | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <span className="inline-flex items-center gap-1">Limited</span> | <span className="inline-flex items-center gap-1">Limited</span> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> |
| **Web Vitals**           | <span className="inline-flex items-center gap-1">Paid</span> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> |
| **Session Details**      | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> |
| **User Profiles**        | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> |
| **Session Replays**      | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> |
| **Funnels**              | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <span className="inline-flex items-center gap-1">Paid</span> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> |
| **User Journeys**        | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> |
| **Retention Analysis**   | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> |
| **Goals & Events**       | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> |
| **Real-time Dashboard**  | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> |
| **Custom Events (JSON)** | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <span className="inline-flex items-center gap-1">Limited</span> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> |
| **Error Tracking**       | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> |
| **Public Dashboards**    | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> |
| **Organizations**        | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> |
| **Frog 🐸**              | <Check className="w-5 h-5 inline" color="green" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> | <Minus className="w-5 h-5 inline" color="gray" strokeWidth={3} /> |

\* Plausible's Community Edition has very limited features compared to their cloud version

---

### Content Docs (Docs) Data Import (`docs/content/docs/(docs)/data-import.mdx`)

---
title: Data Import
description: Import historical analytics data from other platforms
---

import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';

The data import feature allows you to migrate your historical analytics data from other platforms to Rybbit. This preserves your analytics history when switching from competitors, giving you a complete view of your data without losing valuable insights.

## Supported Platforms

Currently, Rybbit supports importing data from:

- Umami
- Simple Analytics
- Plausible

Additional platforms may be added in the future based on user demand.

<Callout type="info">
  Umami and Simple Analytics are imported from a CSV export, while Plausible is imported from its ZIP export. The Import dialog automatically expects the correct file type for the platform you select.
</Callout>

## Plan Requirements

### Cloud Version

| Plan | Access | Historical Window | Monthly Event Limit |
|------|--------|-------------------|---------------------|
| Free | ❌ Not available | - | - |
| Standard | ✅ Available | 36 months (3 years) | Plan limit applies |
| Pro | ✅ Available | 60 months (5 years) | Plan limit applies |

### Self-Hosted Version

Self-hosted installations have **unlimited** access to data imports with no restrictions on:
- Historical data window (import data from any date)
- Event quotas (no monthly limits)
- Concurrent imports (no limit on active imports)

## Import Limits

### Historical Data Windows

The historical data window determines how far back you can import data:

- **Standard plans**: Import data from the past 36 months (3 years)
- **Pro plans**: Import data from the past 60 months (5 years)
- **Self-hosted**: Unlimited - import data from any date

Events outside your plan's historical window will be automatically skipped during import.

### Event Quotas

Imported events count toward your monthly event limit:

- If you're close to your monthly limit, imports may be partially skipped
- Skipped events are counted and displayed in the import history
- Consider upgrading your plan if you need to import large amounts of data

### Concurrent Imports

For cloud users:
- **Maximum 1 active import per organization**
- New imports are blocked until the current one completes
- Maximum 2-hour timeout per import

Self-hosted users have no concurrent import restrictions.

## How to Import Data

<Steps>
<Step>

### Export Data from Source Platform

**Umami:**
1. Navigate to Settings → Data
2. Export your data as CSV
3. Ensure the export includes all required fields

**Simple Analytics:**
1. Go to your site settings
2. Navigate to Export section
3. Download your analytics data as CSV

**Plausible:**
1. Open your site's dashboard
2. Use the CSV export option to download your data
3. Keep the exported **ZIP** archive as-is — do not unzip it

</Step>

<Step>

### Navigate to Import Settings

1. Open your Rybbit dashboard
2. Navigate to Site Settings
3. Click on the **Import** tab

</Step>

<Step>

### Select Platform and Upload File

1. Select your source platform from the dropdown
2. Click "Choose File" and select your exported file (a **CSV** for Umami and Simple Analytics, or a **ZIP** for Plausible)
3. Click the **Import** button

<Callout type="warn">
  For very large imports (multiple GB), consider splitting your CSV into smaller files and importing them sequentially.
</Callout>

</Step>

<Step>

### Monitor Import Progress

Your import will begin processing in the background. You can:
- View the import status in the "Import History" section
- Monitor the number of imported, skipped, and invalid events
- Continue using Rybbit while the import runs

<Callout type="info">
  Large imports may take several minutes to complete. The page will automatically refresh to show updated progress.
</Callout>

</Step>
</Steps>

## Import History

The Import History section displays all your past and current imports.

### Import Metrics

Each import shows three key metrics:

**Imported Events**
- Number of events successfully imported into Rybbit
- These events are now visible in your analytics dashboard

**Skipped Events**
- Events that were excluded during import
- Common reasons:
  - Outside your plan's historical data window
  - Would exceed your monthly event quota

**Invalid Events**
- Events that failed validation during import
- Common reasons:
  - Malformed CSV data
  - Missing required fields
  - Invalid date/time formats
  - Data type mismatches

<Callout type="warn">
  If you see a high number of invalid events, verify your CSV export matches the expected format for your platform.
</Callout>

### Managing Imports

**Deleting Completed Imports**
- Click the trash icon next to any completed import
- Confirm the deletion in the dialog
- This will permanently remove the import record **and all associated events** from your analytics
- Active imports cannot be deleted

<Callout type="error">
  Deleting an import removes all events that were imported. This action cannot be undone. Make sure you have a backup of your original CSV file.
</Callout>

## Troubleshooting

### Events Skipped Due to Quota Limits

**Cause:** Importing the events would exceed your monthly event limit.

**Solution:**
- Wait until the next billing cycle when your quota resets
- Upgrade to a plan with a higher event limit
- Import smaller date ranges to stay within quota

### Events Skipped Due to Date Range

**Cause:** Events are older than your plan's historical data window.

**Solution:**
- Upgrade to Pro plan for 60-month history (vs 24-month on Standard)
- For self-hosted, no action needed - all dates are allowed
- Filter your CSV export to only include recent data

### High Invalid Event Count

**Cause:** CSV format doesn't match the expected structure for your platform.

**Solution:**
1. Verify you selected the correct platform
2. Check that your CSV export includes all required fields
3. Look for special characters or malformed data in the CSV
4. Try importing a small sample file (first 100 rows) to test

### Import Appears Stuck

**Cause:** Very large files can take time to process, or a timeout occurred.

**Solution:**
- Wait up to 2 hours for the import to complete
- If still stuck after 2 hours, the import will be automatically marked as complete
- Check the imported event count to see if any data was processed
- Contact support if no events were imported

## Best Practices

### Before You Import

1. **Export a complete backup** from your current analytics platform
2. **Test with a small sample** (first 1,000 rows) before importing your full dataset
3. **Check your event quota** to ensure you have sufficient capacity
4. **Verify date ranges** match your plan's historical window
5. **Review CSV format** matches the required structure for your platform

### During Import

1. **Don't close the browser tab**
2. **Monitor the progress** in the Import History section
3. **Check for skipped/invalid events** early to catch format issues
4. **Wait for completion** before starting another import (cloud users)

### After Import

1. **Verify your data** appears correctly in analytics dashboards
2. **Check date ranges** to ensure historical data is visible
3. **Compare totals** with your source platform to validate accuracy
4. **Keep the original CSV** as a backup before deleting the import

---

### Content Docs (Docs) Definitions (`docs/content/docs/(docs)/definitions.mdx`)

---
title: Definitions
description: Understanding Rybbit's metrics and data points
---


## Aggregate Metrics

### Unique Users
The total number of distinct individuals who visited your website, identified by a unique user ID. Each user is counted only once regardless of how many times they visit.

### Sessions
A series of user interactions with your website within a given time frame. A session ends after 30 minutes of inactivity from the same user ID. Multiple pageviews can occur within one session.

### Pageviews
The total number of times visitors have viewed pages on your website. Each time a page is loaded or reloaded counts as a pageview.

### Pages Per Session
The average number of pages viewed during a session. Calculated by dividing the total number of pageviews by the total number of sessions.

### Bounce Rate
The percentage of sessions where a user leaves your site after viewing only one page without any interaction. A high bounce rate might indicate that landing pages aren't relevant to visitors.

### Session Duration
The average length of time users spend on your site during a session. Longer sessions typically indicate more engaged visitors.

## Individual Datapoints

### User ID
A unique identifier generated from a hash of the visitor's IP address and browser user-agent. This anonymized ID helps track unique visitors without storing personally identifiable information.

### Hostname
The domain name of the website being visited (e.g., example.com).

### Pathname
The path portion of the URL that follows the hostname (e.g., /blog/article).

### Entry Page
The first page viewed by a user during a session. Identifies which pages commonly serve as entry points to your site.

### Exit Page
The last page viewed by a user before ending their session. Helps identify where users tend to leave your site.

### Querystring
The portion of a URL that follows a question mark, containing parameters (e.g., ?utm_source=newsletter).

### Page Title
The title of the webpage as defined in the HTML `<title>` tag.

### Referrer
The URL that a user was on before coming to your site. Shows how users are discovering your content.

### UTM Source
Identifies which website, search engine, or other source is sending traffic to your site (e.g., google, newsletter, twitter).

### UTM Medium
Identifies the marketing medium (e.g., cpc, email, social).

### UTM Term
Identifies paid search keywords. Used primarily for paid search campaigns.

### UTM Campaign
Identifies the specific campaign or promotion that brought users to your site.

### UTM Content
Identifies what specifically was clicked to bring the visitor to your site (e.g., banner ad, text link).

### Channel
A categorization of traffic sources into standard groups such as direct, search, social, email, or referral.

### Browser
The web browser used by the visitor (e.g., Chrome, Firefox, Safari).

### Browser Version
The specific version of the browser being used.

### Operating System
The operating system used by the visitor (e.g., Windows, macOS, iOS, Android).

### Operating System Version
The specific version of the operating system being used.

### Language
The preferred language setting of the visitor's browser.

### Country
The country where the user is located, based on IP geolocation.

### Region
The state or region where the user is located, based on IP geolocation.

### City
The city where the user is located, based on IP geolocation.

### Latitude
The geographic latitude coordinate of the user's approximate location.

### Longitude
The geographic longitude coordinate of the user's approximate location.

### Screen Width
The width of the visitor's screen or device viewport in pixels.

### Screen Height
The height of the visitor's screen or device viewport in pixels.

### Device Type
The type of device being used (e.g., desktop, tablet, mobile).

### Event Name
The name of a custom event triggered by user interaction (e.g., "button_click", "form_submit").

### Event Properties
Additional contextual data associated with custom events, stored as key-value pairs. Used to provide more detail about specific interactions.

---

### Content Docs (Docs) Embed Widget (`docs/content/docs/(docs)/embed-widget.mdx`)

---
title: Embeds
description: Embed a live visitor widget or analytics dashboard on your own site
---

Rybbit supports two iframe embed options:

- [Widget Embeds](/docs/embeds/widget) show a compact live visitor widget on any web page.
- [Dashboard Embed](/docs/embeds/dashboard) shows your analytics dashboard in an iframe.

Both embeds are self-contained iframes. There is no script tag, no JavaScript on your site, and no dependency on the Rybbit tracking script.

## Which Embed Should I Use?

Use [Widget Embeds](/docs/embeds/widget) when you want a small live visitor count, chart, or top-countries summary. Widget Embeds do not make your dashboard public.

Use [Dashboard Embed](/docs/embeds/dashboard) when you want to publish the full read-only analytics dashboard. Dashboard Embed uses a generated private link, so Public Analytics is not required.

---

### Content Docs (Docs) Experiments (`docs/content/docs/(docs)/experiments.mdx`)

---
title: Experiments
description: Run A/B tests on feature flag variants and measure conversion lift
---

import { Callout } from 'fumadocs-ui/components/callout';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { Step, Steps } from 'fumadocs-ui/components/steps';

Experiments are A/B tests built on top of Rybbit. Each experiment splits your visitors across the variants of a [multivariate feature flag](/docs/feature-flags), then measures how often each variant completes a [conversion goal](/docs/goals). Rybbit calculates the conversion rate for every variant, the **lift** relative to your control, and how confident you can be that the difference is real.

<Callout type="info">
An experiment ties together two features you may already use: a **multivariate feature flag** decides which variant each visitor sees, and a **goal** defines the conversion you are optimizing for. If you understand those two pieces, you understand experiments.
</Callout>

## How Experiments Work

An experiment has three moving parts:

| Part | What it does |
| --- | --- |
| **Assignment** | A multivariate feature flag assigns each visitor to one stable variant (for example `control` or `variant_a`) based on its rollout percentages. |
| **Exposure** | When your code reads the flag with `rybbit.flag()`, Rybbit records an exposure for that variant. The number of distinct sessions exposed to a variant is the denominator for its conversion rate. |
| **Conversion** | The experiment's primary goal — a page visit or a custom event — counts as a conversion for whichever variant the visitor was assigned. |

Because assignment is sticky for a given visitor, a person sees the same variant on repeat visits, which keeps your results consistent.

## Creating an Experiment

Open your site in the Rybbit dashboard, go to **Experiments**, and click **New experiment**. A guided wizard walks you through each step.

<Steps>
<Step>
### Basics

Give the experiment a **name** and, optionally, a **hypothesis** and **description**. The hypothesis is internal context for your team — something like *"Changing the CTA copy will increase signups."* It does not affect assignment or tracking.

New experiments are created as **drafts** so you can add the implementation code before any traffic is split.
</Step>

<Step>
### Assignment

Choose how visitors are split into variants:

- **Create a new flag** — Define a new multivariate flag with its own variants and rollout split. Each variant needs a unique key, and the rollout percentages must add up to **100%**.
- **Use an existing flag** — Pick a multivariate flag that is not already attached to another experiment.

<Callout type="info">
Each experiment uses its own multivariate flag. The flag's variant keys (such as `control` and `variant_a`) are what you branch on in code.
</Callout>
</Step>

<Step>
### Goal

Pick the conversion you want to compare variants against:

- **Create a new goal** — Define a **page goal** (a URL path such as `/signup/complete`) or an **event goal** (a custom event such as `signup_completed`). The form provides autocomplete suggestions based on paths and events already seen on your site.
- **Use an existing goal** — Connect a goal you already track.
- **Skip for now** — Create the experiment without a goal and add one later. Conversion results require a goal.
</Step>

<Step>
### Review and Implement

Review the assignment flag and goal, then create the experiment. Rybbit creates any new flag or goal you configured, connects them, and shows you the **implementation code** for the flag. The experiment stays in **draft** until you start it from the experiment list.
</Step>
</Steps>

## Implementing in Code

Read the experiment's flag in your app and branch your UI or behavior on the returned variant. Reading the flag also records the exposure that powers your results, so make sure it runs wherever the tested experience is shown.

<Tabs items={['JavaScript', 'TypeScript']}>
<Tab value="JavaScript">
```javascript
window.rybbit.onReady((rybbit) => {
  const variant = rybbit.flag("checkout_cta", "control");

  if (variant === "variant_a") {
    // Render the variant experience.
  } else {
    // Render the control experience.
  }
});
```
</Tab>
<Tab value="TypeScript">
```typescript
type ExperimentVariant = "control" | "variant_a";

window.rybbit.onReady((rybbit) => {
  const variant = rybbit.flag("checkout_cta", "control") as ExperimentVariant;

  switch (variant) {
    case "variant_a":
      // Render the variant experience.
      break;
    default:
      // Render the control experience.
      break;
  }
});
```
</Tab>
</Tabs>

If your primary goal is an **event goal**, fire that event when the conversion action happens:

```javascript
window.rybbit.onReady((rybbit) => {
  rybbit.event("signup_completed");
});
```

**Page goals** need no extra code — Rybbit counts sessions that reach the goal's path automatically.

<Callout type="info">
Use a stable variant for the `rybbit.flag()` fallback (usually `"control"`) so visitors still get a sensible experience before the flag resolves or if the experiment is paused.
</Callout>

## Reading Results

Each experiment shows a results panel comparing every variant. Results respect the global date range and filters applied on the page.

| Metric | Description |
| --- | --- |
| **Variant** | The variant key, with the **Control** marked as the baseline. |
| **Sessions** | Sessions exposed to that variant, alongside the number that converted. |
| **Conversion rate** | Conversions divided by the number of exposed sessions (the **Sessions** figure) for that variant. |
| **Lift** | The percentage change in conversion rate relative to the control. Positive lift is shown in green, negative in red. |
| **Confidence** | How confident Rybbit is that the variant's difference from the control is real, based on a two-proportion comparison. |

Rybbit summarizes the state of each experiment at a glance:

- **Gathering data** — Not enough exposures or conversions yet to draw a conclusion.
- **Leading** — A variant is outperforming the control with at least **95% confidence**.
- **Winner** — The variant recorded as the winner once the experiment is decided.

<Callout type="warn">
Avoid stopping an experiment the moment a variant looks ahead. Let it run until it reaches 95% confidence with a meaningful number of conversions, otherwise early noise can masquerade as a real difference.
</Callout>

## Experiment Lifecycle

Experiments move through four statuses, which you control from the experiment list:

| Status | Meaning |
| --- | --- |
| **Draft** | Created but not yet collecting results. Add your implementation code here. |
| **Running** | Actively splitting traffic and recording exposures and conversions. |
| **Paused** | Temporarily stopped. Visitors keep their existing assignments. |
| **Completed** | Finished. Use this once you have a result and have shipped (or rolled back) the change. |

Use **Start**, **Pause**, and **Complete** on each experiment to move between states.

## Best Practices

- Change one thing at a time so you can attribute any lift to a single difference.
- Make sure your variant rollout percentages add up to 100% unless you intentionally want unassigned traffic.
- Read the flag wherever the tested experience renders so every exposed visitor is counted.
- Pick a primary goal that directly reflects the outcome you care about, not a proxy metric.
- Wait for 95% confidence and a healthy sample size before declaring a winner.
- After completing an experiment, ship the winning variant and remove the flag branch to keep your code clean.

---

**Related Documentation:**
- [Feature Flags](/docs/feature-flags) — The assignment mechanism behind experiments
- [Goals](/docs/goals) — Define the conversion an experiment measures
- [Track Events](/docs/track-events) — Fire custom events for event goals
- [Tracking Script](/docs/script) — Load the script and use `onReady()`

---

### Content Docs (Docs) Feature Flags (`docs/content/docs/(docs)/feature-flags.mdx`)

---
title: Feature Flags
description: Roll out features, serve remote config, and target variants in Rybbit
---

import { Callout } from 'fumadocs-ui/components/callout';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { TypeTable } from 'fumadocs-ui/components/type-table';

Feature flags let you change product behavior without deploying new code. In Rybbit, a flag can be evaluated in the browser, on your server, or in both places. Flag assignments are also attached to analytics events so you can filter and compare behavior by flag value.

<Callout type="info">
Feature flags are different from [tags](/docs/tagging). Tags label traffic that you already routed yourself. Feature flags decide which value a visitor receives at runtime.
</Callout>

## Create a Flag

1. Open your site in the Rybbit dashboard.
2. Go to **Feature Flags**.
3. Click **New flag**.
4. Choose a stable **key**, such as `new_checkout` or `pricing_banner`.
5. Select the flag type, runtime, and targeting conditions.
6. Enable the flag when you are ready for it to evaluate as active.

The **key** is the public identifier used in code. Treat it as the flag name and avoid changing it after the flag is in use.

## Flag Types

| Type | Returns | Best for |
| --- | --- | --- |
| **Boolean** | `true` or `false` | Releasing a feature on/off, gradual rollouts, allowlists |
| **Multiple variants** | A variant key like `"control"` or `"variant_a"` | Comparing more than one experience or configuration |
| **Remote config** | `true` plus a payload from `flagPayload()` | Shipping structured config such as copy, colors, limits, or JSON settings |

### Boolean Flags

Boolean flags support a rollout percentage for each condition set. If the visitor matches the condition set and falls inside the rollout bucket, `window.rybbit.flag()` returns `true`.

```javascript
window.rybbit.onReady((rybbit) => {
  const enabled = rybbit.flag("new_checkout", false);

  if (enabled) {
    showNewCheckout();
  }
});
```

### Multiple Variants

Multiple variant flags return the selected variant key. Each variant has its own rollout percentage and optional payload.

```javascript
window.rybbit.onReady((rybbit) => {
  const variant = rybbit.flag("homepage_cta", "control");

  if (variant === "variant_a") {
    renderAlternateCTA();
  }
});
```

Variant rollout percentages are evaluated in order. If the visitor falls outside the total allocated percentage, the flag returns `false`.

### Remote Config

Remote config flags are useful when you want Rybbit to choose a JSON payload. For remote config, `flag()` returns whether a config matched, and `flagPayload()` returns the actual config.

```javascript
window.rybbit.onReady((rybbit) => {
  const config = rybbit.flagPayload("checkout_config", {
    buttonText: "Continue",
    theme: "default",
  });

  renderCheckout(config);
});
```

Remote config can still use targeting. This lets you serve one config to beta users, another config to a country, and a default config to everyone else.

## Runtime

Each flag has a runtime:

| Runtime | Evaluated by | Use when |
| --- | --- | --- |
| **Client** | The browser tracking script | UI changes, frontend copy, client-only experiments |
| **Server** | Your backend through the API | Backend behavior, pricing logic, data access, anything private |
| **Both** | Browser and server | Both sides need the same decision |

<Callout type="warn">
Do not put secrets in client or both-runtime flag payloads. Browser-evaluated flags and payloads are visible to the user.
</Callout>

## Targeting and Condition Sets

Flags use ordered condition sets. Each set contains targeting rules plus the rollout, variants, or payload for users who match that set.

Evaluation works like this:

1. Rybbit checks condition sets from top to bottom.
2. The first condition set whose rules all match is selected.
3. Rybbit applies the rollout, variant allocation, or remote config payload from that selected set.
4. Later condition sets are not evaluated.

This means condition set order matters. Put narrow segments first and broad defaults last.

<Callout type="warn">
If a visitor matches a boolean condition set but falls outside that set's rollout percentage, Rybbit returns `false` and does not continue to later condition sets.
</Callout>

### Targeting Fields

| Field | Description |
| --- | --- |
| Hostname | Current hostname, such as `example.com` |
| Pathname | Current path, such as `/pricing` |
| Query parameter | A specific URL query parameter |
| Referrer | The document referrer |
| Language | Browser language, such as `en-US` |
| Country | Visitor country resolved from IP |
| Region | Visitor region resolved from IP |
| City | Visitor city resolved from IP |
| Device type | Device category derived from user agent and screen size |
| User ID | The identified user ID, falling back to the anonymous visitor ID |
| User trait | A trait set through `identify()` or `setTraits()` |

### Operators

Targeting rules support:

- `equals`
- `not_equals`
- `contains`
- `starts_with`
- `ends_with`
- `regex`

All rules in a condition set must match. Multiple condition sets act as ordered alternatives.

## Browser Runtime

The tracking script automatically evaluates client and both-runtime flags when it loads. It sends context such as anonymous visitor ID, identified user ID, URL, referrer, language, screen size, IP-derived location, and saved user traits.

The anonymous visitor ID is a random ID that keeps percentage rollouts stable across page loads. As soon as a site has at least one client or both-runtime flag, the script stores it in `localStorage` under `<namespace>-visitor-id` (`rybbit-visitor-id` by default). Sites without feature flags never read or write this visitor-ID key. If you use feature flags on sites with EU visitors, treat this stored ID like a cookie under Art. 5(3) ePrivacy when you assess consent requirements.

Use `onReady()` before reading flags if you need the evaluated value during page initialization:

```javascript
window.rybbit.onReady((rybbit) => {
  const enabled = rybbit.flag("new_checkout", false);
  const payload = rybbit.flagPayload("new_checkout", {});
});
```

Available browser methods:

<TypeTable
  type={{
    "flag(key, fallback)": {
      description: "Returns a single flag value. Reading a flag also records a feature flag exposure event once per flag value/version.",
      type: "function",
    },
    "flagPayload(key, fallback)": {
      description: "Returns the payload for a flag or variant. Use this for remote config and multivariate payloads.",
      type: "function",
    },
    "flags()": {
      description: "Returns all evaluated flag values keyed by flag key.",
      type: "function",
    },
    "flagPayloads()": {
      description: "Returns all available flag payloads keyed by flag key.",
      type: "function",
    },
  }}
/>

Assignments are included on analytics events as a feature flag map. This lets you filter dashboard reports by flag key and value.

### Identify and Traits

If you call `identify()` or `setTraits()`, the browser runtime refreshes feature flags using the updated user context.

```javascript
window.rybbit.identify("user_123", {
  plan: "pro",
  beta: true,
});
```

You can then target flags using **User ID** or **User trait** rules.

## Server Runtime

Use server runtime for decisions that should not happen in the browser. The server endpoint returns server and both-runtime flags only.

```
POST /api/sites/:siteId/feature-flags/evaluate
```

### Request

<TypeTable
  type={{
    anonymousId: {
      description: "Stable anonymous visitor ID used for rollout bucketing. Keep this stable for sticky assignments.",
      type: "string",
      required: true,
    },
    identifiedUserId: {
      description: "Logged-in user ID. Enables user ID rules and loads saved user traits.",
      type: "string",
    },
    hostname: {
      description: "Hostname used by hostname targeting rules.",
      type: "string",
    },
    pathname: {
      description: "Path used by pathname targeting rules.",
      type: "string",
    },
    query: {
      description: "Parsed query parameter object used by query targeting rules.",
      type: "Record<string, string>",
    },
    querystring: {
      description: "Raw query string. Used if query is not provided.",
      type: "string",
    },
    referrer: {
      description: "Referrer used by referrer targeting rules.",
      type: "string",
    },
    language: {
      description: "Language code used by language targeting rules.",
      type: "string",
    },
    screenWidth: {
      description: "Screen width used with user agent parsing to derive device type.",
      type: "number",
    },
    screenHeight: {
      description: "Screen height used with user agent parsing to derive device type.",
      type: "number",
    },
  }}
/>

<Tabs items={['cURL', 'JavaScript']}>
  <Tab value="cURL">
```bash
curl -X POST "https://app.rybbit.io/api/sites/123/feature-flags/evaluate" \
  -H "Authorization: Bearer rb_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "anonymousId": "visitor_abc",
    "identifiedUserId": "user_123",
    "hostname": "example.com",
    "pathname": "/checkout",
    "query": { "plan": "pro" },
    "language": "en-US",
    "screenWidth": 1440,
    "screenHeight": 900
  }'
```
  </Tab>
  <Tab value="JavaScript">
```javascript
const response = await fetch(
  "https://app.rybbit.io/api/sites/123/feature-flags/evaluate",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer rb_your_api_key_here",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      anonymousId: "visitor_abc",
      identifiedUserId: "user_123",
      hostname: "example.com",
      pathname: "/checkout",
      query: { plan: "pro" },
      language: "en-US",
      screenWidth: 1440,
      screenHeight: 900,
    }),
  }
);

const data = await response.json();
const newCheckout = data.flags.new_checkout?.value === true;
```
  </Tab>
</Tabs>

### Response

```json
{
  "flags": {
    "new_checkout": {
      "key": "new_checkout",
      "value": true,
      "flagType": "boolean",
      "payload": { "copy": "Try it now" },
      "conditionSet": "Default",
      "version": 3,
      "reason": "rollout",
      "matched": true,
      "rolloutPercentage": 100
    }
  },
  "generatedAt": "2026-05-27T12:00:00.000Z"
}
```

Assignment fields:

| Field | Description |
| --- | --- |
| `value` | The evaluated flag value. Boolean flags return `true` or `false`; multiple variant flags return a variant key or `false`; remote config returns `true` when matched. |
| `payload` | Optional payload from the selected condition set or selected variant. |
| `variant` | Selected variant key for multiple variant flags. |
| `conditionSet` | Name of the condition set that matched. |
| `reason` | Why the assignment was produced, such as `rollout`, `variant`, `remote_config`, `fallthrough`, `target_mismatch`, or `disabled`. |
| `matched` | Whether the flag produced an active match. |
| `rolloutPercentage` | The rollout percentage that was evaluated. |
| `version` | Flag version. Increments when the flag is edited. |

## Analytics and Filtering

Feature flag assignments are stored on events. Browser-tracked events include evaluated client flags automatically. Server-side events can include a `feature_flags` object in the [HTTP API](/docs/api/sending-events) payload:

```json
{
  "site_id": "123",
  "type": "custom_event",
  "event_name": "purchase",
  "pathname": "/checkout",
  "feature_flags": {
    "new_checkout": "true",
    "homepage_cta": "variant_a"
  }
}
```

Use feature flag filters in the dashboard to compare behavior between flag values.

## Best Practices

- Keep flag keys stable and descriptive, such as `new_checkout` or `pricing_page_copy`.
- Use **Client** runtime only for values that are safe to expose in the browser.
- Use **Server** runtime for permission checks, pricing logic, and private backend behavior.
- Put specific condition sets before broad default condition sets.
- Keep remote config payloads small and JSON-serializable.
- Use a stable `anonymousId` for server-side evaluation so rollout assignments remain sticky.
- Remove stale flags after rollout is complete to keep your dashboard and codebase clean.

**Related Documentation:**
- [Tracking Script](/docs/script)
- [Identify Users](/docs/identify-users)
- [Tagging](/docs/tagging)
- [API - Sending Events](/docs/api/sending-events)

---

### Content Docs (Docs) Filter Traffic (`docs/content/docs/(docs)/filter-traffic.mdx`)

---
title: Filter traffic
description: Exclude traffic by IP address, country, path, hostname, or user agent
---

import { Callout } from 'fumadocs-ui/components/callout';

Rybbit provides server-side traffic filtering to keep unwanted data out of your analytics. You can exclude your own traffic, staging and preview environments, internal tools, monitoring checks, or entire countries.

Filters are configured from **Site Settings → Exclusions**.

## Available Filters

| Filter | Use it for | Matching behavior | Limit |
| --- | --- | --- | --- |
| **IP Exclusions** | Your home, office, VPN, or internal network traffic | Single IPs, CIDR ranges, and IPv4 ranges | 100 entries |
| **Country Exclusions** | Regional filtering or compliance requirements | Two-letter ISO country codes, such as `US`, `GB`, or `DE` | 250 entries |
| **Path Exclusions** | Admin pages, private pages, preview routes, or test pages | Case-insensitive glob patterns using `*` | 100 entries |
| **Hostname Exclusions** | Localhost, staging domains, preview deployments, or temporary domains | Case-insensitive glob patterns using `*` | 100 entries |
| **User Agent Exclusions** | Monitoring tools, headless browsers, or custom crawlers | Case-insensitive substring matching | 100 entries |

When a request matches an exclusion, Rybbit returns a successful response but does not write the event to analytics storage.

## IP Exclusions

Block traffic from specific IP addresses or ranges at the server level. This is usually the best way to hide your own traffic.

Supported formats:

- **Single IPv4**: `192.168.1.1`
- **Single IPv6**: `2001:db8::1`
- **CIDR notation**: `192.168.1.0/24`, `2001:db8::/32`
- **IPv4 ranges**: `192.168.1.1-192.168.1.10`

For setup instructions and best practices, see [Hiding Your Own Traffic](/docs/hiding-own-traffic).

## Country Exclusions

Block all traffic geolocated to specific countries. Rybbit resolves the visitor IP address with the MaxMind GeoLite2 database, then compares the resulting country code to your exclusion list.

Use two-letter ISO country codes:

- `US` for United States
- `GB` for United Kingdom
- `CN` for China
- `DE` for Germany

Country codes are stored as uppercase values in the dashboard and API.

<Callout type="warning">
**Private IP addresses**: Country exclusion cannot work for private IP addresses such as `127.0.0.1`, `192.168.x.x`, `10.x.x.x`, or `172.16.x.x` because they cannot be geolocated. This includes localhost and Docker container IPs during local development.
</Callout>

<Callout type="info">
**VPNs and proxies**: Visitors using VPNs or proxies are detected based on the VPN or proxy server location, not their physical location.
</Callout>

## Path Exclusions

Exclude traffic to specific page paths. Path exclusions match the pathname only, not the query string.

Examples:

```txt
/admin/*
/preview
/internal/*
```

Matching is case-insensitive. A `*` wildcard matches any sequence of characters, so `/admin/*` matches `/admin/users` and `/admin/settings`.

Use path exclusions for pages that should not appear in analytics at all, such as admin dashboards, internal previews, or test routes.

<Callout type="info">
For client-side-only pageview skipping, you can also use the tracking script's `data-skip-patterns` option. Site-level path exclusions are enforced on the server and also apply to session replay ingestion.
</Callout>

## Hostname Exclusions

Exclude traffic from specific hostnames. This is useful when the same site ID is used across production, staging, preview, and local environments.

Examples:

```txt
localhost
staging.example.com
*.vercel.app
*.netlify.app
```

Matching is case-insensitive. A `*` wildcard matches any sequence of characters, so `*.vercel.app` matches `my-preview.vercel.app` but not `vercel.app`.

## User Agent Exclusions

Exclude traffic from requests whose user agent contains a configured substring.

Examples:

```txt
HeadlessChrome
MyMonitor
Pingdom
```

Matching is case-insensitive and uses substring matching, so `HeadlessChrome` matches `Mozilla/5.0 HeadlessChrome/120`.

Use user-agent exclusions for uptime monitors, synthetic checks, internal crawlers, or other tools with predictable user-agent strings.

## What Filters Apply To

Traffic filters run before normal analytics data is stored. They apply to:

- Pageviews
- Custom events
- Performance and Web Vitals events
- Error events
- Autocapture events
- Session replay batches

## How Filters Are Evaluated

Rybbit checks exclusions in this order:

1. IP address
2. Country
3. Path
4. Hostname
5. User agent

If any filter matches, the event is accepted with a `200` response but is not tracked. Changes usually take effect within 60 seconds because site configuration is cached briefly on the server.

## Testing Filters Locally

To test country filtering locally, send a public IP through the `X-Real-IP` header:

```bash
curl -X POST http://localhost:3001/api/track \
  -H "Content-Type: application/json" \
  -H "X-Real-IP: 8.8.8.8" \
  -d '{
    "type": "pageview",
    "site_id": "your-site-id",
    "hostname": "example.com",
    "pathname": "/test"
  }'
```

For path and hostname filters, set `pathname` and `hostname` in the event payload to values that should match your configured exclusions.

---

### Content Docs (Docs) Funnels (`docs/content/docs/(docs)/funnels.mdx`)

---
title: Funnels
description: Analyze conversion through multi-step user journeys
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { Step, Steps } from 'fumadocs-ui/components/steps';



Funnels let you define a sequence of steps — page visits, custom events, or autocaptured interactions — and measure how many users progress through each stage. You can see where users convert, where they drop off, and drill into the sessions at each step to understand why.

## Creating a Funnel


<Steps>
<Step>
### Click "Create Funnel"
Each funnel has:

- **Name** — A label for the funnel (e.g., "Signup Flow", "Checkout Process")
- **Steps** — Two or more sequential steps, each tracking a page visit, custom event, outbound link, button click, form submission, or copy event

</Step>
<Step>
### Add Steps

A funnel always must have at least two steps.

There are six types of funnel steps. Path and custom event steps require a value. For the four autocaptured interaction types, the pattern is optional—leave it empty to match every interaction of that type.

<Tabs items={['Path', 'Custom Event', 'Outbound Link', 'Button Click', 'Form Submit', 'Copy']}>
<Tab value="Path">
Path steps match pageview events by URL path.

- Enter a URL path starting with `/`
- Wildcards are supported:
  - `*` matches a single path segment — `/products/*` matches `/products/shoes`
  - `**` matches multiple segments — `/docs/**` matches `/docs/api/v2/intro`

</Tab>
<Tab value="Custom Event">
Custom event steps match events by name (e.g., `add_to_cart`, `purchase_completed`).

Events must be tracked using the Rybbit tracking script. See [Track Events](/docs/track-events) for setup instructions.
</Tab>
<Tab value="Outbound Link">
Outbound link steps match clicks on links that lead away from your site.

- Enter a destination URL pattern such as `https://example.com/pricing` or `https://*.example.com/**`
- Leave the pattern empty to match any outbound click

Outbound link tracking is enabled by default.
</Tab>
<Tab value="Button Click">
Button click steps match clicks on buttons, button/submit inputs, and elements with `role="button"`.

- Enter a visible button-text pattern such as `Sign Up` or `Start * Trial`
- Leave the pattern empty to match any button click

Buttons that already use `data-rybbit-event` are recorded as custom events instead.
</Tab>
<Tab value="Form Submit">
Form submit steps match submissions of HTML forms.

- Enter a form name or ID pattern such as `signup-form`
- The pattern can also match the form's action URL
- Leave the pattern empty to match any form submission
</Tab>
<Tab value="Copy">
Copy steps match text that visitors copy from your site.

- Enter a copied-text pattern such as `PROMO*`
- Leave the pattern empty to match any copied text
</Tab>
</Tabs>

Autocaptured interaction patterns support wildcards: `*` matches one or more characters and `**` also matches an empty value. The form suggests values already observed on your site.

<Callout type="info" title="Enable autocapture first">
Button click, form interaction, and copy tracking are disabled by default. Enable the event types you want to use in the **Tracking** tab of your site settings. Only interactions collected after tracking is enabled can appear in a funnel. See [Autocapture](/docs/autocapture) for the properties collected with each type.
</Callout>
</Step>
<Step>
### Configure Step Options (optional)



Each step has additional configuration options:

- **Label** (optional) — A friendly display name for the step
- **Hostname** (optional) — Restrict matching to a specific domain, useful for multi-domain sites
- **Property Filters** (optional) — Filter by URL parameters (for path steps) or event properties (for all other step types). Multiple filters can be added per step, and all must match.

</Step>
</Steps>
## Reading Funnel Results

The funnel visualization updates in real time as you configure steps. For each step, you'll see:

- **Sessions** — How many sessions reached this step
- **Conversion Rate** — Percentage of the first step's sessions that reached this step
- **Dropped** — How many sessions completed the previous step but not this one
- **Dropoff Rate** — Percentage of the previous step's sessions lost at this point

The horizontal bar for each step shows two layers:
- **Solid bar** — Overall conversion from the first step
- **Striped overlay** — Conversion from the immediately previous step

Steps are evaluated sequentially — each step must occur after the previous one within the same session.

## Viewing Sessions



Click any step to expand it and view the sessions associated with that step:

- **Reached** tab — Sessions that completed this step
- **Dropped Off** tab — Sessions that reached the previous step but didn't make it to this one (available for step 2 and beyond)

Each session shows entry/exit pages, duration, location, device info, referrer, UTM parameters, and user ID if identified. Sessions are paginated.

---

**Related Documentation:**
- [Autocapture](/docs/autocapture) — Configure automatically captured interactions
- [Goals](/docs/goals) — Track single-action conversions
- [Track Events](/docs/track-events) — Set up custom event tracking

---

### Content Docs (Docs) Goals (`docs/content/docs/(docs)/goals.mdx`)

---
title: Goals
description: Track conversions from page visits, custom events, and autocaptured interactions
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { Step, Steps } from 'fumadocs-ui/components/steps';



Goals let you define specific actions — from visiting a page to clicking a button — and measure how often your visitors complete them. Each goal tracks a conversion rate against total sessions, giving you a clear picture of how effectively your site drives key outcomes.

## Creating a Goal


<Steps>
<Step>
### Click "Create Goal"
Every goal has:

- **Name** (optional) — A friendly label like "Completed Checkout" or "Signed Up"
- **Type** — Page, custom event, outbound link, button click, form submission, or copied text
- **Value** — A required path or custom event name, or an optional pattern that narrows an autocaptured interaction

</Step>
<Step>
### Choose a Goal Type

There are six goal types. Page and custom event goals require a value. For the four autocaptured interaction types, the pattern is optional—leave it empty to count every interaction of that type.

<Tabs items={['Page', 'Custom Event', 'Outbound Link', 'Button Click', 'Form Submit', 'Copy']}>
<Tab value="Page">
Page goals track when a user visits a URL matching a given path pattern.

- The value must be a path starting with `/` (not a full URL)
- Wildcards are supported:
  - `*` matches a single path segment — `/blog/*` matches `/blog/my-post`
  - `**` matches multiple segments — `/docs/**` matches `/docs/api/v2/intro`

The form provides autocomplete suggestions based on actual paths seen on your site.

</Tab>
<Tab value="Custom Event">
Custom event goals track when an event with a specific name is fired. The value is the event name (e.g., `signup_clicked`, `purchase_completed`).

Events must be tracked using the Rybbit tracking script. See [Track Events](/docs/track-events) for setup instructions.
</Tab>
<Tab value="Outbound Link">
Outbound link goals track clicks on links that lead away from your site.

- Enter a destination URL pattern such as `https://example.com/pricing` or `https://*.example.com/**`
- Leave the pattern empty to count clicks to any external URL

Outbound link tracking is enabled by default.
</Tab>
<Tab value="Button Click">
Button click goals track clicks on buttons, button/submit inputs, and elements with `role="button"`.

- Enter a visible button-text pattern such as `Sign Up` or `Start * Trial`
- Leave the pattern empty to count any button click

Buttons that already use `data-rybbit-event` are recorded as custom events instead.
</Tab>
<Tab value="Form Submit">
Form submit goals track submissions of HTML forms.

- Enter a form name or ID pattern such as `signup-form`
- The pattern can also match the form's action URL
- Leave the pattern empty to count any form submission
</Tab>
<Tab value="Copy">
Copy goals track text that visitors copy from your site.

- Enter a copied-text pattern such as `PROMO*`
- Leave the pattern empty to count any copied text
</Tab>
</Tabs>

Autocaptured interaction patterns support wildcards: `*` matches one or more characters and `**` also matches an empty value. The form suggests values already observed on your site.

<Callout type="info" title="Enable autocapture first">
Button click, form interaction, and copy tracking are disabled by default. Enable the event types you want to use in the **Tracking** tab of your site settings. Only interactions collected after tracking is enabled can count toward a goal. See [Autocapture](/docs/autocapture) for the properties collected with each type.
</Callout>
</Step>
<Step>
### Add Property Filters (optional)



You can narrow a goal to only count conversions when specific properties match:

- **Page goals** — Filter by URL parameters (e.g., only count visits where `utm_source=adwords`)
- **Custom event goals** — Filter by event properties (e.g., only count events where `plan=premium`)
- **Autocaptured interaction goals** — Filter by captured properties such as `url`, `text`, `formName`, `formId`, or `formAction`

Toggle **Match specific URL parameters** or **Match specific event properties** to add filters. You can add multiple filters per goal — all filters must match for a session to count as a conversion.

</Step>
</Steps>
## Goal Metrics

Each goal card displays:

- **Total Conversions** — The number of distinct sessions that completed the goal
- **Conversion Rate** — Conversions divided by total sessions in the selected time period, shown as a percentage with a visual progress bar

Goals respect the global date range and filters (country, device, browser, etc.) applied on the page.

## Viewing Converted Sessions



Click a goal card to expand it and see the sessions that converted. Each session shows:

- Entry and exit pages
- Session duration
- Location (country, region, city)
- Device, browser, and OS
- Referrer, channel, and UTM parameters
- User ID (if identified)

Sessions are sorted by most recent first and paginated.

---

**Related Documentation:**
- [Autocapture](/docs/autocapture) — Configure automatically captured interactions
- [Track Events](/docs/track-events) — Set up custom event tracking
- [Funnels](/docs/funnels) — Analyze multi-step conversion paths

---

### Content Docs (Docs) Hiding Own Traffic (`docs/content/docs/(docs)/hiding-own-traffic.mdx`)

---
title: Hiding your own traffic
description: Prevent your own visits from being tracked in analytics
---

import { Tabs, Tab } from 'fumadocs-ui/components/tabs';
import { Steps, Step } from 'fumadocs-ui/components/steps';
import { Callout } from 'fumadocs-ui/components/callout';



There are several ways to disable Rybbit tracking to hide your own visits from analytics.

<Tabs items={['IP Exclusion', 'Browser Extensions', 'localStorage Method']}>
  <Tab>
    **IP Exclusion (Recommended)**: Block traffic from specific IP addresses or ranges at the server level for complete accuracy.

    ### Setting Up IP Exclusion

    1. **Navigate to Site Settings**: Go to your site's settings page in the Rybbit dashboard
    2. **Find IP Exclusions section**: Scroll down to the "IP Exclusions" configuration
    3. **Add your IP addresses**: Enter the IP addresses or ranges you want to exclude

    ### Supported IP Formats

    Rybbit supports multiple IP exclusion formats:

    - **Single IPv4**: `192.168.1.1`
    - **Single IPv6**: `2001:db8::1`, `::1`
    - **CIDR notation**: `192.168.1.0/24`, `2001:db8::/32`
    - **IPv4 ranges**: `192.168.1.1-192.168.1.10` (IPv4 only)

    ### Examples

    ```
    # Your home IP address
    203.0.113.45

    # Your office network (CIDR)
    203.0.113.0/24

    # IP range for your team
    198.51.100.10-198.51.100.20

    # IPv6 address
    2001:db8::1
    ```

    ### Key Features

    - **Server-side filtering**: Traffic is blocked before it reaches your analytics, providing highly accurate exclusion
    - **Multiple formats**: Supports individual IPs, CIDR notation, and ranges
    - **IPv4 and IPv6**: Full support for both IP versions
    - **Near real-time updates**: Changes take effect within seconds
    - **No browser dependency**: Works regardless of browser or device

    <Callout type="info">
    **IP Stability Note**: IP addresses can change due to factors like mobile networks, VPN usage, ISP changes, and IPv6 privacy extensions. You may need to periodically update your excluded IPs to maintain effective filtering.
    </Callout>

    <Callout type="info">
    **Dynamic IP Addresses**: If you frequently use VPNs, mobile networks, or have a dynamic IP address that changes regularly, consider these approaches:
    - Use CIDR ranges that cover your typical IP pool (consult your ISP or VPN provider)
    - Combine IP exclusion with browser extensions for comprehensive coverage
    - Check your current IP periodically and update exclusions as needed
    - For corporate networks, exclude the entire office IP range rather than individual addresses
    </Callout>

    <Callout type="warning">
    **Note**: IPv6 range notation (e.g., `2001:db8::1-2001:db8::ff`) is not supported. Use CIDR notation instead (e.g., `2001:db8::/32`).
    </Callout>

    <Callout type="info">
    **Finding your IP**: You can find your current IP address by visiting sites like [whatismyipaddress.com](https://whatismyipaddress.com) or [ipinfo.io](https://ipinfo.io).
    </Callout>
  </Tab>

  <Tab>
    **Browser Extensions**: Easy client-side solution using community-maintained browser extensions.

    ### Available Extensions

    - **[Chrome Extension](https://chrome.google.com/webstore/detail/gofnfdfidbkpellionhpmkejgaajieoo)**: Install from the Chrome Web Store
    - **[Firefox Extension](https://addons.mozilla.org/en-US/firefox/addon/rybbit-opt-out/)**: Install from Firefox Add-ons

    These extensions automatically disable Rybbit tracking on all websites where you have Rybbit installed, without requiring any manual configuration.

    <Callout type="info">
    **Self-hosted users**: These extensions require Rybbit version 1.1.0 or higher. If you're running an older version, please update your installation or use the localStorage method.
    </Callout>

    *These extensions are community-maintained by [@davidfiala](https://github.com/davidfiala).*
  </Tab>
  
  <Tab>
    **localStorage Method**: Manual configuration using your browser's developer console for more granular control.

    <Steps>

    ### 1. Open your browser's developer console

    Press **F12** or right-click the page and select "Inspect"

    Navigate to the "Console" tab

    ### 2. Disable tracking

    Enter this command to disable tracking:

    ```js
    localStorage.setItem('disable-rybbit', 'true');
    ```

    ### 3. Re-enable tracking (when needed)

    If you want to re-enable tracking later, enter:

    ```js
    localStorage.removeItem('disable-rybbit');
    ```

    </Steps>
  </Tab>
</Tabs>

## Summary

Multiple methods are available for hiding your own traffic, each with different advantages:

- **IP exclusion** provides the most accurate solution by blocking traffic at the server level, working regardless of browser or device
- **Browser extensions** offer convenience by automatically working across all your Rybbit-enabled websites  
- **localStorage method** gives granular control for specific websites or temporary disabling

For most users, **IP exclusion is recommended** as it provides highly accurate filtering for all devices accessing your site from the excluded IP addresses. The other methods work at the browser level and may be useful for additional flexibility or temporary exclusions.

---

### Content Docs (Docs) Identify Users (`docs/content/docs/(docs)/identify-users.mdx`)

---
title: Identify Users
description: How to identify and track logged-in users with Rybbit
---

<Callout type="warn" title="GDPR & Privacy Compliance">
User identification involves processing personal data. Before using these features, ensure you have a lawful basis for processing (typically user consent), have disclosed tracking in your privacy policy, and provide users the ability to access and delete their data. Only collect traits you have explicit consent to process.
</Callout>

Rybbit allows you to identify logged-in users and associate their activity with a persistent identity across devices and sessions. This enables powerful user-level analytics, including tracking user journeys, understanding retention, and personalizing experiences.



## How It Works

By default, Rybbit assigns an anonymous ID to each visitor based on their device fingerprint. When you call `identify()`, you link this anonymous visitor to a known user ID, allowing you to:

- Track the same user across multiple devices
- Associate pre-login activity with the logged-in user
- Store user metadata (traits) like email, name, and custom fields
- View identified users in your dashboard with their real names

When a new alias is created (i.e., the first time `identify()` is called for a given device), Rybbit retroactively updates all past anonymous events from that device to include the identified user ID. This means pre-login pageviews and events are attributed to the user once they identify themselves.

## Special Trait Fields

When identifying users, certain trait fields are treated specially by Rybbit:

| Field | Description |
|-------|-------------|
| `username` | **Primary display name** - Shown in the users list and session cards instead of the user ID |
| `name` | **Fallback display name** - Used if username is not provided |
| `email` | Displayed below the user's name in the dashboard |

You can also include any custom fields you need for segmentation and analysis.

## Limits

- **Traits size**: Maximum 2KB per identify call. Traits are stored as JSON, so keep payloads concise.
- **User ID length**: Maximum 255 characters.

## API Reference

### `window.rybbit.identify(userId, traits?)`

Sets a custom user ID for tracking logged-in users across devices and sessions, with optional user traits (metadata).

- `userId` (String): The unique identifier for the user. This will be stored in localStorage and persist across browser sessions. **Note: Custom user IDs are stored exactly as provided without any hashing or modification.**
- `traits` (Object, Optional): An object containing user metadata like email, name, username, or any custom fields. This data is stored in Rybbit and can be used to identify users in the dashboard. Setting a trait to `null` removes it from the user's profile.

```javascript
// Identify a user when they log in (without traits)
window.rybbit.identify("user_12345");

// Identify a user with traits (recommended)
window.rybbit.identify("user_12345", {
  username: "johndoe",           // Primary display name
  name: "John Doe",              // Fallback display name
  email: "john@example.com",
  plan: "premium",
  company: "Acme Inc"
});

// All subsequent events and pageviews will be associated with this user ID
```

### `window.rybbit.setTraits(traits)`

Updates the traits for the currently identified user without re-identifying them. Useful for updating user properties after initial identification.

- `traits` (Object): An object containing user metadata. New traits are merged with existing traits. **Setting a trait to `null` removes it from the user's profile.**

```javascript
// Update user traits after they upgrade their plan
window.rybbit.setTraits({
  plan: "enterprise",
  upgraded_at: "2024-01-15"
});

// Remove a trait by setting it to null
window.rybbit.setTraits({
  temporary_flag: null  // This trait will be removed
});
```

**Note:** You must call `identify()` before using `setTraits()`. If no user is identified, this will log a warning.

### `window.rybbit.clearUserId()`

Clears the stored user ID. Useful when a user logs out.

```javascript
// Clear user identification when user logs out
window.rybbit.clearUserId();
```

### `window.rybbit.getUserId()`

Returns the currently set user ID, or `null` if none is set.

```javascript
// Check if a user is identified
const currentUserId = window.rybbit.getUserId();
if (currentUserId) {
  console.log('Current user:', currentUserId);
} else {
  console.log('No user identified');
}
```

## Important Notes

- **Unique user counts**: Every user count in the dashboard — Unique Users, the users chart, the Users table, traits, and retention — keys on the custom user ID wherever one is present, falling back to the anonymous fingerprint only for visitors who have never identified. Two people sharing an IP address and browser count as two users, and one person on a laptop and a phone counts as one. Sites that never call `identify()` are unaffected. Because the count follows the IDs you pass, sending a fresh ID per visit (rather than a stable per-person ID) will inflate it.
- **Identified sessions**: When a custom user ID is present, session assignment includes both the anonymous device fingerprint and the custom user ID. Different identified users therefore remain in separate sessions even when they share the same IP address and user agent.
- **Shared devices**: If multiple users log in from the same device (same IP address and user agent), calling `identify()` with a new user ID updates the alias to point to the new user. Future events will be attributed to the most recently identified user. Past events that were already backfilled retain their original attribution.
- **Cross-device tracking**: Identifying the same user on different devices creates separate aliases. Events from each device are linked to the user independently via the `identified_user_id` field.
- **Retroactive backfill**: The backfill runs asynchronously when a new alias is first created. ClickHouse mutations may take a short time to propagate depending on table size. Subsequent `identify()` calls from the same device (even with a different user ID) do not re-backfill old events.
- **`clearUserId()` behavior**: Calling `clearUserId()` removes the user ID from localStorage so future events are anonymous. It does not delete the alias in the database or modify previously attributed events.

## Complete Example

Here's a typical workflow for user identification:

```javascript
// When user logs in
function handleLogin(userData) {
  // Set user identification with traits
  window.rybbit.identify(userData.id, {
    username: userData.username,
    name: userData.fullName,
    email: userData.email,
    plan: userData.plan
  });

  // Track login event
  window.rybbit.event("User Login", {
    method: "email"
  });
}

// When user upgrades their plan
function handlePlanUpgrade(newPlan) {
  // Update traits without re-identifying
  window.rybbit.setTraits({
    plan: newPlan,
    upgraded_at: new Date().toISOString()
  });

  // Track upgrade event
  window.rybbit.event("Plan Upgraded", {
    new_plan: newPlan
  });
}

// When user logs out
function handleLogout() {
  // Track logout event
  window.rybbit.event("User Logout");

  // Clear user identification
  window.rybbit.clearUserId();
}

// Check identification status
function checkUserStatus() {
  const userId = window.rybbit.getUserId();
  if (userId) {
    console.log("User is identified:", userId);
  } else {
    console.log("Anonymous user");
  }
}
```

**Related Documentation:**
- [Track Events](/docs/track-events)
- [Behavior Analytics - Users Tab](/docs/behavior-analytics/users)
- [API - Sending Events](/docs/api/sending-events)

---

### Content Docs (Docs) Inviting Users (`docs/content/docs/(docs)/inviting-users.mdx`)

---
title: Inviting users
description: Invite team members to your Rybbit organization and control their access to specific sites
---

import { Steps, Step } from 'fumadocs-ui/components/steps';
import { Callout } from 'fumadocs-ui/components/callout';

<Callout type="info">
  On self-hosted instances, this workflow is slightly different. Instead of inviting users, you can create a new user and set their password directly since self-hosted instances don't send emails.
</Callout>

<Steps>
<Step>
### Add the user to your organization

Go to **Settings > Organization** and click the "Invite Member" button. Enter the user's email address.
</Step>

<Step>
### Select the user's role

You can assign any role up to your own (only owners can make owners):

- **Owner** — everything, including billing and deleting the organization
- **Admin** — manages members, teams and sites, but not billing
- **Editor** — configures sites (tracking settings, feature flags, experiments, imports, Search Console), but doesn't manage members
- **Member** — views analytics and builds reports: goals, funnels, dashboards, and their own segments and annotations
- **Viewer** — read-only

See [Member Roles](/docs/organization-settings#member-roles) for the full list.

#### Restricting site access

For editors, members and viewers you can restrict access to specific sites. Check "Restrict access to specific sites" and select the sites they should see. If you don't, they can see every site in your organization (except sites that belong to a [team](/docs/teams) they aren't on).

You can also pick a **role on these sites** to raise their role on the selected sites only. For example, invite a client as a **Viewer**, restricted to their own site, with **Editor** on that site: they can configure their site and nothing else. The site role only ever raises the organization role, never lowers it.

<Callout type="info">
  Admins and owners always have access to every site, so site restrictions don't apply to them. A restricted member is treated as a viewer for anything that spans the whole organization, such as organization-wide segments.
</Callout>
</Step>

<Step>
### Assign teams (optional)

If your organization has [teams](/docs/teams) set up, you can assign the new member to a team during the invitation. When the member accepts the invitation, they will automatically be added to the selected team.

This is useful for ensuring new members immediately have access to the right group of sites without needing to configure access separately.
</Step>

<Step>
### Send the invite

Click the "Invite" button. The user will receive an email with a link to sign up for Rybbit. You can manage all pending invitations in the Organization settings page.
</Step>
</Steps>

## Managing site access for existing members

You can change a member's site access at any time after they've joined your organization.

1. Go to **Settings > Organization**
2. Find the member in the list and click the edit (pencil) icon in the Actions column for that member
3. Toggle "Restrict access to specific sites", select the sites they should have access to, and optionally a role on those sites
4. Click "Save changes"

The member will immediately only see the sites you've selected. If you disable the restriction, they'll regain access to all sites.

<Callout type="info">
  A person can belong to several organizations, each with its own role. To hand a site over to someone so they can manage it in their own organization, [transfer the site](/docs/site-settings#transfer-site) instead.
</Callout>

---

### Content Docs (Docs) Localhost Tracking (`docs/content/docs/(docs)/localhost-tracking.mdx`)

---
title: Localhost tracking
description: How Rybbit handles traffic from localhost during development
---

import { Callout } from 'fumadocs-ui/components/callout';

Rybbit does not restrict tracking to your registered domain — the tracking script works from `localhost` (or any other origin) out of the box. As long as your snippet has a valid `data-site-id`, events sent during local development are recorded like any other traffic.

```html
<script
  defer
  src="https://app.rybbit.io/api/script.js"
  data-site-id="YOUR_SITE_ID">
</script>
```

<Callout type="warning">
  Because localhost traffic is tracked by default, the usual problem is the opposite one: development visits polluting your production analytics. Use one of the approaches below to keep your data clean.
</Callout>

## Keeping development traffic out of production data

### Option 1: Use a separate development site

Create a second site in your Rybbit dashboard (e.g. "My App (dev)") and use its site ID during development. Your production site's data stays untouched, and you can still verify events end-to-end.

```jsx
// Next.js example
export function Analytics() {
  const siteId =
    process.env.NODE_ENV === 'development'
      ? process.env.NEXT_PUBLIC_RYBBIT_DEV_SITE_ID
      : process.env.NEXT_PUBLIC_RYBBIT_SITE_ID;

  return (
    <script
      defer
      src="https://app.rybbit.io/api/script.js"
      data-site-id={siteId}
    />
  );
}
```

### Option 2: Only load the script in production

If you don't need analytics while developing, skip loading the script entirely outside production:

```jsx
// Next.js example
export function Analytics() {
  if (process.env.NODE_ENV !== 'production') return null;

  return (
    <script
      defer
      src="https://app.rybbit.io/api/script.js"
      data-site-id={process.env.NEXT_PUBLIC_RYBBIT_SITE_ID}
    />
  );
}
```

```javascript
// Vanilla JS example
if (process.env.NODE_ENV === 'production') {
  const script = document.createElement('script');
  script.defer = true;
  script.src = 'https://app.rybbit.io/api/script.js';
  script.setAttribute('data-site-id', 'YOUR_SITE_ID');
  document.head.appendChild(script);
}
```

### Option 3: Filter the traffic instead

If you want the script loaded everywhere but excluded from your stats, use your site's exclusion settings — see [Filtering Traffic](/docs/filter-traffic) and [Hiding Your Own Traffic](/docs/hiding-own-traffic).

## Troubleshooting

**Events not showing up from localhost?**

- Confirm `data-site-id` matches the site you're looking at in the dashboard
- Check the browser console for errors and the Network tab for `POST` requests to `/api/track`
- Disable ad blockers or privacy extensions — they commonly block analytics scripts on any origin, including localhost
- Make sure the page isn't excluded by your site's IP, path, or user-agent exclusion rules

---

### Content Docs (Docs) Managing Your Installation (`docs/content/docs/(docs)/managing-your-installation.mdx`)

---
title: Managing your installation
description: Managing your Rybbit installation
---

import { Steps, Step } from 'fumadocs-ui/components/steps';
import { Callout } from 'fumadocs-ui/components/callout';

## Helpful scripts
<Callout>
You probably need to run `chmod +x *.sh` before running the scripts.
</Callout>

### Updating Your Installation

Rybbit is updated frequently with new features and bug fixes. You can update your installation by running the following commands:

**update.sh**: Update to the latest version by pulling new code and rebuilding containers
  ```bash
  git pull
  ./update.sh
  ```

### Restarting Your Installation

**restart.sh**: Restart all services (useful when you've changed .env settings)
  ```bash
  ./restart.sh
  ```

## Docker Compose Commands

You can also manage services directly with Docker Compose:

- **Stop all services**:
  ```bash
  docker compose stop
  ```

- **Stop and remove containers** (preserves data):
  ```bash
  docker compose down
  ```

- **Start services** (if you used setup.sh with default webserver):
  ```bash
  docker compose --profile with-webserver up -d
  ```

- **Start services** (if you used setup.sh with --no-webserver):
  ```bash
  docker compose up -d
  ```

- **View logs**:
  ```bash
  docker compose logs -f
  ```

<Callout>
If you are using your self-hosted Rybbit without anyone else, you can disable new user signups by setting `DISABLE_SIGNUP=true` in the `.env` file at the root of the repository.
</Callout>

## Database Access

You can access the databases by running the following commands:

- **Access Clickhouse**:
  ```bash
  docker exec -it clickhouse clickhouse-client --database analytics
  ```

- **Access Postgres**:
  ```bash
  docker exec -it postgres psql -U frog -d analytics
  ```
### Edit Session Replay Retention Policy

The default session replay retention policy is 30 days. This may be too long for some users as session replays can take up a lot of space. You can edit it by running the following command:

```bash
docker exec -it clickhouse clickhouse-client --database analytics
ALTER TABLE session_replay_events MODIFY TTL toDateTime(timestamp) + INTERVAL 14 DAY;
ALTER TABLE session_replay_metadata MODIFY TTL start_time + INTERVAL 14 DAY;
```

## Anonymous Usage Telemetry

To help us improve Rybbit, self-hosted instances automatically send anonymous usage statistics to our cloud service. This includes:

- Number of rows in a few core ClickHouse tables (events and the session replay tables)
- Total database size
- Your Rybbit version
- A hashed instance identifier

No personal data, website content, or visitor information is ever collected.

<Callout type="info">
You can completely disable telemetry by adding `DISABLE_TELEMETRY=true` to your `.env` file and restarting your services.
</Callout>

---

### Content Docs (Docs) Organization Settings (`docs/content/docs/(docs)/organization-settings.mdx`)

---
title: Organization settings
description: Manage your organization, team members, roles, and permissions
---

import { Callout } from 'fumadocs-ui/components/callout';

Access organization settings by clicking on "Organization" or going to [app.rybbit.io/settings/organization](https://app.rybbit.io/settings/organization).



## Organization

### Organization Name

Update your organization's display name. Owners and admins can change this setting.

### Delete Organization

Permanently delete the organization and all associated data including sites, analytics, reports, and member associations. Only owners can delete an organization.

<Callout type="warn">
  This action cannot be undone. You must type the organization name to confirm deletion. If you have an active subscription, you must cancel it before deleting the organization.
</Callout>

## Members

View and manage all members in your organization. The members table shows:
- **Name** - Member's display name
- **Email** - Member's email address
- **Role** - Owner, Admin, Editor, Member, or Viewer
- **Site Access** - Which sites the member can access
- **Joined** - When they joined the organization

### Member Roles

Each role can do everything the roles below it can.

| Role | What it adds |
|------|-------------|
| **Owner** | Billing and plans, deleting the organization, making other owners |
| **Admin** | Inviting and managing members and teams, organization API keys, creating, deleting and [transferring](/docs/site-settings#transfer-site) sites |
| **Editor** | Site configuration: tracking settings, exclusions, public and private links, feature flags, experiments, data imports, Google Search Console, deleting session replays and tracked users, and editing anyone's segments and annotations |
| **Member** | Building reports: goals, funnels, dashboards, and their own segments and annotations |
| **Viewer** | Viewing analytics, sessions, replays and reports, without changing anything |

Admins and owners always reach every site in the organization. Editors, members and viewers reach every site too, unless you [restrict them to specific sites](/docs/inviting-users#restricting-site-access) or the site belongs to a [team](/docs/teams) they aren't on.

A member's role can also be raised on individual sites: when you restrict someone to specific sites, or add a site to a team, you can give them a higher role on just those sites. For example, a client can be a **Viewer** in your organization and an **Editor** on their own site. Nobody can give a role above their own, and only owners can make owners.

### Inviting Members

Owners and admins can invite new members or create user accounts. See the [Inviting Users](/docs/inviting-users) guide for detailed instructions. A person can belong to several organizations.

### Teams

You can organize sites into teams to control which members have access to which sites. See the [Teams](/docs/teams) guide for details.

---

### Content Docs (Docs) Roadmap (`docs/content/docs/(docs)/roadmap.mdx`)

---
title: Roadmap
description: Planned features and upcoming development for Rybbit analytics platform
---


Rybbit is a quickly growing project. We already have quite a few features, but there is much more to come.

## Planned features

- Data import from Google Analytics, PostHog, and more
- More tracking script/SDK integration guides
- Custom themes

Once again, join our [Discord server](https://discord.gg/DEhGb4hYBj) or follow me on [X](https://x.com/yang_frog) to share your feedback and ideas. We would love to hear from you!

---

### Content Docs (Docs) Script Troubleshooting (`docs/content/docs/(docs)/script-troubleshooting.mdx`)

---
title: Troubleshooting
description: Common issues and solutions when the Rybbit tracking script isn't sending traffic
---

import { Callout } from 'fumadocs-ui/components/callout';
import { Steps, Step } from 'fumadocs-ui/components/steps';

If you've installed the Rybbit tracking script but aren't seeing any traffic in your dashboard, this guide will help you diagnose and fix common issues.

## Check if the Script is Loaded

The first step is to verify that the Rybbit script is properly loaded on your page.

<Steps>
<Step>
### Open Your Browser's Developer Console

Right-click anywhere on your webpage and select **Inspect** (or **Inspect Element**). Then click on the **Console** tab at the top of the panel that opens.

Alternatively, you can use keyboard shortcuts:
- **Windows/Linux**: Press `Ctrl + Shift + J` or `F12`
- **Mac**: Press `Cmd + Option + J`
</Step>

<Step>
### Type "rybbit" and Press Enter

In the console, type `rybbit` and press Enter.

**If the script is loaded correctly**, you'll see an object with functions like this:
```javascript
{event: ƒ, pageview: ƒ, identify: ƒ, setTraits: ƒ, clearUserId: ƒ, …}
```

**If the script is NOT loaded**, you'll see an error like:
```
Uncaught ReferenceError: rybbit is not defined
```

If you see the error, the script is not loading properly. Continue reading for common causes.
</Step>
</Steps>

## Common Issues

### Data Attributes Being Stripped

Some platforms, content management systems, tag managers, and "delay JavaScript" optimizers automatically remove or sanitize `data-*` attributes from script tags, or rebuild the tag from a `data-src` placeholder and drop everything else. If your site ID was in a `data-site-id` attribute, it gets removed and the script logs `Please provide a valid site ID`.

**Platforms known to strip attributes:**
- Google Tag Manager
- WordPress optimization plugins with "delay JavaScript" enabled (WP Rocket, Perfmatters, FlyingPress, LiteSpeed Cache)
- Some WordPress page builders
- Certain website builders (Webflow, Wix, Squarespace in some configurations)
- Some CDN or caching plugins

**Solution:** Put the site ID in the script URL instead of a data attribute. Optimizers preserve the URL:

```html
<script src="https://app.rybbit.io/api/script.js?siteId=YOUR_SITE_ID" defer></script>
```

If your platform also rewrites the `src`, inject the script from JavaScript instead:

```javascript
<script>
  (function() {
    var el = document.createElement("script");
    el.src = "https://app.rybbit.io/api/script.js?siteId=YOUR_SITE_ID";
    el.defer = true;
    document.head.appendChild(el);
  })();
</script>
```

<Callout type="info">
For detailed Google Tag Manager setup instructions, see our [Google Tag Manager Guide](/docs/guides/google-tag-manager).
</Callout>

### Ad Blockers

In rare cases, ad blockers may block the Rybbit tracking script. While Rybbit is privacy-focused and doesn't use cookies for tracking, some aggressive ad blockers may still flag analytics scripts.

**How to check if an ad blocker is the issue:**
1. Temporarily disable your ad blocker
2. Refresh the page
3. Check the console again for the `rybbit` object

**Solutions:**
- **For your own testing**: Disable your ad blocker or add your site to the allowlist
- **For your visitors**: Consider setting up a [proxy](/docs/proxy-guide) to serve the script from your own domain, which significantly reduces the chance of being blocked

### Script Not in the Right Location

The Rybbit script should be placed in the `<head>` section of your HTML. If it's placed in the wrong location or loaded after the page has already finished loading, it may not work correctly.

**Verify the script placement:**
1. View your page source (right-click → **View Page Source**)
2. Search for `rybbit` or `script.js`
3. Confirm it appears within the `<head>...</head>` tags

### Incorrect Site ID

Double-check that the `siteId` in your script URL (or your `data-site-id` attribute) matches the site ID shown in your Rybbit dashboard.

1. Go to your Rybbit dashboard
2. Navigate to your site's settings
3. Copy the site ID and compare it with what's in your script tag

### Content Security Policy (CSP) Blocking

If your site has a strict Content Security Policy, it may block the Rybbit script from loading or executing.

**Check for CSP errors:**
1. Open the browser console
2. Look for errors mentioning "Content Security Policy" or "CSP"

**Solution:** Add the Rybbit script domain to your CSP headers:

```
script-src 'self' https://app.rybbit.io;
connect-src 'self' https://app.rybbit.io;
```

If you're self-hosting Rybbit, replace `app.rybbit.io` with your Rybbit instance domain.

### Script Loading Too Late in SPAs

In single-page applications (React, Vue, Next.js, etc.), if the script loads after the initial page render, the automatic pageview might not fire.

**Solution:** Ensure the script is in your root HTML template (like `index.html` or `_document.js` in Next.js), not dynamically imported in a component.

For Next.js specifically, see our [Next.js Guide](/docs/guides/nextjs).

## Testing Your Installation

Once you've made changes, verify that tracking is working:

1. **Check the console**: Type `rybbit` to confirm the object exists
2. **Trigger a test event**: In the console, run:
   ```javascript
   rybbit.event("test_event")
   ```
3. **Check your dashboard**: Navigate to the Events tab in your Rybbit dashboard and look for "test_event" (it may take a few seconds to appear)
4. **Check the Network tab**: In browser dev tools, go to the Network tab, filter by "track" or "rybbit", and verify requests are being sent with 200 status codes

## Still Having Issues?

If you've tried all the above and are still not seeing traffic:

1. **Check the browser Network tab** for any failed requests to your Rybbit instance
2. **Verify your site ID** is correct in the dashboard
3. **Check if traffic is being filtered** - review your [IP exclusions](/docs/hiding-own-traffic) and [country exclusions](/docs/filter-traffic) to ensure your traffic isn't being blocked
4. **For self-hosted instances** - ensure your Rybbit server is running and accessible

---

**Related Documentation:**
- [Tracking Script Installation](/docs/script)
- [Track Events](/docs/track-events)
- [Proxy Guide](/docs/proxy-guide)
- [Google Tag Manager Guide](/docs/guides/google-tag-manager)
- [Hide Your Own Traffic](/docs/hiding-own-traffic)
- [Filter Traffic](/docs/filter-traffic)

---

### Content Docs (Docs) Script (`docs/content/docs/(docs)/script.mdx`)

---
title: Tracking script
description: How to install and configure the Rybbit tracking script
---

import { Callout } from 'fumadocs-ui/components/callout';

To start tracking user activity on your website, add the Rybbit tracking script to your site's HTML.

## Installation
The webpage should give you this script tag like this when you first add a website. Place it within the `<head>` section of your website. Include it on every page you wish to track.

```html
<script src="https://app.rybbit.io/api/script.js?siteId=YOUR_SITE_ID" async></script>
```
If you are self-hosting Rybbit `app.rybbit.io` will be domain of your Rybbit instance.

The site ID is passed in the script URL. The older `data-site-id="YOUR_SITE_ID"` attribute still works, but the URL form is recommended because script optimizers (WP Rocket, Perfmatters, FlyingPress and similar "delay JavaScript" features) rebuild script tags and drop `data-*` attributes, while leaving the URL intact.

<Callout type="info">
  **Bypass Ad Blockers**: Some ad blockers may block analytics scripts regardless of privacy compliance. To improve tracking accuracy, you can proxy the tracking script through your own domain. See our [Proxy Guide](/docs/proxy-guide) for setup instructions for Next.js, Nginx, Caddy, and more.
</Callout>

## Configuration

### Site Settings Configuration

Most tracking options are configured through your site settings in the Rybbit dashboard. The script automatically fetches these settings when it loads:

- **Automatic Initial Pageview**: Track the first pageview when the script loads
- **SPA Navigation**: Automatically track navigation in single-page applications
- **URL Parameters**: Include or exclude query strings from tracked URLs
- **Outbound Links**: Track clicks to external websites
- **Error Tracking**: Capture JavaScript errors and unhandled promise rejections
- **Session Replay**: Record user interactions for debugging and UX analysis
- **Web Vitals**: Collect Core Web Vitals performance metrics

### Script Attributes

These are the attributes you can pass to the script tag.

| Attribute         | Type       | Default | Description                                                                                                                                                             |
| :---------------- | :--------- | :------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data-skip-patterns` | `string` | `[]`   | A JSON string array of URL path patterns to ignore. Pageviews matching these patterns won't be tracked. Supports two types of wildcards: `*` (matches within a segment) and `**` (matches across segments), plus regex patterns prefixed with `re:`. |
| `data-mask-patterns` | `string` | `[]`   | A JSON string array of URL path patterns to mask for privacy. Pageviews matching these patterns will be tracked, but the actual URL path will be replaced with the pattern itself in analytics data. Supports the same wildcards and `re:` regex patterns as `data-skip-patterns`. |
| `data-debounce`   | `string`   | `"500"`| The delay (in milliseconds) before tracking a pageview after URL changes via the History API (`pushState`, `replaceState`). Set to `0` to disable debouncing. |
| `data-tag`        | `string`   | `""`   | A label applied to all events from this script instance. Useful for A/B testing, deploy versioning, or cohort segmentation. Max 256 characters. See [Tagging](/docs/tagging). |

{/* ### Feature Flags

The tracking script automatically evaluates client and both-runtime feature flags when it loads. Read evaluated flags from `window.rybbit` after the script is ready:

```javascript
window.rybbit.onReady((rybbit) => {
  const enabled = rybbit.flag("new_checkout", false);
  const config = rybbit.flagPayload("checkout_config", {});
});
```

Available methods:

| Method | Description |
| --- | --- |
| `flag(key, fallback)` | Returns a boolean, variant key, or fallback value. Also records a feature flag exposure once per flag value/version. |
| `flagPayload(key, fallback)` | Returns the payload for a flag, variant, or remote config. |
| `flags()` | Returns all evaluated flag values. |
| `flagPayloads()` | Returns all available flag payloads. |

See [Feature Flags](/docs/feature-flags) for targeting rules, remote config, and server-side evaluation. */}

### Hash routes and URL fragments

Rybbit automatically recognizes hash routes beginning with `#/` or `#!/`. For example, `/#/shipping` and `/#!/shipping` are both tracked as `/shipping`.

Ordinary fragments such as `#shipping` are ignored by default. If your site uses them for checkout stages or tabs, enable **Track URL fragments** in your site's **Tracking** settings. Rybbit will then track `/checkout/#shipping` and `/checkout/#payment` as separate paths that you can use in funnels and goals. Enable **SPA Navigation** to automatically record changes between these stages.

Automatic navigation records a pageview only when the tracked path or tracked query string changes. Ignored anchors and repeated navigation events do not add pageviews. Explicit calls to `rybbit.pageview()` still record a pageview for the current URL.

This setting applies to future events; previously discarded fragments cannot be recovered.

### Session Replay Attributes

These attributes configure rrweb session replay behavior. They only apply when session replay is enabled in your site settings.

| Attribute         | Type       | Default | Description                                                                                                                                                             |
| :---------------- | :--------- | :------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data-replay-mask-text-selectors` | `string` | `[]` | A JSON string array of CSS selectors for elements whose text content should be masked. Example: `'[".user-name", "#email"]'`. |
| `data-replay-block-class` | `string` | `"rr-block"` | CSS class name for elements to exclude entirely from recording. Elements with this class won't appear in replays. |
| `data-replay-block-selector` | `string` | `null` | CSS selector for elements to exclude from recording. Example: `".sensitive-content, #private-section"`. |
| `data-replay-ignore-class` | `string` | `"rr-ignore"` | CSS class name for elements whose input events should be ignored. The element appears but interactions aren't recorded. |
| `data-replay-ignore-selector` | `string` | `null` | CSS selector for elements whose input events should be ignored. Example: `"input[name='credit-card']"`. |
| `data-replay-mask-text-class` | `string` | `"rr-mask"` | CSS class name for elements whose text content should be masked (shown as asterisks). |
| `data-replay-mask-all-inputs` | `boolean` | `true` | When true, all input values are masked as asterisks for privacy. Set to `"false"` to disable. |
| `data-replay-mask-input-options` | `JSON object` | `{"password":true,"email":true}` | Specify which input types to mask. Example: `'{"password":true,"email":true,"tel":true}'`. |
| `data-replay-collect-fonts` | `boolean` | `true` | When true, website fonts are collected for accurate replay rendering. Set to `"false"` to reduce data size. |
| `data-replay-sampling` | `JSON object` | See below | Configure event sampling to reduce data volume. See sampling options below. |
| `data-replay-slim-dom-options` | `JSON object` | See below | Configure which DOM elements to exclude from recording. See slimDOM options below. |


### Pattern Matching Details

For `data-skip-patterns` and `data-mask-patterns`, two types of wildcards are supported:
- `*` - Matches any characters within a single path segment (doesn't match across `/`)
- `**` - Matches any characters across multiple path segments (includes `/`)

Examples:
- `/admin/*` matches `/admin/dashboard` but not `/admin/users/list`
- `/admin/**` matches both `/admin/dashboard` and `/admin/users/list`
- `/blog/*/comments` matches `/blog/post-123/comments` but not `/blog/category/post/comments`

Regex patterns are also supported when prefixed with `re:`. The regex is used as-is, so add `^` and `$` if you want a full-path match.

Examples:
- `re:^/users/\\d+$` matches `/users/123` but not `/users/abc`
- `re:^/private/.*$` matches anything under `/private/`

Example usage:
```html
data-skip-patterns='["/admin/**", "/blog/drafts/*", "/preview/**"]'
data-mask-patterns='["/users/*/settings", "/accounts/**", "/orders/*/details"]'
```

Regex usage:
```html
data-skip-patterns='["re:^/private/.*$"]'
data-mask-patterns='["re:^/users/\\d+/settings$"]'
```

**Example with debugging overrides:**

```html
<script
  src="https://app.rybbit.io/api/script.js"
  defer
  data-site-id="456"
  data-skip-patterns='["/admin/**", "/preview/*"]'
  data-mask-patterns='["/users/*/profile", "/orders/**"]'
  data-replay-mask-text-selectors='[".user-name", "#email"]'
  data-debounce="300"
></script>
```

## Session Replay Configuration

When session replay is enabled in your site settings, you can fine-tune the recording behavior using the attributes above. This section covers the more complex configuration options.

### Privacy Controls

Protect sensitive user information using these options:

| Option | Best For |
|--------|----------|
| `data-replay-block-*` | Completely hide elements (e.g., admin panels, modals with PII) |
| `data-replay-ignore-*` | Show elements but don't record interactions (e.g., form fields) |
| `data-replay-mask-*` | Show element structure but hide text content (e.g., names, emails) |

**Example - Comprehensive privacy setup:**
```html
<script
  src="https://app.rybbit.io/api/script.js"
  defer
  data-site-id="123"
  data-replay-block-selector=".admin-panel, #payment-modal"
  data-replay-ignore-selector="input[name='ssn'], input[name='credit-card']"
  data-replay-mask-text-selectors='[".user-name", "#email", "[data-pii]"]'
  data-replay-mask-input-options='{"password":true,"email":true,"tel":true}'
></script>
```

### Sampling Configuration

The `data-replay-sampling` attribute controls how frequently events are recorded. Lower sampling rates reduce data volume but may miss some interactions.

**Default sampling configuration:**
```json
{
  "mousemove": false,
  "mouseInteraction": {
    "MouseUp": false,
    "MouseDown": false,
    "Click": true,
    "ContextMenu": false,
    "DblClick": true,
    "Focus": true,
    "Blur": true,
    "TouchStart": false,
    "TouchEnd": false
  },
  "scroll": 500,
  "input": "last",
  "media": 800
}
```

**Sampling options:**
- `mousemove`: `false` to disable, or number (ms) for sampling interval
- `scroll`: Sampling interval in milliseconds (e.g., `500` = sample every 500ms)
- `input`: `"last"` to only record final value, or `"all"` to record every keystroke
- `media`: Sampling interval for media events in milliseconds

**Example - Enable mouse tracking for UX analysis:**
```html
data-replay-sampling='{"mousemove":100,"scroll":150}'
```

### SlimDOM Options

The `data-replay-slim-dom-options` attribute controls which DOM elements are excluded from recording to reduce data size.

**Default slimDOM configuration:**
```json
{
  "script": false,
  "comment": true,
  "headFavicon": true,
  "headWhitespace": true,
  "headMetaDescKeywords": true,
  "headMetaSocial": true,
  "headMetaRobots": true,
  "headMetaHttpEquiv": true,
  "headMetaAuthorship": true,
  "headMetaVerification": true
}
```

Set to `true` to enable all slimDOM options, or `false` to disable all:
```html
data-replay-slim-dom-options="true"
```

### Complete Example

```html
<script
  src="https://app.rybbit.io/api/script.js"
  defer
  data-site-id="456"
  data-skip-patterns='["/admin/**", "/preview/*"]'
  data-mask-patterns='["/users/*/profile", "/orders/**"]'
  data-debounce="300"
  data-replay-block-selector=".sensitive-content"
  data-replay-mask-text-selectors='[".user-name", "#email"]'
  data-replay-mask-all-inputs="true"
  data-replay-collect-fonts="false"
  data-replay-sampling='{"mousemove":false,"scroll":500}'
></script>
```

---

--- METRICS ---
- Files Extracted: 97
- Estimated Token Budget: ~171874 tokens
- Canonical Reference: https://codewiki.google/github.com/rybbit-io/rybbit
