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
~4 KB, 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.

Analytics filter workspace

The core analytics hubs share one responsive filter workspace. Date ranges, tabs, searches, and contextual facets live in the URL, making investigations refresh-safe and shareable. The Overview graph follows its selected range and includes zero-activity dates.

  • Traffic: search pages, sources, and campaigns; segment by engagement, AI attribution, channel, and volume.
  • Audience: find visitors, sessions, and actions; segment by visitor type, intent, country, and event volume.
  • Agents: inspect crawlers, CLI, MCP, and API activity by category and performance health.
  • Visibility: filter AI platforms, rankings, mentions, and referring domains by signal and authority.
02

Web Tracking

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

HTML -- add to <head>
<script defer data-site="example.com" src="https://bettermeter.com/api/script"></script>

Optional: add a no-JS tracking pixel before the closing </body> tag to detect bots and crawlers that don't execute JavaScript:

HTML -- add before </body>
<img src="https://bettermeter.com/api/pixel?s=example.com" alt="" style="position:absolute;width:0;height:0;overflow:hidden" />

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("account_123");

// Clear the durable identity on sign-out
window.bettermeter.reset();

identify() stores the opaque ID for this site in local storage and attaches it to later visits; call reset() on sign-out. Do not pass an email address or other direct personal data.

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
data-fingerprint
boolean
Opt in to browser fingerprint fields; disabled by default
data-behavior
boolean
Opt in to scroll and mouse behavior sampling; disabled by default
data-attribution
boolean
Opt in to parking the ad click ID (gclid, fbclid, msclkid and peers) for 90 days; disabled by default
data-attribution-fields
boolean
Fill matching hidden form inputs from the parked ad click; requires data-attribution

Ad Click Attribution

An ad click arrives with a platform-issued ID on the URL — gclid from Google, fbclid from Meta. It appears on exactly one pageview, the landing one, and it is the only token those platforms accept back when you later tell them the click converted. BetterMeter parks the most recent one for 90 days, the same lifetime Google's and Meta's own cookies use, and writes it to the visitor as bm_* properties.

HTML
<script defer data-site="example.com" data-attribution="true" data-attribution-fields="true" src="https://bettermeter.com/api/script"></script>

With data-attribution-fields, any empty hidden input whose name matches is filled. Both gclid and bm_gclid resolve, so an existing CRM field needs no renaming. Visible fields and fields that already carry a value are never touched.

HTML -- hidden fields on your form
<input type="hidden" name="gclid">
<input type="hidden" name="fbc">
<input type="hidden" name="utm_source">
<input type="hidden" name="utm_campaign">

Three methods drive it by hand — useful behind a consent banner, or for a form injected after page load.

JavaScript
// Capture after a consent banner is accepted
window.bettermeter.captureAttribution();

// Read the parked click, e.g. to POST it yourself
window.bettermeter.getAttribution();
// { bm_gclid: "Cj0KCQ...", bm_utm_source: "google",
//   bm_landing_page: "/pricing", bm_first_seen: "2026-08-15T11:34:26.422Z", ... }

// Fill a form injected after page load (HubSpot legacy onFormReady)
hbspt.forms.create({
  formId: "...",
  onFormReady: (form) => window.bettermeter.fillAttributionFields(form),
});

Suppressed entirely under Do Not Track and Global Privacy Control, and cleared by reset(). A later click replaces the parked one wholesale rather than merging, so one conversion is never credited to two platforms. A value over 255 characters is dropped rather than truncated, because a partial click ID is rejected by the platform. HubSpot's newer embedded forms render inside an iframe and cannot be filled from the page — use HubSpot's own query-parameter prefill there. On iOS, Google sends `gbraid` or `wbraid` instead of `gclid`; both are captured, but only the Google Ads API accepts them on a conversion upload — the manual UI import rejects them as invalid GCLIDs.

CRM Conversions (HubSpot)

Connect HubSpot and BetterMeter reads back the records your click IDs ended up on. The two systems are joined on the ad click ID, never on an email address — the tracker writes the token into your hidden form fields, so both sides already hold the same value and no personal data has to cross between them. Connecting also creates the bm_* contact properties, which is not a convenience: HubSpot silently discards a form field whose property does not exist, so without them the hidden fields are dropped on submit with no error anywhere.

Connect a portal in Settings → Customer records: create a private app in HubSpot, grant it the CRM contact read and write scopes, and paste its access token. BetterMeter verifies it and creates the bm_* contact properties for you.

Terminal
# Or connect from a script (creates the bm_* contact properties for you)
curl -X POST https://bettermeter.com/api/integrations/crm \
  -H "Authorization: Bearer bm_..." -H "Content-Type: application/json" \
  -d '{"siteId":"<site-id>","provider":"hubspot","accessToken":"pat-na1-..."}'

# Then pull conversions
bettermeter crm -s <site-id>
bettermeter crm:sync -c <connection-id>
bettermeter conversions --won

# The ad behind a visit. --utm-content resolves one exact creative;
# without it the answer is campaign-level and says so.
bettermeter ad-creative -s <site-id> --campaign "Summer Sale"
bettermeter ad-creative -s <site-id> --utm-content 120001234567890

The private app needs crm.objects.contacts.read, crm.objects.contacts.write and crm.schemas.contacts.write. Syncs are incremental on HubSpot's last-modified date; HubSpot refuses to page past 10,000 results for one query, so a large portal is read in moving windows and a run that stops early reports truncated: true rather than looking complete.

Server-Side Bot Detection

Most bots (Googlebot, GPTBot, ClaudeBot, etc.) don't execute JavaScript, so the browser tracker never fires for them. To detect bot and crawler traffic, add one line to your Next.js middleware:

middleware.ts
import { reportBotVisit } from "@bettermeter/node/middleware";

export function middleware(request) {
  reportBotVisit(request, "example.com", { apiKey: "bm_..." });
  // ... rest of your middleware
}

Install the SDK:

Terminal
npm install @bettermeter/node

Edge-compatible, non-blocking, zero latency impact. Detects 25+ known bots including AI crawlers, search engines, and monitoring services. Bot visits appear in your Crawlers dashboard automatically.