# Introduction (/docs)
WebSight is open-source, privacy-first web analytics. You get a realtime dashboard next to product analytics like goals, funnels, and retention, all in one place. It is cookieless by default, so stateless mode needs no consent banner, and the tracker is under 3 KB gzipped.
## Get started [#get-started]
## Explore the dashboard [#explore-the-dashboard]
## How it works [#how-it-works]
You add one script tag to your site. The tracker sends pageviews and events to WebSight's ingest endpoint, where each visitor is counted with a daily-rotating salted hash in stateless mode, or a localStorage id in persistent mode. Everything lands in a single realtime dashboard. No cookies are ever set, and no raw IP or user agent is stored. See [Privacy](/docs/resources/privacy) for the full picture.
## Two privacy modes [#two-privacy-modes]
You choose a privacy mode when you create a site, and you can change it later in Settings. A switch applies to new traffic only: visits already recorded keep the ids they were counted with.
| Mode | How visitors are counted | Trade-off |
| ------------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Stateless (default) | No cookies, no stored ids. Visitors are counted with a salted hash that resets daily. | No consent banner needed. Visitor ids reset each day. |
| Persistent | A random id in localStorage gives visitors a durable identity across days. Still no cookies. | Unlocks [Profiles](/docs/dashboard/profiles) and [Retention](/docs/dashboard/retention), which need a stable identity. |
# Audience (/docs/dashboard/audience)
Audience breaks your visitors down by where they are and what they use to browse. It respects the shared [date range and filters](/docs/dashboard/filters).
## What you see [#what-you-see]
Five ranked cards:
* **Countries** - top 20, with flags.
* **Devices** - top 10.
* **Browsers** - top 10.
* **Operating systems** - top 10.
* **Languages** - top 10.
## What you can do [#what-you-can-do]
* Click any row to filter the whole dashboard to that country, device, browser, OS, or language (see [click to filter](/docs/dashboard/filters#click-to-filter)).
* Stack filters to answer questions like "mobile Safari visitors from Germany" and watch every other screen narrow with it.
Device, browser, and OS are parsed from the user agent at ingest, and the raw user agent is never stored. Country comes from your CDN's request headers, so it is available at country level but not for local or dev traffic. See [privacy](/docs/resources/privacy) for what is and is not retained.
# Errors (/docs/dashboard/errors)
Errors captures JavaScript exceptions from real visitors, groups them so one bug is one row, and gives you a triage workflow to work through them.
## Enable it [#enable-it]
Add `data-errors` to the snippet. Its presence loads the lazy tracker chunk that does the capturing, so it costs nothing on sites that leave it off.
```html
```
See [The script](/docs/tracking/script) for the full attribute reference.
**What is captured**: uncaught errors and unhandled promise rejections, with the message, error type, source file, line and column, and stack trace. Resource-load failures (a broken image or a script that 404s) are not captured.
## Groups and triage [#groups-and-triage]
Errors are fingerprinted by a normalized message plus the top stack frames, so the same bug groups together across different visitors and stays stable across deploys. Work through them using the tabs **Open**, **Ignored**, **Resolved**, and **All**, with actions to **Resolve**, **Ignore**, or **Reopen** a group.
If a resolved group starts happening again it auto-reopens, flagged **Regressed**, so a fixed bug that comes back does not slip past you. An **External** badge marks groups coming from third-party scripts not served from your origin.
## Limits [#limits]
* The client caps 5 identical errors per page load, so one runaway loop cannot flood a session.
* Ingest caps 120 occurrences per group per minute; anything beyond that is counted as dropped rather than stored.
## Error detail [#error-detail]
Open a group to see its occurrences, affected visitors, first and last seen, and a trend over time. Breakdowns split the group by page, browser, OS, and country, and a sample stack trace shows you where to start looking.
# Events (/docs/dashboard/events)
Events is where your custom events show up: the actions you send from your own code, alongside the automatic ones the tracker captures.
## Before you start [#before-you-start]
Custom events only appear once your site sends them. Add a call wherever the action happens:
```js
websight.track("signup", { plan: "pro" });
```
See [Custom events](/docs/tracking/custom-events) for the full API, the no-code `data-ws-event` attribute, and property limits. Automatic events like outbound clicks and downloads show up here too without any extra work.
## What you see [#what-you-see]
The main screen lists every event name your site has sent, with three columns:
| Column | Meaning |
| --------- | --------------------------------------- |
| Count | Total occurrences in the selected range |
| Visitors | Unique visitors who triggered it |
| Last seen | Time since the most recent occurrence |
An event that has not been seen in more than 30 days gets a **Stale** badge, so you can spot instrumentation you have since removed or renamed.
## Event detail [#event-detail]
Click an event name to open its detail page. It shows:
* A count-over-time chart of occurrences.
* A property explorer: every property key on the event and a breakdown of its values. Value breakdowns are sampled, so treat them as a representative shape rather than an exact tally.
* Recent occurrences, each expandable to the full JSON properties, with a link to the visitor's [profile](/docs/dashboard/profiles).
## Event dictionary [#event-dictionary]
Every event name gets a dictionary row created automatically the first time it is ingested, so you never have to register events up front. As the site owner you can annotate each row to keep your team aligned:
* A **description** of what the event means, up to 280 characters.
* The **expected properties** it should carry, up to 30 keys of 60 characters each.
These annotations are documentation only. They do not change ingestion or filter what is stored, and editing them is limited to the site owner.
# Filters and date ranges (/docs/dashboard/filters)
Date range, comparison, and filters are shared controls that sit above every dashboard screen. Set them once and they follow you as you move between [Overview](/docs/dashboard/overview), [Sources](/docs/dashboard/sources), [Sessions](/docs/dashboard/sessions), and the rest. They also live in the URL, so any view you are looking at is a link you can share.
## Date ranges [#date-ranges]
Pick from four presets: 24h, 7d, 30d, and 90d. The default is 7d. There is no custom date input yet, so you cannot enter an arbitrary start and end.
Chart granularity follows the range automatically:
| Range | Granularity |
| ----- | ----------- |
| 24h | Hourly |
| 7d | Daily |
| 30d | Daily |
| 90d | Weekly |
The selected range is stored in the URL as `?range=`, so a view stays put when you refresh or send the link to a teammate.
## Comparisons [#comparisons]
Turn on a comparison to see how the current range stacks up against an earlier one. Three modes:
* **Off** - no comparison.
* **Previous period** - the equal-length window immediately before the current range.
* **Year over year** - the same range one year earlier.
When a comparison is on, each metric card shows a delta against the comparison window. Bounce rate is the exception: its delta is inverted, because a lower bounce rate is the improvement.
## Filters [#filters]
A filter narrows every screen to the traffic you care about. Each filter is a dimension, an operator, and one or more values.
Operators: **is**, **is not**, **contains**, **does not contain**.
Filterable dimensions:
* Page (path), Entry page, Exit page
* Channel, Referrer
* UTM source, medium, campaign, term, content
* Country, Region, City
* Device type, Browser, OS, Language
* Event name
You can also filter on any custom event property with the `prop:` syntax, for example `prop:plan`. As you type a value, WebSight suggests matches ranked by traffic. A single filter accepts up to 20 values.
## Click to filter [#click-to-filter]
You rarely need to build filters by hand. Click any row in any breakdown, anywhere in the dashboard, and it becomes a filter. Click a country in [Audience](/docs/dashboard/audience), a referrer in Sources, a page in [Pages](/docs/dashboard/pages), and the whole dashboard narrows to it.
Filters are encoded in the URL alongside the range, so a filtered view is fully shareable. Press **Esc** to clear all filters at once.
# Funnels (/docs/dashboard/funnels)
A funnel is an ordered sequence of steps that a single visitor completes in order, so you can see exactly where people fall out of a flow.
## Build a funnel [#build-a-funnel]
Add between 2 and 8 steps. Each step is one of:
* **Page**, matched exactly, by contains, or by wildcard (`*`).
* **Event**, an [event name](/docs/dashboard/events) with optional property filters.
* **Goal**, a reference to a saved [goal](/docs/dashboard/goals).
Reorder or remove steps as you build. Set a **conversion window**, the maximum time a visitor has to get from the first step to the last: 30 minutes, 1 day, 7 days, or 30 days. The default is 1 day. A live preview over the last 7 days shows how many visitors entered, how many completed, and the overall rate.
Stateless sites cap the conversion window at 1 day, because visitor ids reset daily and cannot be matched across days. Switch the site to persistent mode or call [identify()](/docs/tracking/identify) to unlock the 7-day and 30-day windows.
## Reading a funnel [#reading-a-funnel]
Matching is sequential and per visitor: a visitor counts at a step only if they reached every step before it, in order, within the window. For each step you see:
* Unique visitors who reached it, drawn as a bar relative to the first step.
* Drop-off count and percentage from the previous step.
* Median time from the previous step.
Click any step to split its visitors into converted and dropped lists. For the visitors who finished the whole funnel, a time-to-convert histogram shows how long completion took. Funnels are comparison-aware, so they respect the comparison mode set on the dashboard.
# Globe (/docs/dashboard/globe)
Globe plots your audience on a rotating 3D world. It has two modes: a Live mode for who is on the site right now, and a Range mode for where traffic came from over the selected [date range](/docs/dashboard/filters).
## What you see [#what-you-see]
A side panel lists your top 20 locations with tabs for Countries, Regions, and Cities, plus a badge for how many people are online. The globe itself changes with the mode.
### Live mode [#live-mode]
One avatar per active session from the last 2 minutes, capped at 80 avatars. Each avatar sits at real city coordinates when the location is known, otherwise it is scattered near the country's center. This is the map counterpart to [Realtime](/docs/dashboard/realtime).
### Range mode [#range-mode]
Country aggregates instead of individual sessions: up to 4 avatars per country and 55 avatars in total, sized to reflect volume over the range.
## What you can do [#what-you-can-do]
* In Live mode, click an avatar to open that visit's [session](/docs/dashboard/sessions) details.
* In Range mode, click a country to filter the whole dashboard to it.
* Switch the side-panel tabs to break locations down by country, region, or city.
Location comes from your CDN's request headers, not a local database. Traffic from localhost or a dev environment has no location, so it will not appear on the globe.
# Goals (/docs/dashboard/goals)
A goal turns a pageview or a custom event into a conversion you can track over time, attach a value to, and use as a funnel step.
## Create a goal [#create-a-goal]
Every goal needs a name (up to 120 characters) and one of two kinds:
* **Page view goal** matches a path. Choose how it matches: **is exactly**, **contains**, or **matches wildcard** using `*` (for example `/blog/*`).
* **Custom event goal** matches an [event name](/docs/dashboard/events), with optional property conditions using **is**, **is not**, or **contains**.
You can attach an optional value per conversion, a currency amount. The goal's total Value is unique converters multiplied by that value.
While you edit, a live preview tells you how the goal would perform: "would have matched N events from M visitors in the last 7 days". If your site has no goals yet, WebSight offers starter templates (a Newsletter signup event, a Purchase page, a Signed up event) to fill in the dialog.
## Reading the list [#reading-the-list]
Each goal is a row with a sparkline plus three numbers:
| Column | Meaning |
| ----------- | ----------------- |
| Uniques | Unique converters |
| Conversions | Total conversions |
| Rate | Conversion rate |
Edit and Archive appear on hover. Archiving is a soft delete: the goal stops counting but its history is not destroyed.
## Goal detail [#goal-detail]
Click a goal to open its detail page:
* Tiles for Conversions, Unique converters, Conversion rate, and Value.
* A conversion trend over time.
* Top countries and referrers of the visitors who converted.
* The recent converting sessions, so you can inspect the real journeys behind the number.
# Journeys (/docs/dashboard/journeys)
Journeys is a Sankey diagram of the pageview paths visitors take within a session, so you can see the routes people actually follow rather than the ones you designed. On mobile it falls back to a top-paths list.
## What you see [#what-you-see]
Each column is a step in the journey and each node is a page, sized by how many sessions passed through it. The flows between nodes show where visitors went next. Loops stay distinct: a page revisited later in the session appears as a separate node in its own column, so a cycle never collapses into itself.
## Controls [#controls]
* **Direction**: build paths forward from where sessions **start**, or backward from where they **end**.
* **Steps**: how many columns to show, 2 to 6 (default 4).
* **Top N per column**: how many nodes to keep in each column, 5, 8, or 12. The rest are grouped away.
Right-click a node to set it as the start or end anchor, filter the whole dashboard to that page, or view the sessions that passed through it.
## Collapse rules [#collapse-rules]
To keep noisy URL spaces readable, add wildcard patterns like `/blog/*` (up to 20). Every path that matches a pattern merges into a single node, so hundreds of article URLs become one "blog" step instead of crowding the diagram.
## Sampling and export [#sampling-and-export]
Above 200,000 sessions the diagram is computed from a sample and flagged **sampled**, so read proportions rather than exact counts at that scale. You can export the current diagram as a PNG.
# Overview (/docs/dashboard/overview)
Overview is the home screen for a site. It puts your headline metrics, a trend chart, and your top breakdowns on one page, all responding to the shared [date range and filters](/docs/dashboard/filters).
## What you see [#what-you-see]
Six metric cards run across the top. Each definition follows the counting rules described in [how counting works](/docs/getting-started/how-counting-works).
| Card | What it counts |
| --------------- | ------------------------------------------------------ |
| Unique Visitors | Distinct visitors over the range. |
| Sessions | Visits, grouped by a 30-minute inactivity window. |
| Page Views | Total pageview events. |
| Views/Session | Page views divided by sessions. |
| Bounce Rate | Share of sessions with one pageview or fewer. |
| Avg. Duration | Average time from a session's first event to its last. |
Below the cards is a timeseries chart. When a [comparison](/docs/dashboard/filters#comparisons) is on, the comparison window is drawn as an overlay and each card shows its delta.
Four tabbed breakdown cards show the top 8 rows each:
* **Pages** - Top, Entry, Exit
* **Sources** - Channels, Referrers, Campaigns
* **Locations** - Countries, Regions, Cities
* **Devices** - Browser, OS, Device
Members also see a **Custom Events** card and a **Goals** summary card. These are hidden on public shares by default.
## What you can do [#what-you-can-do]
* Click **Visitors**, **Sessions**, or **Page Views** on their cards to switch which series the chart plots.
* Switch tabs on any breakdown card to change the dimension.
* Click any breakdown row to add it as a filter (see [click to filter](/docs/dashboard/filters#click-to-filter)). The whole page narrows, and you can drill again from there.
* Jump to a full screen for any breakdown: [Pages](/docs/dashboard/pages), [Sources](/docs/dashboard/sources), [Audience](/docs/dashboard/audience).
## Getting started checklist [#getting-started-checklist]
Until a site has recorded at least 50 events, a getting-started checklist sits on Overview: install the snippet, record a first pageview, send a first custom event, and set a first goal. It hides itself automatically once the site passes 50 events, so an established site never sees it.
# Pages (/docs/dashboard/pages)
Pages ranks the URLs on your site by traffic, with tabs to switch between the most viewed pages and where visits begin and end.
## What you see [#what-you-see]
A single **All Pages** card listing your top 100 paths, with three tabs:
* **Top** - pages ranked by pageviews, with the pageview count shown per row.
* **Entry** - the first page of each session. This is where visits begin.
* **Exit** - the last page of each session. This is where visits end.
## What you can do [#what-you-can-do]
* Switch tabs to move between Top, Entry, and Exit.
* Click any row to filter the whole dashboard to that page (see [click to filter](/docs/dashboard/filters#click-to-filter)).
To see how visitors move between pages rather than which are most popular, use [Journeys](/docs/dashboard/journeys). To narrow this list to a segment first, set a [filter](/docs/dashboard/filters).
# Profiles (/docs/dashboard/profiles)
Profiles gives you one lifetime record per identity: every session and event tied to a single person, rather than a windowed aggregate.
## Before you start [#before-you-start]
Profiles is locked in stateless mode. A lifetime record needs a stable identity, and stateless visitor ids reset daily. Switch the site to persistent mode or call [identify()](/docs/tracking/identify) to unlock it.
## What you see [#what-you-see]
There is one profile per identity: the user id you set with [identify()](/docs/tracking/identify) when the visitor is identified, otherwise the persistent visitor id. Each profile summarizes:
* Sessions and pageviews.
* First seen and last seen.
* Top country and device.
* Any traits passed to `identify()`.
Search by id to jump to a specific person. The list shows up to 100 profiles.
## Profile detail [#profile-detail]
Open a profile to see its full session history. Each session expands in a drawer with its event timeline, and a per-event-name frequency shows which actions this person takes most.
## Deleting a visitor [#deleting-a-visitor]
Deleting a visitor is a permanent, GDPR-style erasure. It removes every event, session, and profile row tied to that identity, with no recovery. Only the site owner can do this.
# Realtime (/docs/dashboard/realtime)
Realtime shows activity as it happens: how many people are on the site, what they are viewing, and a live feed of events. It is the screen to watch after a launch or a post going out.
## What you see [#what-you-see]
* **Visitors online now** - a count of visitors active in the last 5 minutes. Presence is kept alive by the tracker heartbeat, which pings every 45 seconds while the tab is visible, so someone reading a page still counts as online.
* **Pageviews per minute** - a bar chart of the last 30 minutes, refreshing every 30 seconds.
* **Live lists** - Active pages, Referrers, and Countries over a rolling 5-minute window, refreshing every 10 seconds.
* **Activity feed** - a reverse-chronological stream of the last 30 minutes, up to 50 events, refreshing every 5 seconds.
## What you can do [#what-you-can-do]
* Click any row in a live list to jump to [Overview](/docs/dashboard/overview) pre-filtered to that page, referrer, or country.
* Hover the activity feed to freeze it, so a fast-moving stream holds still while you read.
The activity feed never exposes visitor ids; it shows what happened, not who. For a durable, per-visit record rather than a live stream, use [Sessions](/docs/dashboard/sessions).
The heartbeat stops when a tab is closed or hidden, so someone who leaves drops out of the online count within a few minutes rather than lingering.
# Session replay (/docs/dashboard/replays)
Session replay records a visual reproduction of a visitor's session and lets you play it back. It is the one WebSight feature that captures how someone actually used your site, rather than only counting what they did, so it is off by default, opt-in per site, sampled, and heavily masked.
Replay records real user behavior. The cookieless, no-banner posture that covers WebSight's core analytics does not extend to replay. Read [Privacy and masking](#privacy-and-masking) and [Consent](#does-replay-need-consent) before you turn it on.
## What it records [#what-it-records]
Replay stores DOM snapshots, not video. When a sampled visit starts, the recorder takes a full snapshot of the page, then streams the changes as the visitor scrolls, clicks, types, and navigates. Playback reconstructs the page from those snapshots, so you see the real layout and content, styled by your site's own CSS.
It is not a screen recording. There is no camera, no microphone, and nothing outside the browser tab is captured.
## How to enable it [#how-to-enable-it]
Turning replay on takes two steps, both required:
1. In the site's **Settings**, open the **Session replay** card and turn on the toggle. The first time you enable it, a dialog explains what recording captures and the consent question. Confirm to switch it on.
2. Add `data-replay` to your tracker snippet so the recorder chunk loads on your site:
```html
```
The recorder ships as a separate lazy chunk that only loads when `data-replay` is present and the site setting is on, so pages without replay never pay for it.
If you serve the tracker first-party to get past adblockers, your proxy rule must also forward `/api/replay`, the same way it forwards `/api/track`. See [Adblockers and proxying](/docs/tracking/adblockers); the `/stats/*` prefix rule already covers it.
## Sampling [#sampling]
Set the sample rate in the settings card. It is the share of page loads chosen for recording, from 5% to 100%. The decision is made once per visit and holds across in-app navigations, so a recorded visit stays recorded end to end and an unrecorded one is never partly captured.
Sample down on high-traffic sites. You rarely need every session to spot a pattern, and sampling keeps storage and cost in proportion to what you review.
## Privacy and masking [#privacy-and-masking]
Masking is on by the strongest default and layered:
* **Input fields are always masked.** Every value typed into a form is replaced before it leaves the browser. This cannot be turned off. Password, email, and payment fields are never recorded.
* **Mask all text** is a per-site setting in the replay card. Turn it on to replace every text node with blocks, so only structure and layout are recorded, never words.
* **Per-element control** with attributes: add `data-ws-mask` to any element to hide its contents, or `data-ws-unmask` to allow a specific element through when you have masked broadly.
* **Cross-origin iframes are not recorded.** Anything from another origin, such as an embedded payment form, is left out.
* **Canvas and WebGL are not recorded** in this version, so nothing drawn to a canvas is captured.
Masking happens in the browser, before anything is sent. A masked value is never transmitted and never stored.
## Does replay need consent [#does-replay-need-consent]
Stated plainly: recording a session may require visitor consent under the GDPR and the ePrivacy directive, even though WebSight's core analytics does not. WebSight's [cookieless, no-banner design](/docs/resources/privacy) is what lets many teams run the tracker without a consent prompt. Replay is different in kind, because it records behavior, so that reasoning does not carry over.
We are describing a mechanism, not giving legal advice. Whether you need consent, and how you collect it, is a decision for you and your own counsel. WebSight gives you the tools to record as little as you need: keep replay off, sample low, and mask hard.
## Retention and deletion [#retention-and-deletion]
Each site sets its own retention window in the replay card: 7, 14, 30, 60, or 90 days, defaulting to 30. When a recording reaches the end of that window it is deleted permanently. Both the metadata row and the stored snapshots go, with nothing left to restore from.
Deletion also happens on demand:
* Erasing a visitor from [Profiles](/docs/dashboard/profiles) cascades to their recordings, so a GDPR erasure request removes the replays along with the events and sessions.
* You can delete any single recording from the player.
## Limits per recording [#limits-per-recording]
To keep one runaway tab from filling storage, each recording stops at hard caps: **60 minutes** of duration, **10 MB** of compressed snapshots, or **500 chunks**, whichever comes first. When a cap is hit the recorder stops silently and the visit continues to be tracked as normal. Core analytics is never affected by replay, so a storage error only ever costs you the recording, never the pageview.
## Self-hosting [#self-hosting]
Replay stores its snapshots in object storage over the S3 protocol, kept separate from your analytics database. Point it at any S3-compatible endpoint with the `REPLAY_S3_*` environment variables:
| Variable | Purpose |
| ----------------------------- | ------------------------------------ |
| `REPLAY_S3_ENDPOINT` | The S3 endpoint URL. |
| `REPLAY_S3_BUCKET` | The bucket that holds recordings. |
| `REPLAY_S3_ACCESS_KEY_ID` | Access key. |
| `REPLAY_S3_SECRET_ACCESS_KEY` | Secret key. |
| `REPLAY_S3_REGION` | Region, or `auto` for Cloudflare R2. |
Cloudflare R2, MinIO, AWS S3, and Supabase Storage's S3 endpoint all work. Until these variables are set, the replay feature stays hidden, because there is nowhere to store recordings.
# Retention (/docs/dashboard/retention)
Retention groups visitors by when they first arrived and tracks how many come back in each following period, so you can see whether your product actually keeps people.
## Before you start [#before-you-start]
Retention is locked in stateless mode. Measuring who returns across days needs a stable identity, and stateless visitor ids reset daily. Switch the site to persistent mode or call [identify()](/docs/tracking/identify) to unlock it.
## What you see [#what-you-see]
The cohort triangle has one row per cohort and one column per period since entry. Each cell is the share of that cohort still active in that period.
* **Interval**: Daily, Weekly, or Monthly (default Weekly).
* **Periods**: how many columns to show, 8, 12, or 26 (default 12).
* **Basis**: read each cell as a percentage of the original cohort, or as a percentage of the previous period.
Two criteria define what counts:
* **Entry criterion**: first seen (default) or a saved [goal](/docs/dashboard/goals).
* **Return criterion**: any visit (default) or a saved goal.
## Reading the cells [#reading-the-cells]
* **Dashed** cells mark a period still in progress, so its number will keep rising.
* **Hatched** cells mark low sample, fewer than 10 visitors, where the percentage is noisy.
Click a cell to drill into its visitor list. Click a cohort label to plot its decay curve, and compare up to 6 cohorts on one chart. A weighted average retention curve summarizes all cohorts together.
# Sessions (/docs/dashboard/sessions)
Sessions is the per-visit record: one row per session, newest first, each expandable into its full event timeline. It works in stateless mode, so you get the visit-level view without cookies or stored ids.
## What you see [#what-you-see]
The list shows the newest sessions first and loads 50 at a time as you scroll. Each row carries:
| Column | What it shows |
| ------------ | --------------------------------- |
| Visitor | A short label for the visitor. |
| Location | Country and city where known. |
| Device | Device, browser, and OS. |
| Channel | How the visit was acquired. |
| Entry / Exit | First and last page of the visit. |
| Pageviews | Pageviews in the session. |
| Duration | First event to last event. |
| Bounce | Whether the session bounced. |
Open sessions carry a **LIVE** badge.
## What you can do [#what-you-can-do]
* Scroll to load more; the list pages in 50 sessions at a time.
* Click a row to open a drawer with the session's metadata and a timeline of its events. Live sessions refresh every 5 seconds. From the drawer you can jump to the visitor's [profile](/docs/dashboard/profiles).
* Export the current view to CSV. The file is generated client-side and carries these columns: Started, Visitor, Country, City, Device, Browser, OS, Channel, Referrer, Entry, Exit, Pageviews, Duration, Bounce.
## How filters apply [#how-filters-apply]
The shared [filters](/docs/dashboard/filters) apply here too, with one rule worth knowing: a session matches a filter if any of its events match. Filter by a page and you get every session that touched that page, not only the ones that entered on it.
# Sources (/docs/dashboard/sources)
Sources shows how people arrive at your site, from the broad channel down to the exact campaign. Everything here respects the shared [date range and filters](/docs/dashboard/filters).
## What you see [#what-you-see]
Four cards:
* **Channels** - top 10, the high-level bucket each visit is grouped into.
* **Referrers** - top 20, the sites that linked to you.
* **UTM Sources** - top 20, the `utm_source` values on your inbound links.
* **UTM Campaigns** - top 20, the `utm_campaign` values on your inbound links.
## What you can do [#what-you-can-do]
* Click any row to filter the dashboard to that channel, referrer, or UTM value.
* Combine with a [comparison](/docs/dashboard/filters#comparisons) to see which channels are growing.
## How channels are classified [#how-channels-are-classified]
Every visit is sorted into exactly one of six channels. WebSight checks in priority order and stops at the first match:
1. **Paid** - a paid medium (cpc, ppc, display, affiliate, and similar) or a click id such as `gclid`, `fbclid`, `msclkid`, `ttclid`, `twclid`, or `li_fat_id`.
2. **Email** - an email medium, an email source, or a webmail referrer domain.
3. **Social** - a social medium or a known social domain (Facebook, X, LinkedIn, Reddit, Hacker News, Bluesky, and others).
4. **Organic Search** - a known search domain (Google, Bing, DuckDuckGo, and others).
5. **Referral** - any other referring site. UTM tags with no referrer also land here.
6. **Direct** - no referrer and no campaign tags at all.
A self-referral, where the referrer is one of your own domains, counts as **Direct** rather than Referral.
## UTM tracking [#utm-tracking]
WebSight keeps only a small allowlist of query parameters and strips the rest for privacy. The parameters retained are `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`, `ref`, and `source`. Anything else in the URL is dropped before storage.
For example, this inbound link:
```
https://yourdomain.com/pricing?utm_source=newsletter&utm_medium=email&utm_campaign=launch&session=abc123
```
is stored as `utm_source=newsletter`, `utm_medium=email`, `utm_campaign=launch`, and the `session=abc123` parameter is discarded.
# Web Vitals (/docs/dashboard/web-vitals)
Web Vitals reports Google's Core Web Vitals measured on your real visitors' devices, so you see field performance rather than a lab score.
## Enable it [#enable-it]
Turning this on takes two switches:
1. Add `data-vitals` to the snippet. Its presence loads the lazy tracker chunk that measures vitals.
2. Confirm it in the dashboard: a "Turn on Web Vitals" card appears with an **I've added it** button.
```html
```
On a high-traffic site you can sample by giving `data-vitals` a rate between 0 and 1, for example `data-vitals="0.1"` to measure 10% of page loads. See [The script](/docs/tracking/script) for the full attribute reference.
## The metrics [#the-metrics]
All values are reported at the 75th percentile (p75), and ratings are computed server-side from the raw values.
| Metric | Good | Poor |
| ------------------------------- | ------- | ------- |
| LCP (Largest Contentful Paint) | 2500 ms | 4000 ms |
| INP (Interaction to Next Paint) | 200 ms | 500 ms |
| CLS (Cumulative Layout Shift) | 0.1 | 0.25 |
| FCP (First Contentful Paint) | 1800 ms | 3000 ms |
| TTFB (Time to First Byte) | 800 ms | 1800 ms |
## Reading the screen [#reading-the-screen]
* p75 tiles, one per metric. Each stays hidden until it has at least 30 samples, so an early number never misleads you.
* A device toggle for All, Desktop, or Mobile.
* A trend chart, plus breakdowns by device, browser, and country.
* A per-page table with the attributed element: the DOM element responsible for that page's score.
Safari does not report INP or LCP, so those metrics reflect other browsers only. The web-vitals library measures hard page loads, so SPA soft navigations are excluded from these numbers.
# How WebSight counts (/docs/getting-started/how-counting-works)
Every metric in the dashboard comes from the same event stream. This page explains how those events become visitors, sessions, and the rates you see. Bots are never counted in any of them.
## Visitors [#visitors]
A unique visitor is a distinct visitor id over your events. How that id is formed depends on your [privacy mode](/docs/resources/privacy):
* **Stateless** (default): the server derives the id from a salted daily hash. The salt rotates every day, so visitor ids reset each day. A visitor returning tomorrow is counted as new.
* **Persistent**: a durable id stored in localStorage gives visitors a stable identity across days.
Neither mode uses cookies, and no raw IP or user agent is ever stored.
## Sessions [#sessions]
A session groups a visitor's activity using a 30-minute inactivity window. An event more than 30 minutes after the previous one starts a new session. Each visitor has at most one open session per site at a time, and stale sessions are closed by a background job.
## Bounce rate [#bounce-rate]
A session is a bounce when it has at most one pageview. Bounce rate is bounces divided by total sessions.
## Duration [#duration]
Session duration is the time from the first event to the last event in that session. A session with a single hit has a duration of 0. Average duration is the mean across sessions.
## Pageviews and views per session [#pageviews-and-views-per-session]
Pageviews are the count of `pageview` events. Views per session is pageviews divided by sessions.
## What gets filtered [#what-gets-filtered]
Some traffic and data never make it into your reports:
* **Bots.** The server drops known bots by user agent, covering crawlers and spiders, headless browsers, AI crawlers, and uptime monitors. A request with a missing or empty user agent is treated as a bot.
* **Query parameters.** The tracker keeps only `utm_*`, `ref`, and `source` from the URL query string. Everything else is stripped for privacy before anything is stored.
For how this shapes the numbers, see [Privacy](/docs/resources/privacy). To slice the traffic that is kept, see [Filters](/docs/dashboard/filters).
# Install on your platform (/docs/getting-started/install)
Pick your platform below. Each snippet is the same tracker, reframed for where that platform expects custom code. Replace `yourdomain.com` with your site's domain.
```html
```
Paste inside your ``, before the closing tag.
```tsx
import Script from "next/script";
// in app/layout.tsx or root layout
```
Use `next/script` with the defer strategy in your root layout. Prefer a component? Install the [JavaScript SDK](/docs/tracking/sdk) and drop `` into your layout instead.
```html
```
Add to `index.html` so it loads once, outside the React tree.
```html
```
Add to `index.html` in your project root.
```html
```
Paste into your theme's `header.php` before ``, or use a "header scripts" plugin (e.g. WPCode). No plugin required.
```html
```
Online Store → Themes → Edit code → `layout/theme.liquid`, before ``.
```html
```
Project Settings → Custom Code → Head Code. Publish to apply.
```html
```
Site Settings → General → Custom Code → End of `` tag.
```html
```
New Tag → Custom HTML, paste the snippet, trigger on All Pages. Prefer direct install if possible (GTM adds latency).
## Single-page apps [#single-page-apps]
Route changes are tracked automatically. The tracker patches the History API (`pushState` and `replaceState`) and listens for `popstate`, so client-side navigations count as pageviews with no extra setup. If your app uses a hash router, add `data-hash` to the snippet so `hashchange` navigations are tracked too. See the [script reference](/docs/tracking/script) for every attribute.
## Behind an adblocker? [#behind-an-adblocker]
Adblockers can block third-party analytics domains. Serve the tracker from your own domain to make requests same-origin and unblockable. See [Adblockers](/docs/tracking/adblockers) for the one-rule proxy setup.
# Quickstart (/docs/getting-started/quickstart)
Create a site, drop one script tag on your pages, and watch your first event arrive.
### Create your site [#create-your-site]
Sign in with Google at [websight.srexrg.me](https://websight.srexrg.me). Your account is created automatically on first sign-in.
Enter your domain, like `yourdomain.com`. The protocol and any `www.` prefix are stripped for you, so you never type `https://`.
Then choose a privacy mode:
* **Stateless** (recommended): no cookies, no consent banner, visitor ids reset daily.
* **Persistent**: a durable visitor id in localStorage that unlocks Profiles and Retention.
You can change the mode later under Settings → Privacy mode. Switching applies to new traffic only, so history keeps whichever ids it was recorded with.
### Add the snippet [#add-the-snippet]
Paste this into your site's ``, replacing `yourdomain.com` with your domain:
```html
```
Using a framework or a CMS? See [Install on your platform](/docs/getting-started/install) for ready-made snippets for all nine platforms, including Next.js, WordPress, and Shopify. On a build step, you can install the [JavaScript SDK](/docs/tracking/sdk) instead, which ships typed `init`/`track` calls and a React `` component.
### Verify [#verify]
The setup screen polls until your first event arrives, then echoes it back with the visitor's location and browser. The tracker flushes the very first event immediately, so this usually takes only seconds after you deploy.
The tracker is disabled on localhost. Visit your deployed site to trigger the first event. See [Exclude traffic](/docs/tracking/exclude-traffic) for how self-exclusion works.
### What's next [#whats-next]
* Send [custom events](/docs/tracking/custom-events) for signups, clicks, and anything else you want to measure.
* Turn key actions into [goals](/docs/dashboard/goals) to track conversions.
* Serve the tracker first-party to get past [adblockers](/docs/tracking/adblockers).
# Privacy and data (/docs/resources/privacy)
WebSight is built to measure traffic without tracking people. This page describes exactly how, so you can decide what fits your site. It describes mechanisms, not legal verdicts. For your obligations, consult your own counsel.
## No cookies by default [#no-cookies-by-default]
Every new site starts in stateless mode. In this mode WebSight sets no cookies and stores no identifier in the browser. There is nothing to consent to on the client.
To count unique visitors, the server derives a visitor id from a one-way hash of a daily salt, the site, the IP address, and the user agent:
```
visitor_id = sha256(daily_salt : site : ip : user_agent)
```
The raw IP and user agent are used only inside this hash and are never stored. The salt rotates every day and the previous day's salt is deleted at 00:05 UTC. Once a salt is gone, its hashes cannot be reversed or reproduced, so visitor ids reset each day and yesterday's ids cannot be linked to today's.
Because there is no persistent identifier and no cookie, stateless mode is designed so that no consent banner is required for the tracker itself. Many teams run it without a banner. Whether that is enough for your site is a decision for your own counsel.
## What is stored [#what-is-stored]
For each event WebSight stores only the following. There is no free-form capture of the page or the person.
| Field | Notes |
| --------------------- | ---------------------------------------------------------------------- |
| Path | The URL path, without the raw query string. |
| Query params | Only allowlisted marketing params (see below). |
| Referrer and channel | Referrer, referrer domain, and the derived channel. |
| UTMs | `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`. |
| Device, browser, OS | Parsed from the user agent, then the user agent is discarded. |
| Country, region, city | Derived from CDN request headers. |
| Coordinates | Coarse, rounded to about 1 km. |
| Screen size | Width and height. |
| Language | The browser language. |
| Timestamps | When the event happened. |
| Custom event props | Only the properties you choose to send. |
## What is never stored [#what-is-never-stored]
WebSight does not store any of these, anywhere:
* IP addresses
* Raw user agent strings
* Cookies
* Cross-site or third-party identifiers
* Page content, form values, or keystrokes
## Persistent mode [#persistent-mode]
If you opt a site into persistent mode, WebSight generates a random 20-character id and keeps it in `localStorage`. This gives visitors a durable identity across days, which unlocks [Profiles](/docs/dashboard/profiles) and [Retention](/docs/dashboard/retention).
This id is random, not a fingerprint. WebSight still sets no cookies and does no fingerprinting. If you call [`identify()`](/docs/tracking/identify), activity is tied to your own user id, which you supply, so the mapping between a visitor and a person exists only because you created it.
## Data ownership and deletion [#data-ownership-and-deletion]
WebSight is open source. When you [self-host](/docs/resources/self-hosting), analytics data lives in your own Supabase project and never leaves it.
You can permanently erase a single visitor from [Profiles](/docs/dashboard/profiles): the delete action removes all of that identity's events, sessions, and profile rows.
Data is kept until you delete it. There is no automatic retention or purge today. If you need data to expire after a set period, you delete it yourself.
## Query string hygiene [#query-string-hygiene]
The tracker cleans the URL before anything is sent. Only marketing attribution params leave the browser: `utm_*`, `ref`, `source`, and the common paid-click ids (`gclid`, `fbclid`, `msclkid`, `ttclid`, `twclid`, `li_fat_id`). Every other query parameter is stripped on the client and never transmitted, so tokens, emails, and session values in your URLs never reach WebSight.
# Self-hosting (/docs/resources/self-hosting)
WebSight is open source and MIT licensed. If you want to own the data end to end, run it yourself.
## What you are running [#what-you-are-running]
WebSight is a single Next.js 16 application backed by a Supabase Postgres database. The tracker (`t.js`, its lazy chunk, and `embed.js`) is built into the app's `public/` directory, so there is no separate service to deploy. You host one Next.js app and point it at one Supabase project.
## Setup [#setup]
### Clone the repository [#clone-the-repository]
```bash
git clone https://github.com/srexrg/websight.git
cd websight
```
### Create a Supabase project and apply the migrations [#create-a-supabase-project-and-apply-the-migrations]
Create a project at [supabase.com](https://supabase.com). The schema lives as SQL migrations in `supabase/migrations`. Apply them with the Supabase CLI (already a dev dependency of the repo):
```bash
npx supabase link --project-ref
npx supabase db push
```
This creates the tables, the row-level security policies, and the SECURITY DEFINER functions the app reads through.
### Configure environment variables [#configure-environment-variables]
Copy the example file and fill in the values:
```bash
cp .env.example .env.local
```
| Variable | Purpose |
| -------------------------------------- | ----------------------------------------------------------------------------------- |
| `NEXT_PUBLIC_SUPABASE_URL` | Your Supabase project URL. |
| `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` | Public key for browser and auth clients. |
| `SUPABASE_SECRET_KEY` | Server-only key. Required by the analytics pipeline (ingestion, queries, backfill). |
| `NEXT_PUBLIC_APP_URL` | The public origin of your instance, e.g. `https://yourdomain.com`. |
The legacy JWT keys (`NEXT_PUBLIC_SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY`) still work as fallbacks if your project has not migrated to the new key system.
### Install and build [#install-and-build]
```bash
npm install
npm run build
```
The tracker is compiled automatically: `prebuild` runs `build:tracker`, which bundles the tracker into `public/` before Next.js builds.
### Deploy [#deploy]
Deploy to Vercel or any Node host that can run `npm start`. Point your domain at the deployment and you are live.
## Point the snippet at your own origin [#point-the-snippet-at-your-own-origin]
Once your instance is running, load the snippet from it instead of the hosted origin:
```html
```
The tracker posts events to the origin it was served from, so no `data-api` is needed when loading the script straight from your instance. Add `data-api` only when you put a [first-party proxy](/docs/tracking/adblockers) in front.
## Things to get right [#things-to-get-right]
**Geo needs a CDN.** Country, region, and city come from CDN request headers, not a bundled database. There is no MaxMind fallback. Deploy behind a CDN that sets location headers (Vercel, Cloudflare, Fastly, or CloudFront). A self-host without one records no location.
* **Google sign-in.** Authentication is Google OAuth only. Configure the Google provider in your Supabase project's Auth settings, otherwise nobody can sign in.
* **Scheduled jobs.** Salt rotation (daily, for the cookieless hash) and stale-session closing (every 15 minutes) run as `pg_cron` jobs. They ship with the migrations and are scheduled automatically when `pg_cron` is available on your database. If `pg_cron` is not enabled, the migration logs a notice and skips scheduling, and you will need to run these jobs yourself.
# Troubleshooting (/docs/resources/troubleshooting)
Most issues come down to the snippet not loading, traffic being filtered, or a feature that needs a second opt-in. Work top to bottom.
## No data after installing [#no-data-after-installing]
Open your deployed site and check the browser's Network tab:
1. The `
```
The badge shows live visitors and pageviews and refreshes every 15 seconds. Its live count reflects a rolling 5-minute window.
The badge needs sharing to be on and uses the same token as your share link. Rotating the link changes the token, so update the `data-share` value if you rotate.
# Site settings (/docs/sharing/site-settings)
Everything you configure for a single site lives here: its identity, the install snippet, and the handful of features you can turn on.
## Site details [#site-details]
The details panel shows the core facts about your site. These are read-only:
| Field | What it is |
| ------------ | ------------------------------------------ |
| Name | The display name for the site. |
| Domains | The domain(s) the tracker reports for. |
| Site ID | The site's internal identifier. |
| Privacy mode | Stateless or persistent. |
| Timezone | The timezone your reports are bucketed in. |
Privacy mode is fixed when the site is created. It is not editable afterwards. If you need to switch between stateless and persistent, create a new site. See [Privacy and data](/docs/resources/privacy) for what each mode does.
## Installation [#installation]
The installation tab carries the snippet for every supported platform: HTML, Next.js, React, Vue, WordPress, Shopify, Webflow, Framer, and Google Tag Manager. Copy the one that fits your stack. Full walkthroughs live in [Install WebSight](/docs/getting-started/install).
## Feature toggles [#feature-toggles]
Two features are opt-in and enabled from the dashboard:
* **Web Vitals**: enabled from the "Turn on Web Vitals" card. You also need `data-vitals` on the snippet. See [Web Vitals](/docs/dashboard/web-vitals).
* **Errors**: JS error capture, enabled alongside the `data-errors` attribute. See [Errors](/docs/dashboard/errors).
## Public sharing [#public-sharing]
The public sharing card lives in site settings too. Turn it on to create a read-only link to your dashboard, set a password, and choose which screens are visible. See [Public dashboards](/docs/sharing/public-dashboards).
## Excluding traffic [#excluding-traffic]
There is no IP-exclusion setting. It is not needed:
* Localhost and loopback addresses are excluded by the tracker automatically, so local development never shows up.
* To exclude your own visits from a live site, set the `websight_ignore` flag in your browser. See [Exclude traffic](/docs/tracking/exclude-traffic).
# Adblockers and proxying (/docs/tracking/adblockers)
Client-side analytics can be blocked. Serving the tracker from your own domain fixes that for almost everyone. This page shows the one rule you need per platform.
## Why blockers win by default [#why-blockers-win-by-default]
A visitor on Brave with Shields up, or anyone running uBlock Origin whose filter lists include the analytics domain or the `/api/track` path, is simply never counted. This is true of every script-based analytics tool, Google Analytics included.
The fix used by every privacy-first analytics tool is first-party proxying: serve the tracker script and receive its events from your own domain, so the browser only ever makes same-origin requests. A network filter cannot tell them apart from your site's own assets, so it cannot block them. This lifts capture to nearly all traffic, including most blocker users.
It cannot defeat a visitor with JavaScript disabled. Nothing script-based can. That residual blind spot is unavoidable and tiny.
## How the tracker resolves URLs [#how-the-tracker-resolves-urls]
Two facts about `t.js` make proxying a one-rule change:
1. The ingest endpoint is the `data-api` attribute if set, otherwise the origin the script was served from plus `/api/track`.
2. The lazy chunk `t-x.js` (Web Vitals and error capture) is loaded relative to the script's own URL.
So if you serve `t.js` from your own domain under a path prefix and point `data-api` at that same prefix, the script, its lazy chunk, and the event stream are all first-party. One proxy rule covers all three.
## The recipe [#the-recipe]
Proxy a single neutral path prefix on your domain to the WebSight origin. Use a bland name such as `/stats`, `/s`, or `/insights`, and avoid `/analytics`, `/track`, or `/tracker`, which some cosmetic filter lists flag by name alone.
Map `yourdomain.com/stats/*` to `https://websight.srexrg.me/*`. That single rule routes:
| Request on your domain | Proxied to |
| ------------------------------- | -------------------------------------- |
| `/stats/t.js` | `https://websight.srexrg.me/t.js` |
| `/stats/t-x.js` (vitals/errors) | `https://websight.srexrg.me/t-x.js` |
| `/stats/api/track` (events) | `https://websight.srexrg.me/api/track` |
Then point the snippet at the prefix:
```html
```
`data-api` is a same-origin relative path, so events post to your own domain. `data-vitals` and `data-errors` are optional; include them to load `t-x.js`, or drop them to stay at the core.
## Platform rules [#platform-rules]
Pick the one matching where your site is hosted. All map `/stats/*` to the WebSight origin.
In `next.config.js` or `next.config.ts`:
```js
async rewrites() {
return [
{ source: "/stats/:path*", destination: "https://websight.srexrg.me/:path*" },
];
}
```
Runs on the Next server or edge. Note that `output: "export"` (static export) has no server to proxy through, so use your CDN's rule instead.
In `vercel.json`:
```json
{
"rewrites": [
{ "source": "/stats/:path*", "destination": "https://websight.srexrg.me/:path*" }
]
}
```
In `netlify.toml`:
```toml
[[redirects]]
from = "/stats/*"
to = "https://websight.srexrg.me/:splat"
status = 200
force = true
```
A Worker:
```js
export default {
async fetch(request) {
const url = new URL(request.url);
if (url.pathname.startsWith("/stats/")) {
const target =
"https://websight.srexrg.me/" +
url.pathname.slice("/stats/".length) +
url.search;
return fetch(target, request);
}
return fetch(request);
},
};
```
Or use a Cloudflare Origin Rule pointing `/stats/*` at the WebSight host if you prefer no Worker.
```nginx
location /stats/ {
proxy_pass https://websight.srexrg.me/;
proxy_set_header Host websight.srexrg.me;
proxy_ssl_server_name on;
}
```
The trailing slash on both `location` and `proxy_pass` strips the `/stats/` prefix before forwarding, so `/stats/t.js` becomes `/t.js` upstream.
```text
handle_path /stats/* {
reverse_proxy https://websight.srexrg.me {
header_up Host websight.srexrg.me
}
}
```
## Verify it works [#verify-it-works]
After deploying the rule and updating the snippet:
1. Open your site and go to DevTools, Network.
2. Confirm `stats/t.js` returns 200 from your own domain, not from `websight.srexrg.me`.
3. Navigate a page and confirm the POST to `/stats/api/track` returns 202.
4. Check that the WebSight [Realtime](/docs/dashboard/realtime) screen shows the visit.
5. Turn on Brave Shields or uBlock and repeat. The requests should still go through, because they are same-origin.
## Caveats [#caveats]
Proxy the whole prefix, not just `t.js`. If you only proxy the one file, `t-x.js` and the event POST fall back to third-party and get blocked again. The `/stats/*` prefix rule covers all of them.
* Do not cache the event endpoint. `t.js` and `t-x.js` are cacheable and a CDN in front of them is fine, but `/api/track` is a POST, so make sure no rule caches it.
* Keep the destination current. If WebSight later moves off `websight.srexrg.me`, update the one rewrite rule.
* If you self-host WebSight, replace `websight.srexrg.me` with your own instance's origin everywhere above.
# Server-side API (/docs/tracking/api)
There is a small server-side endpoint for recording an event from your backend with an API key. It is a legacy, limited interface. For almost everything, the [script tag](/docs/tracking/script) and [`websight.track`](/docs/tracking/custom-events) are the right tools.
## Get your key [#get-your-key]
Your account has a single API key on the Settings screen. Use the card there to generate one, copy it, and regenerate it if it leaks. Regenerating invalidates the old key immediately.
## Send an event [#send-an-event]
Make a `POST` to `/api/events` with your key as a Bearer token:
```bash
curl -X POST https://websight.srexrg.me/api/events \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{"name": "purchase", "domain": "yourdomain.com", "description": "42 EUR"}'
```
The body has three fields:
| Field | Required | Notes |
| ------------- | -------- | --------------------------------------------------------------- |
| `name` | Yes | Event name. Lowercased on the server. |
| `domain` | Yes | Your site's domain. A leading protocol and `www.` are stripped. |
| `description` | No | Free text, stored with the event. |
A missing or unknown key returns `401`. Missing `name` or `domain` returns an error. On success the endpoint returns a JSON success message.
## What it does and does not do [#what-it-does-and-does-not-do]
Server events are not sessionized. They are recorded against a synthetic `api:` visitor and never create a session, so they do not affect visitor counts, sessions, or any of the session-based reports. They record that a named event happened, and little more.
This is a legacy interface kept for backward compatibility. It takes a single `name`, `domain`, and `description`, not arbitrary event properties. For real custom events with properties, use [`websight.track`](/docs/tracking/custom-events) from the browser, which is sessionized and counts toward your reports.
The endpoint and its shape may change. For status and the latest, see the repository at [github.com/srexrg/websight](https://github.com/srexrg/websight).
# Automatic events (/docs/tracking/automatic-events)
The snippet tracks more than pageviews with no extra code. As soon as `t.js` loads it watches for navigations, outbound clicks, downloads, and form submits, and it keeps a lightweight presence heartbeat running.
| Event | Trigger | Props |
| ---------------- | -------------------------------------- | ---------------------------------------- |
| `pageview` | Initial load and every SPA navigation | - |
| `outbound_click` | Click on a link to a different host | `url` |
| `download` | Click on a link to a downloadable file | `url` |
| `form_submit` | A `