Documentation

Documentation

Badgerlytics Docs

Everything you need to install, configure, and get the most out of Badgerlytics. Use the sidebar to jump around — it highlights where you are as you scroll.

What is Badgerlytics

Badgerlytics is an analytics and experimentation platform built for ecommerce and SaaS teams. We help you understand how visitors use your site, what drives revenue, and which changes actually move the needle — without juggling five different tools.

It bundles three things that usually live in three separate products:

  • Analytics: traffic, engagement, revenue, MRR, usage revenue, subscription lifecycle, cart abandonment, and page performance.
  • Experimentation: A/B tests and feature flags that plug directly into your revenue and analytics data.
  • AI assistance: automatic AI insights, a chat analyst that answers questions in plain English, and a UX analyst that reviews your site and recommends improvements.

You bring your site; Badgerlytics brings the tracking script, the reports, the SDKs, and the dashboards. Add the script tag (or one of our framework SDKs), tell us what your funnel looks like, and you're live in under an hour. Use window.badgerlytics from anywhere in your client code to track product views, cart updates, conversions, or any custom event you care about.

Visitor privacy controls (anonymous identifiers, consent/disconnected mode, no data selling) are included. Badgerlytics is not a privacy-only analytics tool — it is an analytics and experimentation platform with AI assistance on top.

Organizations

Setup and purpose

An organization is the top-level container for your team and billing. When you sign up you create (or get invited to) exactly one organization. Inside that organization you create one or more properties, which are the websites or apps you actually track.

The split lets you:

  • Bill for multiple sites under one plan.
  • Share a team across several properties.
  • Keep ownership of audiences, custom events, and feature flag definitions inside the property so they don't leak between sites.

Users and roles

Each user in your organization has one of three roles:

  • Owner: can manage billing, invite users, and create or delete properties. There's always at least one. Owners can't be removed from the organization, but ownership can be transferred to another member from Organization settings → Users. When you transfer ownership, you become an admin.
  • Admin: can do everything the owner can do — invite users, create properties, edit feature flags, change funnels.
  • Edit: read access to reports, can run AI Chat Analyst queries, can't change configuration.

Property-level access can be further restricted from Organization settings → Members. Adding a user to your org doesn't automatically grant access to every property — you pick which ones they can see.

Properties

Purpose

A property represents a single site — typically one primary domain, and optionally additional hosts on the same site. It owns its own analytics data, feature flags, funnels, custom events, audiences, and (for SaaS) subscription plans. The tracking script identifies which property an event belongs to via the property ID in window.badgerlyticsConfig.

Setup

To create a property:

  • From your org dashboard, click + New property.
  • Give it a name (e.g. "Store — Production"), the domain, pick an environment label, choose a display currency (USD by default), and confirm the reporting time zone (detected from your browser).
  • Copy the install snippet — or grab the SDK package — and drop it on your site.
  • Visit your site once with Test mode on to confirm events are landing.

Hosts

Every property has a primary domain (the main hostname you enter at create time). By default, tracking is accepted on that domain only — plus its automatic www variant. If your product lives across more than one hostname on the same site, add them under Additional hosts when you create or edit the property.

Additional hosts keep everything in one property: one install ID, one set of funnels and flags, and one continuous visitor journey across those hostnames. You can still filter reports by host when you need to compare traffic on each.

This is especially useful for SaaS products that split the experience:

  • Marketing site — landing pages, pricing, and content (e.g. www.example.com).
  • App site — signup, the product itself, and often billing or account management (e.g. app.example.com).

With both hosts on the same property, you can follow someone from a marketing visit through signup and into billing without stitching together two separate properties.

A few rules to keep in mind:

  • Hosts must share the same site root as the primary domain (for example, app.example.com with primary www.example.com). Unrelated domains need their own properties.
  • www variants of a listed host are accepted automatically — you do not need to add both.
  • You can add up to five additional hosts beyond the primary domain.
  • For local development, different ports on localhost count as the same site (for example, localhost:3003 alongside primary localhost:3002).

Same snippet on every host

Install the same property ID on each allowed host. Events from all of them land in that property's reports.

Display currency

Each property supports one display currency at a time. This controls how dashboards, reports, and AI summaries format monetary values — for example, which symbol appears next to revenue and MRR. Choose it when you create a property or update it anytime from the property edit dialog.

Supported currencies (USD is the default):

  • USD — US Dollar
  • CAD — Canadian Dollar
  • GBP — British Pound
  • EUR — Euro
  • AUD — Australian Dollar
  • NZD — New Zealand Dollar
  • CHF — Swiss Franc

The same list is available when you set currency on SaaS subscription plans in SaaS Setup. We currently support only these codes because our reporting and display layer assumes standard two-decimal minor units (cents, pence, and the like). Currencies such as JPY that do not fit that model are not offered yet.

Display currency is for presentation only. It does not convert amounts stored in your analytics, enforce a single currency in your app code, or change what the tracking script sends on each event. If you track mixed currencies on one property, totals are still summed as raw numbers; pick the currency that best matches how you report revenue.

Badgerlytics subscription billing for your organization remains in USD regardless of this setting.

Reporting time zone

Each property has one reporting time zone. Dashboards, reports, CSV exports, and AI summaries use it when they group events into calendar days — for example what counts as "today", "last 7 days", or a daily row on a chart. Choose it when you create a property or update it anytime from the property edit dialog.

New properties pick up the time zone from the browser of the person who creates them. If that cannot be detected, they default to UTC. Set the zone your team actually reports in — usually headquarters or the market you care about most — rather than leaving whatever the creator's laptop happened to be set to.

Reports show a short "Times in …" note under the description so you can always see which zone the numbers are using. Changing the setting takes effect immediately: the same events are regrouped into that zone's days. It does not rewrite your stored analytics, and it is not the visitor's own time zone from geolocation.

Considerations for environments

We strongly recommend using separate properties for production and non-production environments. Don't point your staging or local-dev script at the same property ID as production — you'll pollute your real reports with QA traffic and feature flags that were never meant for users.

A common setup is two properties:

  • Production property: your live customers, real traffic only.
  • Stage property: integration testing, internal QA, smoke tests, and most local development — point your dev build at the stage ID so QA traffic never hits production reports.

One snippet, two IDs

Pick the property ID in your build config via an env var. Same snippet, same SDK calls — only the ID changes between stage and prod.

Syncing between properties

Because you usually want test/stage and production to behave identically, we provide a one-click sync for the things you tend to configure once and keep aligned:

  • Feature flags — including iterations and variants
  • Custom events — including their dashboard tokens
  • Audience traits — labels, types, and tokens for segment rules
  • Funnels — step names and orderings

Open the source property, find the resource you want to copy ("Copy to property…"), and pick the destination. For custom events, funnels, and SaaS plans, you can create a new copy or overwrite an existing one with the same identifier. Feature flags and audience traits are create-only: flags get a new token when the destination already has one; traits always copy under the same token and are blocked with an "already exists" message when the destination already has that token. Analytics data itself is never copied — each property owns its own events.

Script Installation

Installing Badgerlytics is the same shape everywhere: configure your property ID, load the tracking script, and you're tracking. Pick the guide below that matches your stack.

Static site / general installation

Works for any HTML page, static site generator, or platform that lets you drop a <script> tag into the page.

index.htmlmarkup
<!-- In your <head> or just before </body> -->
<script>
window.badgerlyticsConfig = {
propertyId: 'your-property-id',
};
</script>
<script
src="https://cdn.badgerlytics.com/scripts/badgerlytics.min.js"
async
></script>

Where to find your property ID

Sign in to the app, open a property, and click Install in the sidebar. The snippet there has your property ID baked in.

Shopify

Add the snippet to your theme so it loads on every storefront page — no app install required. A/B tests on Shopify are assigned client-side — the script picks the variation in the browser. See SSR vs. client rendering for what that means for your test design.

layout/theme.liquidmarkup
<!-- In your <head> or just before </body> -->
<script>
window.badgerlyticsConfig = {
propertyId: 'your-property-id',
};
</script>
<script
src="https://cdn.badgerlytics.com/scripts/badgerlytics.min.js"
async
></script>
  1. From your Shopify admin, go to Online Store → Themes.
  2. On your live theme, open the ••• menu and choose Edit code.
  3. Open layout/theme.liquid and paste the snippet just before the closing </head> tag.
  4. Click Save, then load your storefront and confirm events with Test mode.

Checkout pages

Shopify does not run theme scripts during checkout, so tracking covers storefront pages up through the start of checkout. Product views, cart updates, and funnel steps before checkout work normally.

WooCommerce

WooCommerce runs on WordPress, so install the snippet site-wide through a header-scripts plugin or your theme's custom-scripts panel. Cart and checkout pages are part of your own site, so the full path to purchase is tracked. A/B tests on WooCommerce are assigned client-side — the script picks the variation in the browser. See SSR vs. client rendering for what that means for your test design.

Site header (before </head>)markup
<!-- In your <head> or just before </body> -->
<script>
window.badgerlyticsConfig = {
propertyId: 'your-property-id',
};
</script>
<script
src="https://cdn.badgerlytics.com/scripts/badgerlytics.min.js"
async
></script>
  1. Install a header-scripts plugin such as WPCode (Insert Headers and Footers), or use your theme's built-in custom scripts option if it has one.
  2. Paste the snippet into the Header area so it renders before </head> on every page.
  3. Save, and if you use a caching plugin, purge the page cache so the snippet appears everywhere.
  4. Visit your shop, cart, and checkout pages and confirm events with Test mode.

BigCommerce

BigCommerce has a built-in Script Manager, so you never need to edit theme files. A/B tests on BigCommerce are assigned client-side — the script picks the variation in the browser. See SSR vs. client rendering for what that means for your test design.

Script Manager → script contentsmarkup
<!-- In your <head> or just before </body> -->
<script>
window.badgerlyticsConfig = {
propertyId: 'your-property-id',
};
</script>
<script
src="https://cdn.badgerlytics.com/scripts/badgerlytics.min.js"
async
></script>
  1. From your store admin, go to Storefront → Script Manager and click Create a Script.
  2. Name it Badgerlytics, set Placement to Head, Location to All pages, category to Analytics, and script type to Script.
  3. Paste the snippet into the script contents and save.

Wix

Wix supports third-party snippets through the Custom Code panel in your site dashboard. A/B tests on Wix are assigned client-side — the script picks the variation in the browser. See SSR vs. client rendering for what that means for your test design.

Settings → Custom Codemarkup
<!-- In your <head> or just before </body> -->
<script>
window.badgerlyticsConfig = {
propertyId: 'your-property-id',
};
</script>
<script
src="https://cdn.badgerlytics.com/scripts/badgerlytics.min.js"
async
></script>
  1. From your site dashboard, go to Settings → Custom Code.
  2. Click + Add Custom Code and paste the snippet.
  3. Name it Badgerlytics, apply it to All pages with Load code once, and place it in the Head.
  4. Click Apply and confirm events with Test mode.

Plan requirement

Wix only allows custom code on premium plans with a connected domain.

Magento (Adobe Commerce)

Magento Open Source and Adobe Commerce both let you add the snippet from the admin without touching theme code. A/B tests on Magento (Adobe Commerce) are assigned client-side — the script picks the variation in the browser. See SSR vs. client rendering for what that means for your test design.

Content → Design → Configuration → HTML Headmarkup
<!-- In your <head> or just before </body> -->
<script>
window.badgerlyticsConfig = {
propertyId: 'your-property-id',
};
</script>
<script
src="https://cdn.badgerlytics.com/scripts/badgerlytics.min.js"
async
></script>
  1. In the admin, go to Content → Design → Configuration and edit your store view.
  2. Expand HTML Head and paste the snippet into Scripts and Style Sheets.
  3. Click Save Configuration, then flush the cache from System → Cache Management.

React

@badgerlytics/sdk/react loads the tracking script and exposes hooks like useVariation() and useSetTraits(). Wrap your app in BadgerlyticsProvider with a config object — the provider sets window.badgerlyticsConfig and injects the script for you.

Use this for SPAs (Create React App, Vite, etc.) and as the script install step for Next.js and Remix — pair those frameworks with SSR middleware (SDK) when you need server-side assignment before HTML is sent.

npm install @badgerlytics/sdk
App.jsxjsx
import { BadgerlyticsProvider, useVariation } from '@badgerlytics/sdk/react';
export default function App() {
return (
<BadgerlyticsProvider
config={{ propertyId: 'your-property-id' }}
>
<Home />
</BadgerlyticsProvider>
);
}
function Home() {
const heroVariation = useVariation('hero_test', 'control');
return heroVariation === 'v1' ? <HeroB /> : <HeroA />;
}

Already using the HTML snippet?

Pass autoLoad={false} and skip config so the provider only bridges React to an existing script tag.
index.html (optional)markup
<!-- Optional: load via HTML instead of the provider -->
<script>window.badgerlyticsConfig = { propertyId: 'your-property-id' };</script>
<script
src="https://cdn.badgerlytics.com/scripts/badgerlytics.min.js"
async
></script>
<!-- Then use <BadgerlyticsProvider autoLoad={false}> so hooks wait for the script -->

SSR vs. client rendering considerations

Both work — they just hand you different trade-offs:

  • SSR (Next.js, Remix, Astro, Nuxt): SDK middleware decides the variation on the server before HTML is sent — no flash of the wrong variant. Setup, API reference, and per-framework examples are in SSR middleware (SDK).
  • Client-only (SPAs, static HTML, hosted platforms): the script loads in the browser and assignments apply after first paint. Use the HTML snippet above or BadgerlyticsProvider.

Server and client bucketing agree for the same (visitor, flag, iteration) tuple when both are in use, so you can mix SSR reads and client hooks without skewing results.

Platform support at a glance — SDK frameworks support both rendering modes; hosted platforms run tests client-side:

A/B test rendering support by platform
PlatformClient-side testsSSR tests
Next.jsSupportedSupportedSSR middleware (SDK)
Remix / React RouterSupportedSupportedSSR middleware (SDK)
NuxtSupportedSupportedSSR middleware (SDK)
AstroSupportedSupportedSSR middleware (SDK)
React SPASupportedNot available@badgerlytics/sdk/react only
Static HTML / script tagSupportedNot available
ShopifySupportedNot available
WooCommerceSupportedNot available
BigCommerceSupportedNot available
WixSupportedNot available
Magento (Adobe Commerce)SupportedNot available

Flash of original content

Client-side tests apply the variation after the page first paints, so visitors may briefly see the original content on load. Plan for this in your test design: favor changes below the fold or triggered by interaction, or briefly hide the tested element with CSS and reveal it once the script has applied the assignment. SSR middleware avoids this — see SSR middleware (SDK).

Test mode

Add debug: true to window.badgerlyticsConfigwhile you're wiring things up. Badgerlytics logs every event it sends to the console, including payloads and validation warnings — invaluable for double-checking your funnel and product-view shapes before going live.

window.badgerlyticsConfig = {
propertyId: 'your-property-id',
debug: true,
debugLevel: 'info', // 'error' | 'warn' | 'info' | 'debug'
};

Heads up

Turn off debug in production. The script is tiny, but console noise on every page view is rough on browser perf and a little embarrassing in DevTools.

Useful script functions

Once the script is loaded, window.badgerlytics exposes everything you need. For the full reference (config, automatic events, every method, and payload shapes), see Tracking script API in the sidebar. Quick examples:

// Track when someone views a product (PDP impression)
badgerlytics.trackProductView({
product_code: 'SKU-123',
name: 'Running Shoes',
category: 'footwear',
price: 9999, // cents
});
// Track a step in a funnel you've set up in the dashboard
badgerlytics.trackFunnelStep('purchase', 3, 'add_to_cart');
// Track a custom event (must be registered in the dashboard first)
badgerlytics.trackCustomEvent('custom_newsletter_signup', {
source: 'footer',
});
// Set audience traits for personalization / segmentation
badgerlytics.setTraits({
plan: 'pro',
signed_in: true,
});
// Read a feature flag variation
const variation = badgerlytics.getVariation('hero_test'); // 'control' | 'v1' | ...
const isOn = badgerlytics.isFlagEnabled('new_pricing_table');

Wait for the tracker

Once the script tag has executed, window.badgerlytics exists immediately — tracking calls made while it's still initializing are buffered and sent automatically in one batch when it's ready, so you won't lose events to the load race. If your code might run before the script tag itself executes, guard on window.badgerlytics or use the SDK's callWhenTrackerReady(fn) helper (from @badgerlytics/sdk/react). React hooks do this automatically.

Chrome DevTools extension

Badgerlytics DevTools is a free Chrome extension for debugging on your own sites. It works alongside the tracking script — nothing is published to the Chrome Web Store; you install it locally in developer mode. The extension does not send data to Badgerlytics servers — see the Chrome extension privacy policy.

Version 0.4.7 · Last updated September 19, 2026 at 4:36 AM UTC · badgerlytics-devtools-0.4.7-09-19-2026.zip

When the tracker is on the page, the extension lets you:

  • Overview — property and session details, active feature flags, and a summary of audience traits.
  • Flags — see experiment assignments, URL-based force overrides, and reset sticky flag cookies for a clean re-bucket.
  • Traits — view built-in and custom traits, set or clear the persisted trait bag, and re-evaluate flags after changes.
  • Events — record a live feed of payloads the tracker sends (this tab or all tabs), with search and filters.
  • Cookies & storage — list and delete Badgerlytics cookies and _bai_* script storage on the current site.
  • Custom — fire whitelisted custom events or arbitrary test events with metadata.
  • Session tools — end the current session or forget the visitor from the Overview tab.

Not a replacement for Test mode

Use Test mode in the app to exclude traffic from reports. The extension is for local inspection and debugging only.

Install (developer mode)

Version 0.4.7 · Last updated September 19, 2026 at 4:36 AM UTC

  1. Download badgerlytics-devtools-0.4.7-09-19-2026.zip.
  2. Extract the archive to a folder. The folder you select in Chrome must contain manifest.json at its root.
  3. Open chrome://extensions in Chrome.
  4. Enable Developer mode (toggle in the top-right).
  5. Click Load unpacked and choose the extracted folder.
  6. Open a page with the Badgerlytics script, then click the extension icon in the toolbar.

Updating after a new release

Download the latest ZIP from this page, extract it (replace your old folder or use a new one), then on chrome://extensions click Reload on the Badgerlytics DevTools card.

Internal browser pages

Chrome blocks extensions on chrome:// pages, the Web Store, and PDF viewer tabs. Use a normal website with the tracker installed.

Privacy

The extension does not send analytics, crash reports, or telemetry to Badgerlytics. Event captures and preferences stay in your browser. See the full Chrome extension privacy policy.

Tracking script API

Reference for window.badgerlytics and window.badgerlyticsConfig. Each event and method below includes an example and a field table (purpose, type, required, and SaaS notes where relevant).

Configuration

badgerlyticsConfig

