Self-hosted product telemetry

See where users get stuck on your site.

Hotspot records what visitors do with one script tag and stores it on your own server. The dashboard replays each session as the visitor saw it, and shows who is on the site right now, how visitors move between pages, where they drop out of a funnel and which clicks go nowhere.

Install snippet
<script src="https://hotspot.buxa.ai/hotspot.js" data-api-key="hs_demo" defer></script>
  • 14 KB gzipped, plus a 24 KB recorder when replay is on
  • No third-party cookies
  • No IP addresses stored
  • Click
  • Rage click
  • Dead click
  • Scroll depth

Events

What the script records

Page views, clicks, scroll depth, form submits, errors, web vitals and a recording of the page are captured on their own. Identify and custom events are calls you add.

$pageview

Page views and time on page

Recorded on load with the path, title, referrer, UTM parameters and viewport size. When the visitor leaves, the time on page goes with it.

$autocapture

Clicks with element and position

Every click, with the element's selector, text, tag and link target, plus where it landed in the viewport and on the page.

$rageclick

Rage clicks

Three or more clicks within 1.2 seconds and 40 pixels of each other. A sign that something looks clickable and does not respond.

$is_dead_click

Dead clicks

A click on something that is not interactive: no link, button, form control, role, handler or pointer cursor within five ancestors.

$scroll_depth_percent

Scroll depth

How far down each page the visitor got, as a percentage, sent with the page leave event.

$form_submit

Form submits

The form's id, name, action and method, not what the visitor typed. Typed values appear in the session replay unless the field is masked.

$exception

JavaScript errors

Uncaught errors and unhandled promise rejections, with the message, type and stack trace.

$pageview

Single-page-app route changes

A route change in a single-page app records a new page view, so client-side navigation shows up like any other page.

$identify

Identify and custom events

Call hotspot.identify() after sign-in to attach an email or name to a visitor, and hotspot.capture() for events of your own.

rrweb

Session replay

One snapshot of the page's DOM, then every change, input, scroll and mouse movement, so the dashboard can play the session back as the visitor saw it. Password and payment fields are masked in the browser; data-hotspot-mask masks anything else.

console, network

Console output and network requests

Console output at the log, info, warn, error and debug levels, and fetch and XHR requests with their method, URL, status and duration. Bodies and headers are never recorded. Both play back next to the replay.

$web_vitals

Web vitals

LCP, FCP, TTFB, CLS and INP, sent once per page when the visitor leaves or hides it. The dashboard reports the 75th percentile per page and for the whole site.

Your server adds the browser, operating system and device type from the User-Agent header, and the country when your proxy sends a country header. IP addresses are not stored.

Dashboard

What you see in the dashboard

Every section is computed from the sessions on your server, over the time range, device, browser and operating system you pick.

Sessions and filters

Every session with its pages, clicks, device and duration. Filter by time range, device, browser and operating system, narrow the list to converted, rage, error, bounced, engaged, active, identified or dead-click sessions, or search.

Session replay

The page as the visitor saw it, played back from the DOM recording, with the session's events, console output and network requests next to the player. Masked fields stay masked.

Funnels you define

Steps are a path, an event, a clicked selector or clicked text. The funnel shows how many sessions reached each step, the drop-off between steps and the median time to the next one.

Flow

An animated map of how visitors move from page to page, with the drop-off at each page, rage clicks and errors marked on the page where they happened, and the landing, exit and most common journeys underneath.

Breakage

Rage clicks, dead clicks and JavaScript errors in one place: the share of sessions each one hit, grouped by page and element, with a button that opens the sessions it happened in.

Pages and referrers

Views, sessions, average time, average scroll depth, exit rate and web vitals at the 75th percentile per page, plus a site-wide web vitals card. Sessions and conversion rate per referrer.

Overview

Visitors, sessions, page views, time on site, bounce and conversion rate against the period before, a trend chart, top pages, where visitors come from, devices and countries, and plain sentences about what stands out.

