BetterMeter
Documentation
Documentation

Track everything.
Know everything.

BetterMeter is a privacy-first analytics platform for the AI era. Track your websites, CLI tools, MCP servers, and APIs — all from one place.

01

Getting Started

BetterMeter tracks four types of sources. Each uses a lightweight integration that sends events to the same privacy-preserving pipeline.

Websites
<script> tag
~1KB, no cookies
CLI Tools
Node SDK
trackCommand()
MCP Servers
Node SDK
wrapMcpServer()
APIs
Node SDK
expressMiddleware()

All sources flow through the same pipeline: event → processing (referrer parsing, bot detection, geo lookup, privacy hashing) → database. You view everything in the dashboard, CLI, or via MCP tools.

02

Web Tracking

Add a single script tag to your site. No cookies, no CNAME configuration, no complex setup. Under 1KB gzipped.

HTML
<script defer data-site="example.com" src="https://bettermeter.com/api/script"></script>
<noscript><img src="https://bettermeter.com/api/pixel?s=example.com" alt="" /></noscript>

The tracker automatically captures pageviews, SPA navigation (History API), and tab returns. For custom events and user identification:

JavaScript
// Track custom events
window.bettermeter.track("signup", { plan: "pro" });

// Identify users (optional)
window.bettermeter.identify("user_123");

Attributes

data-siterequired
string
Your domain as registered in BetterMeter
data-api
string
Custom API endpoint for proxy setups (e.g., /api/collect)
data-no-heartbeat
flag
Disable live visitor heartbeat tracking for this page
03

CLI Tracking

Track how developers use your CLI tool. See which commands are popular, how long they take, and what errors occur — without collecting PII.

Install
npm install @bettermeter/node
Auto-track with Commander.js
import { BetterMeter } from "@bettermeter/node";
import { Command } from "commander";

const bm = new BetterMeter({
  siteId: "my-cli-tool",
  apiKey: "bm_...",
});

const program = new Command();

// Wraps all commands — tracks name, flags, exit code
bm.wrapCommander(program, { version: "1.0.0" });

program.command("deploy").action(() => { /* ... */ });
program.parse();

// Flush on exit
process.on("SIGTERM", () => bm.shutdown());
Manual tracking
bm.trackCommand({
  command: "deploy",
  subcommand: "preview",
  flags: ["--prod", "--verbose"],
  version: "2.1.0",
  durationMs: 4500,
  exitCode: 0,
  isCi: !!process.env.CI,
});

What gets tracked

Command names and flag names only. Flag values, arguments, and file paths are never sent. The SDK captures OS and architecture for environment analytics.

04

MCP Server Tracking

Track which tools AI clients call on your MCP server, how often, which clients (Claude, Cursor, Windsurf) drive the most usage, and error rates.

Auto-track all tools
import { BetterMeter } from "@bettermeter/node";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

const bm = new BetterMeter({
  siteId: "my-mcp-server",
  apiKey: "bm_...",
});

const server = new McpServer({
  name: "my-server",
  version: "1.0.0",
});

// Wraps server.tool() — auto-tracks every invocation
bm.wrapMcpServer(server);

// Tools registered after wrapping are automatically tracked
server.tool("search_docs", { query: z.string() }, async (args) => {
  // ... your tool logic (unchanged)
});
Manual tracking
bm.trackTool({
  tool: "search_docs",
  client: "claude-code",
  protocolVersion: "2024-11-05",
  durationMs: 250,
  success: true,
  inputTokens: 150,
  outputTokens: 800,
});

What gets tracked

Tool names, client names, duration, success/failure, and optional token counts. Tool input parameters and output content are never sent.

05

API Tracking

Track API endpoint usage, latency, error rates, and caller patterns with Express middleware or manual calls.

Express middleware
import { BetterMeter } from "@bettermeter/node";
import express from "express";

const bm = new BetterMeter({
  siteId: "my-api",
  apiKey: "bm_...",
});

const app = express();

// Auto-track all requests
app.use(bm.expressMiddleware());
Manual tracking
bm.trackApi({
  method: "POST",
  endpoint: "/api/users",    // Use patterns, not actual paths with IDs
  statusCode: 201,
  durationMs: 45,
});

What gets tracked

HTTP method, endpoint pattern, status code, and duration. Request/response bodies, headers, query parameters, and path parameters are never sent. Use endpoint patterns (/api/users/:id) not actual paths (/api/users/abc123).

