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, 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: daily 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.

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. For SSR experiment assignment, see SSR middleware (SDK) — full API and examples for each framework.

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.

React (client-side)

For SPAs and other client-rendered React apps. Install the SDK and wrap your app in BadgerlyticsProvider with a config object — the provider sets window.badgerlyticsConfig and loads the tracking script for you. Hooks like useVariation() work once the tracker is ready. For SSR (Next.js, Remix, Nuxt, Astro), use middleware for server-side flags and keep the provider (or HTML snippet) for page views — see SSR middleware (SDK).

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 -->

Next.js (server-side)

Quick start — full middleware API (traits, manual assignment, all read helpers) is in SSR middleware (SDK) → Next.js.

npm install badgerlytics-sdk
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).*)'],
};
app/page.jsxjsx
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 />;
}

Note

You still need the tracking script in your layout for page views and commerce events. Middleware sets cookies the script reads on load.

Remix / React Router

Quick start — see SSR middleware (SDK) → Remix / React Router for the complete API.

npm install badgerlytics-sdk
app/middleware.tstypescript
// app/middleware.ts
import { createBadgerlyticsMiddleware } from 'badgerlytics-sdk/remix';
export const middleware = [
createBadgerlyticsMiddleware({ propertyId: 'your-property-id' }),
];
app/routes/_index.jsxjavascript
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'),
};
}

Astro

Quick start — use getVariationFromLocals, not getVariation(request). Full API in SSR middleware (SDK) → Astro.

npm install badgerlytics-sdk
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' })
);
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 />}

Nuxt

Quick start — use getVariationFromEvent in server routes. Full API in SSR middleware (SDK) → Nuxt.

npm install badgerlytics-sdk
server/middleware/badgerlytics.tstypescript
// server/middleware/badgerlytics.ts
import { createBadgerlyticsMiddleware } from 'badgerlytics-sdk/nuxt';
export default createBadgerlyticsMiddleware({
propertyId: 'your-property-id',
});
server/api/variant.jsjavascript
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'),
};
});

SSR vs. client rendering considerations

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

  • SSR (Next.js, Remix, Astro, Nuxt): the variation is decided on the server before any HTML reaches the browser. The user never sees the "wrong" version flash, then swap. Best for tests that change above-the-fold content like hero copy, layout, or pricing. See SSR middleware (SDK) for setup.
  • Client-side (React without SSR, static sites with the script tag): the script loads, asks for the user's assignments, and hooks like useVariation() update once ready. Simpler to set up; fine for in-page changes below the fold or for behavioral tests.

Server and client bucketing agree for the same (visitor, flag, iteration) tuple, so you can mix and match without skewing results.

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

If your code runs before the script finishes loading, use the SDK's callWhenTrackerReady(fn) helper (from badgerlytics-sdk/react) to queue your call until the tracker is ready. The hooks in our React/Next.js packages do this for you automatically.

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).

Note

Monetary fields are integers in the smallest currency unit (cents for USD). Auto means the script sets the field; you do not pass it.

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_sourceReferring site hostname, or direct when none.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 landing URL.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

baitestforce (query parameter)

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

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

Commerce & funnels

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 for v1/on, false for control/off/v0/v2 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

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 and start time JSON.JSONAuto30 min idle
_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).

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+ (includes h3)
  • badgerlytics-sdk/astro — Astro 4+

Middleware shares one assignment engine across frameworks. You still need the browser tracking script for analytics events — middleware only handles assignment cookies and SSR reads:

<!-- 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>

Client-only apps

SPAs without SSR should use badgerlytics-sdk/react (useVariation, useSetTraits) instead of middleware. See Script Installation → React.

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) { flagKey: { i, v } } for the request.
  • getVisitorId(source) — stable visitor id for this request.
  • getTraits(source) — merged trait bag used for segment rules (cookie + built-ins like UTM).
  • isFlagEnabled(source, flagKey) true for on/off flags in the v1 arm.
  • 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: 'strict', // optional — 'strict' | 'lax' | 'none'
