Analytiics live stats pill
Add a small “3 people here right now · live stats” link like the one on Diiverge. Style it to match your app. Clicking it opens the project's public Analytiics dashboard.
Public stats are required
Finish setup and instrumentation first. In the project's Analytiics settings, publish the dashboard and enable its realtime widget. Review the other visible widgets and events before publishing: this endpoint returns the public count and filtered activity feed, not a private count-only view. Do not publish someone's dashboard just to make a pill work without their permission. Keep the pill absent if stats should stay private.
Use the exact project value from analytics.yaml, which may differ from a legacy project's website domain. No write key, API token, cookies or server proxy are needed. Never put ANALYTIICS_WRITE_KEY in this component.
GET https://www.analytiics.co/api/online?project=example.com
Link: https://www.analytiics.co/p/example.comA successful response contains only online (a non-negative integer), with a 30-second shared cache and no activity-feed query. It counts unique visitors with recorded activity in the trailing five minutes; it is not an exact count of open tabs. Avoid UTM parameters on the dashboard link: they filter that dashboard's data.
React / Next.js example
Place this client component in your header or navigation once. It uses ordinary CSS, so it does not require Tailwind. Replace the project in the usage example. It polls every 30 seconds while visible, never overlaps requests, times out slow requests, and hides on zero, invalid data or failure instead of inventing a count.
"use client";
import { useEffect, useState } from "react";
export function LiveStatsPill({ project }: { project: string }) {
const [online, setOnline] = useState<number | null>(null);
const encoded = encodeURIComponent(project);
useEffect(() => {
let active = true;
let timer: ReturnType<typeof setTimeout> | undefined;
let request: AbortController | undefined;
setOnline(null);
const stop = () => {
clearTimeout(timer);
const previous = request;
request = undefined;
previous?.abort();
};
const load = async () => {
if (!active || document.hidden) return;
const controller = new AbortController();
request = controller;
const timeout = setTimeout(() => controller.abort(), 8000);
let delay = 30000;
try {
const response = await fetch(
"https://www.analytiics.co/api/online?project=" + encoded,
{ signal: controller.signal, credentials: "omit", cache: "no-store" },
);
if (response.status === 429) {
delay = 60000;
const retry = response.headers.get("Retry-After");
const seconds = Number(retry);
const wait = Number.isFinite(seconds)
? seconds * 1000
: Date.parse(retry ?? "") - Date.now();
if (Number.isFinite(wait)) delay = Math.max(delay, wait);
}
if (!response.ok) throw new Error("Live stats unavailable");
const data = await response.json();
if (!Number.isSafeInteger(data.online) || data.online < 0) {
throw new Error("Invalid live count");
}
if (active && request === controller) setOnline(data.online);
} catch {
if (active && request === controller) setOnline(null);
} finally {
clearTimeout(timeout);
if (request === controller) {
request = undefined;
if (active && !document.hidden) timer = setTimeout(load, delay);
}
}
};
const visibilityChanged = () => {
stop();
setOnline(null);
if (!document.hidden) void load();
};
void load();
document.addEventListener("visibilitychange", visibilityChanged);
return () => {
active = false;
stop();
document.removeEventListener("visibilitychange", visibilityChanged);
};
}, [encoded]);
if (online === null || online < 1) return null;
return (
<a className="live-stats-pill"
href={"https://www.analytiics.co/p/" + encoded}
target="_blank" rel="noopener noreferrer"
title="Visitors active in the last five minutes; opens live stats in a new tab">
<span className="live-stats-dot" aria-hidden="true" />
<span>{online.toLocaleString()} {online === 1 ? "person" : "people"} here right now</span>
<span className="live-stats-label">· live stats</span>
</a>
);
}Add these styles to your app's stylesheet. They inherit its text color and work on light or dark backgrounds. The dot is static, avoiding motion distractions; if you add a pulse, disable it with prefers-reduced-motion: reduce.
.live-stats-pill {
display: inline-flex;
align-items: center;
gap: .6rem;
padding: .55rem .85rem;
border: 1px solid currentColor;
border-radius: 999px;
color: inherit;
background: transparent;
text-decoration: none;
font: 500 .75rem/1.3 system-ui, sans-serif;
}
.live-stats-pill:hover { text-decoration: underline; }
.live-stats-pill:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }
.live-stats-dot { width: .5rem; height: .5rem; border-radius: 50%; background: #10b981; flex-shrink: 0; }
@media (max-width: 640px) { .live-stats-label { display: none; } }<LiveStatsPill project="example.com" />For Vue, Svelte or plain JavaScript, use the same endpoint, link and lifecycle: fetch once on mount, schedule the next request after completion, stop while hidden, clear stale counts, and abort/clean up when removed. This is a display widget; it does not install the tracker or send pageviews itself.
Verify and troubleshoot
- Visit the instrumented app, then check the endpoint and pill. Use the same exact project value for tracker, manifest, endpoint and dashboard link.
- Check the public dashboard signed out. A private or realtime-disabled project returns
404withcode: realtime_disabled; hide the pill. Publishing other widgets alone is insufficient. Unpublishing should hide it on the next poll. - A real zero renders nothing in this example. Never clamp the count to one. Localhost tracking is off by default; follow the deliberate development-test instructions in the agent guide.
- On
429, wait at least 60 seconds and honorRetry-Afterwhen readable. The current cross-origin response does not expose that header to browsers, so the 60-second fallback matters. On other failures, hide and retry later. - If the app has a Content Security Policy, allow
https://www.analytiics.coinconnect-srcfor this fetch; the tracker separately useshttps://in.analytiics.co. Don't disable the policy or usemode: "no-cors". - Do not put a CDN, service worker cache or cached proxy in front of this public response. It uses
private, no-storeso publication is checked on every poll. Public responses can include a filtered activity feed: never use a privileged server request as a workaround for a private dashboard.
See the online count API reference for the full response and rate limits.
Last updated 2026-09-06. This page is also available as Markdown at /docs/live-stats-pill.md, or from this URL with Accept: text/markdown.