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.

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, and choose a display currency (USD by default).
  • 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.

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.4 · Last updated August 24, 2026 at 3:21 AM UTC · badgerlytics-devtools-0.4.4-08-24-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.4 · Last updated August 24, 2026 at 3:21 AM UTC

  1. Download badgerlytics-devtools-0.4.4-08-24-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_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

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

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 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({
// 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 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 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, 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 subscriptions

New-subscription MRR comes from the trackConversion call at checkout. If your webhook also forwards a subscription-created event, every signup is counted twice. Only forward changes that happen after signup — cancellations, upgrades, downgrades, reactivations, and trial conversions.

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 — 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.
// - New subscriptions are NOT forwarded here: new MRR is already
// reported by your checkout's trackConversion call, so forwarding
// customer.subscription.created would double-count it.
export default {
async fetch(req, env) {
const event = await req.json();
const sub = event.data?.object || {};
const prevItems = event.data?.previous_attributes?.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);
};
let change_type;
let mrr_delta_cents;
if (event.type === 'customer.subscription.deleted') {
change_type = 'cancel';
mrr_delta_cents = -mrrCents(sub.items);
} 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 {
// Everything else (incl. customer.subscription.created — see note above).
return new Response(null, { 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_' + (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). 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.

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

Audience traits can be set from your code via setTraits() (see Audiences) 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.

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

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

  • 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

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 on your property dashboard into a short, copy-ready update for your team. Open it from the Overview or Active Tests tab — it summarizes the metrics you are already looking at for the selected date range (or the current test iteration), 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.

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.

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.

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.

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, 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, AI Team Summary, 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.

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 / property
Starter52255
Pro1545015
Business30810030
UltimateUnlimitedUnlimited20035

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, 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 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, AI Team Summary (on Pro and above), UX Analyst (where the plan includes it) — 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.