cookieMaxAgeSec: 31536000, // optional — default 365 days
});

source for read helpers depends on the framework — see each section below. Next.js and Remix forward x-bai-* headers on the request; Nuxt uses event.context; Astro uses Astro.locals.

Next.js

Add root middleware.js, then read assignments from req (Pages Router) or headers() (App Router).

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).*)'],
};

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

Register middleware in app/middleware.ts. Loaders and actions receive a request with forwarded x-bai-* headers — pass that to getVariation(request, …).

app/middleware.tstypescript
// app/middleware.ts
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 (h3), 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.

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.

// 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 7-day window after the last cart update. The report only includes streaks whose window has fully closed (see complete_through in the app).
  • Conversions that happen later — even after those 7 days — 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 three streams: in-page events from the tracking script (your marketing funnel), a conversion event at signup or upgrade, and subscription_changeevents from your backend for everything that happens after (cancellations, upgrades, downgrades, reactivations). Here's how to wire each one up.

Define your plans first

SaaS reporting keys off your property's plan catalogue. Define plans under Property settings → Subscription plans before instrumenting — the plan_code values you use below must match the catalogue, and the SaaS Setup wizard in the app generates this code pre-filled with your actual plans.

Funnels

A typical SaaS funnel: landing → pricing → signup → activation → conversion. Set this up under Custom events / Funnel setup → 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,
},
],
});

Conversion

Fire a conversion event when a subscription actually starts. For SaaS, a conversion can mean any of these — pick the definition that matches your business:

  • Trial start: credit card collected; use order_total: 0and the plan's eventual MRR price on the product line.
  • Paid start: first paid invoice. Fire conversion with the actual amount charged.
  • Both: some teams fire one conversion at trial start and another at trial → paid. Use distinct order_id values to keep them separate.

The recurring billing_interval + interval_count on the product line are what mark this conversion as new MRR (rather than a one-time purchase).

// Fire from your checkout success handler when a subscription starts.
// Including billing_interval + interval_count is what marks this conversion
// as recurring (new MRR) instead of a one-time purchase.
badgerlytics.trackConversion({
order_id: 'order_' + Date.now(),
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',
},
],
});

Lifecycle events webhook

Conversion fires once, at signup. Everything after that — cancellations, upgrades, downgrades, renewals, trial conversions — 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.

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 Organization settings → API keys. 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, and trial_converted. Include a positive or negative mrr_delta_cents matching the direction of the change ( 0 for plain renewals).

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>"
}
}'

Or skip writing the adapter yourself and drop in a Stripe webhook forwarder — the app's SaaS Setup wizard generates this exact snippet pre-configured for your property:

stripe-webhook.jsjavascript
// Cloudflare Worker / Vercel edge function.
// Receives Stripe customer.subscription.* webhooks and forwards them as
// Badgerlytics subscription_change events. Set BADGERLYTICS_SECRET_KEY
// and PROPERTY_ID as environment variables.
export default {
async fetch(req, env) {
const event = await req.json();
const ev = event.data?.object || {};
let change_type;
let mrr_delta_cents;
if (event.type === 'customer.subscription.deleted') {
change_type = 'cancel';
mrr_delta_cents = -(ev.items?.data?.[0]?.price?.unit_amount || 0);
} else if (event.type === 'customer.subscription.updated') {
const prev = event.data.previous_attributes?.items?.data?.[0]?.price?.unit_amount;
const next = ev.items?.data?.[0]?.price?.unit_amount || 0;
if (prev && next && next !== prev) {
change_type = next > prev ? 'upgrade' : 'downgrade';
mrr_delta_cents = next - prev;
} else {
change_type = 'renewal';
mrr_delta_cents = 0;
}
} else if (event.type === 'customer.subscription.created') {
change_type = 'trial_converted';
mrr_delta_cents = ev.items?.data?.[0]?.price?.unit_amount || 0;
} else {
return new Response('ignored', { status: 204 });
}
const body = {
property_id: env.PROPERTY_ID,
event_type: 'subscription_change',
unique_event_id: crypto.randomUUID(),
session_id: 'srv_' + crypto.randomUUID(),
visitor_id: 'srv_' + (ev.customer || 'unknown'),
event_time: new Date().toISOString(),
customer_id: String(ev.customer || ''),
subscription_data: {
subscription_id: String(ev.id || ''),
customer_id: String(ev.customer || ''),
change_type,
mrr_delta_cents,
previous_plan_code:
event.data.previous_attributes?.items?.data?.[0]?.price?.lookup_key || '',
new_plan_code: ev.items?.data?.[0]?.price?.lookup_key || '',
currency: (ev.currency || 'usd').toUpperCase(),
cancel_reason: ev.cancellation_details?.reason || '',
},
};
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),
});
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.