Realtime

Who is on the site right now on a world map, visitors per minute, the pages being viewed and the raw event feed as it arrives.

How it works

From script tag to dashboard

The script, the server and the dashboard are the whole product. Nothing is sent anywhere else.

01

Add the script tag

Put one tag in the head of your site or in the root layout of your app. The script is 14 KB gzipped and loads with defer, so it does not block rendering. When replay is on, it loads the recorder (24 KB gzipped) as a separate file.

02

Events batch to your server

The script queues events and posts them to your Hotspot server every 2.5 seconds, or sooner when ten are waiting. The recording goes out in chunks every 5 seconds. Sessions and recordings are kept in Redis, or in process memory when no Redis is configured.

03

Open the dashboard

Sign in with the dashboard password you set and look at the overview, the realtime map, sessions with their replays, the flow between pages, funnels, breakage, pages and sources, and a guided install page.

What it does not do

Some things are never recorded. Others are masked in the browser before they are sent.

  • No video or screenshots The replay is rebuilt from a snapshot of the page's DOM and the changes that followed. Hotspot does not capture pixels.
  • No passwords or card numbers Password fields, payment fields and one-time codes are masked in the browser. Add data-hotspot-mask to mask other fields, or data-mask-inputs="all" on the script tag to mask every input. A form submit event carries only the form's id, name, action and method.
  • No request bodies or headers A network request is recorded as its method, URL, status and duration, and nothing else.
  • No IP addresses They are not stored. The country comes from a header your proxy sends, when there is one.
  • No third-party cookies The visitor id and session id live in localStorage, with a first-party cookie as the fallback.
  • No data leaving your server The script posts to your Hotspot host and nowhere else.

Install

Add the script to your site

One tag in the head of every page, or in the root layout of your app. It starts recording on load. The snippet below already has this server's address and key.

Install snippet
<script src="https://hotspot.buxa.ai/hotspot.js" data-api-key="hs_demo" defer></script>

Script attributes

data-api-host
Where events are sent. Defaults to the host the script was loaded from.
data-autocapture="false"
Turn off click and form submit capture, including rage and dead clicks.
data-capture-pageview="false"
Turn off automatic page views and page leaves.
data-capture-errors="false"
Turn off error capture.
data-capture-web-vitals="false"
Turn off web vitals.
data-replay="false"
Turn off session replay. The recorder file is not loaded, and console and network capture stop with it.
data-mask-inputs="all"
Mask every input value in the replay. Password fields, payment fields and one-time codes are masked either way.
data-mask-text="true"
Mask all text on the page in the replay.
data-capture-console="false"
Leave console output out of the replay.
data-capture-network="false"
Leave network requests out of the replay.
data-debug="true"
Log every captured event to the browser console.
data-debug-hud="true"
Show an overlay of your own clicks on the page while you test. Toggle it with Alt+H, or open any page with ?hotspot_debug_hud=1.

Masking in your markup

data-hotspot-mask
Masks the text and input values of the element and everything inside it.
data-hotspot-block
Replaces the element with an empty box of the same size in the replay.
data-hotspot-ignore
Skips the element's input events, so changes to its value are not recorded.

Password fields, payment fields (autocomplete="cc-*") and one-time codes are always masked. Masking happens in the browser, so masked values never reach the server. The classes hs-mask and hs-block work like the first two attributes.

JavaScript API

hotspot.capture(name, props)
Record an event of your own.
hotspot.identify(id, { email, name, company })
Attach a user id and profile to the visitor. Call it after sign-in.
hotspot.setPersonProperties(props)
Update the profile without changing the id.
hotspot.register(props)
Properties sent with every event from now on.
hotspot.reset()
Forget the visitor id and start a new session. Call it on sign-out.
hotspot.optOut() and hotspot.optIn()
Stop or resume recording in this browser.