Set on window before the script loads. Without propertyId the script exits and does not track.

window.badgerlyticsConfig = {
propertyId: 'your-property-id',
traits: { plan: 'pro', signed_in: true },
debug: false,
debugLevel: 'info',
disconnected: false,
onLoad(tracker) {
console.log(tracker.getVariation('hero_test'));
},
};
Fields for badgerlyticsConfig
FieldPurposeTypeRequiredNotes
propertyIdPublic property hashid from the app Install snippet.stringYes—
traitsInitial audience traits merged into _bai_traits on load.objectNo—
debugLog initialization and queued events to the console.booleanNo—
debugLevelMinimum log level when debug is true.error | warn | info | debug | verboseNo—
disconnectedBuild events but do not send (testing or consent).booleanNo—
onLoadCallback after property config finishes loading.(tracker) => voidNoRuns even if tracking blocked

On every analytics event

All automatic and manual analytics events include these blocks in addition to any event-specific fields below.

Event envelope

Identifiers and timestamps attached to every ingested event.

Fields for Event envelope
FieldPurposeTypeRequiredNotes
property_idPublic property hashid the event belongs to.stringAuto—
visitor_idAnonymous visitor id from the _bai_visitor cookie.stringAuto—
session_idCurrent session id from the _bai_session cookie.stringAuto—
session_start_timeISO timestamp when this session began.stringAuto—
unique_event_idUnique id for deduplication on this event.stringAuto—
event_typeEvent name sent to ingestion (e.g. page_view, conversion).stringAuto—
event_timeISO timestamp when the event was recorded.stringAuto—
event_nameInternal routing label; always analytics for script events.stringAutoValue: analytics

Shared context

Device, traffic, and experiment context from getCommonEventData().

Fields for Shared context
FieldPurposeTypeRequiredNotes
country_codeCountry derived from browser language, when available.string | nullAuto—
referrer_sourceHostname of the session's first referring site, or direct when none. Stays the landing source for later events (checkout, OAuth, in-site navigation).stringAuto—
device_typeDevice class for segmentation and reports.mobile | tablet | desktopAuto—
browser_nameDetected browser family.chrome | safari | firefox | edge | opera | ie | otherAuto—
os_nameDetected operating system.windows | macos | ios | android | linux | otherAuto—
utm_dataUTM parameters from the first page of the session, not later URLs.object | omittedNoKeys: source, medium, campaign, content, term
active_flagsFlag assignments active when the event fired.{ [flag]: { v, i } }Autov = variation, i = iteration
organization_idPublic organization hashid from the property embed.string | nullAuto—

Automatic events

You do not call these — the script emits them when tracking is allowed.

session_start (automatic)

Emitted when a new 30-minute session starts. Includes the event envelope and shared context on every analytics event. The ingest response for this event includes a geoLocation object (IP-derived) that the script stores in localStorage for getLocation().

// Automatic — no call required
// event_type: "session_start"
// Ingest response (when session_start is accepted):
// { ok: true, geoLocation: { country: "US", city: "Austin", ... } }
Fields for session_start (automatic)
FieldPurposeTypeRequiredNotes
is_new_visitorWhether this is the visitor’s first-ever session.booleanAuto—

page_view (automatic)

Emitted on each full page load or SPA route change (after the document load event on first paint).

// Automatic — no call required
// event_type: "page_view"
Fields for page_view (automatic)
FieldPurposeTypeRequiredNotes
page_data.pathPathname of the page viewed.stringAuto—
session_pageview_number1-based pageview index within this session.integerAuto—
page_data.page_load_timeLoad duration in milliseconds when measurable.integerNoNav Timing or SPA paint

page_exit (automatic)

Emitted when the user hides the tab or leaves the page. Scroll metrics are collected during the pageview, not as separate events.

// Automatic on tab hide / unload
// event_type: "page_exit"
Fields for page_exit (automatic)
FieldPurposeTypeRequiredNotes
page_data.pathPathname of the page being exited.stringAuto—
time_on_pageMilliseconds spent on this page.integerAuto—
session_durationMilliseconds since session_start_time.integerAuto—
session_pageview_countTotal pageviews in this session so far.integerAuto—
max_scroll_depthMaximum scroll depth reached on this page (0–100).integerAuto—
time_to_first_scroll_msMs from page entry to first scroll, if the user scrolled.integer | nullNo—

non_bounce_session (automatic)

Emitted on the second pageview in a session for engagement and bounce reporting. No fields beyond the event envelope and shared context.

// Automatic on 2nd pageview in session
// event_type: "non_bounce_session"
Fields for non_bounce_session (automatic)
FieldPurposeTypeRequiredNotes
(event-specific)None — only envelope and shared context.———

experiment_evaluation (automatic)

One impression per (flag, iteration, variation) tuple when the visitor is enrolled or first acknowledged client-side.

// Automatic after flag assignment
// test_data: { flag_key, variation_key, iteration, visitor_id, reason }
Fields for experiment_evaluation (automatic)
FieldPurposeTypeRequiredNotes
test_data.flag_keyFeature flag key.stringAuto—
test_data.variation_keyAssigned variation (e.g. control, v1).stringAuto—
test_data.iterationTest iteration number.integerAuto—
test_data.visitor_idVisitor id used for bucketing.stringAuto—
test_data.reasonWhy this evaluation was recorded.bucketed | sticky | forced | iteration_change | variation_retiredAuto—

bai_testForce (query parameter)

Force flag variations in the browser for local QA (not an API call).

// https://yoursite.com/?bai_testForce=hero_test:v1,pricing_test:control
Fields for bai_testForce (query parameter)
FieldPurposeTypeRequiredNotes
bai_testForceComma-separated flag:variation pairs.stringNoURL query param

bai_bypassCache (query parameter)

Bypass CDN and in-browser caches for the tracking script and property config JSON. Useful when testing flag or property changes.

// https://yoursite.com/?bai_bypassCache=true
Fields for bai_bypassCache (query parameter)
FieldPurposeTypeRequiredNotes
bai_bypassCacheWhen true, fetches the latest script and property embed instead of cached copies.booleanNoHonored for one hour in localStorage after the first page load with this param

Commerce & funnels

Send money as whole numbers (cents)

Every price, total, and revenue field below is a whole number in the smallest unit of the currency — cents for USD, EUR, and GBP. Multiply the amount the shopper sees by 100 and drop the decimal: a $19.99 order is order_total: 1999, and $1,250.00 is order_total: 125000. Passing a decimal like 19.99 will be misread and skew your revenue reports. Fields marked Auto are set by the script for you — you do not pass them.

Calls made during initialization are buffered

The script loads its property configuration asynchronously. Tracking calls made after the script tag has executed but before that finishes (e.g. a conversion fired inline on a checkout success page) are buffered and sent automatically as one batch once initialization completes. If configuration fails to load, buffered events are discarded along with all other tracking.

trackConversion(conversionData)

Record a completed purchase or signup. Invalid payloads throw before send. Monetary values are integers in cents.

badgerlytics.trackConversion({
order_id: 'ord_8f2a',
order_total: 9950,
total_tax: 800,
total_discount: 500,
total_shipping: 0,
currency: 'USD',
customer_id: 'cus_abc',
subscription_id: 'sub_xyz',
products: [{
product_code: 'plan_pro',
name: 'Pro plan',
price: 9950,
quantity: 1,
billing_interval: 'month',
interval_count: 1,
}],
});
Fields for trackConversion(conversionData)
FieldPurposeTypeRequiredNotes
order_idYour order or transaction id.stringYes—
order_totalOrder total in smallest currency unit.integerYesCents; ≥ 0
total_taxTax amount in cents.integerNoDefault 0
total_discountDiscount amount in cents.integerNoDefault 0
total_shippingShipping amount in cents.integerNoDefault 0
currencyISO 4217 currency code.stringNoDefault USD
customer_idStable customer id for retention and churn.stringNoSaaS
subscription_idLinks purchase to subscription lifecycle webhooks.stringNoSaaS
discountsApplied discounts on the order.arrayNo—
discounts[].discount_nameHuman-readable discount label.stringYesIf discounts sent
discounts[].discount_amountDiscount value in cents.integerYesIf discounts sent
discounts[].codePromo or coupon code.stringNo—
productsLine items on the order.arrayNo—
products[].product_codePrimary product or plan identifier.stringYesWhen products[] is sent
products[].nameDisplay name shown in reports.stringYesWhen products[] is sent
products[].skuSecondary line-item id for joins.stringNoWhen products[] is sent; Defaults to product_code
products[].variantVariant label (size, tier, etc.).stringNoWhen products[] is sent
products[].categoryProduct category for breakdowns.stringNoWhen products[] is sent
products[].priceUnit price in smallest currency unit.integerNoWhen products[] is sent; Cents; default 0
products[].quantityUnits in the line item.integerNoWhen products[] is sent; Min 1; default 1
products[].billing_intervalBilling cadence for MRR and subscription reports.one_time | day | week | month | yearNoWhen products[] is sent; SaaS
products[].interval_countIntervals per billing period (e.g. 3 months).integerNoWhen products[] is sent; SaaS
page_data.pathPath where conversion fired.stringAuto—

trackProductView(productData)

Product detail (PDP) impression. Duplicate product_code on the same pageview is ignored.

badgerlytics.trackProductView({
product_code: 'SKU-7421',
name: 'Trail Runner',
category: 'footwear',
price: 12999,
});
Fields for trackProductView(productData)
FieldPurposeTypeRequiredNotes
product_codePrimary product identifier.stringYes—
nameDisplay name for reports.stringYes—
skuSecondary id for view-to-purchase joins.stringNoDefaults to product_code
variantVariant label when applicable.stringNo—
categoryProduct category.stringNo—
priceListed price in cents.integerNoDefault 0
page_data.pathPath where the product was viewed.stringAuto—

trackCartUpdate(cartData)

Snapshot of cart or plan-selection intent after any change. Powers abandoned-cart reporting when no conversion follows within the window.

badgerlytics.trackCartUpdate({
subtotal: 25998,
currency: 'USD',
cart_id: 'cart_42',
customer_id: 'cus_abc',
products: [{
product_code: 'plan_pro',
name: 'Pro plan',
price: 25998,
quantity: 1,
billing_interval: 'month',
}],
});
Fields for trackCartUpdate(cartData)
FieldPurposeTypeRequiredNotes
subtotalCurrent cart total in cents.integerYes≥ 0
currencyISO 4217 currency code.stringNoDefault USD
cart_idYour cart correlator.stringNo—
customer_idStable customer id when known.stringNoSaaS
subscription_idSubscription correlator when upgrading.stringNoSaaS
productsLine items currently in the cart.arrayYesMin 1 item
products[].product_codePrimary product or plan identifier.stringYesPer line item
products[].nameDisplay name shown in reports.stringYesPer line item
products[].skuSecondary line-item id for joins.stringNoDefaults to product_code
products[].variantVariant label (size, tier, etc.).stringNoPer line item
products[].categoryProduct category for breakdowns.stringNoPer line item
products[].priceUnit price in smallest currency unit.integerNoCents; default 0
products[].quantityUnits in the line item.integerNoMin 1; default 1
products[].billing_intervalBilling cadence for MRR and subscription reports.one_time | day | week | month | yearNoSaaS
products[].interval_countIntervals per billing period (e.g. 3 months).integerNoSaaS
page_data.pathPath where the cart was updated.stringAuto—

trackFunnelStep(funnelName, stepNumber, stepName)

Record progress through a named funnel configured in the dashboard.

badgerlytics.trackFunnelStep('purchase', 3, 'add_to_cart');
Fields for trackFunnelStep(funnelName, stepNumber, stepName)
FieldPurposeTypeRequiredNotes
funnelNameFunnel name matching dashboard setup.stringYesMethod argument
stepNumberNumeric step index (1-based).integerYesMethod argument
stepNameHuman-readable step label.stringYesMethod argument
funnel_data.funnel_nameEcho of funnelName on the event.stringAuto—
funnel_data.step_numberEcho of stepNumber on the event.integerAuto—
funnel_data.step_nameEcho of stepName on the event.stringAuto—
funnel_data.furthest_stepHighest step number reached this session.integerAuto—
funnel_data.is_step_advanceTrue when this step is new progress.booleanAuto—

trackCustomEvent(eventToken, metadata?)

Fire a dashboard-registered custom event. Unregistered tokens are dropped.

badgerlytics.trackCustomEvent('custom_newsletter_signup', {
source: 'footer',
});
Fields for trackCustomEvent(eventToken, metadata?)
FieldPurposeTypeRequiredNotes
eventTokenToken from Custom events in the dashboard.stringYesMethod argument; custom_ prefix
metadataOptional key/value payload.objectNoMethod argument
event_nameSame value as eventToken on the ingested event.stringAuto—
event_metadataMetadata object attached to the event.objectNo—

Feature flags & traits

getVariation(flagKey)

Return the assigned variation key for a flag.

const hero = badgerlytics.getVariation('hero_test');
Fields for getVariation(flagKey)
FieldPurposeTypeRequiredNotes
flagKeyFeature flag key from the dashboard.stringYes—

Returns: string | null

getActiveFlags(flagKey?)

Return all flag assignments, or one variation when flagKey is passed.

const all = badgerlytics.getActiveFlags();
const hero = badgerlytics.getActiveFlags('hero_test');
Fields for getActiveFlags(flagKey?)
FieldPurposeTypeRequiredNotes
flagKeyOptional single flag to look up.stringNo—

Returns: object | string | null

getFlagIteration(flagKey)

Return the current test iteration number for a flag.

const iter = badgerlytics.getFlagIteration('hero_test');
Fields for getFlagIteration(flagKey)
FieldPurposeTypeRequiredNotes
flagKeyFeature flag key.stringYes—

Returns: number | null

isFlagEnabled(flagKey)

Convenience check: true when assigned to v1, false for control or no assignment.

if (badgerlytics.isFlagEnabled('new_pricing_table')) { ... }
Fields for isFlagEnabled(flagKey)
FieldPurposeTypeRequiredNotes
flagKeyFeature flag key.stringYes—

Returns: boolean

onFlagsReady(callback)

Run callback when flag initialization completes (immediately if already ready).

badgerlytics.onFlagsReady(() => {
const v = badgerlytics.getVariation('hero_test');
});
Fields for onFlagsReady(callback)
FieldPurposeTypeRequiredNotes
callbackFunction invoked when assignments are available.() => voidYes—

Returns: void

refreshFlags()

Re-run segment and bucketing after traits change. Preserves sticky assignments.

badgerlytics.setTraits({ plan: 'enterprise' });
// refreshFlags() is called automatically by setTraits

No parameters.

Returns: Promise<void>

getAllFlags()

Raw experiment config from the property embed (organizationId + experiments).

const cfg = badgerlytics.getAllFlags();

No parameters.

Returns: { organizationId, experiments } | null

setTraits(partial)

Merge custom audience traits into the cookie and re-evaluate segment rules. Pass null to clear on logout.

badgerlytics.setTraits({ plan: 'pro', signed_in: true });
badgerlytics.setTraits(null); // logout
Fields for setTraits(partial)
FieldPurposeTypeRequiredNotes
partialTraits to merge, or null to clear all custom traits.object | nullYes—

Returns: void

getTraits()

Read-only snapshot of custom traits plus built-ins derived at call time.

const traits = badgerlytics.getTraits();
// { visitor_type, device_type, page_path, utm_source?, plan?, ... }
Fields for getTraits()
FieldPurposeTypeRequiredNotes
visitor_typenew or returning based on visitor cookie.stringAutoBuilt-in
device_typeCurrent device class.stringAutoBuilt-in
page_pathCurrent pathname.stringAutoBuilt-in
utm_sourceUTM source when present on URL.stringNoBuilt-in
utm_mediumUTM medium when present.stringNoBuilt-in
utm_campaignUTM campaign when present.stringNoBuilt-in
(custom keys)Keys you set via setTraits or config.traits.string | number | booleanNo—

Returns: object

Visitor location

Location is derived from the visitor's IP at ingest time. It is returned on the session_start ingest response only, then cached for the session. Call getLocation() after onSessionStartReady — it returns null if you read it before that response arrives.

getLocation()

Read IP-derived location for the current session. Returns null until the session_start ingest response arrives. Stored in localStorage (_bai_user_location) and expires with the session (30 minutes from session start). Only set on new sessions — a resumed session within the window reuses the stored value.

badgerlytics.onSessionStartReady(() => {
const loc = badgerlytics.getLocation();
if (!loc) return;
console.log(loc.city, loc.regionCode);
});
Fields for getLocation()
FieldPurposeTypeRequiredNotes
countryTwo-letter country code (e.g. US).string | nullAuto—
cityCity name when known.string | nullAuto—
continentContinent code (e.g. NA).string | nullAuto—
regionFirst-level region name (e.g. Texas).string | nullAuto—
regionCodeISO 3166-2 region code (e.g. TX).string | nullAuto—
timezoneIANA timezone (e.g. America/Chicago).string | nullAuto—
longitudeApproximate longitude from the visitor IP.string | nullAuto—
latitudeApproximate latitude from the visitor IP.string | nullAuto—
postalCodePostal or ZIP code when known.string | nullAuto—
metroCodeNielsen DMA (Designated Market Area) code for US TV markets.string | nullAutoDMADMA code listings (opens in new tab)

Returns: object | null

onSessionStartReady(callback)

Run callback when the session_start ingest response has returned and geoLocation is stored for this session. Fires immediately if location is already available (e.g. resumed session within 30 minutes).

badgerlytics.onSessionStartReady(() => {
const loc = badgerlytics.getLocation();
if (loc?.metroCode === '635') {
// Austin DMA
}
});
Fields for onSessionStartReady(callback)
FieldPurposeTypeRequiredNotes
callbackFunction invoked when getLocation() will return data.() => voidYes—

Returns: void

Session control

disconnect()

Stop sending events and clear the outbound queue. Cookies unchanged.

badgerlytics.disconnect();

No parameters.

Returns: void

connect()

Resume sending after disconnect() or when loaded with disconnected: true.

badgerlytics.connect();

No parameters.

Returns: void

Cookies & identifiers

First-party cookies (path /, SameSite=Strict). SSR middleware may also write flags and traits; the script reads them on load.

Tracking script cookies
FieldPurposeTypeRequiredNotes
_bai_visitorAnonymous visitor id for bucketing and cross-session reports.stringAuto365 days
_bai_sessionSession id, start time, and pageview count JSON.JSONAuto30 min idle
_bai_attrFirst-touch referrer and UTM parameters for this session. Used on every event so off-site checkout/OAuth returns keep the landing source.JSONAuto30 min idle; same lifetime as _bai_session
_bai_flagsSticky flag assignments { flag: { i, v } }.JSONNo365 days; SSR may set
_bai_flags_trackedDedupes experiment_evaluation impressions.JSONAutoScript only
_bai_traitsCustom audience traits for segment rules.JSONNo365 days
localStorage keys
FieldPurposeTypeRequiredNotes
_bai_user_locationIP-derived geo from the session_start ingest response. Read via getLocation().JSONAuto30 min (session lifetime); sessionStorage key _bai_pvc is separate

