Tracking
Burst Statistics tracks visitors and sessions by collecting page hits from the browser via JavaScript and processing them server-side. This document covers the full tracking lifecycle, session handling, IP processing, and all available extension points.
Overview
When a visitor loads a page, Burst enqueues a JavaScript tracking script (burst.min.js or burst-cookieless.min.js). The script sends a JSON payload to the server on each page view or time-on-page update. The server validates, sanitizes, and stores the hit in the burst_statistics and burst_sessions database tables.
Each hit is classified as either a create (new row in burst_statistics) or an update (accumulate time_on_page on an existing row). Session records in burst_sessions are created on the first hit and updated on subsequent hits within the same session window (30 minutes).
Tracking endpoints
Burst supports two server-side collection mechanisms depending on the site's server configuration.
REST API endpoint (default)
POST /wp-json/burst/v1/track
Content-Type: application/json
The request body must be a JSON-encoded string (double-encoded). The endpoint is registered via rest_api_init and has permission_callback set to __return_true (no authentication required). A { "request": "test" } body returns { "success": "test" } without recording a hit.
Beacon endpoint (fallback)
When the REST API is unavailable, Burst falls back to a beacon endpoint that reads raw php://input. A body of request=test returns HTTP 200 without recording a hit.
The active endpoint is determined by Endpoint::get_tracking_status(). When the beacon endpoint is active, the tracking script does not depend on wp-api-fetch.
Tracking transport status
Endpoint::get_tracking_status_and_time() returns the currently active transport and the timestamp of the last successful test hit. This is what powers the tracking-status indicator in the admin UI and the burst/tracking-status ability described in Hooks and filters.
Return value:
| Key | Type | Description |
|---|---|---|
status | string | Active transport identifier (for example rest, beacon, or error) |
last_test | int | Unix timestamp of the last successful test hit, or 0 if no test has run |
$tracking = \Burst\Frontend\Endpoint::get_tracking_status_and_time();
// [ 'status' => 'rest', 'last_test' => 1714377600 ]
Hit payload
The JavaScript client sends the following fields in the JSON payload:
| Field | Type | Description |
|---|---|---|
url | string | Full URL of the current page, including query string |
referrer_url | string | HTTP referrer URL |
user_agent | string | Browser user-agent string |
uid | string | Cookie-based unique visitor identifier |
fingerprint | string | Canvas/audio fingerprint (cookieless mode) |
time_on_page | int | Seconds spent on the current page |
completed_goals | array | Array of goal IDs completed client-side |
page_id | int | WordPress post/term ID |
page_type | string | Page type identifier (see Page Type Identifiers) |
should_load_ecommerce | bool | Whether to load ecommerce tracking |
Tracking lifecycle
:::caution Breaking change
As of v3.3.0 the IP-block check is performed at the very top of track_hit(), before prepare_tracking_data() runs. Previously the check was performed separately in each endpoint handler. Any custom code that relied on the check occurring after sanitization must be updated.
:::
Show code
Browser sends POST payload
│
▼
IP block check ──blocked──► return 'ip blocked'
│
▼
prepare_tracking_data() — sanitize & parse user-agent
│
▼
Custom block rules check ──blocked──► return 'blocked by custom rule'
│
▼
Referrer spam check ──spam──► return 'referrer is spam'
│
▼
get_hit_type() — determine 'create' or 'update', fetch last row
│
▼
burst_before_track_hit filter
│
▼
Session: create or update burst_sessions
│
├── hit_type = 'update' & same URL ──► update_statistic() (accumulate time_on_page)
│
└── hit_type = 'create' ──► burst_before_create_statistic action
► create_statistic()
► burst_after_create_statistic action
│
▼
create_goal_statistic() (if goals completed)
Hit type determination
A hit is classified as an update when all of browser_id, browser_version_id, platform_id, and device_id are 0 — meaning the client did not send a user-agent (typically a heartbeat ping). The last matching row is fetched from burst_statistics for the same uid/fingerprint within the last 30 minutes.
| Condition | Result |
|---|---|
| Last row found, all device IDs = 0 | update — accumulate time_on_page |
| Last row found, device IDs present | create — new pageview row |
| No last row found, all device IDs = 0 | Error — returns empty (no action) |
| No last row found, device IDs present | create — first visit |
Session handling
Sessions are stored in {prefix}_burst_sessions. A new session is created when no prior hit exists for the visitor within the last 30 minutes.
Session table schema
| Column | Type | Description |
|---|---|---|
ID | int | Auto-increment primary key |
first_visited_url | TEXT | First page URL in the session |
last_visited_url | TEXT | Most recently visited URL |
host | varchar(255) | Site host (populated when domain filtering is enabled) |
referrer | varchar(255) | Traffic source referrer |
goal_id | int | Associated goal ID |
city_code | int | GeoIP city lookup ID |
browser_id | int | Browser lookup table ID |
browser_version_id | int | Browser version lookup ID |
platform_id | int | OS/platform lookup ID |
device_id | int | Device type lookup ID |
first_time_visit | tinyint | 1 if this is the visitor's first visit |
bounce | tinyint | 1 if the session is still a bounce (single page) |
Changed in v3.3.0: browser_id, browser_version_id, platform_id, device_id, first_time_visit, and bounce were moved from burst_statistics to burst_sessions. All session-level fields are now written exclusively to this table. See the Database Tables section for details.
A session is updated when the visitor navigates to a different URL within the same session window. The last_visited_url and bounce flag are updated accordingly.
Multi-domain tracking
Burst detects multi-domain setups automatically. The first domain seen is stored in burst_first_domain. If a subsequent hit comes from a different hostname, burst_is_multi_domain is set to true. When the Filter by domain setting is enabled, the host column is populated on sessions to allow per-domain filtering.
IP processing
IP address resolution is handled by Burst\Frontend\Ip\Ip. The following headers are checked in priority order:
| Priority | Header | Source |
|---|---|---|
| 1 | HTTP_CF_CONNECTING_IP | Cloudflare real client IP |
| 2 | HTTP_TRUE_CLIENT_IP | Akamai / Cloudflare Enterprise |
| 3 | HTTP_X_FORWARDED_FOR | Reverse proxies (leftmost public IP used) |
| 4 | HTTP_X_REAL_IP | nginx proxy |
| 5 | HTTP_X_CLUSTER_CLIENT_IP | Cluster environments |
| 6 | HTTP_CLIENT_IP | General proxy header |
| 7 | REMOTE_ADDR | Direct connection |
Public (non-private, non-reserved) IPs are preferred. Both IPv4 and IPv6 are supported, including IPv4-mapped IPv6 addresses.
IP blocklist
IPs are blocked before any data is recorded. The blocklist is configured under Settings → IP Blocklist (ip_blocklist option) as one IP or CIDR range per line. Both IPv4 and IPv6 CIDR notation is supported (e.g., 192.168.1.0/24, 2001:db8::/32).
Test hit bypass
A request can bypass the IP block only when the URL contains burst_test_hit and includes a valid nonce query parameter that matches the transient stored under burst_onboarding_token.
Changed in v3.3.0: Previously, any URL containing burst_test_hit bypassed the IP block. A nonce check is now required to prevent unauthorized bypass of the IP block.
# URL that will bypass the IP block check (nonce must match burst_onboarding_token transient)
https://example.com/?burst_test_hit=1&nonce=<valid-nonce>
Visitor identification
Burst supports two visitor identification modes:
Cookie-based (default)
A uid value is generated by the JavaScript client and stored in a browser cookie. The cookie retention period defaults to 30 days and is configurable via the burst_cookie_retention_days filter.
Cookieless (fingerprint)
When cookieless tracking is enabled, a browser fingerprint (canvas/audio based) is used as the uid. The fingerprint is also stored in a PHP session ($_SESSION['burst_fingerprint']) when server-side goals or ecommerce tracking is active.
PHP sessions for fingerprint storage fall back to the WordPress uploads directory (wp-content/uploads/burst-sessions/) if the default session save path is not writable.
Headless mode
When Burst runs in headless mode (the BURST_HEADLESS_DOMAIN constant is defined on the analytics site), the tracked site passes the visitor uid to the server via the tracking API rather than reading it from a cookie on the analytics domain. Server-side goals triggered via Goals_Tracker::handle_hook() accept the uid as a second argument so goal attribution still works when the tracked and analytics sites live on different domains.
User-agent parsing
Browser, browser version, platform, and device type are extracted from the User-Agent header by Burst\UserAgentParser\UserAgentParser. The results are stored in normalised lookup tables:
{prefix}_burst_browsers{prefix}_burst_browser_versions{prefix}_burst_platforms{prefix}_burst_devices
Lookup table IDs are cached in memory per request and via the WordPress object cache (burst cache group, key burst_{item}_all).
Page type identifiers
The page_type field is inserted into the <body> tag as a data-burst_type attribute by PHP output buffering. Valid values are:
| Value | Description |
|---|---|
front-page | Static homepage |
blog-index | Posts homepage |
date-archive | Date archive |
404 | Not found page |
archive-generic | Generic archive |
wc-shop | WooCommerce shop page |
tag | Tag archive |
tax | Custom taxonomy archive |
author | Author archive |
search | Search results |
category | Category archive |
| Any public post type slug | e.g., post, page, product |
The page ID (data-burst_id) and type are injected into the opening <body> tag using output buffering (ob_start/ob_end_flush).
Custom block rules
Settings → Custom Block Rules (custom_block_rules option) accepts one rule per line. Rules are matched against the page URL, referrer, and user-agent. Rules can be plain strings (case-insensitive substring match) or regex patterns.
Regex pattern format: Must start and end with / and may include flags (i, m, s, x, u).
Show code
# Plain string — blocks any URL, referrer, or UA containing this text
example-spam.com
# Regex — blocks URLs ending in a number
/text-in-url[0-9]+/i
# Regex — blocks all traffic from a specific domain
/^https:\/\/domain\./
# Regex — blocks known bots
/facebook(bot|crawler)/i
Invalid regex patterns are logged and skipped. Block rule matches are logged when BURST_DEBUG is defined and truthy.
Excluding visitors from tracking
By user role
Logged-in users can be excluded by role via Settings → User Role Blocklist (user_role_blocklist option). The roles list is filterable:
burst_roles_excluded_from_tracking
Filter the list of WordPress user roles that are excluded from tracking.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$roles | array | Array of role slugs excluded from tracking |
Example:
Show code
add_filter( 'burst_roles_excluded_from_tracking', function( $roles ) {
$roles[] = 'editor';
return $roles;
} );
By headless mode
Setting headless to true in Burst options (or defining the constant BURST_HEADLESS_DOMAIN) prevents the tracking script from being enqueued entirely.
Page builder previews
Tracking is automatically suppressed when the current request is detected as a page builder or plugin preview.
Hooks and filters reference
burst_before_track_hit
Fires after data sanitization but before session handling and database writes. Use this hook to modify tracking data or abort tracking by returning modified data.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$sanitized_data | array | Sanitized tracking data (see payload fields) |
$hit_type | string | 'create' or 'update' |
$previous_hit | array | Previous hit row from burst_statistics, or empty array |
Example:
Show code
add_filter( 'burst_before_track_hit', function( $data, $hit_type, $previous_hit ) {
// Add a custom field or modify the data before it is stored.
return $data;
}, 10, 3 );
burst_before_create_statistic
Fires immediately before a new row is inserted into burst_statistics.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$statistic | array | Statistic data about to be inserted |
Changed in v3.3.0: $statistic no longer contains session-level fields (bounce, browser_id, browser_version_id, platform_id, device_id, referrer, city_code, first_time_visit). These fields are now written exclusively to burst_sessions before this action fires.
Example:
Show code
add_action( 'burst_before_create_statistic', function( $statistic ) {
// Inspect or log data before insert.
// Note: session-level fields (browser_id, device_id, etc.) are not present in $statistic.
} );
burst_after_create_statistic
Fires immediately after a new row is inserted into burst_statistics.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$insert_id | int | ID of the newly created statistic row |
$statistic | array | Statistic data that was inserted |
Changed in v3.3.0: $statistic no longer contains session-level fields (bounce, browser_id, browser_version_id, platform_id, device_id, referrer, city_code, first_time_visit). These fields are now written exclusively to burst_sessions.
Example:
Show code
add_action( 'burst_after_create_statistic', function( $insert_id, $statistic ) {
// React to a new pageview being recorded.
// Note: session-level fields (browser_id, device_id, etc.) are not present in $statistic.
}, 10, 2 );
burst_tracking_options
Filter the options array that is passed to the frontend JavaScript via wp_localize_script. The returned value populates the global burst JavaScript object.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$options | array | Full tracking options array |
Example:
Show code
add_filter( 'burst_tracking_options', function( $options ) {
// Disable debug mode regardless of BURST_DEBUG constant.
$options['options']['debug'] = 0;
return $options;
} );
The $options array structure:
Show code
[
'tracking' => [
'isInitialHit' => true,
'lastUpdateTimestamp' => 0,
'beacon_url' => 'https://example.com/path/to/beacon',
'ajaxUrl' => 'https://example.com/wp-admin/admin-ajax.php',
],
'options' => [
'cookieless' => 0, // 1 = cookieless mode
'pageUrl' => 'https://…',
'beacon_enabled' => 0, // 1 = use beacon endpoint
'do_not_track' => 0, // 1 = respect DNT header
'enable_turbo_mode' => 0, // 1 = defer script
'track_url_change' => 0, // 1 = track SPA navigation
'cookie_retention_days' => 30,
'debug' => 0,
],
'goals' => [
'completed' => [],
'scriptUrl' => 'https://…/burst-goals.js?v=…',
'active' => [],
],
'cache' => [
'uid' => null,
'fingerprint' => null,
'isUserAgent' => null,
'isDoNotTrack' => null,
'useCookies' => null,
],
]
burst_cookie_retention_days
Filter the number of days the visitor UID cookie is retained.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$days | int | Cookie lifetime in days (default: 30) |
Example:
add_filter( 'burst_cookie_retention_days', function( $days ) {
return 365; // Keep cookie for one year.
} );
burst_goals_script_url
Filter the URL of the goals JavaScript bundle.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$url | string | Full URL to burst-goals.js |
Example:
add_filter( 'burst_goals_script_url', function( $url ) {
return 'https://cdn.example.com/burst-goals.js';
} );
burst_script_dependencies
Filter the script handles that the main tracking script depends on.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$deps | array | Array of registered script handles |
Example:
Show code
add_filter( 'burst_script_dependencies', function( $deps ) {
$deps[] = 'my-custom-script';
return $deps;
} );
burst_ip_blocklist
Filter the array of blocked IP addresses and CIDR ranges before the current visitor's IP is checked.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$blocked_ips | array | Array of IP addresses and CIDR ranges |
Example:
Show code
add_filter( 'burst_ip_blocklist', function( $blocked_ips ) {
$blocked_ips[] = '203.0.113.0/24'; // Block an additional CIDR range.
return $blocked_ips;
} );
burst_visitor_ip
Filter the resolved visitor IP address before it is used for blocklist checks.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$ip | string | Resolved IP address (may be empty string if unresolvable) |
Example:
Show code
add_filter( 'burst_visitor_ip', function( $ip ) {
// Override IP resolution for requests behind a trusted proxy.
if ( ! empty( $_SERVER['HTTP_X_CUSTOM_IP'] ) ) {
return sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_CUSTOM_IP'] ) );
}
return $ip;
} );
burst_obfuscate_filename
Filter whether ghost mode (filename obfuscation) is active. When truthy, all tracking assets are renamed with a b- prefix and served from the uploads directory.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$enabled | bool | Whether ghost mode is active |
Example:
add_filter( 'burst_obfuscate_filename', '__return_true' );
Server-side goal tracking
Goals of type hook are triggered server-side by calling Burst\Frontend\Goals\Goals_Tracker::handle_hook(). The method accepts the hook name and an optional $uid argument.
Method signature:
public function handle_hook( string $hook_name, string $uid = '' ): void
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
$hook_name | string | — | Name of the hook to record as a completed goal |
$uid | string | '' | Visitor UID. Only used when BURST_HEADLESS_DOMAIN is defined; ignored otherwise |
Changed in v3.4.0: handle_hook() now accepts an optional $uid parameter. When BURST_HEADLESS_DOMAIN is defined, the UID is read from $uid instead of the browser cookie, enabling goal attribution on headless setups where the tracked site passes the UID through the tracking API. On non-headless setups the UID continues to be read from the burst_uid cookie and the $uid argument is ignored.
Also in v3.4.0: get_active_goals() now returns an empty array when the burst_goals table does not exist, instead of issuing a database query. This prevents errors on setups where the Burst tables have not yet been installed (for example, immediately after activation).
Settings reference
The following plugin settings directly affect tracking behaviour:
| Option key | Type | Description |
|---|---|---|
enable_cookieless_tracking | bool | Use fingerprinting instead of cookies |
enable_do_not_track | bool | Honour the browser's DNT: 1 header |
enable_turbo_mode | bool | Load tracking script with defer (in footer) instead of async |
track_url_change | bool | Track SPA URL changes (query string included in URL comparison) |
filtering_by_domain | bool | Store and filter by host in sessions |
custom_block_rules | string | Newline-separated block rules (strings or regex) |
ip_blocklist | string | Newline-separated IP addresses or CIDR ranges |
user_role_blocklist | array | WordPress user roles excluded from tracking |
ghost_mode | bool | Obfuscate tracking asset filenames |
headless | bool | Disable script enqueueing (API-only mode) |
combine_vars_and_script | bool | Inline tracking options into a single pre-built JS file |
Debug mode
Define BURST_DEBUG as true in wp-config.php to enable verbose logging:
define( 'BURST_DEBUG', true );
When debug mode is active:
- Custom block rule matches are logged
- Failed statistic inserts/updates are logged
- Client-side HTTP 400 tracking errors are accepted via the
burst_tracking_errorAJAX action and written to the error log - The
debug: 1flag is included in the JavaScriptburstoptions object
Database tables
| Table | Purpose |
|---|---|
{prefix}_burst_statistics | One row per pageview. Foreign key to burst_sessions. |
{prefix}_burst_sessions | One row per visitor session. |
{prefix}_burst_goal_statistics | Many-to-many join between statistics rows and completed goals. |
{prefix}_burst_browsers | Browser name lookup table. |
{prefix}_burst_browser_versions | Browser version lookup table. |
{prefix}_burst_platforms | Operating system lookup table. |
{prefix}_burst_devices | Device type lookup table. |
Lookup tables are populated automatically on the first hit containing a new value. IDs are cached in the WordPress object cache under the burst group.
:::caution Breaking change
As of v3.3.0 the columns browser_id, browser_version_id, platform_id, device_id, first_time_visit, and bounce have been removed from burst_statistics and are stored exclusively in burst_sessions. A background database migration (move_columns_to_sessions) runs automatically on upgrade. Any custom SQL queries or third-party integrations that read these columns directly from burst_statistics must be updated to join burst_sessions instead.
:::