06

SDK Reference

The @bettermeter/node SDK is zero-dependency (uses built-in fetch and crypto). Requires Node.js 18+.

Constructor

const bm = new BetterMeter(config);
siteIdrequired
string
Domain or identifier registered in BetterMeter
apiKeyrequired
string
API key (Bearer token) from dashboard settings
apiUrl
string
BetterMeter API URL. Default: "https://bettermeter.com"
disabled
boolean
Disable all tracking. Default: false
batch
boolean
Queue events and flush on interval. Default: false
batchInterval
number
Flush interval in ms. Default: 5000

trackCommand(options)

commandrequired
string
Command name
subcommand
string
Subcommand (e.g., "deploy preview")
flags
string[]
Flag names used (values stripped)
version
string
CLI version
durationMs
number
Execution time in milliseconds
exitCode
number
Process exit code (0 = success)
isCi
boolean
Running in CI environment
userId
string
Custom user identifier
properties
object
Additional custom properties

trackTool(options)

toolrequired
string
MCP tool name
client
string
AI client name (e.g., "claude-code", "cursor")
protocolVersion
string
MCP protocol version
durationMs
number
Execution time in milliseconds
success
boolean
Whether the call succeeded
errorType
string
Error classification (e.g., "validation_error")
inputTokens
number
Input token count
outputTokens
number
Output token count
userId
string
Custom user identifier
properties
object
Additional custom properties

trackApi(options)

methodrequired
string
HTTP method (GET, POST, etc.)
endpointrequired
string
Endpoint pattern (use :param for dynamic segments)
statusCode
number
HTTP response status code
durationMs
number
Response time in milliseconds
userId
string
Custom user identifier
properties
object
Additional custom properties

Auto-wrappers

wrapCommander(program, options?)

Hooks into Commander.js postAction to auto-track all commands.

wrapMcpServer(server)

Monkey-patches server.tool() to wrap all handlers with timing and error tracking.

expressMiddleware()

Returns an Express/Connect middleware that tracks every request on res.end.

Lifecycle

flush(): Promise<void>

Send all queued events immediately.

shutdown(): Promise<void>

Stop batch timer and flush remaining events. Call before process exit.

07

CLI Reference

The BetterMeter CLI lets you query analytics from the terminal with beautiful visual output. All commands accept -s/--site, -r/--range, -l/--limit, and --json.

Install & authenticate
npm install -g bettermeter
bettermeter login

Output Formats

By default, the CLI renders rich visual output with ASCII art charts, colored text, sparklines, and box-drawn stat cards. All output uses Unicode characters compatible with every modern terminal. Colors auto-detect terminal capabilities and respect the NO_COLOR environment variable.

Default (visual)Line charts, bar charts, sparklines, styled tables with color
--jsonRaw JSON data — ideal for scripting, piping to jq, or programmatic use

Visual output includes:

  • Line charts for timeseries data (daily visitors, invocations)
  • Horizontal bar charts for ranked lists (pages, sources, countries)
  • Sparklines inline with overview stats for quick trend visualization
  • Box-drawn stat cards with colored change indicators for overviews
  • Styled tables with box-drawing borders for detailed data
Example: visual vs JSON
# Visual output (default)
bettermeter stats -s example.com

# JSON output for scripting
bettermeter stats -s example.com --json | jq '.visitors'

Real-Time

live -s <siteId>Live visitor count (--json for raw output)

Web Analytics

statsOverview: visitors, pageviews, sessions + % change
pagesTop pages by visitor count
sourcesTraffic sources (--filter all|ai|traditional)
ai-trafficAI referral breakdown by platform
botsBot/crawler traffic (--category all|ai-crawler|search|monitoring|scraper)
timeseriesDaily visitor/pageview trend
countriesVisitors by country
devicesDevice breakdown
browsersBrowser breakdown
visitorsRecent visitors with activity summary
eventsCustom events with counts
exportFull report (--format json|csv|md)

CLI Analytics

cli-overviewInvocations, callers, success rate, avg duration
cli-commandsTop commands by invocation count
cli-timeseriesDaily CLI activity

MCP Analytics

mcp-overviewInvocations, callers, success rate, avg duration
mcp-toolsTop MCP tools by invocation count
mcp-clientsClient breakdown (Claude, Cursor, etc.)
mcp-timeseriesDaily MCP activity