Session-only sessionStorage: _bai_pvc (pageview count), _bai_funnel_{name} (furthest funnel step per name).

Helpful Analytics Definitions / Timeframes

Quick reference for how Badgerlytics defines sessions, visitors, bounces, and retention windows. These match the tracking script, session recorder, and retention jobs.

Analytics definitions and timeframes
TermValue
Session length30 minutes from the first pageviewThe _bai_session cookie is set when a visit starts and expires after 30 minutes. Activity within that window stays in the same session; after it expires, the next page load starts a new session.
Visitor ID lifespan365 daysThe anonymous _bai_visitor cookie persists for one year. Reports use it to recognize the same browser across sessions during that window.
New vs returning visitorCookie-based at session startNew = no _bai_visitor cookie on the first pageview (a new id is created). Returning = the cookie was already present. This label is fixed for that session — it does not change mid-visit.
BounceOne pageview in the sessionA bounce is a session where the visitor only viewed one page. The tracker emits non_bounce_session when a second pageview is recorded.
Engaged sessionTwo or more pageviewsUsed as the threshold for session replay capture and for non-bounce metrics in reports. Single-page visits are excluded from replay storage.
Abandoned cart1 day after the last cart update (same-day carts still maturing)A contiguous run of cart_update events becomes an abandoned streak when no conversion is found after the streak ends. The report only counts streaks once the 1-day locked window has closed — same-day carts are excluded until the next day. A conversion seen up to 30 days later still marks the streak as converted. Same logic applies to SaaS plan-selection flows.
Session replay lengthUp to 2 hours per browser tab (max 4 tabs)Each tab records its own replay, up to four concurrent tabs per visit. Capture stops at the two-hour wall-clock cap, or after 30 minutes with no activity on that tab.
Session replay daily cap3,000 per property on Business; 6,000 on Ultimate (UTC)Each property has its own daily recording budget — limits are not shared across properties. After the cap is reached, new recordings for that property pause until the next UTC day.
Session replay retention14 daysRecordings and their AI analysis results are removed automatically after fourteen days. Download or review anything you need to keep within that window.
Analytics data retention2 years, then permanently deletedRaw events and rolled-up report tables are kept for two years, then dropped by our daily retention job. Manual property deletion wipes data immediately on request.
Geo location cache30 minutes (session-bound)Country/region from the session_start response is cached in localStorage for the current session only — same window as the session cookie.

SSR middleware (SDK)

Overview

The @badgerlytics/sdk middleware packages assign A/B test and feature-flag variations on the server before HTML is sent, so visitors never see a flash of the wrong variant. Install the package for your framework, add middleware once, then read assignments in pages, loaders, or server components.

npm install @badgerlytics/sdk

Import paths and peer dependencies:

  • @badgerlytics/sdk/nextjs — requires next>= 13.4
  • @badgerlytics/sdk/remix — React Router 7+ or Remix 2+
  • @badgerlytics/sdk/nuxt — Nuxt 3+
  • @badgerlytics/sdk/astro — Astro 4+

Middleware handles assignment cookies and SSR reads only. You still need the browser tracking script for page views, commerce events, and client-side hooks.

Next.js: middleware + React provider

On Next.js, use @badgerlytics/sdk/nextjs middleware and @badgerlytics/sdk/react BadgerlyticsProvider in your root layout. Middleware assigns on the server; the provider loads the script and powers useVariation() in client components. For script installation, see Script Installation → React.

Astro and Nuxt: use the HTML snippet below (or your framework's equivalent). Remix / React Router can use BadgerlyticsProvider the same way as Next.js.

<!-- Still required for page views, commerce events, and client-side flags -->
<script>
window.badgerlyticsConfig = { propertyId: 'your-property-id' };
</script>
<script src="https://cdn.badgerlytics.com/scripts/badgerlytics.min.js" async></script>

Tracking script embed — also documented under Script Installation.

Shared API

These exports are available from every middleware package (/nextjs, /remix, /nuxt, /astro):

  • createBadgerlyticsMiddleware(options) — assign variants before SSR; write _bai_visitor and _bai_flags cookies.
  • getVariation(source, flagKey, fallback?) — variation key (e.g. control, v1).
  • getAssignments(source) — assignment map for the request ({ flagKey: { i, v, reason? } }).
  • getVisitorId(source) — stable visitor id for this request.
  • getTraits(source) — merged trait bag used for segment rules (cookie traits plus built-ins like visitor_type, device_type, page_path, and UTM params).
  • isFlagEnabled(source, flagKey) — true when the visitor is on the v1 arm (control is off).
  • isVariation(source, flagKey, expected) — compare to a specific variation key.
  • setTraitsOnResponse(response, traits) — set or clear _bai_traits on login/logout (Next.js, Remix, Astro API routes).
  • runBadgerlyticsAssignment(request, options) — run assignment outside global middleware.
  • readVisitorFromRequest / readTraitsFromRequest / readFlagsFromRequest — read cookies from a Web Request.
  • Cookie/header constants: VISITOR_COOKIE, FLAGS_COOKIE, TRAITS_COOKIE, x-bai-assignments, x-bai-visitor, x-bai-traits.

createBadgerlyticsMiddleware accepts:

createBadgerlyticsMiddleware({
propertyId: 'your-property-id', // required — public hashid from the app
skipBots: true, // default — skip assignment for bot user-agents
skip: (request) => false, // optional — return true to skip this request
cookieDomain: '.example.com', // optional — Set-Cookie Domain attribute
cookieSecure: true, // optional — Set-Cookie Secure flag
cookieSameSite: 'lax', // optional — default 'lax'; also 'strict' | 'none'
cookieMaxAgeSec: 31536000, // optional — default 365 days
});

source for read helpers depends on the framework — see the Next.js, Remix, Nuxt, and Astro sections below. Next.js and Remix forward x-bai-* headers on the request; Nuxt uses event.context; Astro uses Astro.locals. The skip callback receives a NextRequest or Web Request on Next.js and Remix, an h3 event on Nuxt, and MiddlewareContext on Astro.

Next.js

Next.js uses two packages: @badgerlytics/sdk/nextjs (this section) for SSR assignment, and @badgerlytics/sdk/react for BadgerlyticsProvider — which loads the tracking script and client hooks without a manual embed. Use the same propertyId in both.

1. Middleware — add root middleware.js:

middleware.jsjavascript
// middleware.js (project root)
import { createBadgerlyticsMiddleware } from '@badgerlytics/sdk/nextjs';
export const middleware = createBadgerlyticsMiddleware({
propertyId: 'your-property-id',
});
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

2. Provider — wrap your app in app/layout.jsx (or pages/_app.jsx):

app/layout.jsxjsx
// app/layout.jsx — use with middleware.js (same propertyId)
import { BadgerlyticsProvider } from '@badgerlytics/sdk/react';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<BadgerlyticsProvider config={{ propertyId: 'your-property-id' }}>
{children}
</BadgerlyticsProvider>
</body>
</html>
);
}

Read assignments — Pages Router:

import {
getVariation,
getAssignments,
getVisitorId,
getTraits,
isFlagEnabled,
} from '@badgerlytics/sdk/nextjs';
export async function getServerSideProps({ req }) {
return {
props: {
hero: getVariation(req, 'hero_test', 'control'),
allFlags: getAssignments(req),
visitorId: getVisitorId(req),
traits: getTraits(req),
pricingOn: isFlagEnabled(req, 'new_pricing_table'),
},
};
}

Read assignments — App Router:

import { headers } from 'next/headers';
import {
getVariation,
getAssignments,
getTraits,
isFlagEnabled,
} from '@badgerlytics/sdk/nextjs';
export default async function Page() {
const h = await headers();
const hero = getVariation(h, 'hero_test', 'control');
const traits = getTraits(h);
const pricingOn = isFlagEnabled(h, 'new_pricing_table');
return hero === 'v1' ? <HeroB traits={traits} /> : <HeroA />;
}

Set audience traits — Route Handlers and legacy pages/api:

import { setTraitsOnResponse } from '@badgerlytics/sdk/nextjs';
// App Router Route Handler
export async function POST(request) {
const user = await authenticate(request);
const response = Response.json({ ok: true });
setTraitsOnResponse(response, { signed_in: true, plan: user.plan });
return response;
}
// Legacy pages/api — Node res is supported
export default function handler(req, res) {
setTraitsOnResponse(res, { signed_in: true });
res.status(200).json({ ok: true });
}

Manual assignment (without global middleware):

import { runBadgerlyticsAssignment } from '@badgerlytics/sdk/nextjs';
export async function GET(request) {
const result = await runBadgerlyticsAssignment(request, {
propertyId: 'your-property-id',
});
if (!result.ok) return Response.json({ skipped: result.reason });
const response = Response.json({ assignments: result.assignments });
for (const cookie of result.setCookieHeaders) {
response.headers.append('Set-Cookie', cookie);
}
return response;
}

Next.js only: writeAssignmentCookies(NextResponse, opts) — used by the built-in middleware to set visitor + flags via NextResponse.cookies.

Remix / React Router

Enable route middleware in react-router.config.js, then register Badgerlytics in app/middleware.ts or export from app/root.jsx. Loaders and actions receive a request with forwarded x-bai-* headers — pass that to getVariation(request, …).

react-router.config.jsjavascript
// react-router.config.js — required for route middleware (React Router 7+)
export default {
ssr: true,
future: {
v8_middleware: true,
},
};
app/middleware.tstypescript
// app/middleware.ts (or export from app/root.jsx)
import { createBadgerlyticsMiddleware } from '@badgerlytics/sdk/remix';
export const middleware = [
createBadgerlyticsMiddleware({ propertyId: 'your-property-id' }),
];

Read assignments in a loader:

import {
getVariation,
getAssignments,
getVisitorId,
getTraits,
isFlagEnabled,
isVariation,
} from '@badgerlytics/sdk/remix';
export async function loader({ request }) {
return {
hero: getVariation(request, 'hero_test', 'control'),
allFlags: getAssignments(request),
visitorId: getVisitorId(request),
traits: getTraits(request),
isHeroV1: isVariation(request, 'hero_test', 'v1'),
pricingOn: isFlagEnabled(request, 'new_pricing_table'),
};
}

Set audience traits in an action:

import { setTraitsOnResponse } from '@badgerlytics/sdk/remix';
import { redirect } from 'react-router';
export async function action() {
const user = await login();
const response = redirect('/app');
setTraitsOnResponse(response, { signed_in: true, plan: user.plan });
return response;
}

Manual assignment:

import {
runBadgerlyticsAssignment,
writeAssignmentCookiesOnResponse,
} from '@badgerlytics/sdk/remix';
export async function loader({ request }) {
const result = await runBadgerlyticsAssignment(request, {
propertyId: 'your-property-id',
});
if (!result.ok) return { skipped: result.reason };
const headers = new Headers(result.forwardHeaders);
// Merge forwardHeaders onto a sub-request, or return assignments in loader data
return { assignments: result.assignments };
}

Also available: writeAssignmentCookiesOnResponse(response, opts) to append visitor + flags Set-Cookie headers on a Web Response.

Nuxt

Add server/middleware/badgerlytics.ts. Server routes read from event.context.badgerlytics via the *FromEvent helpers — not getVariation(event).

server/middleware/badgerlytics.tstypescript
// server/middleware/badgerlytics.ts
import { createBadgerlyticsMiddleware } from '@badgerlytics/sdk/nuxt';
export default createBadgerlyticsMiddleware({
propertyId: 'your-property-id',
});

Read assignments:

import {
getVariationFromEvent,
getAssignmentsFromEvent,
getVisitorIdFromEvent,
getTraitsFromEvent,
isFlagEnabledFromEvent,
} from '@badgerlytics/sdk/nuxt';
export default defineEventHandler((event) => {
return {
hero: getVariationFromEvent(event, 'hero_test', 'control'),
allFlags: getAssignmentsFromEvent(event),
visitorId: getVisitorIdFromEvent(event),
traits: getTraitsFromEvent(event),
pricingOn: isFlagEnabledFromEvent(event, 'new_pricing_table'),
};
});

Set audience traits — use setTraitsOnEvent, not setTraitsOnResponse:

import { setTraitsOnEvent } from '@badgerlytics/sdk/nuxt';
export default defineEventHandler(async (event) => {
const user = await login(event);
setTraitsOnEvent(event, { signed_in: true, plan: user.plan });
return { ok: true };
});

Manual assignment:

import { runBadgerlyticsAssignmentForEvent } from '@badgerlytics/sdk/nuxt';
export default defineEventHandler(async (event) => {
const result = await runBadgerlyticsAssignmentForEvent(event, {
propertyId: 'your-property-id',
});
if (!result.ok) return { skipped: result.reason };
return { assignments: result.assignments };
});

Nuxt-specific exports:

  • getVariationFromEvent, getAssignmentsFromEvent, getVisitorIdFromEvent, getTraitsFromEvent, isFlagEnabledFromEvent
  • setTraitsOnEvent(event, traits)
  • runBadgerlyticsAssignmentForEvent(event, options)

Astro

Wrap createBadgerlyticsMiddleware in defineMiddleware. Read assignments from Astro.locals — Astro does not replace the incoming request, so header-based getVariation will not work in .astro frontmatter.

src/middleware.jsjavascript
// src/middleware.js
import { defineMiddleware } from 'astro:middleware';
import { createBadgerlyticsMiddleware } from '@badgerlytics/sdk/astro';
export const onRequest = defineMiddleware(
createBadgerlyticsMiddleware({ propertyId: 'your-property-id' })
);

Read assignments in a page:

src/pages/index.astromarkup
---
import {
getVariationFromLocals,
getAssignmentsFromLocals,
getVisitorIdFromLocals,
getTraitsFromLocals,
isFlagEnabledFromLocals,
} from '@badgerlytics/sdk/astro';
const hero = getVariationFromLocals(Astro.locals, 'hero_test', 'control');
const traits = getTraitsFromLocals(Astro.locals);
const pricingOn = isFlagEnabledFromLocals(Astro.locals, 'new_pricing_table');
---
{hero === 'v1' ? <HeroB traits={traits} /> : <HeroA />}

Set audience traits in an API route:

import { setTraitsOnResponse } from '@badgerlytics/sdk/astro';
export const POST = async ({ request }) => {
const user = await authenticate(request);
const response = new Response(JSON.stringify({ ok: true }), {
headers: { 'Content-Type': 'application/json' },
});
setTraitsOnResponse(response, { signed_in: true, plan: user.plan });
return response;
};

Manual assignment:

import { runBadgerlyticsAssignment } from '@badgerlytics/sdk/astro';
export const GET = async ({ request }) => {
const result = await runBadgerlyticsAssignment(request, {
propertyId: 'your-property-id',
});
if (!result.ok) return new Response(JSON.stringify({ skipped: result.reason }));
const response = new Response(JSON.stringify({ assignments: result.assignments }));
for (const cookie of result.setCookieHeaders) {
response.headers.append('Set-Cookie', cookie);
}
return response;
};

Astro-specific exports (all use Astro.locals):

  • getVariationFromLocals, getAssignmentsFromLocals, getVisitorIdFromLocals, getTraitsFromLocals, isFlagEnabledFromLocals

Framework differences

  • Where to read flags: Next.js and Remix → getVariation(request | req | headers()). Nuxt → getVariationFromEvent(event). Astro → getVariationFromLocals(Astro.locals).
  • Setting traits on login: Next.js, Remix, and Astro API routes → setTraitsOnResponse. Nuxt → setTraitsOnEvent.
  • Manual assignment without middleware: Web Request → runBadgerlyticsAssignment. Nuxt → runBadgerlyticsAssignmentForEvent.
  • Edge vs Node: Middleware runs wherever your framework deploys it. The property CDN embed must be reachable from that runtime.
  • Bucketing parity: Server and browser use the same (visitorId, flagKey, iteration) seed, so SSR and client assignments stay aligned when cookies are present.
  • QA overrides: Append ?bai_testForce=flag_key:variation to force a variation for that request (same format as the tracking script). Forced assignments are forwarded to SSR but not written to cookies.

Consent and assignment cookies

Middleware can bucket visitors before the tracking script loads. If analytics consent applies to experiment cookies in your jurisdiction, coordinate assignment with your CMP — see Analytics Reporting → Accuracy / sampling.

Ecommerce Reporting Setup

Ecommerce reports are entirely client-side — fire the four event types below from the right points in your shopping flow and the dashboards light up.

Funnels

A standard purchase funnel works well for most stores:

  • 1 — Product listing page (PLP)
  • 2 — Product display page (PDP)
  • 3 — Add to cart
  • 4 — View cart
  • 5 — Payment / review
  • 6 — Conversion

Set these up once under Funnels and call trackFunnelStep('purchase', n, name) as users reach each step.

Product view

Fire one product view per PDP visit. The event is what powers the "most-viewed products" report and the view-to-purchase conversion rate.

// On every product detail page
window.badgerlytics.trackProductView({
product_code: 'SKU-7421',
name: 'Trail Runner — Size 10',
category: 'footwear',
price: 12999, // cents
});

Cart update

Fire after any change to the cart. The payload should always reflect the full cart, not just the delta — we use it as a snapshot for the abandoned-cart reports.

// After any cart change: add, remove, qty update
window.badgerlytics.trackCartUpdate({
subtotal: 25998, // cents — full cart subtotal
currency: 'USD',
cart_id: cartId,
products: cart.items.map((item) => ({
product_code: item.sku,
name: item.name,
category: item.category,
price: item.priceCents,
quantity: item.quantity,
})),
});

Skip empty carts

If subtotal is 0 (or you just cleared the cart), it's safe to skip the call — empty cart updates are ignored by reports anyway.

Conversion

Fire on the order-confirmation page. Include the full line-item breakdown — we use it for revenue-by-category, revenue-by-SKU, and to attribute the purchase back to the product views that led to it. Calls made while the tracking script is still initializing are buffered and sent automatically once it's ready, so an inline call on the confirmation page is safe.

// On the order-confirmation page
badgerlytics.trackConversion({
order_id: order.id, // your order id
order_total: order.totalCents,
total_tax: order.taxCents, // optional
total_discount: order.discountCents, // optional
total_shipping: order.shippingCents, // optional
currency: 'USD',
customer_id: order.customerId, // optional, enables cohort retention
discounts: order.discounts?.map((d) => ({
code: d.code,
discount_name: d.name,
discount_amount: d.amountCents,
})),
products: order.lineItems.map((item) => ({
product_code: item.sku,
sku: item.sku,
name: item.name,
category: item.category,
price: item.priceCents,
quantity: item.quantity,
// billing_interval defaults to 'one_time' for ecommerce
})),
});