Common Considerations

Bot handling

By default we try to keep crawlers and headless automation out of your analytics. Checks run in the browser before the tracking script sends any events:

  • User-agent patterns — known bots, crawlers, link preview fetchers, and common automation clients (for example Googlebot, Lighthouse, curl).
  • Lightweight browser signals — for example navigator.webdriver, headless-Chrome tells, empty plugin lists, 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. Server-side flag assignment can use the same user-agent list (see skipBots in the 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 these 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 within seconds so the tracking script and our edge see the same rules on the next page load.

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 (see below); 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).

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 daily cadence and use the previous day's finalized data.

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 / CDN caching

Each property publishes a small JSON config (active feature flags, audience definitions, plan catalogue for SaaS, etc.) to a CDN that our tracking script reads on every page. We make a real effort to send Cache-Control: no-store on this object and keep it fresh, but please:

  • Exclude the property CDN from your own caching layer. If you proxy our script or its config through Cloudflare, CloudFront, or Fastly, set a bypass rule for cdn.badgerlytics.com and app.badgerlytics.com.
  • Updates to feature flags, audiences, custom events, traffic filters, and SaaS plans propagate to the CDN within seconds of saving in the dashboard. Changes are essentially immediate to your visitors.

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 website or app. 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, and pick whether it's an ecommerce site, a SaaS product, or both.
  • Copy the install snippet — or grab the SDK package — and drop it on your site.
  • Visit your site once with debug mode on to confirm events are landing.

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.

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.

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 (7-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, and subscription-lifecycle reports. Powered by your conversion events plus the subscription lifecycle webhook (see SaaS reporting setup below).

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 pivot results by funnel step, audience trait, or device. 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). 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: 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.

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
  • Anyone who has previously triggered a custom event

Audience traits can be set from your code via setTraits() (see Audiences below) or inferred from behavior and request context.

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.

Heads up

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

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.

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 is a named group of visitors who share traits or behavior. Use them to:

  • Filter reports ("revenue from logged-in users only").
  • Target feature flags ("run the pricing test on free-tier users").
  • Power personalization ("show this promo to returning visitors from the UK").

Setup

Audiences combine traits and behavior to define who belongs in a group. On feature flags, use the Audience segment editor to target experiments (for example "logged-in pro users only").

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). Built-in traits such as visitor_type, device_type, and UTM fields are always available and do not need to be registered.

Segment rules can combine:

  • Trait conditions: signed_in = true, plan = pro, etc.
  • Behavior conditions: has triggered a specific event in the last N days; reached a particular funnel step; viewed a specific page.
  • Context conditions: country, device, referrer source.

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.

No PII in traits

Don't pass names, emails, phone numbers, or any other personal identifiers as traits. Use IDs that are opaque to you (e.g. plan, tenure_bucket) and you stay safely anonymous.

Considerations

  • Audiences are evaluated live whenever an event fires. New audience definitions retroactively apply to existing data for reporting, but only affect new flag assignments going forward.
  • Keep audience names descriptive — "logged-in pro users" reads better than "cohort_3" six months from now.
  • For SaaS apps, the most useful audiences are usually based on subscription state (trialing, active, past_due) — these change as lifecycle events come in via webhook.

