{"id":"ade2b906-2993-467a-b5f5-923ee67e1af4","slug":"litemetrics","kind":"skill","name":"litemetrics","summary":"Open-source analytics SDK you can integrate into your product. Give your users a beautiful analytics dashboard in 5 minutes.","owner":"metehankurucu","repoUrl":"https://github.com/metehankurucu/litemetrics","license":"MIT","licenseTier":"permissive","trust":"community","stars":4,"match":null,"fewStars":true,"format":"skill","createdAt":"2026-02-07T13:35:44.000Z","addedAt":"2026-10-22T00:29:05.050Z","logoUrl":"https://avatars.githubusercontent.com/u/43188732?u=1d1cf1b80e176e41764122729e3713971c866b6b&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["data-analytics","developer-tools"],"tags":["analytics","analytics-tracking","react","self-hosted","tracking","litemetrics"],"runtime":"binary","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-25T19:12:08.371Z","sample":false,"url":"https://conduits.dev/tools/litemetrics","language":"TypeScript","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"<p align=\"center\">\n  <img src=\"logo.png\" alt=\"Litemetrics\" width=\"200\" />\n</p>\n\n<h1 align=\"center\">Litemetrics</h1>\n\n<p align=\"center\">\n  <strong>Open-source analytics you can integrate into your product.</strong><br/>\n  Give your users a beautiful analytics dashboard in 5 minutes.\n</p>\n\n<p align=\"center\">\n  <a href=\"https://demo.litemetrics.dev\"><strong>Live Demo</strong></a> •\n  <a href=\"#quick-start\">Quick Start</a> •\n  <a href=\"#packages\">Packages</a> •\n  <a href=\"#deploy\">Deploy</a> •\n  <a href=\"#docs\">Docs</a> •\n  <a href=\"https://github.com/metehankurucu/litemetrics/issues\">Issues</a>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://demo.litemetrics.dev\"><img src=\"https://img.shields.io/badge/%E2%96%B6%EF%B8%8F_Live_Demo-demo.litemetrics.dev-00c853?style=for-the-badge&labelColor=1a1a2e\" alt=\"Live Demo\" /></a>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://railway.com/template/OQI8lX?referralCode=LpQIoM\"><img src=\"https://railway.com/button.svg\" alt=\"Deploy on Railway\" /></a>\n</p>\n\n<br/>\n\n<p align=\"center\">\n  <img src=\"examples/demo.png\" alt=\"Litemetrics Dashboard\" width=\"100%\" style=\"border-radius: 8px;\" />\n</p>\n\n<br/>\n\n<p align=\"center\">\n  <strong>AI Agent Skill:</strong>&nbsp; <code>npx skills add metehankurucu/litemetrics</code><br/>\n  <sub>Gives your AI coding agent (Claude Code, Cursor, Windsurf) full knowledge of Litemetrics integration patterns.</sub>\n</p>\n\n<br/>\n\n## Why Litemetrics?\n\n| | |\n|---|---|\n| **Embed into your product** | Drop a full analytics stack into your app — tracker, server, query API, and a pre-built dashboard UI. Your customers get analytics without you building anything. |\n| **Your brand, your colors** | 10 built-in theme presets. CSS custom properties for full control. Dark mode included. Ship analytics that look like they belong in your app. |\n| **Multi-tenant ready** | Each customer gets isolated analytics via `site_id`. One database, zero cross-contamination. Built for SaaS from day one. |\n| **Lightweight tracker** | ~3 KB gzipped. Auto-tracks pageviews, sessions, scroll depth, button clicks, outbound links — or go fully manual. |\n| **ClickHouse, Postgres & MongoDB** | Choose the database that fits. ClickHouse for speed at scale, Postgres for the DB you already run, MongoDB for simplicity. Swap with one env var, full feature parity. |\n| **One-click deploy** | Docker Compose, Railway, or a single Docker container. Up and running in under a minute. |\n\n<br/>\n\n## Quick Start\n\n### 1. Add a dashboard to your app\n\n```bash\nnpm install @litemetrics/ui\n```\n\n```tsx\nimport { LitemetricsProvider, AnalyticsDashboard } from '@litemetrics/ui';\n\nfunction CustomerDashboard({ customerId }) {\n  return (\n    <LitemetricsProvider baseUrl=\"/api/stats\" siteId={customerId}>\n      <AnalyticsDashboard theme=\"midnight\" />\n    </LitemetricsProvider>\n  );\n}\n```\n\n### 2. Add tracking to your site\n\n```html\n<script src=\"https://your-server.com/litemetrics.js\"></script>\n<script>\n  Litemetrics.createTracker({\n    siteId: 'your-site-id',\n    endpoint: 'https://your-server.com/api/collect'\n  });\n</script>\n```\n\n### 3. Add the server (Express)\n\n```bash\nnpm install @litemetrics/node\n```\n\n```ts\nimport express from 'express';\nimport { createCollector } from '@litemetrics/node';\n\nconst app = express();\napp.use(express.json());\n\nconst collector = await createCollector({\n  db: { url: 'http://localhost:8123' },\n});\n\napp.all('/api/collect', (req, res) => collector.handler()(req, res));\napp.all('/api/stats', (req, res) => collector.queryHandler()(req, res));\n\napp.listen(3002);\n```\n\n> For Postgres: `db: { adapter: 'postgres', url: 'postgres://user:pass@localhost:5432/myapp' }`\n>\n> For MongoDB: `db: { adapter: 'mongodb', url: 'mongodb://localhost:27017/myapp' }`\n\n### 4. Run the mobile app (Expo)\n\n```bash\ncd apps/mobile\nbun install\nbun run start\n```\n\nFor platform-specific launches:\n\n```bash\nbun run ios\nbun run android\n```\n\n### 5. Query from the terminal (CLI)\n\n```bash\nbun add -g @litemetrics/cli\n```\n\n```bash\n# Configure once\nexport LITEMETRICS_URL=https://your-server.com\nexport LITEMETRICS_ADMIN_SECRET=your-secret\nexport LITEMETRICS_SITE_ID=your-site-id\n\n# Overview of all metrics\nlitemetrics overview -p 7d --compare\n\n# Top pages\nlitemetrics stats top_pages -p 30d -l 10\n\n# Time series\nlitemetrics timeseries visitors -p 30d -g day\n\n# JSON output for AI agents\nlitemetrics overview -p 7d -f json\n```\n\nSee [`packages/cli/README.md`](./packages/cli/README.md) for all commands.\n\n<br/>\n\n## Deploy\n\n### Docker Compose (recommended)\n\n```bash\ngit clone https://github.com/metehankurucu/litemetrics.git\ncd litemetrics\nADMIN_SECRET=your-secret docker compose up -d\n```\n\nOpen `http://localhost:3002` for the dashboard.\n\nPrefer PostgreSQL over ClickHouse? Use the Postgres variant (the database stays on the internal network and is not exposed to the host):\n\n```bash\nPOSTGRES_PASSWORD=change-me ADMIN_SECRET=your-secret \\\n  docker compose -f docker-compose.postgres.yml up -d\n```\n\n### Docker (standalone)\n\n```bash\ndocker build -t litemetrics .\ndocker run -p 3002:3002 \\\n  -e CLICKHOUSE_URL=http://your-clickhouse:8123 \\\n  -e ADMIN_SECRET=your-secret \\\n  litemetrics\n```\n\n### Railway (one click)\n\n1. Click the **Deploy on Railway** button above\n2. Add a database plugin — Postgres, ClickHouse, or MongoDB\n3. Set `DB_ADAPTER` and the matching URL (`POSTGRES_URL`, `CLICKHOUSE_URL`, or `MONGODB_URL`) plus `ADMIN_SECRET`\n4. Done — dashboard, API, and tracker served from one container\n\n### Environment Variables\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `DB_ADAPTER` | Database adapter (`clickhouse`, `postgres`, or `mongodb`) | `clickhouse` |\n| `CLICKHOUSE_URL` | ClickHouse connection URL | `http://localhost:8123` |\n| `POSTGRES_URL` | Postgres connection string | `postgres://postgres:postgres@localhost:5432/litemetrics` |\n| `MONGODB_URL` | MongoDB connection string | `mongodb://localhost:27017/litemetrics` |\n| `ADMIN_SECRET` | Secret for admin access and site management | _(none)_ |\n| `PORT` | Server port | `3002` |\n| `GEOIP` | Enable GeoIP lookup | `true` |\n| `TRUST_PROXY` | Trust X-Forwarded-For headers | `true` |\n| `BOT_FILTER_MODE` | Server-wide bot filter default: `off` / `standard` / `strict` / `shadow` | `standard` |\n| `BOT_RATE_WINDOW_MS` | Sliding-window size for the per-IP rate limiter (ms) | `60000` |\n| `BOT_RATE_MAX` | Max collect **requests** per window per IP before rate-limit fires (one request can carry up to 100 events) | `60` |\n| `BOT_VELOCITY_WINDOW_MS` | Sliding-window size for the per-visitor velocity layer (ms) | `10000` |\n| `BOT_VELOCITY_MAX_PAGEVIEWS` | Max **pageviews** one visitor may send per window before the velocity layer fires. `0` switches this layer off on its own | `60` |\n| `BOT_VELOCITY_MAX_KEYS` | Cap on tracked `siteId:visitorId` windows (LRU-evicts the oldest) | `50000` |\n| `BOT_LOG_MAX_PER_MIN` | Detail `[bot-filter]` log lines allowed per minute; the overflow is counted as `suppressed=` on the `[collect]` summary | `20` |\n| `COLLECT_ERROR_LOG_MAX_PER_MIN` | Detail `[collect-error]` log lines allowed per minute; every failure is still counted in `err_codes=` on the `[collect]` summary | `5` |\n\n> `DATABASE_URL` and `LITEMETRICS_ADMIN_SECRET` also work as aliases.\n\n<br/>\n\n## Architecture\n\n```\n┌──────────────────┐         ┌──────────────────┐         ┌──────────────────┐\n│                  │  POST   │                  │  GET    │                  │\n│  @litemetrics/   │ /collect│  @litemetrics/   │ /stats  │  @litemetrics/   │\n│  tracker         ├────────>│  node            │<────────┤  dashboard       │\n│                  │         │  (collector)     │         │                  │\n│  ~3KB, browser   │ events  │  ClickHouse /    │ queries │  React UI        │\n│                  │         │  Postgres /      │         │                  │\n│                  │         │  MongoDB         │         │                  │\n└──────────────────┘         └──────────────────┘         └──────────────────┘\n     Browser / App                Your Server           ▲     Dashboard\n                                                        │\n                                                   ┌────┴─────────────┐\n                                                   │  @litemetrics/   │\n                                                   │  cli             │\n                                                   │  Terminal / AI   │\n                                                   └──────────────────┘\n```\n\nThe tracker handles session management, visitor IDs, batching, and SPA detection client-side. The server stores events and runs queries. The CLI provides terminal access to all analytics data — designed for both humans and AI agents. The Docker image bundles everything into a single container.\n\n<br/>\n\n## Packages\n\n| Package | Description |\n|---------|-------------|\n| [`@litemetrics/ui`](./packages/ui) | Pre-built React dashboard components (10 themes, dark mode, CSS variables) |\n| [`@litemetrics/tracker`](./packages/tracker) | Browser tracker (~3 KB gzipped) |\n| [`@litemetrics/node`](./packages/node) | Server collector, ClickHouse/Postgres/MongoDB adapters, query API |\n| [`@litemetrics/react`](./packages/react) | React provider and hooks |\n| [`@litemetrics/react-native`](./packages/react-native) | React Native / Expo provider |\n| [`@litemetrics/client`](./packages/client) | Typed client for reading analytics data |\n| [`@litemetrics/cli`](./packages/cli) | CLI tool for querying analytics and managing sites |\n| [`@litemetrics/core`](./packages/core) | Shared types and constants |\n\n### Apps\n\n| App | Description |\n|-----|-------------|\n| [`@litemetrics/dashboard`](./apps/dashboard) | Analytics dashboard (React + Vite + Tailwind) |\n| [`@litemetrics/server`](./apps/server) | Self-hosted server (serves dashboard + API) |\n| [`@litemetrics/mobile`](./apps/mobile) | Native mobile analytics app (Expo Router + React Native) |\n| [`@litemetrics/landing`](./apps/landing) | Landing page (React + Vite + Tailwind) |\n\n<br/>\n\n## Metrics\n\n`pageviews` · `visitors` · `sessions` · `events` · `conversions` · `top_pages` · `top_referrers` · `top_countries` · `top_cities` · `top_events` · `top_conversions` · `top_exit_pages` · `top_transitions` · `top_scroll_pages` · `top_button_clicks` · `top_link_targets` · `top_devices` · `top_browsers` · `top_os` · `top_os_versions` · `top_device_models` · `top_app_versions` · `top_utm_sources` · `top_utm_mediums` · `top_utm_campaigns` · `top_utm_terms` · `top_utm_contents` · `top_channels` · `timeseries` · `retention`\n\n<br/>\n\n## Auto Events & Insights\n\n- Auto events are tagged with `event_source=auto` and a subtype: `link_click`, `outbound_click`, `file_download`, `button_click`, `scroll_depth`, `rage_click`.\n- Link, outbound and download rows carry `element_text` and `element_selector`. Outbound rows store the destination as `host + path + query`, so a `wa.me/1555...` or `api.whatsapp.com/send?phone=...` click stays recoverable instead of collapsing to a bare path.\n- A click on an element carrying `data-litemetrics-event` is recorded **only** as that declared event (`event_source=manual`, `event_subtype=attribute`); the auto click row is suppressed, so a labelled element is never double-counted.\n- Ad click IDs (`gclid`, `gbraid`, `wbraid`, `fbclid`) are captured at landing and kept for 90 days, so a conversion fired days later still carries the click that paid for it. Meta's `_fbp` cookie is read (never set) and forwarded only alongside a captured click ID.\n- Manual `track()` events default to `event_source=manual` and `event_subtype=custom`.\n- All metrics and time series support segmentation filters (geo, device, UTM, referrer, event metadata).\n- The dashboard **Insights** view surfaces exit pages, transitions, scroll-heavy pages, and click hotspots.\n\n<br/>\n\n## Bot Filtering\n\nLitemetrics ships with multi-layer bot filtering enabled by default. Bot traffic is excluded from every query unless you explicitly opt in.\n\n- **Tracker short-circuit** — when `navigator.webdriver === true`, the browser tracker becomes a no-op. Catches Selenium / Puppeteer / Playwright at the source before the event ever leaves the page.\n- **Layer 1 (signature)** — server-side match against the maintained [`isbot`](https://github.com/omrilotan/isbot) list of known crawlers / preview bots.\n- **Layer 2 (heuristic)** — catches scrubbed or empty user agents (no UA, bare `Mozilla/5.0`, missing platform tokens, etc). A request that carries a real browser UA *and* `Accept-Language` *and* `Referer` clears this layer, which is why Layer 4 exists.\n- **Layer 3 (rate limit)** — sliding-window per-IP cap (`BOT_RATE_WINDOW_MS` / `BOT_RATE_MAX`) for traffic that escapes Layers 1 and 2. It is evaluated **before** Layer 4, and deliberately: Layer 4 judges one visitor and stores the rest of the batch, so a request it flags still lands rows, and a request that lands rows has to be counted against its address. A Layer 1 or Layer 2 hit does skip this window, because neither ever admits an **unflagged** row: either the request returns and nothing is stored, or the whole batch is stored carrying the flag. Layer 4 is the one that stores unflagged rows alongside a hit, which is why it may not skip. The window counts collect requests, not events, and is shared across every site served by the process, so one IP browsing two of your sites reaches the cap twice as fast. With `TRUST_PROXY` on (the default), the per-IP key is the first `X-Forwarded-For` entry, falling back to `X-Real-IP`. Unless the proxy in front of the collector overwrites `X-Forwarded-For`, that entry is whatever the client sent: a client can rotate it to get a fresh Layer 3 window on every request, or set it to someone else's address to spend that address's budget, which under `standard` hides the address's traffic from the default reports — the per-IP counterpart of the forged-`visitorId` case below. Some hosts document only `X-Real-IP` as the client address, Railway among them; check what yours sets.\n- **Layer 4 (visitor velocity)** — sliding-window cap on how many **pageviews** a single `visitorId` may send (`BOT_VELOCITY_WINDOW_MS` / `BOT_VELOCITY_MAX_PAGEVIEWS`, default 60 per 10 seconds). Once a visitor is over the line, its pageviews stay flagged while it keeps above that rate, and it clears one window after it slows. The three layers above all read one request in isolation: two read headers, and the per-IP window counts *requests*, so a batched flood arrives as a handful of calls from an address that rotates. This layer measures the visitor instead. Only pageviews count toward it — rage clicks and scroll-depth events fire dozens of times a minute by design — and the window is per `siteId:visitorId`, so it is keyed to a visitor rather than to an address: a shared NAT is not pooled by its IP. **A `visitorId` is a device fingerprint for one day, not a person, and the client sends it.** The browser tracker derives it from `hostname|day|User-Agent|language|timezone|screen` (`packages/tracker/src/session.ts`), which is a hash of things two people can easily share: same site, same day, same browser build, same locale, same timezone, same screen size. Two such visitors send the *same* id and DO share one window, so about 6 pageviews a second between them reaches the threshold. Because the id travels in the request body and its inputs are observable, it can also be **forged**: anyone can compute a common visitor's id and burn that window deliberately, which in `standard` hides that visitor's next pageviews from the default reports and in `strict` drops them. Keying the window on the IP as well would close that, and would also hand the batching, address-rotating client this layer exists for a free pass, so it is not done. The React Native SDK is unaffected either way: it draws a random per-install id. If your own visitors cluster like that on a URL-syncing page, raise `BOT_VELOCITY_MAX_PAGEVIEWS` or run `shadow` first. Once a visitor is over the line, **all** of its events in that batch are acted on, custom events included; only pageviews *count* toward the window.\n\n**App sites (`type: 'app'`) never run Layers 1 or 2.** Those are browser heuristics — a native app SDK sends no browser User-Agent, no `Accept-Language` and no `Referer`, so on an app site they only ever misfire (on Android, React Native's `fetch` goes out as `okhttp/<version>`, which `isbot` matches, and every Android event was being dropped). In `standard`, an app site runs **Layer 4 only**: the SDK's 5-second send timer and carrier NAT put many devices on one public address, so the per-IP window would hide real users once a handful of devices shared an address. `strict` and `shadow` still run Layer 3 too, alongside Layer 4 — dropping (`strict`) or flagging (`shadow`) an over-limit request. Layer 4 is not a browser heuristic — it measures how fast one visitor moves — so it applies to app traffic the same way the per-IP window does on web sites. This makes the site type load-bearing: **a site that receives app SDK traffic must be created with `type: 'app'`** (`litemetrics sites create --type app`, or `POST` / `PUT /api/sites` with `{\"type\":\"app\"}`), otherwise it is still filtered as browser traffic, and that now costs more than it used to. The SDK's own `litemetrics-react-native/<version> (<platform>)` User-Agent escapes Layer 1, but it resolves to no browser and no engine, and the SDK sets no `Accept-Language` or `Referer`. On Android nothing below the SDK adds them (OkHttp does not), so Layer 2 flags the request: on a mis-typed site `standard` hides that Android traffic from every report (recoverable with `?includeBots=true`) and `strict` drops it outright. iOS has not been measured: its network stack (`NSURLSession`) may add `Accept-Language` on its own, in which case iOS requests do not trip Layer 2 and a mis-typed site shows its iOS traffic while hiding its Android traffic. The server logs `[site-type-mismatch] site=<id> type=<type> platform=<platform> mode=<mode>` once per site when it sees app SDK payloads on a non-app site; the payload alone never bypasses the filter.\n\nModes are configured server-wide via `BOT_FILTER_MODE` and overridable per-site (on `app` sites Layer 4 always applies; Layer 3 joins it in `strict` and `shadow`, see above):\n\n| Mode | Behavior |\n|------|----------|\n| `off` | All filtering disabled |\n| `standard` (default) | Layer 1 drops the event; Layers 2, 3 and 4 flag it (kept in DB, hidden from queries) |\n| `strict` | Every layer drops the event |\n| `shadow` | Every layer flags only — useful for tuning before going live |\n\nEach detection emits a structured audit log line:\n\n```\n[bot-filter] dropped layer=signature reason=ua-signature mode=standard events=1 site=site_abc ip=203.0.113.4 ua=\"okhttp/4.12.0\"\n```\n\n`layer` says which of the four layers fired; `reason` says why, which is what makes a drop diagnosable from the log line alone:\n\n| `reason` | Layer | Meaning |\n|------|-------|---------|\n| `empty-ua` | signature / heuristic | No `User-Agent` header at all. Usually a misconfigured SDK, not a crawler |\n| `ua-signature` | signature | The UA matched the `isbot` list. Usually a real crawler — but also catches HTTP client defaults like `okhttp/*` |\n| `no-browser-signals` | heuristic | Browser, engine, `Accept-Language` and `Referer` were all absent |\n| `rate-limit` | rate-limit | The per-IP sliding window overflowed |\n| `velocity` | velocity | One `visitorId` sent more pageviews inside the velocity window than a person can read |\n\n`ua` is the raw User-Agent, sanitized to a single line and capped at 200 characters. Detail lines are capped at `BOT_LOG_MAX_PER_MIN` per minute so a bot storm cannot flush the rest of your log window; the overflow is counted as `suppressed=` on the summary line below.\n\nPass `?includeBots=true` to `/api/stats`, `/api/events`, or `/api/users` to see flagged traffic. The dashboard exposes the same toggle on the Analytics page, and per-site bot mode lives on the Settings page.\n\n<br/>\n\n## Request Logs\n\n`/api/collect` is the only high-volume route, so it is **not** logged per request — one line per request would fill a fixed-size platform log window in hours. Instead each wall-clock minute that saw traffic emits a single summary:\n\n```\n[collect] minute=2026-08-18T17:35 reqs=11 ok=8 3xx=0 4xx=0 5xx=0 aborted=3 dur_p50=302 dur_p95=712 dur_max=712 bot_dropped=5 bot_flagged=0 reasons=ua-signature:4,empty-ua:1 bot_sites=site_e2e:5 suppressed=2 err_codes=-\n```\n\n| Field | Meaning |\n|-------|---------|\n| `reqs` / `ok` / `3xx` / `4xx` / `5xx` | Requests in the minute, split by response status class |\n| `aborted` | Requests the client gave up on — body never finished arriving, or hung up before the answer went out. Its own class, not part of a status class: `reqs = ok + 3xx + 4xx + 5xx + aborted` |\n| `dur_p50` / `dur_p95` / `dur_max` | Response time in ms (`-` when the minute saw no requests) |\n| `bot_dropped` / `bot_flagged` | Bot-filter outcomes, counted per **request**; these totals survive even after the individual detail lines age out of the window. A Layer 4 drop is per visitor, so one `bot_dropped` can mean part of a batch went and the rest was stored: the per-hit `events=` field on the detail line below is what says how much |\n| `reasons` | Drop reasons for the minute, by count (see the table above) |\n| `bot_sites` | Sites by **bot hit** count — not request volume, which is `reqs` |\n| `suppressed` | Detail bot-filter lines withheld by `BOT_LOG_MAX_PER_MIN` |\n| `err_codes` | Top 10 collect failure keys as `<stage>:<class>:<count>`, plus `other:N` for omitted occurrences and `untracked:N` for occurrences beyond the 50-key tracking cap (`-` when there were none) |\n\nA minute with no traffic emits nothing, and the open minute is flushed on `SIGTERM` / `SIGINT` so a redeploy does not lose it. Every other route keeps a per-request line with status and duration, with a trailing `aborted` marker when the client left before the answer:\n\n```\n14:59:31 GET /api/stats?siteId=site_x 200 42ms [secret]\n```\n\nA `/api/collect` request that ends in a 500 also writes a detail line saying where it failed and with what:\n\n```\n[collect-error] stage=insert class=ECONNRESET site=site_abc events=3 msg=\"connect ECONNRESET 10.0.0.4:8123\"\n```\n\n`stage` is how far the request got (`parse`, `validate`, `site`, `identity`, `insert`), `class` is the driver's error code when it supplied one and the error class name otherwise, and `events` is the batch size, so a lost batch is countable. These lines are capped at `COLLECT_ERROR_LOG_MAX_PER_MIN` per minute. Withheld ones are **not** part of `suppressed=`, which counts bot-filter lines only: to see how many were withheld, subtract the printed lines from the `err_codes=` total for that minute. Sum every named count, `other:N` and `untracked:N` to obtain that total; both tail values count failure occurrences.\n\nUser-Agent, IP, site id and URL all come from the request, so each is sanitized to a single line before it reaches a log entry — otherwise one newline in a header would let a request forge its own log records. The same applies to a driver's error message: credentials in a connection string it quotes back are redacted to `scheme://***@host` before the line is written.\n\n<br/>\n\n## Docs\n\n- [Getting Started](./docs/getting-started.md)\n- [Integration Guide](./docs/integration-guide.md) — React, React Native, Next.js, Vue, Python, PHP, Ruby, Go\n- [API Reference](./docs/api-reference.md)\n- [Dashboard](./docs/dashboard.md)\n- [CLI](./packages/cli/README.md)\n- [Mobile App](./apps/mobile/README.md)\n- [Self-Hosting](./docs/self-hosting.md)\n- [Scaling](./docs/scaling.md)\n\n<br/>\n\n## Development\n\n```bash\ngit clone https://github.com/metehankurucu/litemetrics.git\ncd litemetrics\nbun install\nbun run build\n```\n\n<br/>\n\n## License\n\nMIT\n"}