Abandoned cart considerations

  • A cart streak is "abandoned" when the visitor sends a cart_update and no conversion arrives within a locked 1-day window after the last cart update. The report only includes streaks whose window has fully closed (see complete_through in the app). Same-day carts are still maturing until tomorrow.
  • Conversions that happen later — even after the maturity window — still mark the streak as converted when we see them in your data (we look ahead up to 30 days when classifying streaks). A customer who returns two days later and buys counts as converted, not abandoned.

SaaS Reporting Setup

SaaS reporting joins four streams: in-page events from the tracking script (your marketing funnel), a conversion event at signup or upgrade, subscription_change events from your backend for committed MRR changes after signup, and optional usage_chargeevents for usage-based or pack revenue. Here's how to wire each one up.

Define plans and usage items first

Enable SaaS features on the property (from Properties when creating or editing it), then open SaaS Setup in the property nav. Step 1 lists your plans — use Manage plans before instrumenting conversion and lifecycle events. For usage billing, Step 2 lists usage items — whitelist each meter / overage / pack token before sending usage_charge. Webhook examples for both subscription changes and usage charges live in Step 4 (Lifecycle webhook). The wizard generates snippets pre-filled with your catalogue codes.

Funnels

A typical SaaS funnel: landing → pricing → signup → activation → conversion. Set this up under Funnels and fire each step with badgerlytics.trackFunnelStep(name, n, step) as the user reaches it.

Product view

Fire one product view per plan a visitor sees on your pricing page. This powers the "most-viewed plan" numbers and the plan-level conversion rates.

// Fire when a visitor lands on a plan / pricing page
badgerlytics.trackProductView({
product_code: 'pro_monthly', // your plan_code
name: 'Pro (Monthly)',
category: 'pro', // your tier_label
price: 15900, // cents
});

Cart update

Once the user has selected a specific plan and started checking out, send a cart update so we can attribute their conversion (or abandonment) back to the plan they chose.

// Fire after the visitor picks a plan (e.g. on the checkout / review step)
badgerlytics.trackCartUpdate({
subtotal: 15900,
currency: 'USD',
cart_id: 'session-abc123',
products: [
{
product_code: 'pro_monthly',
name: 'Pro (Monthly)',
category: 'pro',
price: 15900,
quantity: 1,
billing_interval: 'month',
interval_count: 1,
},
],
});

Conversion

Fire a conversion event when a subscription starts. Conversion count and funnels use this event. Recurring MRR does not: it comes from the product line's price plus billing_interval, or from a later trial_converted webhook — not from order_total alone.

  • Paid start: first invoice is payable now. Send the plan's monthly (or yearly) price on the product line with billing_interval so this conversion books new MRR.
  • Trial start: send order_total: 0 and product price: 0. Keep the plan code and subscription_id so the signup still counts as a conversion. Book MRR later with trial_converted when the trial becomes payable. Do not fire a second paid trackConversion on the same subscription to book MRR — later conversions are treated as renewals.
// Paid-from-day-one start. billing_interval + interval_count + the plan
// price on the product line are what mark this conversion as new MRR.
badgerlytics.trackConversion({
// Use your real order / invoice id (not a timestamp) so a refreshed
// success page or double-fire can be deduplicated downstream.
order_id: '<your_order_or_invoice_id>',
order_total: 15900, // cents
customer_id: '<your_user_id>',
subscription_id: '<your_subscription_id>',
products: [
{
product_code: 'pro_monthly',
name: 'Pro (Monthly)',
price: 15900,
quantity: 1,
billing_interval: 'month',
interval_count: 1,
category: 'pro',
},
],
});

No race with script loading

As long as the tracking script tag is present on the page, calls made while it's still initializing are buffered and sent automatically in one batch once it's ready — a conversion fired inline on your success page won't be lost. If your code might run before the script tag itself has loaded, guard on window.badgerlyticsor use the React SDK's hooks, which wait automatically.

Lifecycle events webhook

Conversion fires at signup. Everything after that — cancellations, upgrades, downgrades, renewals, and trial → paid — happens in your billing system. Forward each state change to us as a subscription_changeevent from your server, and we'll keep MRR, churn, expansion, contraction, and cancellation-reason reports accurate. For trials, the checkout conversion is $0; new MRR is the trial_converted event you send when the customer becomes payable.

Endpoint and auth

Endpoint: POST https://events.badgerlytics.com/ingestion-api/events

Auth: subscription_change is a server-only event type. It must be sent with an Authorization: Bearer <secret_api_key> header tied to this property's organization. Generate secret keys under Settings → API Keys, with the Ingest scope ticked. Browser ingestion does not work for this event type — that's intentional, so customers can't mint MRR by calling fetch from devtools.

Change types: cancel, upgrade, downgrade, renewal, reactivate, trial_started, and trial_converted. Any other value is rejected with a 400 at ingest, so typos surface immediately instead of silently skewing reports. Include a positive or negative mrr_delta_cents (an integer, in cents, normalized to a monthly value) matching the direction of the change (0 for plain renewals), and an ISO 8601 effective_at when the change takes effect later (e.g. a cancellation at period end).

Don't forward new paid signups

Paid-start MRR comes from the trackConversioncall at checkout. If your webhook also forwards a "subscription created" event for that same start, every paid signup is counted twice. Do forward trial_converted when a trial becomes payable — that is how trial MRR is booked. Also forward cancellations (after they were paying), upgrades, downgrades, and reactivations. Skip cancel when they never became payable.

Quick curl smoke test:

curl -X POST https://events.badgerlytics.com/ingestion-api/events \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <your_secret_api_key>" \
-d '{
"property_id": "<your_property_id>",
"event_type": "subscription_change",
"unique_event_id": "<uuid-v4>",
"session_id": "<server-side-session-uuid>",
"visitor_id": "<server-side-visitor-uuid>",
"event_time": "<ISO-8601-now>",
"subscription_data": {
"subscription_id": "<your_subscription_id>",
"customer_id": "<your_user_id>",
"change_type": "cancel",
"mrr_delta_cents": -15900,
"previous_plan_code": "pro_monthly",
"currency": "USD",
"cancel_reason": "too_expensive",
"effective_at": "<ISO-8601-effective-date>"
}
}'

Stripe is one mapping of that payload. The app's SaaS Setup wizard generates this exact snippet pre-configured for your property:

stripe-webhook.jsjavascript
// Cloudflare Worker / Vercel edge function — example adapter.
// Forwards Stripe customer.subscription.* webhooks as Badgerlytics
// subscription_change events. Set BADGERLYTICS_SECRET_KEY and
// PROPERTY_ID as environment variables.
//
// Before production:
// - Verify the Stripe-Signature header before trusting the payload.
// - Do not forward customer.subscription.created: a paid checkout's
// trackConversion already books new MRR. Trials book MRR here via
// trial_converted when status leaves trialing.
export default {
async fetch(req, env) {
const event = await req.json();
const sub = event.data?.object || {};
const prev = event.data?.previous_attributes || {};
const prevItems = prev.items;
// Monthly recurring value of a subscription in cents: quantity x
// unit_amount, normalized by billing interval so a yearly plan
// doesn't inflate MRR 12x.
const mrrCents = (items) => {
const item = items?.data?.[0];
const price = item?.price || {};
const amount = (price.unit_amount || 0) * (item?.quantity || 1);
const count = price.recurring?.interval_count || 1;
const divisor = price.recurring?.interval === 'year' ? 12 * count : count;
return Math.round(amount / divisor);
};
const canceledDuringTrial = sub.status === 'trialing'
|| prev.status === 'trialing'
|| (
!prev.status
&& Number(sub.trial_end) > 0
&& Number(sub.canceled_at) > 0
&& Number(sub.canceled_at) <= Number(sub.trial_end) + 120
);
const becamePayable = prev.status === 'trialing'
&& (sub.status === 'active' || sub.status === 'past_due');
let change_type;
let mrr_delta_cents;
let unique_event_id = crypto.randomUUID();
if (event.type === 'customer.subscription.deleted') {
// Never became payable — no churned MRR to book.
if (canceledDuringTrial) return new Response(null, { status: 204 });
change_type = 'cancel';
mrr_delta_cents = -mrrCents(sub.items);
} else if (event.type === 'customer.subscription.updated' && becamePayable) {
change_type = 'trial_converted';
mrr_delta_cents = mrrCents(sub.items);
unique_event_id = 'saas:trial_converted:' + String(sub.id || '');
} else if (event.type === 'customer.subscription.updated' && prevItems) {
const current = mrrCents(sub.items);
const previous = mrrCents(prevItems);
if (current === previous) return new Response(null, { status: 204 });
change_type = current > previous ? 'upgrade' : 'downgrade';
mrr_delta_cents = current - previous;
} else {
return new Response(null, { status: 204 });
}
const body = {
property_id: env.PROPERTY_ID,
event_type: 'subscription_change',
unique_event_id,
session_id: 'srv_' + crypto.randomUUID(),
visitor_id: 'srv_' + (sub.customer || 'unknown'),
event_time: new Date().toISOString(),
customer_id: String(sub.customer || ''),
subscription_data: {
subscription_id: String(sub.id || ''),
customer_id: String(sub.customer || ''),
change_type,
mrr_delta_cents,
previous_plan_code: prevItems?.data?.[0]?.price?.lookup_key
|| sub.items?.data?.[0]?.price?.lookup_key || '',
new_plan_code: sub.items?.data?.[0]?.price?.lookup_key || '',
currency: (sub.currency || 'usd').toUpperCase(),
cancel_reason: sub.cancellation_details?.reason || '',
effective_at: new Date().toISOString(),
},
};
const res = await fetch('https://events.badgerlytics.com/ingestion-api/events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + env.BADGERLYTICS_SECRET_KEY,
},
body: JSON.stringify(body),
});
if (!res.ok) {
// Non-2xx makes Stripe retry the webhook later instead of losing it.
return new Response('forward failed', { status: 500 });
}
return new Response('ok', { status: 200 });
},
};

Idempotency

Always include a stable subscription_id and fire a fresh unique_event_idper call. If your billing provider retries on transient errors, that's fine — duplicates with the same unique_event_id are dropped on our side, so retries are safe.

Usage items

Usage-based billing (overages, metered API calls, credit packs) is tracked separately from subscription MRR. In SaaS Setup → Usage items (or Manage usage items), create each billable line with a display name and a stable usage_item_code. That code is the whitelist token your backend must send on usage_charge — unknown codes are rejected at ingest.

Usage does not affect MRR

Usage revenue is its own report stream. Do not fold overage or pack amounts into mrr_delta_cents on subscription_change — keep committed subscription MRR clean and report recognized usage charges here instead.

Usage charge webhook

Fire a usage_charge when the customer is actually charged — typically when an invoice is finalized or paid, or when a one-time pack purchase completes. Do not send an event for every raw meter tick; wait until the billing system has turned usage into a line item with an amount.

Endpoint and auth

Endpoint: POST https://events.badgerlytics.com/ingestion-api/events

Auth: usage_charge is a server-only event type. Send it with Authorization: Bearer <secret_api_key> (same keys as lifecycle events under Settings → API Keys, with the Ingest scope). Browser ingestion is not allowed.

Required fields on usage_data: usage_item_code (must match an active usage item) and amount_cents (integer). Optional: quantity (defaults to 1), plan_code (when present, Usage Revenue breaks down by plan; when omitted, charges appear under Unaffiliated), plus customer_id / subscription_id for attribution.

Quick curl smoke test:

curl -X POST https://events.badgerlytics.com/ingestion-api/events \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <your_secret_api_key>" \
-d '{
"property_id": "<your_property_id>",
"event_type": "usage_charge",
"unique_event_id": "<uuid-v4>",
"session_id": "<server-side-session-uuid>",
"visitor_id": "<server-side-visitor-uuid>",
"event_time": "<ISO-8601-now>",
"customer_id": "<your_user_id>",
"subscription_id": "<your_subscription_id>",
"usage_data": {
"usage_item_code": "api_overage",
"amount_cents": 4200,
"quantity": 42,
"plan_code": "pro_monthly",
"customer_id": "<your_user_id>",
"subscription_id": "<your_subscription_id>"
}
}'

Or drop in a Stripe invoice adapter — the app's SaaS Setup wizard generates this snippet pre-configured for your property:

stripe-usage-webhook.jsjavascript
// Cloudflare Worker / Vercel edge function — example adapter.
// Forwards Stripe invoice.paid line items as Badgerlytics usage_charge
// events. Map each metered / overage / pack line to a whitelisted
// usage_item_code from SaaS Setup → Usage items.
//
// Fire these when the customer is charged (invoice finalized / paid),
// not on every raw meter increment.
//
// Before production:
// - Verify the Stripe-Signature header before trusting the payload.
// - Use a stable unique_event_id (e.g. invoice line id) so retries
// do not double-count revenue.
const USAGE_ITEM_BY_PRICE_LOOKUP = {
// 'api_overage_monthly': 'api_overage',
// 'token_pack_1000': 'token_pack',
};
export default {
async fetch(req, env) {
const event = await req.json();
if (event.type !== 'invoice.paid' && event.type !== 'invoice.finalized') {
return new Response(null, { status: 204 });
}
const invoice = event.data?.object || {};
const lines = invoice.lines?.data || [];
const forwards = [];
for (const line of lines) {
const lookup = line.price?.lookup_key || '';
const usageItemCode = USAGE_ITEM_BY_PRICE_LOOKUP[lookup];
if (!usageItemCode) continue; // skip subscription base lines / unknown SKUs
if (!line.amount || line.amount === 0) continue;
forwards.push({
property_id: env.PROPERTY_ID,
event_type: 'usage_charge',
unique_event_id: String(line.id || crypto.randomUUID()),
session_id: 'srv_' + crypto.randomUUID(),
visitor_id: 'srv_' + (invoice.customer || 'unknown'),
event_time: new Date(
(invoice.status_transitions?.paid_at || invoice.created || Date.now() / 1000) * 1000
).toISOString(),
customer_id: String(invoice.customer || ''),
subscription_id: String(invoice.subscription || ''),
usage_data: {
usage_item_code: usageItemCode,
amount_cents: line.amount,
quantity: line.quantity || 1,
// Optional: omit or leave blank for Unaffiliated in Usage Revenue.
plan_code: '',
customer_id: String(invoice.customer || ''),
subscription_id: String(invoice.subscription || ''),
},
});
}
for (const body of forwards) {
const res = await fetch('https://events.badgerlytics.com/ingestion-api/events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + env.BADGERLYTICS_SECRET_KEY,
},
body: JSON.stringify(body),
});
if (!res.ok) {
return new Response('forward failed', { status: 500 });
}
}
return new Response('ok', { status: 200 });
},
};

Idempotency

Use a stable unique_event_id per charge (for example the Stripe invoice line id). Retries with the same id are dropped, so webhook redelivery is safe.

Common Considerations

Bot handling

By default we try to keep crawlers and common automation clients out of your analytics.

In the browser — before the tracking script sends any events:

  • User-agent patterns — known bots, crawlers, link preview fetchers, and common non-browser clients (for example Googlebot, Lighthouse, curl).
  • Lightweight browser signals — for example navigator.webdriver, headless-Chrome tells, and missing language preferences. We do not classify bots from scroll depth, clicks, time on page, or bounce-style engagement.

When a visit matches, the script does not initialize tracking, so those sessions never produce browser analytics events.

At the edge — verified bots are blocked on our events API and property CDN before ingest or embed delivery. Authenticated server ingest (Authorization: Bearer …) is excluded from edge bot blocking.

Server-side flag assignment can use the same user-agent list (see skipBots in the SSR middleware SDK). We do not maintain IP-reputation blocklists for bot filtering.

Events you send through our server APIs (webhooks, subscription changes, and similar) are not run through the browser checks.

Traffic filters

Traffic filters let you exclude specific visitors from analytics for a property — for example your office network, a staging hostname, or your own IP while you click around the site. They are a denylist: anything that matches a rule is dropped before it counts toward reports. This is separate from your property's allowed domain, which controls which sites are allowed to send data at all.

Configure them in the app under Settings → Traffic filters. Pick a property, turn filtering on, add one or more rules, and save. Saved rules are published to that property's CDN config when you save.

Not the same as your property domain

Your property domain (e.g. www.example.com) is an allowlist: tracking only works on that site. Traffic filters are extra exclusions on top — useful for "I'm on the right site but I still don't want my visits counted."

Rule types

  • IP address — block a single IPv4 or IPv6 address. Use the address your browser actually sends to our edge (look it up (opens in new tab)); many home networks today use IPv6.
  • IP range (CIDR) — block a subnet, e.g. office Wi‑Fi (203.0.113.0/24) or an IPv6 prefix.
  • Page hostname — block visitors browsing on a specific host, such as staging.example.com or localhost:3000.
  • Referrer hostname — block when traffic arrived from a particular referring site (less common; referrers can be stripped by browser privacy settings).

IPv6 can be more reliable

When blocking your own visits, your IPv6 address is sometimes more reliable than IPv4 — especially on home networks where your public IPv4 may change or be shared. Look up the address your browser sends at whatismyipaddress.com (opens in new tab).

For hostname rules, enter the host only — not a full URL. Use staging.example.com or localhost:3000, not https://staging.example.com. If you paste a full URL, we trim it to the host when you save. Do not use our events API hostname (e.g. events.badgerlytics.com); that is only the delivery address for beacons, not your shop's hostname.

The settings page shows a blocked ingest attempts counter for blocks recorded at the edge (after you save filters). Browser-only blocks are not included in that number. Filtering must be enabled and you need at least one valid rule; an empty rule list does not block anything.

Event ingestion delay

Badgerlytics does not offer real-time analytics. Events are durable and processed in order — they may show up later, but they don't go missing. Under heavy load or during traffic spikes the queue can grow, which adds to the delay.

  • Most reports refresh on a rolling schedule. Same-day data continues to fill in as events are processed.
  • The website performance report updates at most hourly — that is the freshest cadence in the product today.
  • AI insights run on a schedule that depends on your plan — 3× per week on Starter, 5× per week Mon–Fri on Pro and Business, 7× per week on Ultimate — using the previous day's finalized data (~7am Central on run days). Each run compares the last 30 days to the prior period when enough history exists.
  • On new properties, insights use whatever history is available after the tracking script is detected — comparisons get richer throughout your first 30 days.

Maintenance / downtime

We continue to ingest events even when the app dashboard is down. Routine maintenance (deployments, schema changes) only takes the UI offline; the events worker keeps accepting payloads and queues them for processing. When the app comes back up, your reports backfill from the queue. You don't need to do anything.

Property data caching

Each property publishes a small JSON config (active feature flags, audience definitions, traffic filters, plan and usage-item catalogues for SaaS, and so on) to our CDN. The tracking script and SSR middleware read that object to decide which experiments a visitor is eligible for.

To keep page loads fast and reduce infrastructure cost, we cache the property config and tracking scripts at the CDN edge for up to 10 minutes. The browser also reuses the config for the duration of an analytics session (about 30 minutes) so repeat page views in the same tab do not refetch it on every navigation.