Analytics Data Retention

Policy

We retain your analytics data for the period included with your plan (30 days on Starter, 13 months on Growth, 25 months on Scale). Rolled-up summaries used in long-range trend charts are kept longer, but individual event-level data ages out on the retention schedule.

  • Account and billing data (invoices, plan changes) is retained for seven years for tax and compliance reasons.
  • Audit logs (who changed what flag, who deleted what funnel) are retained for the lifetime of your account.

Deleting property data

You can request a full deletion of a property's analytics data at any time from Property settings → Data retention → Delete property data. We confirm with a typed property name, then run the deletion asynchronously. Live reports update as the deletion progresses; a usually-completes-within-24-hours indicator stays on the page until it's done.

Heads up

Deletion is irreversible. There's no "undo" for an analytics wipe — make sure you really mean it. Feature flag definitions and funnel configuration are preserved separately and aren't deleted unless you also delete the property itself.

Account cancellation

When you cancel your account, your data is retained for 30 days in case you want to come back, then permanently deleted. You can download a CSV export of your reports up to the moment of cancellation from the dashboard.

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.

Plan availability

AI Session Replay is included on Business plans and above (Business and Enterprise). 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.

Only enable on production

Keep session recording turned off in your development and staging environments. Every recorded session counts against your monthly session allotment, so capturing internal QA traffic burns through your quota without giving you real visitor insight. Enable it only on the live property your customers actually use.

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.
  • 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 below).

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. Here's how capture and retention work:

  • 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 — up to 2,000 sessions per property per day (UTC). After the daily cap is reached, new sessions are not recorded until the next day.
  • Sampling — the first 150 engaged sessions each day are always recorded. After that, an additional 20% of eligible sessions are sampled. High-traffic properties still get a steady stream of replays without storing every visit.
  • 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.
  • One-hour maximum — an individual recording stops after one hour of active capture. Very long visits are truncated at that point.
  • Multi-tab sessions — when a visitor has several tabs open at once, each tab is captured as its own recording. They appear together as one session in the list and in the replay player, where you can switch between tabs. A multi-tab visit counts as one session toward your daily recording 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.

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 Enterprise). 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.

Usage instructions

  • Open UX Analyst from the property sidebar and click New mission.
  • Pick the focus: full site, a specific funnel, a single page, or a custom URL list.
  • Optionally add a goal in your own words ("help us reduce cart abandonment", "evaluate our pricing page for clarity").
  • Hit run. Missions typically take 5–20 minutes depending on how many pages you've included.
  • 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 with a click.

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.
  • 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.
  • Recommendations are guidance, not gospel. Always validate with an A/B test before declaring victory.

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, and the UX 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, and UX 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.

Token usage

AI Insights, Chat Analyst, the UX 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 — daily / every other day, per planShouldn't exhaust your usage allotment on its own.
AI ChatMediumOn demandDepends on the number of chats and the context length of each chat. Clear long-running conversations frequently.
AI UX AnalystLow–MediumOn demandIndividual missions are medium usage, but most users only run a couple of these sporadically.
AI Session RecorderMediumOn demandDepends on how many sessions you choose to analyze.

Trial period

All paid plans start with a 7-day free trial. You get the full features of whichever plan you signed up to try — Chat Analyst, AI Insights, UX Analyst (where the plan includes it) — for the full week.

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, but you can cancel any time and you'll keep access through the end of your current billing period. There's no contract — pay month to month, stop whenever you want.

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
  • Running A/B tests vs. plan limit
  • Projected overage cost at the current rate-of-burn

Cancellation

Cancel from the Manage Plans page. Confirm the typed organization name, and you're done. Your service continues until the current billing period ends. After that the account pauses, and we delete your data 30 days later (you can export reports as CSV before that).

Note

Looking for a higher-volume Enterprise plan with custom session and token allowances, volume overage discounts, and priority AI queues? Get in touch from the pricing page.

Changelog

Coming soon.