API Analytics

api-overviewInvocations, callers, error rate, avg duration
api-endpointsTop endpoints by invocation count
api-timeseriesDaily API activity

Options

-s, --siterequired
string
Site ID (domain)
-r, --range
string
Date range: today, 7d, 30d, 90d, 12m. Default: 30d
-l, --limit
number
Max results. Default: 10
--json
flag
Output raw JSON instead of visual charts and tables
08

MCP Tools Reference

BetterMeter runs as an MCP server for AI assistants. Every CLI command has a matching MCP tool with the bettermeter_ prefix.

.mcp.json
{
  "mcpServers": {
    "bettermeter": {
      "command": "npx",
      "args": ["bettermeter"]
    }
  }
}

All tools accept site_id (string), range (today|7d|30d|90d|12m), and limit (number). Usage examples for AI assistants:

"How many live visitors right now?"bettermeter_live_visitors
"How's my traffic?"bettermeter_stats
"Show AI referral breakdown"bettermeter_ai_traffic
"Which CLI commands are most used?"bettermeter_cli_commands
"What AI clients call my MCP server?"bettermeter_mcp_clients
"Top API endpoints this week"bettermeter_api_endpoints with range=7d

Full Tool List

Real-Time

bettermeter_live_visitors

Web Analytics

bettermeter_statsbettermeter_pagesbettermeter_sourcesbettermeter_ai_trafficbettermeter_botsbettermeter_timeseriesbettermeter_countriesbettermeter_devicesbettermeter_browsersbettermeter_visitorsbettermeter_visitorbettermeter_eventsbettermeter_export

CLI Analytics

bettermeter_cli_overviewbettermeter_cli_commandsbettermeter_cli_timeseries

MCP Analytics

bettermeter_mcp_overviewbettermeter_mcp_toolsbettermeter_mcp_clientsbettermeter_mcp_timeseries

API Analytics

bettermeter_api_overviewbettermeter_api_endpointsbettermeter_api_timeseries

Site Management

bettermeter_sites_listbettermeter_sites_addbettermeter_sites_removebettermeter_sites_infobettermeter_install

Team & Brand

bettermeter_members_listbettermeter_members_addbettermeter_members_removebettermeter_members_update_rolebettermeter_brand_report
09

API Reference

All analytics endpoints accept GET requests with query parameters. Authenticate with Authorization: Bearer <api_key>.

Event Ingestion

POST /api/eventIngest an event (web, CLI, MCP, or API). Returns 202.
Event payload
{
  "site_id": "example.com",
  "event_name": "pageview",        // or "cli.command", "mcp.tool", "api.request"
  "event_source": "web",           // "web" | "cli" | "mcp" | "api"
  "url": "https://example.com/page",
  "pathname": "/page",
  "hostname": "example.com",
  "referrer": "https://google.com",
  "screen_width": 1920,
  "timezone": "America/New_York",
  "user_id": "optional_user_id",
  "properties": { "key": "value" }
}

Real-Time & Heartbeat

GET /api/analytics/liveLive visitor count. Returns { live: number }. Accepts ?siteId=...
POST /api/heartbeatReceive browser heartbeats for live visitor tracking
POST /api/hStealth alias for /api/heartbeat (ad-blocker resistant)

Query Endpoints

All accept ?siteId=...&from=YYYY-MM-DD&to=YYYY-MM-DD.

Web Analytics

GET /api/analytics/overviewVisitors, pageviews, sessions + % change
GET /api/analytics/pagesTop pages by visitor count
GET /api/analytics/sourcesTraffic sources with AI detection
GET /api/analytics/timeseriesDaily visitor/pageview trend
GET /api/analytics/ai-trafficAI referral breakdown by platform
GET /api/analytics/botsBot/crawler traffic
GET /api/analytics/countriesVisitors by country
GET /api/analytics/devicesDevice type breakdown
GET /api/analytics/browsersBrowser breakdown
GET /api/analytics/visitorsVisitor list with activity
GET /api/analytics/eventsCustom events

CLI Analytics

GET /api/analytics/cli-overviewInvocations, callers, success rate
GET /api/analytics/cli-commandsTop commands
GET /api/analytics/cli-timeseriesDaily CLI activity

MCP Analytics