What this means when you save changes in the dashboard

  • We purge the CDN cache for that property's config when you save, so new visitors and new sessions pick up changes on their next page load.
  • Existing sessions may keep using the config they loaded at the start of the session for up to 10 minutes — or until the session ends — before they see flag, audience, traffic-filter, or SaaS plan or usage-item updates.
  • Visitors who were already enrolled in a test keep their sticky assignment in the _bai_flags cookie; caching mainly affects new enrollments and segment changes.

Testing changes right away

Add ?bai_bypassCache=true to any page URL while you QA. That skips CDN and in-browser caches for both the property config and the tracking script. The bypass is remembered in localStorage for one hour so you do not need the query param on every navigation.

If you proxy our CDN through your own layer (Cloudflare, CloudFront, Fastly, and so on), add a bypass rule for cdn.badgerlytics.com so your edge does not add another cache on top of ours.

Analytics Reporting

Out of the box, every property gets the full suite of reports below. You don't need to wire each one up individually — installing the script and tracking your funnel covers most of them automatically.

Traffic

Where your visitors come from and how many of them there are. Top pages, referrer sources, UTM breakdowns, device and browser splits, country and region rollups, and trends over time. Useful for catching regressions in acquisition channels.

Locations map

The Locations tab on your dashboard plots where your sessions and conversions come from, city by city. Each dot is a city; the bigger the dot, the more traffic (or conversions) it sent you in the selected period. Flip between Sessions and Conversions to see whether the places that bring the most visitors are also the places that buy.

  • Same date presets as the Overview tab — last 24 hours, 7 / 14 / 30 / 60 / 90 / 180 / 365 days, or a custom range. Switching the range on one tab carries over to the other.
  • Opens on your home market — the map starts zoomed to the region that matches your property's display currency (for example, USD opens on the United States and EUR on Europe). Fit to data zooms to wherever your visitors actually are, and World shows everything.
  • Ranked list included — the top locations sit below the map with sessions, conversions, conversion rate, and revenue, so the numbers are readable without hovering.
  • Ask the AI Chat Analyst — questions like "What are my top performing US cities?" or "How many sessions did we get from Akron, OH last month?" use the same city-level data.

Locations are resolved from the visitor's network at the time of the visit and stored at city level — we never collect precise device location. Visits without a resolvable city still count in every other report; they simply don't appear on the map. Country-level totals remain in the Country Performance report under Traffic.

Engagement

How visitors actually use the site once they arrive. Session duration, bounce rates, pages per session, scroll depth, page performance (FCP, LCP, CLS), and event volumes broken out by page and device.

Revenue & Commerce

For ecommerce properties: conversion rate, revenue, top products, cart-abandonment rate (1-day locked abandonment window; conversions within 30 days still recover a streak), average order value, and a full funnel view from product listing → conversion. Joins purchase events back to the product views and cart updates that led to them.

SaaS

For SaaS properties: MRR, ARR, new MRR, expansion MRR, contraction, churned MRR, trial-to-paid conversion, plan distribution, subscription-lifecycle, and Usage Revenue (recognized overage / pack charges, independent of MRR). Powered by your conversion events, the subscription lifecycle webhook, and optional usage_charge events after you whitelist usage items (see SaaS reporting setup).

A/B testing

Each running feature flag gets a dedicated results panel: visitor counts per variation, conversion rates, revenue per visitor, and statistical significance over time. You can break results out by device, funnel step, or focus page. Test results are also joinable with all the other reports — "revenue from variant v1 by device" is a real query you can answer in seconds.

Accuracy / sampling concerns

We don't sample. Every event you send is counted in our reports (browser events skip known bots in the tracker before send). On extremely high-volume properties some reports run on a slight delay during peak traffic, but the totals are accurate — not estimated.

Reports are built from events the tracking script sends from the visitor's browser. We don't estimate missing traffic on the server — if an event never leaves the client, it won't appear in your dashboard. That's different from sampling: we count every event we receive, but we can only receive what the browser is allowed to send.

  • Conversion rates and revenue numbers are exact, not extrapolated.
  • A/B test significance is computed on the full population, with standard frequentist confidence intervals.
  • Privacy and browser settings — visitors who block cookies, use strict tracking protection, or run in private modes may not get a stable session, so page views and conversions can be undercounted compared with tools that use server-side measurement.
  • Blockers and network conditions — ad blockers, corporate proxies, content-security policies, or a failed script load can prevent some or all events from reaching us.
  • Consent and configuration — if you only initialize tracking after a cookie banner, traffic before consent won't be counted. Traffic filters and bot handling (see Common considerations) also intentionally drop some visits.
  • If you notice a discrepancy with another tool, the most common cause is differences in what counts as a session or which events trigger conversions. Open those definitions side by side first — then check whether the gap could be client-side delivery (cookies, blockers, or consent).

Note

AI Chat Analyst answers use the same underlying data the reports do. When you ask "what drove our revenue jump last Tuesday?", the numbers it cites are the same ones in the dashboard.

Feature Flags

Overview

Feature flags in Badgerlytics serve two jobs: gating new features behind a switch, and running A/B tests with rigorous result tracking. Both share the same plumbing — a flag has variations, visitors get bucketed once, and every event we collect knows which variation each visitor saw.

Keep active flags lean

Each active flag adds to the sticky assignment cookie your visitors carry. Too many active flags can bloat that cookie and cause issues on some sites (browser limits, proxies, or strict cookie policies). Your plan also caps how many flags can be running at once — see Billing & Usage → Plan limits. When a test is done, finalize the winner, stop assigning new visitors, deactivate, and archive flags you no longer need.

Types of feature flags

When you create a flag in the dashboard, you choose one of two types (the same labels as in the app):

  • Experiment (A/B/C): compare two or more variants and measure lift vs control — for hypothesis testing, significance, and choosing a winner. Variants use control, v1, v2, … with custom traffic splits.
  • Feature flag (on/off): ramp a feature on or off with weights. Use isFlagEnabled() in your app — not for classical A/B winner analysis. Two arms: control (off) and v1 (on).

Test setup

Each test takes a minute or two to set up:

  • Pick a flag key (e.g. hero_test). This is the stable identifier you'll reference in code forever.
  • Define your variations and traffic split.
  • For experiments only: choose the winning metric that defines success (conversion rate, average order value, or a custom event), and optionally pick focus pages and/or restrict with an audience segment (e.g. logged-in users only). Feature flags (on/off) do not use these controls.
  • Hit Start. Visitors begin getting bucketed immediately.

Winning metric

Experiments only. Every experiment is measured against a single winning metric — the number used to compare variations and pick a leader vs control. Choose one:

  • Conversion rate: the share of sessions that complete a built-in conversion (sent with trackConversion()). The default, and the right choice for most tests.
  • Average order value: the average amount per order from those same built-in conversions. Pick this when you care less about how often people buy and more about how much they spend — for example testing bundles, upsells, or free-shipping thresholds. A variation can win on average order value even if it converts slightly less often.
  • A custom event: the per-session rate of any custom event you send with trackCustomEvent('custom_event_token') — useful for goals like signups, add-to-cart, or demo requests.

When to use average order value

Average order value is its own winning metric — select it directly when you want the test decided on spend per order rather than conversion rate. Like conversion rate, it uses order amounts from built-in conversions sent with trackConversion(). It isn't available as the primary metric when you choose a custom event (AOV still appears for reference if order data exists). Because it's an average of order amounts, it needs enough orders per variation before a result is meaningful — low-volume tests will show "not yet significant" for longer than a conversion-rate test would.

No matter which winning metric you choose, the results still show average order value alongside conversions wherever revenue is available, so you always see the full picture — only the leader and significance follow the metric you selected.

Traffic optimization

For experiments, you choose how traffic is split between variations:

  • Fixed weights: traffic stays at the split you set (for example 50/50) until you change it. Best when you want a clean, even comparison to measure lift and significance.
  • Multi-armed bandit: Badgerlytics automatically shifts more traffic toward the best-performing variations, so fewer visitors land on under-performing variations while the test runs. The bandit optimizes toward the winning metric you chose — conversion rate, average order value, or a custom event.

How the bandit updates

Bandit weights auto-adjust twice a day — at 8am and 8pm Central time — nudging traffic toward the leading variations based on your chosen winning metric so far in the current iteration. When that metric is average order value, the bandit favors the variations that earn the most revenue per visitor, not just the ones that convert most often. Adjustments are gradual and capped each cycle, so traffic shifts smoothly rather than swinging all at once.

Early on, while there isn't enough data to be confident, the split stays put. You can also trigger an optimization yourself at any time from the flag's page, and engage the kill switch to send everyone to control without losing your data.

Code: Static / general

Client-side only — wait for badgerlytics:ready or use onFlagsReady before reading variations.

<script>
// window.badgerlytics is ready once the script finishes loading.
document.addEventListener('badgerlytics:ready', function () {
var variation = window.badgerlytics.getVariation('hero_test');
if (variation === 'v1') {
document.body.classList.add('hero-test-v1');
}
});
</script>

Code: React

Client-side hooks — no middleware. For SSR apps, see SSR middleware (SDK).

import { useVariation, useIsFlagEnabled } from '@badgerlytics/sdk/react';
function Hero() {
const variation = useVariation('hero_test', 'control');
const newPricing = useIsFlagEnabled('new_pricing_table');
return (
<>
{variation === 'v1' ? <HeroB /> : <HeroA />}
{newPricing && <PricingV2 />}
</>
);
}

Code: Next.js

Requires createBadgerlyticsMiddleware — full API (getAssignments, isFlagEnabled, traits, manual assignment) in SSR middleware (SDK) → Next.js.

// Pages Router
import { getVariation } from '@badgerlytics/sdk/nextjs';
export async function getServerSideProps({ req }) {
return { props: { hero: getVariation(req, 'hero_test', 'control') } };
}
// App Router
import { headers } from 'next/headers';
import { getVariation } from '@badgerlytics/sdk/nextjs';
export default async function Page() {
const h = await headers();
const hero = getVariation(h, 'hero_test', 'control');
return hero === 'v1' ? <HeroB /> : <HeroA />;
}

Code: Remix

Pass the loader request (with forwarded x-bai-* headers) to getVariation. See SSR middleware (SDK) → Remix / React Router.

import { getVariation } from '@badgerlytics/sdk/remix';
export async function loader({ request }) {
return { hero: getVariation(request, 'hero_test', 'control') };
}

Code: Astro

Read from Astro.locals, not request headers — see SSR middleware (SDK) → Astro.

---
import { getVariationFromLocals } from '@badgerlytics/sdk/astro';
const hero = getVariationFromLocals(Astro.locals, 'hero_test', 'control');
---
{hero === 'v1' ? <HeroB /> : <HeroA />}

Code: Nuxt

Read from event.context via getVariationFromEvent — see SSR middleware (SDK) → Nuxt.

import { getVariationFromEvent } from '@badgerlytics/sdk/nuxt';
export default defineEventHandler((event) => {
return { hero: getVariationFromEvent(event, 'hero_test', 'control') };
});

Focus pages

Experiments only. Focus pages are an optional list of page paths used for per-page analytics on a test — the Focus Page Analysis report and AI insights for that experiment. Each configured path gets pageviews, sessions, bounce, and engagement broken out by variation. Leave the list empty if you only need site-wide test results.

Each entry must be an exact path starting with / (for example /checkout or /products/widget). Wildcards and regex are not supported. You can add up to 10 paths per experiment.

More than 10 URLs?

If the test affects many pages, treat focus pages as a sample of where you want to check analytics — not a complete inventory of every URL. Choose the paths that matter most: a flagship product page, a key category, checkout, or other pages where you expect the change to show up in the data.

Audiences

Experiments only. Pair an experiment with an audience segment to limit bucketing to a slice of users. Common examples:

  • Returning visitors only
  • Users on the "pro" plan
  • Mobile users only
  • Visitors who landed from a specific UTM campaign

Segment rules read built-in traits (visitor_type, device_type, page_path, UTM fields) and any custom traits you set from your code via setTraits() — see Audiences. Visitors are evaluated against the segment at assignment time.

Iterations

Sometimes a test isn't conclusive and you want to try a tweaked version. Iterations let you re-bucket users and start a fresh run on the same flag without breaking the old data. Each iteration is reported separately, so you keep the full audit trail of what you tried, when, and how it performed.

Note

Bucketing uses (visitorId, flagKey, iteration) as the seed, so a user assigned to variant v1 in iteration 1 might end up in controlin iteration 2. That's by design — old assignments shouldn't bias a new run.

Starting and stopping tests

Use the Start button on the flag detail page when you're ready to go live. Use Stop to freeze bucketing. Stopping a test keeps the historical data intact — the flag still exists, the results panel still works, you just stop assigning new visitors. From there you can:

  • Ship the winner — set the flag to always-return-that-variation while you remove the code path.
  • Iterate — start a new iteration with adjusted variants.
  • Archive — keep the results around but hide the flag from the main list.

Custom Events

Purpose

Custom events let you track anything specific to your product that isn't covered by the built-in events. Common examples: newsletter signups, content downloads, button clicks on a special landing page, integrations enabled, video plays.

Custom events show up in their own report, can be used as funnel steps, can trigger audience membership, and can be the conversion target for A/B tests.

Setup

To use a custom event, you need to register it once:

  • Open Events / Funnel / Audience setup → Custom events.
  • Click + New event and give it a token in the form custom_my_event (lowercase, the custom_ prefix is required).
  • Save. Tokens propagate to the property CDN immediately, so the tracker will accept the event on your very next page load.

Plan limits

Each plan caps how many active (non-archived) custom events a property can have — for example 25 on Starter, 100 on Pro, and 200 on Business. See Billing & Usage → Plan limits for the full table. Archive events you no longer need to free slots.

Heads up

Unregistered tokens are dropped at the tracker. This is intentional — it prevents typos like cusotm_signup from silently fragmenting your data.

Click (DOM-triggered) events