Also on window.hotspot: hasOptedOut(), getDistinctId(), getSessionId(), on(event, callback) and flush().

Using a coding assistant? Give it this instruction and it will place the tag in the right file.

Instruction for your coding assistant
Add <script src="https://hotspot.buxa.ai/hotspot.js" data-api-key="hs_demo" defer></script> to the <head> of the root layout (Next.js app/layout.tsx, Vite index.html, SvelteKit app.html, Astro layout). Do not add other analytics tags. Call window.hotspot.identify(userId, { email, name }) after sign-in. Add data-hotspot-mask to elements that show personal data so the session replay masks them.

Server configuration

HOTSPOT_API_KEYS Comma-separated keys the capture endpoint accepts. Unset accepts any key, which is fine locally and wrong in production.
HOTSPOT_DASHBOARD_PASSWORD Protects the dashboard and its API. Unset leaves the dashboard open.
REDIS_URL Keeps data across restarts and shares it between workers. Unset keeps data in process memory.
HOTSPOT_REPLAY_ENABLED Set to 0 to turn session replay off for every site. The server refuses recordings and the script stops sending them.
HOTSPOT_MAX_RECORDING_BYTES The compressed size a session's recording can reach, 10 MiB by default. The dashboard says when a session hit it.
HOTSPOT_SESSION_TTL_SECONDS How long a session and its recording are kept, 7 days by default.
Every variable, including request limits, is listed in the README in the repository. This instance stores data in Redis This instance stores data in memory, so it is lost on restart Session replay is turned off on this instance

FAQ

Questions

  • Yes. The script records a page view on load and on every route change, so client-side navigation shows up as separate pages. Add the tag once, in the root layout, and nothing else is needed.

  • Not as video or screenshots. The script records the page's DOM with rrweb: one snapshot, then every change, input, scroll and mouse movement. The dashboard rebuilds the page from that, so you see what the visitor saw, with the visitor's console output and network requests next to it.

    Password fields, payment fields and one-time codes are masked in the browser before anything is sent. Add data-hotspot-mask or data-hotspot-block to elements that show personal data, or set data-mask-inputs="all" on the script tag. data-replay="false" on the script tag turns replay off for one site, and HOTSPOT_REPLAY_ENABLED=0 turns it off on the server.

  • On your server. Events and recordings go to Redis when REDIS_URL is set, otherwise they stay in process memory and are lost on restart. Sessions expire after 7 days by default, and their recordings with them; HOTSPOT_SESSION_TTL_SECONDS changes that. Up to 2,000 events are kept per session. A recording is kept compressed, up to 10 MiB per session (HOTSPOT_MAX_RECORDING_BYTES), and the dashboard says when a session hit that limit. The live feed keeps the last 5,000 events. Nothing is sent to anyone else.

  • Yes. Call hotspot.optOut() and the script stops recording in that browser. hotspot.optIn() turns it back on and hotspot.hasOptedOut() tells you the current state. The choice is stored in the visitor's browser.

  • The script is 14 KB gzipped and loads with defer, so it does not block rendering. When replay is on, it loads the recorder (24 KB gzipped) as a separate file. Events are queued and sent in batches every 2.5 seconds, and the recording goes out in chunks every 5 seconds. When the visitor leaves a page, the last batch goes out with sendBeacon, which does not delay the navigation.

  • A dead click is a click on something that is not interactive: no link, button, form control, role, handler or pointer cursor within five ancestors. It usually means something looks clickable and is not.

    A rage click is three or more clicks within 1.2 seconds and 40 pixels of each other. It usually means something is clickable and does not respond. Both show up under Breakage with the page and the element.

  • Yes. A funnel is a list of steps, and each step is a path (path:/signup), an event (event:$form_submit), a click on a selector (click:#create-account) or clicked text (text:Buy now). A trailing * on a path or selector matches a prefix. The dashboard shows how many sessions reached each step, the drop-off between steps and the median time to the next step.