GET /api/analytics/mcp-overviewInvocations, callers, success rate
GET /api/analytics/mcp-toolsTop tools
GET /api/analytics/mcp-clientsClient breakdown
GET /api/analytics/mcp-timeseriesDaily MCP activity

API Analytics

GET /api/analytics/api-overviewInvocations, callers, error rate
GET /api/analytics/api-endpointsTop endpoints
GET /api/analytics/api-timeseriesDaily API activity
10

Proxy Setup

Ad blockers sometimes block third-party analytics scripts. By proxying BetterMeter through your own domain, the script and events appear as first-party requests — making them invisible to blockers. This works because the browser sees requests to yourdomain.com/bm/... instead of bettermeter.com/api/....

How it works

You set up URL rewrites on your server so that three paths on your domain forward to BetterMeter:

/bm/s→ https://bettermeter.com/api/sTracker script
/bm/e→ https://bettermeter.com/api/eEvent endpoint
/bm/p→ https://bettermeter.com/api/pNoscript pixel

Then update your tracking snippet to use the proxied paths:

Proxied snippet
<script defer data-site="example.com" data-api="/bm/e" src="/bm/s"></script>
<noscript><img src="/bm/p?s=example.com" alt="" /></noscript>

The data-api attribute tells the tracker to send events to your proxy endpoint instead of directly to BetterMeter. The src loads the script from your proxy. To the browser (and ad blockers), everything is a same-origin request.

Platform guides

Next.js

next.config.js
module.exports = {
  async rewrites() {
    return [
      { source: "/bm/s", destination: "https://bettermeter.com/api/s" },
      { source: "/bm/e", destination: "https://bettermeter.com/api/e" },
      { source: "/bm/p", destination: "https://bettermeter.com/api/p" },
    ];
  },
};

Nuxt

nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    "/bm/s": { proxy: "https://bettermeter.com/api/s" },
    "/bm/e": { proxy: "https://bettermeter.com/api/e" },
    "/bm/p": { proxy: "https://bettermeter.com/api/p" },
  },
});

SvelteKit

svelte.config.js
const config = {
  kit: {
    // SvelteKit doesn't have built-in rewrites.
    // Use a server hook instead:
  },
};
src/hooks.server.ts
import type { Handle } from "@sveltejs/kit";

const PROXY_MAP: Record<string, string> = {
  "/bm/s": "https://bettermeter.com/api/s",
  "/bm/e": "https://bettermeter.com/api/e",
  "/bm/p": "https://bettermeter.com/api/p",
};

export const handle: Handle = async ({ event, resolve }) => {
  const target = PROXY_MAP[event.url.pathname];
  if (target) {
    const url = new URL(target);
    url.search = event.url.search;
    const res = await fetch(url, {
      method: event.request.method,
      headers: event.request.headers,
      body: event.request.method !== "GET" ? event.request.body : undefined,
      // @ts-expect-error — needed for streaming
      duplex: "half",
    });
    return new Response(res.body, {
      status: res.status,
      headers: res.headers,
    });
  }
  return resolve(event);
};

Astro

src/pages/bm/[...proxy].ts
import type { APIRoute } from "astro";

const PROXY_MAP: Record<string, string> = {
  s: "https://bettermeter.com/api/s",
  e: "https://bettermeter.com/api/e",
  p: "https://bettermeter.com/api/p",
};

export const ALL: APIRoute = async ({ params, request }) => {
  const target = PROXY_MAP[params.proxy ?? ""];
  if (!target) return new Response("Not found", { status: 404 });

  const url = new URL(target);
  url.search = new URL(request.url).search;
  return fetch(url, {
    method: request.method,
    headers: request.headers,
    body: request.method !== "GET" ? request.body : undefined,
    // @ts-expect-error — needed for streaming
    duplex: "half",
  });
};

Remix / React Router