A custom event can fire automatically when a visitor clicks an element on your site, with no code changes. Choose On click (DOM-triggered) when you create the event, then add one or more triggers:

  • Element id — matches an element by its id (the # is added for you). Example: an id of signup-button fires on clicks of that element.
  • data-* attribute — matches an element by an attribute name and value. Example: name data-promo with value spring-sale fires on clicks of any data-promo="spring-sale" element.

The event fires when a click lands on the element or anything inside it. A DOM-triggered event can still be fired manually with trackCustomEvent if you want to record it from code as well.

Capturing details from the clicked element

You can record metadata without writing code: add a metadata field and give it a source attribute (a data-* attribute). On each click we read that attribute from the clicked element and store its value on the event — for example a field plan sourced from data-plan. Only attributes you configure are read; missing attributes are simply left off.

Copying events to another property

When stage and production should track the same custom events, use Copy to property… on an event row under Events / Funnel / Audience setup → Custom events. Pick a destination property in your organization.

You can create a copy or overwrite an existing event on the destination:

  • Create copy — Adds a new event with the same display name and metadata schema. Uses the same token when the destination does not have it yet; otherwise we append a numeric suffix (for example custom_signup_2).
  • Overwrite — Updates the destination event that already has the same token: display name and metadata schema are replaced. Payload fields not declared in the source schema are dropped at ingestion.

Syncing stage and prod

See Properties → Syncing between properties for how copy behaves across all resource types. Event data itself is never copied — only the registration on the destination property.

Code examples

// Register the event in the dashboard first:
// Custom events → New event → token: custom_newsletter_signup
window.badgerlytics.trackCustomEvent('custom_newsletter_signup', {
source: 'footer',
utm_campaign: 'spring_sale',
});

Querying in reports

After events are flowing, open Reports for the property and choose the Engagement tab. The Custom Events report is the main place to explore event volume, how often visits trigger each event, and (when relevant) revenue or conversions tied to sessions that fired the event.

  • Date range — Use the range control at the top (last 7/30/90 days, or a custom start/end). Toggle Compare to previous to see the same metrics for the prior period side by side (this report turns compare on by default). Use Daily breakdown for a day-by-day chart instead; compare and daily cannot run at the same time.
  • Event — Leave the event picker unset to see one row per registered custom event. Pick a single event to focus the table and unlock deeper breakdowns.
  • Group by — Fan out rows beyond the default rollup:A/B flag splits counts and rates by test, variation, and iteration; Page path shows where a chosen event fired; Metadata: … options appear for keys you declared on that event under Events / Funnel / Audience setup → Custom events (page path and metadata breakdowns require selecting an event first).
  • A/B test filter — Optionally narrow to one running or stopped experiment and variation to read event rates in the context of a specific test (lift and significance columns appear when a control arm exists).
  • Other filters — Use the filter panel for visitor type (new vs returning) or, when supported, a specific page path. Click Apply filters after changing them.

On the same Engagement tab, Conversion Funnel reports step-by-step drop-off for funnels you define under Events / Funnel / Audience setup — use that when the question is "where do people leave the journey?" rather than raw event counts. Experiment results on the A/B Testing tab can also use a custom event as the conversion goal when you set one on the flag.

Note

Metadata keys only show up under Group by if you added them to the event's schema in the dashboard before sending data. Declare fields you plan to slice on (for example source or plan) when you create or edit the event.

Considerations

  • Keep metadata small — a handful of key/value pairs, not a JSON blob. We index the keys for filtering in reports.
  • Avoid putting PII in metadata. Use anonymous identifiers and traits instead.
  • One event per real-world action. Two near-identical tokens with different metadata are usually a sign you wanted one event with a metadata field.
  • Prefer low-cardinality metadata values. Each metadata key can be used as a breakdown in reports, so values with a small set of possibilities (a category, plan, or source) are far more useful than high-cardinality ones like unique ids or timestamps. This applies to both manually-sent and click-captured metadata.

Funnels

Purpose

A funnel is an ordered list of steps a visitor takes to complete a goal — the most common being product listing → product page → add to cart → checkout → purchase. Badgerlytics' funnel report shows how many users reached each step, the drop-off between steps, and conversion rate end to end.

Setup

To wire up a funnel:

  • Open Events / Funnel / Audience setup → Funnels and click + New funnel.
  • Give it a token (e.g. purchase) and define its steps — number, name, and (optionally) a description.
  • Save. Each step is now a valid argument for trackFunnelStep() from your code.

One funnel, many places

A funnel typically spans several pages. You don't need a single file that "owns" the funnel — just fire each step from the component that knows the user reached it.

Copying funnels to another property

To align stage and production funnel definitions, use Copy to property… on a funnel row under Events / Funnel / Audience setup → Funnels. Pick a destination property in your organization.

You can create a copy or overwrite the destination's active funnel for the same token:

  • Create copy — Inserts a new funnel with the same name and steps. Uses the same token when the destination does not have it yet; if that token is already taken (including inactive versions), we assign a suffixed token instead.
  • Overwrite — Replaces the destination's active funnel for that token: the current version is deactivated (kept for historical reports) and a new version is created from this funnel's name and steps.

Syncing stage and prod

See Properties → Syncing between properties for how copy behaves across all resource types. Funnel analytics on the destination are not copied — only the step definition.

Code examples

A small helper module keeps your call sites tidy and your step numbers in one place:

lib/tracking.jsjavascript
const FUNNEL_NAME = 'purchase';
export const FUNNEL_STEPS = Object.freeze({
PRODUCT_LISTING_PAGE: { number: 1, name: 'product_listing_page' },
PRODUCT_DISPLAY_PAGE: { number: 2, name: 'product_display_page' },
ADD_TO_CART: { number: 3, name: 'add_to_cart' },
VIEW_CART: { number: 4, name: 'view_cart' },
PAYMENT_AND_REVIEW: { number: 5, name: 'payment_and_review' },
CONVERSION: { number: 6, name: 'conversion' },
});
export function trackFunnelStep(step) {
if (!step || step.number == null || !step.name) return;
window.badgerlytics.trackFunnelStep(FUNNEL_NAME, step.number, step.name);
}

Then use it where each step actually happens:

// In a product detail component
useEffect(() => {
trackFunnelStep(FUNNEL_STEPS.PRODUCT_DISPLAY_PAGE);
}, []);
// In your "add to cart" handler
function addToCart(product) {
cart.add(product);
trackFunnelStep(FUNNEL_STEPS.ADD_TO_CART);
}

Considerations

  • Step numbers don't have to be consecutive in code, but the report treats them as ordered. Don't reorder them after you've started collecting data — pick the order once.
  • Visitors can skip steps (a customer landing on a deep link goes straight to step 2). The funnel report handles this naturally.
  • Fire each step exactly once per session per page where it makes sense. Don't fire add_to_cart on every re-render — fire it on the actual click handler.

Audiences

Purpose

An audience segment is a rule over visitor traits that decides who is bucketed into an experiment or feature flag. Use segments to:

  • Target experiments ("run the pricing test on free-tier users only").
  • Power personalization with on/off flags ("show this promo to returning visitors on the pro plan").

Segments apply to flag assignment. Reports are filtered with their own controls (visitor type, device, country, page, A/B variation) — see Analytics Reporting.

Setup

On a feature flag or experiment, Audience segment starts as All visitors. Switch to Custom segment to add trait conditions (for example "logged-in pro users only"). Only visitors who match at assignment time are bucketed.

Built-in traits are always available and need no registration:

  • visitor_type — new or returning
  • device_type — desktop, mobile, or tablet
  • page_path — the current page path
  • utm_source, utm_medium, utm_campaign — from the first page of the session

Custom traits must be registered on the property before you can reference them in segment rules or set them from your app. Open Events / Funnel / Audience setup → Audience, add a label and type (boolean, string, or number), and save — we generate the token from the label (for example "Logged in" → logged_in).

Each condition compares one trait to a value:

  • Booleans: equals, does not equal, is set, is not set.
  • Strings and numbers: equals, does not equal, is one of / is not one of a list, is set, is not set.
  • Conditions in a segment are combined with AND (all must match) or OR (any may match).

Location, behavior, and other signals

Segment rules only read traits. To target on something else — a visitor's country, whether they have used a feature, their subscription state — set it as a custom trait from your code (for example setTraits({ country: loc.country }) after getLocation() returns). See Code examples.

Copying traits to another property

When stage and production should share the same trait registry, use Copy to property… on a trait row under Events / Funnel / Audience setup → Audience. Pick the destination property in your organization; we copy the label, type, and token as-is.

Audience trait copy is create-only — there is no overwrite option. If the destination already has a trait with that token, the modal tells you it already exists and you cannot copy until you remove the conflicting trait or choose a different destination. Register traits on the destination before copying feature flags whose segment rules reference them.

Syncing stage and prod

See Properties → Syncing between properties for the full list of what can be copied across properties and how each resource behaves on collision.

Code examples

Traits are how you tell us what an anonymous visitor is — without identifying them. Set them whenever the user's state changes (login, logout, plan change):

// On login
window.badgerlytics.setTraits({
signed_in: true,
plan: 'pro',
account_age_days: 124,
});
// On logout
window.badgerlytics.setTraits({
signed_in: false,
plan: null,
});

For SSR, set the _bai_traits cookie on login/logout so segment rules apply on the first render after sign-in. Use setTraitsOnResponse (Next.js, Remix, Astro API routes) or setTraitsOnEvent (Nuxt). Full examples per framework are in SSR middleware (SDK)under each stack's section.

SSR vs client-only

@badgerlytics/sdk/react exposes useSetTraits() for the browser only; traits take effect on the next page load unless you also set _bai_traits on the server.

Avoid PII in traits

We strongly discourage passing names, emails, phone numbers, or other personal identifiers as traits. Prefer IDs that are opaque to you (e.g. plan, tenure_bucket) for anonymous segmentation.

Considerations

  • Segment rules are evaluated when a visitor is assigned to a flag. Changing a rule affects new assignments going forward; visitors already bucketed keep their variation. Start a new iteration if you want everyone re-evaluated against the new rule.
  • Traits set in the browser take effect on the next page load. For server-rendered apps, also set the _bai_traits cookie so the first render after login already matches — see Code examples.
  • Keep trait labels descriptive — "Signed in" reads better than "flag_3" six months from now.
  • For SaaS apps, the most useful traits are usually subscription state (trialing, active, past_due). Set them from your app whenever the state changes.

Analytics Data Retention

Policy

All plans include two years of analytics data retention. Raw events and rolled-up report tables are kept for that period, then permanently deleted on a rolling basis — nothing is archived or recoverable after it ages out.

A daily retention job removes data older than two years. You do not need to take any action; reports and exports only include data still within the window.

Deleting a property

You can permanently delete a property and all of its data at any time from Settings → Manage Data. Select the property, confirm with a typed property name, and we remove the property, its configuration, and all associated analytics in the background.

Heads up

Deletion is irreversible. There's no "undo" — make sure you really mean it. Remove or pause the tracking script on your site before deleting if you have not already.

AI Insights

Overview

AI Insights automatically reviews your property's analytics and surfaces the shifts that matter — traffic, revenue, devices, custom events, A/B tests, and more. Instead of scanning every report yourself, you get prioritized observations with plausible reasons and concrete next steps.

Insights run on a plan schedule (not on demand). Each run compares recent performance to the prior period when enough history exists, then writes short observations you can browse on the AI Insights page, skim as dashboard highlights, or open inline on matching report pages.

Plan availability

AI Insights is included on all paid plans (Starter, Pro, Business, and Ultimate). How often it runs depends on your plan — see Schedule & lookback. It shares your monthly AI token allowance with Chat Analyst, AI Team Summary, UX Analyst, and AI Session Replay analysis.

Getting started

  • Open AI Insights from the property sidebar.
  • Confirm insights are enabled in Settings on that page. Production properties start with insights on; you can turn them off or limit which report types are analysed.
  • Wait for the next scheduled run (around 7am Central on days your plan includes). New properties may show fewer insights until more history accumulates.
  • Use the Report and Priority filters to focus on what you care about, or open a report page to see the matching insight strip for that report.

Anyone with access to the property can read insights. Changing settings requires Edit access or higher.

Property description helps

A clear property description gives the model business context (what you sell, who you serve). Set it under property settings — the same description also helps AI Chat Analyst.

Schedule & lookback

Insights regenerate on run days for your plan, around 7am Central:

  • Starter — 3× per week (Monday, Wednesday, Friday)
  • Pro and Business — 5× per week (Monday–Friday)
  • Ultimate — 7× per week

Standard report insights compare the latest 30 days to the prior period when your history allows. On newer properties (roughly the first 30 days after the tracking script is detected), shorter windows are used until a full compare is possible — comparisons get richer as data builds.

A/B test insights only cover tests in Running status. The window is from the start of the current iteration through generation time — not the date range picker on report pages.

Where insights appear

  • AI Insights page — the full feed, grouped by priority (high, medium, low), with filters by report type, plus a Recently Dismissed tab for insights your team hid.
  • Dashboard highlights — a short list of notable observations on the property overview (and related tabs where highlights are shown).
  • Report pages — a collapsible AI insights strip on reports that participate in scheduled analysis. Jump to the full Insights page from there when you want the wider feed.

Dismissing an observation on the AI Insights page removes it from all of these surfaces for the whole team. See Dismissing insights.

Settings

On the AI Insights page, open Settings to:

  • Enable or disable AI Insights for the property. Turning insights off clears stored insights when you save.
  • Choose which report types to analyse (for example Website Performance, Revenue or MRR, Usage Revenue (when SaaS is enabled), Device Performance, Custom Events, A/B Tests). Disabled types are skipped and their existing insights are cleared.

Changes apply on the next scheduled run — settings do not trigger an immediate regeneration.

Reading observations

Each observation is a short write-up tied to a report type, with a priority level so you can triage quickly. Cards show the evaluation period used for that run. Prefer high-priority items when time is short; medium and low still help spot quieter trends.

Insights are guidance based on your analytics — validate important recommendations with an A/B test or a closer look in Analytics Reporting before shipping large changes.

Dismissing insights

On the AI Insights page, you can dismiss individual observations you have already seen or acted on. Dismissals are team-wide for the property — once one person dismisses an insight, it disappears for everyone with access.

  • Dismissed observations leave the Current tab and also stop showing on the dashboard and report insight strips.
  • They stay hidden for 7 days. During that window, scheduled runs skip regenerating the same (or very similar) observations so the feed does not keep repeating itself.
  • Open the Recently Dismissed tab to review what was hidden, who dismissed it, and when it can return. Use Restore to put an observation back on Current immediately and allow it to regenerate again.
  • After 7 days, the dismissal expires. If the topic is still relevant, it can resurface on a later scheduled run.

Dismiss one at a time

There is no dismiss-all. Clearing insights individually keeps the shared feed predictable for the rest of the team.

Usage & tokens

Scheduled runs consume AI tokens from your organization's monthly allowance. Usage is typically high per run because several reports can be analysed, but runs are automatic and paced by your plan — they should not exhaust a normal allotment on their own. See Billing & Usage → Token usage for how Insights compares with on-demand features like Chat Analyst.

AI Chat Analyst

Overview

AI Chat Analyst lets you ask questions about a property's analytics in plain English and get answers backed by your live data — traffic, conversions, revenue, A/B tests, custom events, and more. Instead of clicking through every report, describe what you want to know and the assistant pulls the relevant numbers, compares them, and summarizes the takeaway.

Answers use the same underlying data as your dashboard reports. When the assistant cites a conversion rate or revenue figure, it matches what you would see in Analytics Reporting for the same date range and filters.

Plan availability

AI Chat Analyst is included on all paid plans (Starter, Pro, Business, and Ultimate). It shares your monthly AI token allowance with AI Insights, AI Team Summary, Chat Analyst, UX Analyst, and AI Session Replay analysis.

Getting started

  • Open AI Chat Analyst from the property sidebar.
  • Type a question in the message box at the bottom of the screen. Press Enter to send, or Shift+Enter for a new line.
  • On an empty chat, use the suggested prompts to see the kinds of questions that work well — traffic sources, A/B test performance, device comparisons, custom events, and page-level bounce rates.
  • Use Clear chat in the header to start a fresh conversation at any time.

Anyone with Edit access or higher on the property can use Chat Analyst. View-only members can read reports but cannot send messages.

What you can ask

Chat Analyst works best with focused, specific questions about one property at a time. Broad questions are fine too — the assistant will narrow down or ask for clarification when a name is ambiguous.

Examples that work well:

  • Traffic & acquisition — "What were our top traffic sources by conversions in the last 30 days?" or "How did mobile vs. desktop perform this month?"
  • Pages & engagement — "Which pages had the highest bounce rate last week?" or "What are our top exit pages?"
  • A/B tests — "How is my most recent test performing?" or "Compare conversion rate between control and variant B on the homepage hero test."
  • Custom events — "Which custom events fired most in the last 7 days?" or "How many signups did we get from the newsletter_click event this quarter?"
  • Revenue & funnels — "What drove our revenue change last month?" or "Where are people dropping off in the checkout funnel?"
  • SaaS metrics (when SaaS reporting is enabled on the property) — MRR trends, usage revenue, cohort retention, cancellation reasons, and subscription lifecycle questions.
  • Ecommerce metrics (when SaaS reporting is off) — product performance, category revenue, and cart abandonment.

Responses often include bullet points and tables when comparing several rows (variants, pages, countries, events). The assistant formats currency using your property's display currency.

Reports & data

Behind the scenes, Chat Analyst queries the same report types available in your property's Analytics Reporting area — aggregated summaries, not raw event streams. That means:

  • Summary-level answers — you get totals and comparisons for a date range (for example, last 30 days), not hour-by-hour or day-by-day charts. For time-series exploration, use the Reports UI directly.
  • Property-scoped — each chat is tied to the property you opened it from. You cannot query another property in the same conversation.
  • Property type matters — SaaS-enabled properties include subscription, MRR, and usage-revenue reports; ecommerce properties include product and cart reports. The assistant only uses reports that apply to your property configuration.
  • A/B test reports — when you ask about a test, the assistant scopes results to that test's current iteration (or a specific iteration you name). For per-page behaviour on a test, it can use your configured Focus pages on that flag.
  • Small samples — if a metric is based on very few sessions or conversions, the assistant should call that out rather than overstate confidence.
  • Some reports are Reports-only — Chat Analyst uses a curated subset of your analytics reports. It does not query these, even though they appear in the Reports UI: Browser Usage, OS Usage, Product View Performance (per-product view counts), and Variant Enrollment vs. Exposure (A/B bucketing diagnostics). Ask about device splits, product sales, or test lift instead — or open those reports directly for browser/OS or enrollment detail.

Note

Numbers may differ slightly from third-party tools because of session definitions, bot filtering, consent gating, or client-side delivery issues — the same caveats documented under Analytics Reporting → Accuracy / sampling apply here too.

Tips for better answers

  • Name tests and events when you can. Say "homepage hero test" or "newsletter_signup" — the assistant looks up the right internal token. If several items match, it will ask you to pick one.
  • State a timeframe when it matters — e.g. "last 7 days", "this month", "Q1". If you omit one, the assistant defaults to roughly the last 30 days.
  • Check canonical names in Feature Flags and Custom Events / Funnels if you want to be precise before asking.
  • Archived custom events and funnels are excluded from analysis. Un-archive them on the Events & Funnels page if you need them included.
  • One question at a time for complex comparisons often works better than a long multi-part prompt — you can follow up in the same thread.
  • Clear long threads when you change topic. Chat uses more AI tokens when it carries a long conversation history, because each reply may call several reports.

Conversation & usage

  • Local conversation history — messages are stored in your browser for up to 72 hours so a refresh does not wipe the thread. After that window, or when you clear chat, you start fresh. History is not synced across devices or browsers.
  • AI token usage — each question may trigger one or more report lookups before the model writes an answer. Longer threads and broader questions use more tokens than a single focused query. See Billing & Usage → Token usage for rough expectations compared with other AI features.

Team Summary

Overview

AI Team Summary turns the aggregates you are already looking at into a short, copy-ready update for your team. Open it from the Overview or Active Tests tab, from most Reports, or from a survey report — it summarizes the selected date range, the current test iteration, or the report on screen, highlights the biggest period-over-period moves, and formats the result for Slack or email.

Summaries are generated on demand when you open the modal. Results are cached in your browser for six hours per view so you are not charged tokens on every refresh. Use Regenerate when you want a fresh take after changing the date range or when numbers have moved.

Plan availability

AI Team Summary is included on Pro, Business, and Ultimate plans. It is not available on Starter. It draws from the same monthly AI token allowance as AI Insights, Chat Analyst, UX Analyst, and Session Replay analysis.

Where to find it

  • Overview tab — next to the date-range controls, above your traffic and revenue widgets. The summary reflects the aggregates for whichever bucket you have selected (for example, last 30 days or 180 days).
  • Active Tests tab — above your running experiment cards. The summary covers the current iteration for each live test.
  • Reports — on most report pages, next to the date range. The summary covers the view you have open. A few reports need a narrower filter first (a specific funnel, page, or product). Daily breakdowns and the full UTM catalog are not included.
  • Survey reports — reach, the leading answer for each question, themes in recent fill-in responses, and one suggested next step.

Anyone with Edit access or higher on the property can generate a summary when their plan includes the feature.

Usage & tokens

Each generation uses a single AI call with a compact snapshot of your dashboard aggregates — not a full report export. That keeps token use low compared with Chat Analyst or AI Insights. See Billing & Usage → Token usage for how Team Summary compares with other AI features.

AI Session Replay

Overview

AI Session Replay captures how real visitors move through your site, then lets you watch those visits back and run AI analysis on them. It's the fastest way to see where people hesitate, get stuck, or abandon — without guessing from aggregate charts alone.

Recordings are tied to the same analytics context you already have: device type, pages visited, custom events, A/B test assignments, and whether the visitor converted. That makes it easy to find the sessions that matter and understand what actually happened.

Session replay uses its own daily recording limits — separate from your monthly session allowance — and those limits apply per property, not pooled across your organization. See Limits & retention for plan-specific caps and sampling rules.

Plan availability

AI Session Replay is included on Business and Ultimate plans. Starter and Pro accounts can upgrade to unlock it.

Getting started

  • Open AI Session Replay from the property sidebar.
  • Turn on Session recording for the property. Changes reach your live site within a few seconds.
  • Make sure the Badgerlytics tracking script is installed — recordings only flow once the script is active on your site.
  • As engaged visitors browse, sessions appear in the list. Select one to see details, press play to watch the replay, or run AI analysis when you want a written breakdown.

What you get in the panel: a scrollable list of recent sessions with device, duration, page count, conversion status, and a tab count when the visit spanned multiple browser tabs. The replay player supports play/pause and scrubbing; multi-tab visits include a tab strip so you can switch between tabs without losing context.

Filtering sessions

Use the filter bar above the session list to narrow down to the visits you care about. Filters can be combined — for example, mobile visitors who converted during a specific A/B test.

  • Device — desktop, mobile, or tablet.
  • Duration — at least 1 minute, 5, 10, 20, or 30 minutes, or 1 hour.
  • Conversion — converted or not converted (based on your property's conversion definition).
  • Visitor — new or returning.
  • Test & variation — filter to sessions that saw a running A/B test, optionally narrowed to a single variation.
  • Custom event — show only sessions where a specific custom event fired.

Clear filters at any time to return to the full list. If nothing matches, try widening your criteria — only sessions from the last fourteen days are kept (see Limits & retention).

AI analysis

For any completed session, you can run Analyze session to get an AI-written review of that specific visit. Analysis typically finishes in under a minute. Each session can be analyzed once; results are saved so you can reopen them later.

The report includes:

  • Summary — a concise narrative of what the visitor did and how the experience felt overall.
  • Issues — friction points or moments of confusion observed during the journey.
  • Opportunities — openings to improve conversion or engagement based on how the visitor behaved.
  • Recommendations — concrete, testable changes worth trying — often good candidates for your next A/B test.

For multi-tab sessions, the analysis considers the full cross-tab journey — including when the visitor switched between tabs — not just a single page in isolation.

AI analysis draws from your plan's monthly AI token allowance, the same pool used by AI Insights, Chat Analyst, and UX Analyst.

Limits & retention

Session replay is designed for high-traffic sites without storing every single visit. Daily recording limits are enforced per property (UTC calendar day) — each property in your organization has its own budget. They are separate from your plan's monthly session allowance.

  • 14-day retention — recordings are stored for fourteen days, then removed automatically. Download or review anything you need to keep within that window.
  • Daily cap (per property) — Business: up to 3,000 recordings per property per day. Ultimate: up to 6,000 recordings per property per day. After a property reaches its cap, new recordings for that property pause until the next UTC day. Other properties in the same organization keep recording until they hit their own caps.
  • Sampling (per property) — the first 150 engaged sessions each day on a property are always recorded. After that, roughly 15% of visitors are sampled for the rest of the day. Sampling is decided per visitor, not per visit: once someone is sampled in, every eligible visit they make that day is captured, and if they are sampled out they stay out until the next UTC day. That keeps a returning visitor's journey whole instead of leaving you with disconnected fragments. High-traffic properties still get a steady stream of replays without storing every visit.
  • Per-visitor limit — a single visitor can start up to twelve recordings on a property in any 24-hour period. This only affects unusually heavy repeat visitors, and it stops one person from consuming a property's daily budget. Continuing a visit across pages or tabs does not count as a new recording.
  • Bounces excluded — single-page visits with no meaningful engagement are not stored. Only sessions where the visitor actually engaged (for example, viewing more than one page or interacting with the site) are captured.
  • Two-hour maximum — an individual recording stops after two hours of active capture per tab. Very long visits are truncated at that point.
  • Multi-tab sessions — when a visitor has several tabs open at once, up to four tabs are captured at a time, each as its own recording. They appear together as one session in the list and in the replay player, where you can switch between tabs. Closing a tab frees its place, so a visitor who works through many tabs one after another keeps being recorded. A multi-tab visit counts as one recording toward that property's daily limit.

Privacy & masking

Session replay is built with privacy in mind. Sensitive data is masked before it ever leaves the visitor's browser:

  • Form fields — all input values are masked automatically. The replay still shows that someone typed in a field, but the actual characters are never stored.
  • Editable content — text in contenteditable regions is masked by default.
  • Custom selectors — add CSS selectors in the Privacy masking panel to hide additional regions on your site (account numbers, internal IDs, support chat widgets, and so on). Masked text appears obscured in the replay rather than as readable copy.

Review your masking rules after enabling recording, especially on pages that display user-specific content. Changes save to the property and apply to new recordings within a few seconds.

Heads up

Masking reduces risk but is not a substitute for keeping truly sensitive flows off the recorded surface. If a page should never be replayed, consider excluding it from recording via your site design or masking every element that could show private data.

Click Maps

Overview

Click Maps show you what visitors actually click on each page of your site, drawn directly on top of the live page. Every button and link gets a colored box with the share of pageviews that clicked it and the total number of clicks, so you can see at a glance which calls to action earn attention and which ones get ignored.

Because the overlay runs on your real site, you can navigate from page to page and the map follows you. Results are aggregated by page (host + path) and can be narrowed to a single A/B test variation so you can compare how each version changes visitor behavior.

Plan availability

Click Maps is included on Pro plans and above (Pro, Business, and Ultimate). Starter accounts can upgrade to unlock it. Collection is turned off by default — enable it per property when you're ready.

Getting started

  • Open Click Maps from the property sidebar.
  • Turn on Click collection. The setting reaches your live site within a few seconds; the tracking script begins recording clicks on the next pageview.
  • Choose the site (any of your property's registered hosts) and the page path you want to look at, then select Open Click Maps on site. Your site opens in a new tab with the overlay and a small control panel in the top-right corner.

Data builds up over time

Click Maps only include clicks recorded after collection is switched on. A brand-new page will look empty at first; give it a few days of traffic before drawing conclusions. Data for the last 90 days is kept.

Using the overlay

Once on your site, the control panel lets you:

  • Show / hide the overlay — toggle the colored boxes without leaving the page.
  • Change the date range — last 7, 30, or 90 days.
  • Filter by variation — pick a running A/B test and variation to see only pageviews from visitors who saw that version. The filter stays active as you navigate between pages.
  • Hide controls — tuck the panel away for a clean screenshot. Refresh the page to bring it back.
  • Log out — end your viewer session on this browser. Sessions otherwise last 30 days.

Each box is colored on a heat scale from cool (rarely clicked) to hot (most clicked) relative to the other elements on that page. The badge on each box reads 12.4% · 1,240: the share of pageviews where the element was clicked at least once, followed by the total number of clicks. Elements that were clicked historically but are not on the page right now are listed in the panel so you can still see their numbers.

The overlay is only visible to you. Visitors never see it, and opening it does not count as traffic on your reports.

A/B test variations

By default, Click Maps show all traffic on the page, regardless of which test variations visitors were in. Filtering to a variation is always an explicit choice — either from the Click Maps page in your dashboard or from the control panel on your site. Your own test assignment while browsing never changes the numbers you see.

When a filter is on, both the percentage and the click totals are limited to pageviews from visitors who were in that variation, so the comparison between versions is apples to apples. You can also force a variation on the page itself with the same test mode parameter you use for QA; the overlay follows whichever variation you are viewing so the boxes line up with the layout on screen.

Which elements are tracked

Click Maps record clicks on buttons, links, radio buttons, checkboxes, form fields, dropdowns, elements with a button role, and elements that respond to clicks. Nothing else on the page is tracked, and no typed text is ever collected. Each element is identified in this order:

  • data-bai-click-id — an explicit label you set. Use this for elements that repeat on a page (for example a "Remove" button on every cart line) so their clicks roll up together.
  • id — the element's id attribute. Stable, unique ids give the most reliable maps.
  • Link destination — for links without an id, the path they point to.
  • Radio buttons and checkboxes — each option is tracked on its own (by name and value), so a product configuration picker shows one box per choice rather than one box around the whole group.
  • Form fields and dropdowns — text fields, text areas and select menus are recorded when a visitor clicks into them (by id or field name). This tells you which fields visitors reach in a form. What they type or choose is never recorded, and password fields are always skipped. A field counts once per pageview no matter how many times it is clicked.

Buttons that have none of the above are skipped. Adding a stable id (or data-bai-click-id) to your important calls to action is the single best thing you can do for accurate click maps. Avoid ids that change on every build or page load. Radios and checkboxes are recorded automatically when they have a name.

Limits & privacy

  • 90-day retention — click data older than 90 days is removed automatically.
  • Per-pageview cap — up to 50 distinct elements are recorded per pageview. Repeat clicks on the same button or link are counted, not duplicated; form fields count once.
  • Labels — the short text on each element is stored to help you recognize it in the panel. Text that looks like an email address or phone number is masked, and any regions you have masked for session replay are masked here too.
  • Traffic filters — the same bot and traffic filters that apply to your analytics apply to Click Maps, so internal or excluded traffic does not skew your percentages.

Surveys

Overview

Surveys let you ask visitors what they think while they are on your site. Show a quick thumbs-up / thumbs-down rating next to a feature, or a short set of multiple-choice questions on a page, and see the answers roll up in your dashboard. Surveys are delivered by the same tracking script you already have installed — there is nothing extra to add to your site.

There are two kinds of survey:

  • Rating — a compact strip with thumbs up, neutral and thumbs down buttons and a short prompt such as "Rate this!". One tap and the visitor is done.
  • Questions — up to five multiple-choice questions shown one at a time with Back and Next buttons. Each question can have up to five choices, and one choice per question can be a fill-in answer where the visitor types their own response.

After answering, visitors see a short thank-you message and the survey closes on its own a few seconds later. Every survey is fully keyboard-accessible and announced to screen readers.

Plan availability

Surveys are included on every plan. Plans differ in how many surveys can be enabled at the same time on a property — see Plan limits. Disabled surveys never count toward the limit, so you can keep as many drafts as you like.

Creating a survey

  • Open Surveys from the property sidebar and select Create survey.
  • On the Setup tab give the survey a name (only your team sees it), pick Rating or Questions, choose when it should appear, and set the prompt or header text and the thank-you message.
  • For question surveys, write your questions and choices on the Questions tab. Tick Fill-in on a choice to let visitors type their own answer (up to 256 characters).
  • Use the Design tab to match your brand. The live preview on the right updates as you type and is fully interactive, so you can try the survey exactly as visitors will.
  • Optionally, on the Targeting tab, limit the survey to visitors in specific A/B test variations.
  • Switch the survey on when you are ready. Changes reach your live site within a few minutes. You can also enable and disable surveys straight from the list.

The survey type can't be changed after it is created, but everything else — including questions and choices — can be edited at any time, even after responses have come in.

When surveys appear

A survey can be triggered in one of two ways:

  • When a visitor lands on a page. Enter an exact path such as /pricing, or end the path with /* to match a whole section — for example /blog/* matches every article. Use /* on its own for every page. Choose where the survey appears (top, centre or bottom; left, centre or right) and a delay of up to 60 seconds (3 seconds by default).
  • When an element appears on the page. Give the element on your site an id attribute, for example <button id="checkout-button">, and enter checkout-button (without the#) as the element id. The survey waits for the element to appear — even if it is added to the page later — and shows after a delay of up to 10 seconds (1 second by default). Rating strips appear to the left or right of the element; question surveys open in the centre of the screen.

On small screens surveys always appear at the bottom of the screen so they never cover the content a visitor is reading. Only one survey is shown at a time on a page; if two are eligible, the next one waits until the first has been answered or closed.

Check the element id

If the element is never found on a page where the survey should appear, that visit is recorded as element not found and highlighted in the report so you can spot a mismatched id. Element ids that change on every page load or build will not work — use a stable id.

Targeting by variation

By default a survey is shown to every visitor who meets the trigger. On the Targeting tab you can instead limit it to visitors who are currently in one or more variations of your running A/B tests or feature flags — a simple way to ask "how does the new version feel?" only to the people who actually see it.

Pick any number of variations across any number of running tests; a visitor qualifies if they are in any of them. Variations from tests that are later stopped are ignored automatically, so the survey keeps working.

How often visitors see a survey

  • A visitor who answers or closes a survey will not see that survey again for 30 days on the same browser.
  • Different surveys are tracked separately, so completing one does not hide the others.
  • Each visitor is counted once per survey for views and once for completions over a 30-day window, so refreshing a page does not inflate the numbers.
  • Bot and traffic filters that apply to your analytics apply to surveys too.

Testing on your site

To check a survey on your live site before switching it on — or to see it again after you have already answered — open the page with ?bai_surveyTest=YOUR_SURVEY_KEY added to the address. The survey key is shown in the edit dialog. In test mode the survey appears immediately regardless of delay or the 30-day rule, and nothing you do is recorded in the report. The survey must still be enabled and its trigger must match the page you are on.

Reports

Select Report next to any survey to see:

  • Views, completions and completion rate, plus how many visitors closed the survey without answering, for the last 7, 30 or 90 days.
  • A day-by-day chart of views against completions.
  • Answer breakdowns — for rating surveys the split between thumbs up, neutral and thumbs down; for question surveys a count and share for every choice of every question.
  • Fill-in answers — download a CSV of everything visitors typed, with the time it was answered (UTC), the question, the answer text and the page it was answered on.

Editing a live survey keeps its history: answers stay attached to the question and choice they were given for. If you remove a choice that visitors had already picked, it still appears in the report marked as Removed option.

Share the results. Download PDF opens a clean, printer-friendly copy of the report in a new tab — choose Save as PDF in the print dialog to keep or send it. Team Summary writes a short, copy-paste-ready recap of the results for the selected period: reach, the leading answer for each question, recurring themes in recent fill-in answers, and one suggested next step. It works the same way as the dashboard Team Summary (Pro plans and up; uses your AI token allotment).

Limits & retention

  • 90-day retention — views, answers and fill-in responses older than 90 days are removed automatically. Download the CSV if you need to keep responses for longer.
  • Enabled surveys per property — Starter 5, Pro 10, Business 15, Ultimate 30. Disabled surveys don't count.
  • Questions — up to 5 questions per survey, 2–5 choices per question, one fill-in choice per question, and 256 characters per fill-in answer.
  • Text lengths — survey name 80 characters, header title 60, question 200, choice 80, thank-you message 120, rating prompt 40.
  • Privacy — surveys record only the choices a visitor makes and any text they type into a fill-in answer. Visitor identity follows the same anonymous visitor id used for your analytics.

AI UX Analyst (Beta)

Overview

UX Analyst is an AI assistant that visits your site, evaluates its user experience against a set of best-practice heuristics, and writes up specific, actionable recommendations. Think of it as a thoughtful second pair of eyes that's read every UX paper of the last decade.

Plan availability

UX Analyst is included on Pro plans and above (Pro, Business, and Ultimate). Starter accounts can upgrade to unlock it.

Beta feature

UX Analyst is in beta. Results are useful, but expect occasional duds and double-check anything that looks surprising. We're improving it quickly based on user feedback. Crawling a site using AI is hard.

Usage instructions

  • Open UX Analyst from the property sidebar and click New mission.
  • Pick the focus: a single funnel, a single page, or a custom URL list. The tighter the focus, the more reliable the results.
  • Optionally add a goal in your own words ("help us reduce cart abandonment", "evaluate our pricing page for clarity").
  • Hit run. Missions typically finish within about 15 minutes, depending on how many pages and steps you've included. Keep missions focused — start on the page you care about — for the most reliable results.
  • When the report is ready, you'll see a list of findings with screenshots, severity, and suggested next steps — many of which include suggested A/B tests you can launch.

Limitations

  • Logged-in flows aren't supported yet. UX Analyst sees what an anonymous visitor sees. (Use a demo-mode URL if you need authenticated content reviewed.)
  • Pages behind hard paywalls, captchas, or geo-blocks may not be reachable.
  • Anything inside an iframe isn't interactable. This would include things like Google Maps, YouTube, or Twitter embeds or Typeform forms.
  • The analyst can't test interactive flows that require real customer data (e.g. placing a paid order). It evaluates the static UX and information design.
  • Each run is capped at about 15 minutes. If a mission times out, try a smaller one — start on the specific page you want reviewed and keep the steps short.
  • Recommendations are guidance, not gospel. Always validate with an A/B test before declaring victory.

AI SEO/GEO Analyst (Beta)

Overview

The AI SEO/GEO Analyst answers two questions in one report: how well can search engines and AI crawlers read your site, and what do AI assistants actually say when a buyer asks them about your category? "GEO" (generative engine optimization) is the second half — earning mentions, recommendations and citations from ChatGPT, Gemini and Perplexity, not just rankings on a results page.

Each report has two parts:

  • Site health. We crawl a representative sample of your pages — your homepage, any priority pages you name, and a spread across page types (product, category, article, and so on) from your sitemap and navigation — and score five categories from 0 to 100: crawlability, structured data, content, AI readiness and performance. Every finding is tagged as a whole-site, page-type or single-page problem so you can tell a one-line robots.txt fix from a template change that touches thousands of pages.
  • AI answer visibility. We ask several answer engines a set of prompts a real buyer might type, several times each, and measure how often your brand is mentioned, how often it is actively recommended, its share of voice against competitors, the sentiment of what is said, and which pages (yours or others) the engines cite as sources.

The two halves are tied together by prioritised recommendations that explain what to fix first, why it matters for both search and AI answers, and how to do it.

Plan availability

The AI SEO/GEO Analyst is included on Business and Ultimate plans. It does not require the tracking script, so you can run it on a property before installing anything. Once you have a solid traffic history you can optionally let your analytics steer which pages get scanned (see below).

Beta feature

Answer engines change constantly and their replies vary from one ask to the next. We sample each prompt more than once to smooth that out, but treat visibility numbers as a trend to watch over several reports rather than a precise score.

Your brand profile

The first time you open the analyst for a property we read your homepage and suggest a brand profile: your brand name and common aliases, the industry you are in, likely competitors, and a set of prompts a buyer might ask an assistant. There is nothing to fill in before your first report.

  • Prompts are the heart of the visibility half. Most should be unbranded ("best project management tool for remote teams") so you learn whether you earn a mention; one or two can be branded ("is [your brand] a good buy?") to see how you are described. Edit them freely — the same prompts are reused on every report so you can compare over time.
  • Competitors drive share of voice and let us tell competitor websites apart from neutral third-party sources in the citations. The report also lists brands the engines named that are not on your list yet.
  • Aliases help us spot your brand when an assistant uses a shortened name, a product line or a legal name.
  • Answer engines default to ChatGPT, Gemini and Perplexity. Your plan sets how many can be enabled at once.

Use Refresh from homepage after a rebrand or a major homepage rewrite to regenerate the suggestions. Anything you have edited by hand is kept.

Choosing which pages get scanned

Reports scan a sample of your site rather than every URL, so every time you click Run report we ask how that sample should be chosen. Your answers are saved as defaults (they also live in the brand profile), so you only set them up once and can tweak them per run. Your homepage is always first.

  • Priority pages. List up to 50 pages that matter to the business no matter how much traffic they get — best-selling or high-margin products, key landing pages, comparison pages. Pick the domain and enter the path one at a time, or use Bulk add to paste a list of paths for one domain. Priority pages are scanned first in the order you set, are always included in the deeper content-quality read, and problems on them count more in the score. If you list more pages than your plan scans per report, the ones at the top of the list are included.
  • Use analytics to choose pages. On by default. When on, the pages your visitors actually reach take a share of the sample, page types are weighted by their traffic, and slow pages are judged on real visitor load times. This only helps once a meaningful history has built up — we suggest waiting until you have roughly 90 days of tracking data. If you turn it off, name at least five priority pages so we know what matters; the rest of the sample then comes from your sitemap and navigation.
  • Never scan paths skip whole sections such as account areas or internal tools. A priority page is scanned even if it falls under one of these rules.

Running a report

  • Open AI SEO/GEO Analyst from the property sidebar, check the brand profile, and click Run report.
  • Reports take roughly 5–15 minutes. The crawl and the answer-engine checks run at the same time, and the page updates itself while the report is in progress. You can cancel a run at any point.
  • One report can run per property every 72 hours. Only reports that finish count toward that window — a failed or cancelled run lets you try again straight away.
  • If a report hits its time limit it is saved as partial: you still get everything that finished, with a note on what was cut short.
  • Past reports stay in your history for six months, and a trend chart shows score, visibility and share of voice across them.

Reading the report

  • Score and categories. The overall score is a weighted blend of the five categories. Whole-site and page-type findings weigh more than single-page ones, and problems on your priority pages (or, with analytics on, your highest-traffic pages) weigh more still.
  • What to fix first. A short, ordered list of recommendations with impact, effort and the findings they resolve. Quick wins are called out separately.
  • Visibility tiles. Visibility is the share of answers that mention you at all; share of voice is your mentions against every brand named; recommended means the assistant actively suggested you. The by-engine table shows where you are strong or invisible.
  • Prompt results. One row per prompt, one column per engine. Expand a row to read what each engine said, which competitors it named and which pages it linked. "Searched" means the engine ran a live web search before answering, so that answer reflects what it can find on your site today.
  • Competitors and sources. Who else gets named, which of your pages engines link to, which competitor and third-party sites they lean on instead, and any claims about your brand that look inaccurate.
  • All findings. Filter by severity, category and scope, expand for the fix and evidence, or download the whole list as a CSV to hand to your developers.
  • Page-level findings. For your most important pages we also judge how quotable they are: does the opening state plainly what the page is and who it is for, how specific is the copy, and does the structured data match what is on the page.
  • Download PDF. Opens a printer-friendly copy of the whole report — every prompt answer and every finding expanded — in a new tab. Choose Save as PDF in your browser's print dialog to share it with clients or colleagues.

Limitations

  • We sample pages rather than crawling everything, so a very large site is judged by its page types. Add the pages that matter to your priority pages if you want them checked every time.
  • Pages behind logins, paywalls, captchas or geo-blocks are not reachable. Content that only appears after interaction (tabs, accordions, infinite scroll) may be partly missed.
  • Answer-engine results are a snapshot. Engines personalise and change their answers, so compare reports over time rather than reacting to a single number.
  • Performance timings come from a single automated visit and are a rough guide, not a substitute for field data.
  • Recommendations are guidance. Validate structural or content changes with your own measurements before rolling them out everywhere.

Integrations

Overview

Integrations bring Badgerlytics into the tools your team already uses, so insights reach people without anyone needing to remember to check a dashboard. Slack is the first integration, and more are on the way.

Slack

The Slack integration posts a digest of new AI Insights to a channel of your choice after each scheduled run. Each message lists the insights that matched your settings — with priority and the headline change — and links back to the full details on your property's AI Insights page.

You connect Slack once for the organization, then choose which properties post and which channel each one uses. That way every property can notify its own channel without repeating the install.

  • In Badgerlytics, open Settings → Integrations and choose Connect Slack. Approve the Badgerlytics app in your Slack workspace when prompted.
  • For each property that should post, pick a Slack channel. Invite the Badgerlytics app to private channels before selecting them.
  • Turn on Push AI Insights to Slack for that property, and choose which reports and priority levels to share. By default every report shares high priority insights only, which keeps the channel focused on what matters most.
  • Save, then use Send test to confirm the message lands in the right channel.

You can also adjust the AI Insights push toggle and report selections from a property's AI Insights → Settings tab once a channel is configured. Delivery status — including the last successful post or any delivery problem — appears in both places.

If Slack is disconnected or the app is removed from your workspace, reconnect from Settings → Integrations to resume posting. Pausing a property or turning off AI Insights push keeps your channel choice so you can turn posting back on later.

Keeping the channel useful

Insights refresh on your plan's schedule, so a topic that's still relevant can appear again on later runs. Dismissing an insight in Badgerlytics keeps similar observations out of both the app and Slack while the dismissal is active. Reports you've turned off in your AI Insights settings never post to Slack.

MCP for AI agents

Overview

The Model Context Protocol (MCP) lets AI assistants such as Cursor, Claude Code, Codex and Claude work with Badgerlytics directly. Ask "How did checkout conversion change last week?" or "Create a feature flag for the new pricing page" and the assistant uses your real data and settings to answer.

Badgerlytics offers two MCP servers, so you decide how much access each tool gets:

  • Reports is read-only. It lists your properties, custom events, funnels, feature flags, SaaS plans, usage items and audience traits, and runs summary reports.
  • Management makes changes. It can create and archive custom events, funnels, SaaS plans and usage billing items, create and delete audience traits, and run the full feature flag lifecycle. It can also read the same lists as Reports.

Plan availability

MCP access is included on Pro plans and above (Pro, Business, and Ultimate). Starter accounts can upgrade to unlock it.

Your API key

Both servers use an organization API key. An admin or owner can create one in Settings → API Keys. Create one key per person or tool (for example "Cursor – Jane") so you can deactivate a single key without disrupting anyone else.

When you create a key you choose its scopes, which decide what it can reach:

  • Ingest — send server-side events to the ingestion API, such as subscription changes and usage charges. This is what your application servers need.
  • Reports (MCP) — connect an assistant to the Reports server. Read-only.
  • Management (MCP) — connect an assistant to the Management server, which changes your settings.

Give each key only the scopes it needs. A key living in your application servers should have Ingest alone, so that if it ever leaks it can't be used to reconfigure your experiments. Scopes are fixed once a key exists — to change them, create a new key and deactivate the old one. If a server tells your assistant the key isn't allowed, the key is missing that server's scope.

Store the key in the BADGERLYTICS_API_KEY environment variable on your computer, then restart your AI tool. Most configs on this page read the key from that variable, so the files themselves don't contain it and are safe to share with your team.

# ~/.zshrc or ~/.bashrc
export BADGERLYTICS_API_KEY="sk_your_key_here"

Never paste your key into a chat

Your AI tool sends the key for you. An assistant never needs to see it, so don't type it into a prompt. Badgerlytics rejects any tool call that includes a key as an argument. If a key leaks, deactivate it in Settings → API Keys and create a new one.

Server addresses:

Badgerlytics Reports: https://api.badgerlytics.com/mcp/reports
Badgerlytics Management: https://api.badgerlytics.com/mcp/management

The Connect AI agents (MCP) panel on the API Keys page has the same instructions with download buttons for each config file.

Setup: Cursor

Save this as ~/.cursor/mcp.json to use it in every project, or as .cursor/mcp.json inside one project. If the file already exists, add the two entries to its mcpServers list. Then turn both servers on in Cursor Settings → MCP.

mcp.jsonjson
{
"mcpServers": {
"badgerlytics-reports": {
"url": "https://api.badgerlytics.com/mcp/reports",
"headers": {
"Authorization": "Bearer ${env:BADGERLYTICS_API_KEY}"
}
},
"badgerlytics-management": {
"url": "https://api.badgerlytics.com/mcp/management",
"headers": {
"Authorization": "Bearer ${env:BADGERLYTICS_API_KEY}"
}
}
}
}

Setup: Claude Code

Run these commands once, in a terminal where BADGERLYTICS_API_KEY is set, to add both servers to all your projects. Claude Code saves the key in your personal settings.

claude mcp add --scope user --transport http badgerlytics-reports https://api.badgerlytics.com/mcp/reports --header "Authorization: Bearer $BADGERLYTICS_API_KEY"
claude mcp add --scope user --transport http badgerlytics-management https://api.badgerlytics.com/mcp/management --header "Authorization: Bearer $BADGERLYTICS_API_KEY"

To share the servers with your team, save this as .mcp.json in the project root instead. Each teammate uses their own key from their own environment.

.mcp.jsonjson
{
"mcpServers": {
"badgerlytics-reports": {
"type": "http",
"url": "https://api.badgerlytics.com/mcp/reports",
"headers": {
"Authorization": "Bearer ${BADGERLYTICS_API_KEY}"
}
},
"badgerlytics-management": {
"type": "http",
"url": "https://api.badgerlytics.com/mcp/management",
"headers": {
"Authorization": "Bearer ${BADGERLYTICS_API_KEY}"
}
}
}
}

Setup: other tools

Any assistant that supports remote MCP servers with a custom header can connect. Add both server addresses and send the header Authorization: Bearer your_api_key. Here are ready-made configs for two popular coding agents.

Codex: add to ~/.codex/config.toml.

config.tomltoml
[mcp_servers.badgerlytics-reports]
url = "https://api.badgerlytics.com/mcp/reports"
bearer_token_env_var = "BADGERLYTICS_API_KEY"
[mcp_servers.badgerlytics-management]
url = "https://api.badgerlytics.com/mcp/management"
bearer_token_env_var = "BADGERLYTICS_API_KEY"

opencode: add to opencode.json.

opencode.jsonjson
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"badgerlytics-reports": {
"type": "remote",
"url": "https://api.badgerlytics.com/mcp/reports",
"headers": {
"Authorization": "Bearer {env:BADGERLYTICS_API_KEY}"
}
},
"badgerlytics-management": {
"type": "remote",
"url": "https://api.badgerlytics.com/mcp/management",
"headers": {
"Authorization": "Bearer {env:BADGERLYTICS_API_KEY}"
}
}
}
}

Desktop assistants (such as Claude Desktop and Cowork) that only add local servers can connect through a small bridge that runs on your computer. Add this to the app's MCP config file and replace the placeholder with your key. Desktop apps don't read your shell profile, so the key goes in this file; keep it private. The bridge requires Node.js.

{
"mcpServers": {
"badgerlytics-reports": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://api.badgerlytics.com/mcp/reports",
"--header",
"Authorization:${AUTH_HEADER}"
],
"env": {
"AUTH_HEADER": "Bearer sk_your_key_here"
}
},
"badgerlytics-management": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://api.badgerlytics.com/mcp/management",
"--header",
"Authorization:${AUTH_HEADER}"
],
"env": {
"AUTH_HEADER": "Bearer sk_your_key_here"
}
}
}
}

Organizations, properties & IDs

Each API key belongs to one organization. When an assistant connects, Badgerlytics tells it which organization the key belongs to and lists its properties. Assistants can call whoami and list_properties at any time to look them up again.

Every property tool needs both the organizationId and propertyId. These are the same short IDs you see in your dashboard address bar, for example /w/Xq7aB2kLmP/p/9vRtY3nWcE. Events, funnels, flags, plans and traits are referred to by their tokens and codes, just like in the tracking script.

Reports server

The Reports server offers the same reports as your dashboard, in summary form: traffic, pages, locations, devices, browsers, referrers and UTMs, exit pages, custom events, funnels, revenue, products and categories, cart abandonment, impact quantification, A/B test results and focus pages. SaaS properties also get MRR, usage revenue, cohort retention, subscription lifecycle and cancellation reasons.

To keep answers fast and focused, reports have a few limits:

  • Summaries only. Reports return totals for the date range. The one exception is website performance, which also has an hourly view of the last 24 hours.
  • Up to 90 days. If no dates are given, the last 30 days are used. Dates use the property's reporting time zone.
  • 25 rows by default, 50 at most. Large reports can be sorted for a top-N list or paged through.
  • One event or funnel at a time. The custom events report needs an event name and the conversion funnel report needs a funnel name.
  • Impact quantification looks back 7 or 30 days.
  • A/B test summary covers the tests you name, or all running tests when you don't name any.
  • Flag lists start with what's live. Asking about feature flags returns the running ones; drafts, stopped and archived flags come back when you ask for them, so a long history doesn't crowd out the answer.

Management server

The Management server follows the same rules as the dashboard, so an assistant can't do anything you couldn't do yourself. Names, tokens and codes are generated the same way, plan limits apply, and every error explains what to fix. It can:

Feature flags move through the same lifecycle as in the dashboard:

  • Draft: everything can be edited. A/B tests need a description before they can start. Drafts can be deleted but not archived.
  • Running: only traffic weights (fixed-weight tests), the rollout percentage (on/off flags), focus pages, audience and description can change. The name, type, goal, optimization mode and variations are locked.
  • Stopped: a stopped test can name a winning variation, be restarted as a new iteration, or be archived.
  • Archived: only stopped flags can be archived. Unarchive a flag before restarting it.
  • Kill switch: available once a flag is running or stopped. Turning it on serves every visitor the control experience straight away.

Ask before big changes

Starting, stopping and archiving tests, the kill switch, and deleting drafts or traits affect your live site or your data. The management skill below tells the assistant to confirm these actions with you first.

Rate limits

Limits apply per organization, not per key, so creating extra keys doesn't raise them. Reports and Management have separate allowances. Every minute, on each server, your organization can make:

  • 20 report or change calls — anything that queries your analytics or edits your settings.
  • 50 lookups — the quick lists an assistant uses to orient itself, such as looking up your properties, events, funnels or flags.

Calls count as soon as they are made, including ones that fail validation. If the limit is reached, the assistant is told how many seconds to wait. Each call also has 15 seconds to finish; an assistant that hits that should narrow the date range or ask for fewer rows.

Skills

Skills are short instruction files that teach an assistant how to use Badgerlytics well: which tool to call, how to stay within the limits and when to ask before acting. They're optional but recommended.

  • Reports skill: finding properties, choosing the right report and keeping queries within the limits.
  • Management skill: creating events, funnels, traits, SaaS plans and feature flags, and the lifecycle rules.
  • Setup skill (optional): lets an assistant connect both servers for you without ever handling your API key.

Save each file under a folder with the skill's name:

  • Cursor: ~/.cursor/skills/badgerlytics-reports/SKILL.md (or .cursor/skills/ in a project).
  • Claude Code: ~/.claude/skills/badgerlytics-reports/SKILL.md (or .claude/skills/ in a project).
  • Codex and opencode: add the file's contents to your AGENTS.md, or use the tool's skills folder if it has one.
  • Claude and other chat assistants: upload the file as a skill, or paste it into your project instructions.

Troubleshooting

  1. The server won't connect, or reports an invalid key. Check that BADGERLYTICS_API_KEY is set in the environment your tool starts from, then restart the tool. Keys that were deactivated stop working straight away.
  2. One server works but the other says the key isn't allowed. The key was created without that server's scope. Create a new key in Settings → API Keys with both MCP scopes ticked, then deactivate the old one.
  3. Every tool says MCP isn't on your plan. MCP needs Pro or above. Upgrading takes effect immediately.
  4. "Rate limit reached". Wait the number of seconds shown, or spread the work over a few minutes.
  5. The organization or property wasn't found. Ask the assistant to run list_properties. The key only works for its own organization.

Billing & Usage

Pricing and usage billing

Plans bundle a monthly base price plus two metered allowances — sessions (visitor sessions ingested across all your properties) and AI tokens (consumed by AI Insights, Chat Analyst, AI Team Summary, the UX Analyst, and the SEO/GEO Analyst). When you exceed an included allowance, the overage is billed at the per-unit rate published on your plan's pricing card.

  • Sessions are counted from ingested events. Browser traffic is filtered by bot checks in the tracker before send; server API traffic is not. A session is a continuous run of activity from a single visitor; idle for ~30 minutes ends it.
  • AI tokens map roughly to the LLM tokens the AI features consume on your behalf. AI Insights, Chat Analyst, AI Team Summary, UX Analyst, and SEO/GEO Analyst all draw from the same pool.
  • If you consistently exceed your allowance, moving up a tier is usually cheaper than paying overage rates — the included allowance jumps quickly between tiers and overage rates drop.

Plan limits

Each paid plan caps how many users can belong to your organization, how many properties you can create, how many active custom events each property can have, and how many running flags each property can have live at once. Draft, stopped, and archived flags do not count toward the running flag cap — only flags actively serving traffic do.

Feature limits by plan
PlanUsersPropertiesActive custom events / propertyRunning flags / propertyEnabled surveys / property
Starter522555
Pro154501510
Business3081003015
UltimateUnlimitedUnlimited2003530

See the pricing page for session and AI token allowances, feature gating, and overage rates.

Running flags and browser cookies

Running flag limits apply per property and reflect practical browser cookie size limits. Leave headroom for audience segmentation and multi-variation tests — finalize and archive flags you no longer need to keep the sticky assignment cookie lean.

If you try to downgrade and your current usage exceeds the target plan's caps — too many users, properties, active custom events on any property, or running flags on any property — the switch is blocked until you reduce usage. Remove extra users, archive custom events you no longer need, stop or archive flags, or remove extra properties first.

Token usage

AI Insights, Chat Analyst, AI Team Summary, the UX Analyst, the SEO/GEO Analyst, and the AI Session Recorder all draw AI tokens from the same monthly allowance. How quickly each one burns through that pool depends on how it's triggered and how much context it works with. The table below is a rough guide to what to expect.

Estimated token usage per feature
FeatureUsage levelActivationNotes
AI InsightsHighAutomatic — per plan (3×, 5×, or 7× per week)Shouldn't exhaust your usage allotment on its own.
AI ChatHighOn demandDepends on the number of chats and the context length of each chat. Clear long-running conversations frequently. Chat calls tools so it results in more tokens than the other features.
AI UX AnalystHighOn demandIndividual missions are high usage, but most users only run a couple of these sporadically.
AI SEO/GEO AnalystHighOn demandEach report reads a sample of your pages and asks several answer engines your tracked prompts, so a single report is heavy — but reports are limited to one per property every 72 hours. Business plan and above.
AI Team SummaryLowOn demandOne compact summary per open from dashboard aggregates. Cached in your browser for six hours per view; use Regenerate for a fresh summary. Pro plan and above.
AI Session ReplayMediumOn demandDepends on how many sessions you choose to analyze.

Trial period

All paid plans start with a 14-day free trial. You get the full features of whichever plan you signed up to try — Chat Analyst, AI Insights, plus AI Team Summary, UX Analyst, and Click Maps on Pro and above, and Session Replay and the SEO/GEO Analyst on Business and Ultimate — for the full trial period.

At the end of the trial, you will be charged the full base price for the plan you signed up to try.

Refund policy

We don't offer refunds. On monthly billing you can cancel any time and keep access through the end of the current billing period — no contract. On annual billing you pay for the full year upfront; you can turn off renewal so you won't be charged again when the year ends, but there's no partial refund for unused months.

Annual billing

Annual plans bill once per year (after a paid monthly plan). You can upgrade to a higher annual tier mid-year; we prorate the difference for the rest of the period. Annual plans cannot downgrade or switch to monthly mid-year. Turn off renewal from Manage plan if you don't want another year — access continues through the period you've already paid for. We send a reminder email about two weeks before renewal.

Account usage

The Usage tab in your organization settings shows:

  • Sessions ingested this billing period vs. included allowance
  • AI tokens used vs. included allowance
  • Properties in use vs. plan limit
  • Active custom events (per property) vs. plan limit
  • Running flags (per property, max in org) vs. plan limit
  • Projected overage cost at the current rate-of-burn

Cancellation

Cancel or turn off renewal from the Manage plan page. On monthly billing, your service continues until the current billing period ends. On annual billing, turning off renewal means you won't be charged for another year — you still have access through the annual period you've paid for. After the plan ends, the account pauses and we delete your data 30 days later (export reports as CSV before that).

Note

Need more than your included sessions or tokens? Ultimate includes our lowest overage rates — billed automatically when you exceed your allowance, with no sales call required. See the pricing page.

Changelog

Coming soon.