app/routes/bm.$.tsx
import type { LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node";

const PROXY_MAP: Record<string, string> = {
  s: "https://bettermeter.com/api/s",
  e: "https://bettermeter.com/api/e",
  p: "https://bettermeter.com/api/p",
};

async function proxy({ request, params }: LoaderFunctionArgs | ActionFunctionArgs) {
  const target = PROXY_MAP[params["*"] ?? ""];
  if (!target) return new Response("Not found", { status: 404 });

  const url = new URL(target);
  url.search = new URL(request.url).search;
  return fetch(url, {
    method: request.method,
    headers: request.headers,
    body: request.method !== "GET" ? request.body : undefined,
    // @ts-expect-error — needed for streaming
    duplex: "half",
  });
}

export const loader = proxy;
export const action = proxy;

Nginx

nginx.conf
location /bm/s {
    proxy_pass https://bettermeter.com/api/s;
    proxy_ssl_server_name on;
    proxy_set_header Host bettermeter.com;
}

location /bm/e {
    proxy_pass https://bettermeter.com/api/e;
    proxy_ssl_server_name on;
    proxy_set_header Host bettermeter.com;
}

location /bm/p {
    proxy_pass https://bettermeter.com/api/p;
    proxy_ssl_server_name on;
    proxy_set_header Host bettermeter.com;
}

Apache

.htaccess or httpd.conf
RewriteEngine On
RewriteRule ^bm/s$ https://bettermeter.com/api/s [P,L]
RewriteRule ^bm/e$ https://bettermeter.com/api/e [P,L]
RewriteRule ^bm/p$ https://bettermeter.com/api/p [P,L]

# Requires mod_proxy and mod_proxy_http enabled:
# a2enmod proxy proxy_http

Caddy

Caddyfile
example.com {
    handle_path /bm/s {
        reverse_proxy https://bettermeter.com/api/s {
            header_up Host bettermeter.com
        }
    }
    handle_path /bm/e {
        reverse_proxy https://bettermeter.com/api/e {
            header_up Host bettermeter.com
        }
    }
    handle_path /bm/p {
        reverse_proxy https://bettermeter.com/api/p {
            header_up Host bettermeter.com
        }
    }
}

Cloudflare Workers

worker.js
export default {
  async fetch(request) {
    const url = new URL(request.url);
    const map = {
      "/bm/s": "https://bettermeter.com/api/s",
      "/bm/e": "https://bettermeter.com/api/e",
      "/bm/p": "https://bettermeter.com/api/p",
    };
    const target = map[url.pathname];
    if (!target) return fetch(request);

    const dest = new URL(target);
    dest.search = url.search;
    return fetch(dest.toString(), {
      method: request.method,
      headers: { ...Object.fromEntries(request.headers), Host: "bettermeter.com" },
      body: request.method !== "GET" ? request.body : undefined,
    });
  },
};

Vercel (without Next.js)

vercel.json
{
  "rewrites": [
    { "source": "/bm/s", "destination": "https://bettermeter.com/api/s" },
    { "source": "/bm/e", "destination": "https://bettermeter.com/api/e" },
    { "source": "/bm/p", "destination": "https://bettermeter.com/api/p" }
  ]
}

Netlify

netlify.toml
[[redirects]]
  from = "/bm/s"
  to = "https://bettermeter.com/api/s"
  status = 200
  force = true

[[redirects]]
  from = "/bm/e"
  to = "https://bettermeter.com/api/e"
  status = 200
  force = true

[[redirects]]
  from = "/bm/p"
  to = "https://bettermeter.com/api/p"
  status = 200
  force = true

WordPress

Add these rewrite rules to your theme's functions.php or a custom plugin:

functions.php
add_action('init', function() {
    add_rewrite_rule('^bm/s$', 'index.php?bm_proxy=s', 'top');
    add_rewrite_rule('^bm/e$', 'index.php?bm_proxy=e', 'top');
    add_rewrite_rule('^bm/p$', 'index.php?bm_proxy=p', 'top');
});

add_filter('query_vars', function($vars) {
    $vars[] = 'bm_proxy';
    return $vars;
});

add_action('template_redirect', function() {
    $proxy = get_query_var('bm_proxy');
    if (!$proxy) return;

    $map = [
        's' => 'https://bettermeter.com/api/s',
        'e' => 'https://bettermeter.com/api/e',
        'p' => 'https://bettermeter.com/api/p',
    ];
    if (!isset($map[$proxy])) return;

    $url = $map[$proxy];
    if ($_SERVER['QUERY_STRING']) $url .= '?' . $_SERVER['QUERY_STRING'];

    $args = ['method' => $_SERVER['REQUEST_METHOD'], 'timeout' => 10];
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        $args['body'] = file_get_contents('php://input');
        $args['headers'] = ['Content-Type' => $_SERVER['CONTENT_TYPE'] ?? 'application/json'];
    }

    $response = wp_remote_request($url, $args);
    if (is_wp_error($response)) { status_header(502); exit; }

    status_header(wp_remote_retrieve_response_code($response));
    foreach (wp_remote_retrieve_headers($response) as $k => $v) {
        if (in_array(strtolower($k), ['content-type', 'cache-control'])) header("$k: $v");
    }
    echo wp_remote_retrieve_body($response);
    exit;
});

// After adding, flush rewrite rules: Settings → Permalinks → Save

Django

urls.py + views.py
# urls.py
from django.urls import path
from . import views

urlpatterns = [
    path("bm/<slug:endpoint>", views.bm_proxy),
]

# views.py
import requests
from django.http import HttpResponse

PROXY_MAP = {
    "s": "https://bettermeter.com/api/s",
    "e": "https://bettermeter.com/api/e",
    "p": "https://bettermeter.com/api/p",
}

def bm_proxy(request, endpoint):
    target = PROXY_MAP.get(endpoint)
    if not target:
        return HttpResponse("Not found", status=404)
    url = target + ("?" + request.META["QUERY_STRING"] if request.META.get("QUERY_STRING") else "")
    resp = requests.request(
        method=request.method, url=url,
        headers={"Host": "bettermeter.com", "Content-Type": request.content_type},
        data=request.body if request.method == "POST" else None,
        timeout=10,
    )
    return HttpResponse(resp.content, status=resp.status_code, content_type=resp.headers.get("Content-Type"))

Rails

config/routes.rb + controller
# config/routes.rb
match "/bm/:endpoint", to: "bm_proxy#proxy", via: [:get, :post], constraints: { endpoint: /[sep]/ }

# app/controllers/bm_proxy_controller.rb
class BmProxyController < ApplicationController
  skip_before_action :verify_authenticity_token

  PROXY_MAP = { "s" => "/api/s", "e" => "/api/e", "p" => "/api/p" }.freeze

  def proxy
    path = PROXY_MAP[params[:endpoint]]
    return head(:not_found) unless path

    uri = URI("https://bettermeter.com#{path}")
    uri.query = request.query_string.presence

    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    req = (request.post? ? Net::HTTP::Post : Net::HTTP::Get).new(uri)
    req["Host"] = "bettermeter.com"
    if request.post?
      req.body = request.body.read
      req["Content-Type"] = request.content_type
    end

    resp = http.request(req)
    render body: resp.body, status: resp.code.to_i, content_type: resp["Content-Type"]
  end
end

Verification

After setting up the proxy, verify it works:

Test
# Should return the tracker JavaScript
curl -s https://yourdomain.com/bm/s | head -5

# Should return 202 (event accepted)
curl -s -o /dev/null -w "%{http_code}" -X POST https://yourdomain.com/bm/e \
  -H "Content-Type: application/json" \
  -d '{"site_id":"yourdomain.com","event_name":"test"}'
11

Data Model

All four event sources share a single Event table. The eventSource column distinguishes them. Source-specific metadata lives in the properties JSON column.

Field Mapping

Field
Web
CLI
MCP
API
eventName
pageview
cli.command
mcp.tool
api.request
eventSource
web
cli
mcp
api
pathname
URL path
/command
/tool_name
/endpoint
referrer
document.referrer
(empty)
mcp://client
(empty)
properties
custom props
command, flags, duration, exit_code, os
tool, client, duration, success
method, endpoint, status_code, duration
12

Privacy

BetterMeter is designed from the ground up to respect user privacy. No consent banners needed. GDPR compliant by default.

01

No cookies

Zero cookies, zero local storage, zero fingerprinting. The tracker script is stateless.

02

Hashed identifiers

IP addresses are SHA-256 hashed with the current date included in the hash input. The raw IP is never stored. Daily visitor hashes rotate every 24 hours. A stable visitor ID (hashed without date) enables visitor profiles within a site but cannot be reversed to reveal the original IP.

03

No PII collection

The SDK tracks command names, tool names, and endpoint patterns — never arguments, file paths, request bodies, or personal data.

04

Fire-and-forget

Analytics calls are async and never throw. A network failure silently drops the event. Analytics should never break the host application.

05

Opt-out

Set disabled: true in the SDK config or BETTERMETER_DISABLED=1 as an environment variable.

06

Open pipeline

Every event goes through the same processEvent() function. You can audit exactly what is collected and stored.

BetterMeter Analytics — Built for the new internet