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.
The beacon endpoint runs under SHORTINIT, so most of WordPress is not loaded. Burst constructs new Tracking() directly on this path, and the Tracking constructor is what registers GeoIP location enrichment on burst_before_track_hit — so location enrichment works on the beacon transport even though init() is never called there.
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 |
external_link_url | string | URL of an outbound link the visitor clicked (collected only when external link tracking is enabled) |
user_agent | string | Browser user-agent string |
uid | string | Unique visitor identifier: the cookie value in cookie mode, a rotating-salt hash in private mode, or the fingerprint in fingerprint mode |
fingerprint | string | Canvas/audio fingerprint (fingerprint mode); null in private 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 of the tracked page (0 when unknown). Used for server-side visits-goal matching |
page_type | string | Page type identifier (see Page Type Identifiers) |
search_term | string | Search term the visitor used to reach the page, sanitized with sanitize_text_field on input |
should_load_ecommerce | bool | Whether to load ecommerce tracking |
Tracking lifecycle
The IP-block check is performed at the very top of track_hit(), before prepare_tracking_data() runs.
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 (GeoIP location enrichment runs here)
│
▼
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()
► attach search_term to $statistic (if present)
► burst_after_create_statistic action
│
▼
create_goal_statistic() (if goals completed, including server-side visits goals matched by page_id)
│
▼
burst_track_external_link action (Pro records the outbound click)
The search_term value is sanitized in prepare_tracking_data() and kept separate from the burst_statistics column fields. After the statistic row is created, a non-empty search_term is attached back onto the $statistic array passed to burst_after_create_statistic, where Burst\Frontend\Search\Search::link_pending_search() ties any pending search record to the new statistic row. See Search term tracking.
GeoIP location enrichment is registered on burst_before_track_hit and resolves the visitor's location on the first hit of a session. See Location enrichment.
When the payload carries a privacy_level of private_mode, the client-side fingerprint is discarded and the uid is replaced server-side with a rotating-salt hash. See Visitor identification.
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 location lookup ID; joins burst_locations |
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) |
browser_id, browser_version_id, platform_id, device_id, first_time_visit, and bounce are stored exclusively on burst_sessions. 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. The nonce check is 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>
Location enrichment
Burst enriches each hit with the visitor's location. The enrichment is registered on the burst_before_track_hit filter by the Tracking constructor, so it runs on both the REST and beacon (SHORTINIT) transports.
The reader class is resolved at hit time through the burst_geoip_handler filter. The free plugin uses Burst\Frontend\Tracking\Tracking_GeoIp, which reads the MaxMind GeoLite2 Country database. Pro swaps in Burst\Pro\Frontend\Tracking\Tracking_GeoIp_Pro, which reads the GeoLite2 City database for country, region/state and city detail.
Edge/CDN country detection
Before the local MaxMind lookup runs, Burst checks for a visitor country supplied by an edge/CDN. Sites behind Cloudflare, Vercel, AWS CloudFront, Fastly, Netlify or an nginx GeoIP module receive the country as an ISO-3166-1 alpha-2 code on a request header. That value is fresher than the local MaxMind snapshot, needs no license key and no on-disk database, so it takes precedence.
In the free (country-level) reader, a valid edge country short-circuits the database lookup entirely and is enriched with its continent. In Pro, the City database stays authoritative when it returns a record (it carries the city/state detail an edge country cannot); the edge country is only used as a fallback when no local city record is available.
Two filters control this behaviour:
| Filter | Parameters | Description |
|---|---|---|
burst_country_code_for_ip | $country_code (string, empty default), $ip (string) | Return a 2-letter ISO-3166-1 alpha-2 code to short-circuit the CDN header detection and the local database lookup |
burst_cdn_country_headers | $headers (string[]) | Ordered list of $_SERVER keys checked for the edge country. Return an empty array to disable built-in CDN header detection |
// Disable built-in CDN header detection (e.g. when the site is not behind a trusted edge).
add_filter( 'burst_cdn_country_headers', '__return_empty_array' );
Location resolution
Location is resolved only on the first hit of a session (when there is no previous hit). The resolved location is written to the burst_locations lookup table and the hit's city_code is set:
- city-level (Pro): a positive
city_coderow keyed by the MaxMind geoname ID - country-only (free): a negative
city_codeplaceholder row per country, so country-only data never reuses an arbitrary city row
burst_locations is seeded on install and upgrade with one negative-city_code row per country, so country tracking resolves to a stable lookup row without a city database. Seeding uses INSERT IGNORE on the city_code primary key, so it is idempotent and safe for City installs that already hold these rows.
Pro - CreatorAvailable in the Creator tier
City and region detail requires a paid license. The free plugin tracks country-level location.
See Geo IP for database management and the burst_geo_ip_enabled, burst_location_data, and burst_localhost_location_data filters.
Visitor identification
The privacy_level option controls how Burst recognizes visitors. It has three values: cookie (default), private_mode, and fingerprint. Both private_mode and fingerprint are cookieless — the cookieless tracking flag is 1 for either. Stronger privacy trades away returning-visitor accuracy.
Cookie (default)
A uid value is generated by the JavaScript client and stored in a first-party browser cookie. The cookie retention period defaults to 30 days and is configurable via the burst_cookie_retention_days filter. This is the most accurate mode for returning-visitor data.
Private mode
Private mode (labelled Cookieless) uses neither a cookie nor a fingerprint. Server-side, the client fingerprint is discarded and the uid is derived from a rotating-salt hash:
$uid = hash( 'sha256', $ip . $user_agent . $rotating_salt );
The salt is stored in the non-autoloaded burst_rotating_salt option and rotates once per day (keyed by the UTC date). Each day's salt is a cryptographically secure 32-byte random value produced with random_bytes() and hex-encoded. Because the salt changes daily, the same visitor produces a different uid on a following day, so private mode does not recognize returning visitors across days.
Device fingerprint
A browser fingerprint (canvas/audio based) is used as the uid (labelled Cookieless fingerprint). The fingerprint is also stored in a PHP session ($_SESSION['burst_fingerprint']) when server-side goals or ecommerce tracking is active. This mode recognizes returning visitors across days without a cookie.
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. Non-file session handlers (redis, memcached, database, custom) are left untouched — the uploads fallback applies only to the files handler.
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).
Browser names are validated against an allowlist generated from the parser library: any name the parser cannot legitimately emit is treated as junk (for example the leading token of an unrecognized user agent). Junk browser rows, along with their sessions and pageviews, are removed by a weekly cleanup.
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).
404 hits
Hits on a not-found page are stored like any other pageview, with page_type set to 404. These rows are recorded but excluded from the default statistics: every statistics query filters out page_type = '404' unless a status filter is explicitly supplied. This keeps 404 traffic out of visitor, session and page reports while still retaining the data.
To include or isolate 404 hits, statistics queries accept a virtual status filter that resolves against page_type:
| Value | Effect |
|---|---|
404 | Only 404 hits |
200 | Everything except 404 hits |
all | No status filtering (includes 404 hits) |
The dedicated not-found-pages datatable (not-found-pages, capability view_burst_statistics) reports the top 404 URLs by hit count within the selected date range.
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. Burst's own GeoIP location enrichment is registered on this filter (see Location enrichment).
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 |
$statistic contains only burst_statistics column fields. Session-level fields (bounce, browser_id, browser_version_id, platform_id, device_id, referrer, city_code, first_time_visit) are written exclusively to burst_sessions before this action fires.
Example:
add_action( 'burst_before_create_statistic', function( $statistic ) {
// Inspect or log data before insert.
} );
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 |
$statistic contains the burst_statistics column fields. Session-level fields (bounce, browser_id, browser_version_id, platform_id, device_id, referrer, city_code, first_time_visit) are written exclusively to burst_sessions. When the hit carried a non-empty search_term, that value is also present on $statistic['search_term'] so subscribers (such as Burst\Frontend\Search\Search::link_pending_search()) can associate the search with the new row.
Example:
add_action( 'burst_after_create_statistic', function( $insert_id, $statistic ) {
// React to a new pageview being recorded.
}, 10, 2 );
burst_track_external_link
Fires after a new statistic row is created, passing the statistic ID and the clicked outbound URL. Shared tracking code fires this action unconditionally with a neutral default; Pro registers the handler that performs the feature gating and records the click, so the free build carries no reference to any Pro class.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$statistic_id | int | ID of the statistic row the click is attributed to |
$url | string | Clicked external URL (empty string when no outbound click occurred) |
Example:
add_action( 'burst_track_external_link', function( $statistic_id, $url ) {
// React to an outbound link click.
}, 10, 2 );
burst_geoip_handler
Filter the class used to resolve the visitor's location during hit processing. The reader is resolved lazily at hit time.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$handler | string | Fully-qualified reader class name (default Burst\Frontend\Tracking\Tracking_GeoIp; Pro returns Burst\Pro\Frontend\Tracking\Tracking_GeoIp_Pro) |
Example:
add_filter( 'burst_geoip_handler', function( $handler ) {
return My_Custom_GeoIp_Reader::class;
} );
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' => [
'privacy_level' => 'cookie', // cookie | private_mode | fingerprint
'cookieless' => 0, // 1 when privacy_level is private_mode or fingerprint
'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
'track_external_links' => 0, // 1 = collect outbound link clicks
'cookie_retention_days' => 30,
'page_id' => 0, // post ID on singular pages, else 0
'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 |
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 is read from the burst_uid cookie and the $uid argument is ignored.
get_active_goals() 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).
Visits goals
Goals of type visits are completed server-side during hit processing. get_completed_goals() loops the active visits goals and calls goal_is_completed() for each, passing the hit's page_id alongside the page URL. A visits goal is completed when its stored page_id matches the hit's page_id, falling back to a normalised URL comparison when no page ID is available.
Matching on page_id is what makes visits goals work on the tracking endpoints (both REST and the SHORTINIT beacon), where there is no main query — is_singular() and get_queried_object_id() are unusable and the beacon does not even load them, so the page ID is taken from the hit payload instead.
Block editor goal tracking
Burst adds a "Track clicks with a Burst Goal" inspector panel to supported blocks in the block editor (visible to users with the manage capability). When a block is marked as a goal, the editor stores two block attributes — burstGoalActive and burstGoalUid — and the goal's selector is [data-burst-goal="<uid>"].
On the frontend, the render_block filter injects a data-burst-goal="<uid>" attribute into the block's first opening HTML tag so the click goal's selector matches at runtime. The attribute is only injected when the block is flagged active and does not already contain a data-burst-goal attribute.
Block-created goals are stored with block_goal = 1 and, when the block lives on a specific post, a page_id. Goal URLs are kept in sync with the post: save_post updates the stored URL when a post's permalink changes, and block goals whose uid no longer appears in the post content are deactivated (or deleted when they hold no statistics). A daily cleanup removes orphaned block goals that were created but never persisted in any post.
Search term tracking
Burst\Frontend\Search\Search records the on-site search terms that visitors use and links them to the statistic row of the matching hit. Search rows live in {prefix}_burst_searches (the distinct term strings) and {prefix}_burst_statistics_searches (the many-to-many link between searches and statistics rows, with the result count). This lets the search-terms datatable report search volume without storing PII in the main statistics table.
Capturing searches
capture_search_and_posts() is hooked to the WordPress the_posts filter and only acts when $query->is_search() is true. It reads the s query var, applies the same exclusion rules as the rest of tracking (exclude_from_tracking()), then sanitizes and filters the term before recording it.
Method signature:
public function capture_search_and_posts( array $posts, \WP_Query $query ): array
The posts array is returned unmodified — the filter is used only to observe the search. The result count is taken from $query->found_posts when WordPress calculated found rows, otherwise from the number of posts on the current page.
A search is discarded before storage when any of the following apply:
- the term is empty after
sanitize_text_fieldand trimming - the term is detected as spam (links, markup, email addresses, repeated characters, symbol-heavy strings, or letters from a script the site's languages do not use)
- the term is a single unspaced token longer than the
burst_search_max_unspaced_lengthlimit (default30) - the term looks like an HTML/JS/CSS injection probe (XSS payload) fired at the search box rather than a genuine search
Recorded searches are also de-duplicated for typing-style ajax searches: when a new term extends a recent shorter term for the same statistic within the burst_search_merge_window (default 2 minutes), the existing link row is repointed to the longer term instead of inserting a new row.
Linking pending searches to statistics
When a search is captured before the visitor's statistic row exists, the search is stored as a pending row with a NULL statistic_id. Once the corresponding hit creates a statistic, link_pending_search() — hooked to burst_after_create_statistic — associates the oldest matching pending row with the new statistic ID.
Method signature:
public function link_pending_search( int $statistic_id, array $statistic ): void
Parameters:
| Parameter | Type | Description |
|---|---|---|
$statistic_id | int | ID of the newly created statistic row |
$statistic | array | Statistic data, including search_term when the hit carried one |
The method returns early when $statistic_id is not positive or when $statistic['search_term'] is empty. Pending rows that are never linked are cleaned up by scheduled maintenance.
Filters
| Filter | Default | Description |
|---|---|---|
burst_search_max_unspaced_length | 30 | Maximum length of a single unspaced token before it is treated as spam |
burst_search_merge_window | 120 | Seconds within which a longer typing-style search merges into the previous one |
burst_search_allowed_scripts | derived from site locales | PCRE Unicode script names allowed in search terms |
burst_search_injection_patterns | built-in list | Regex patterns (with delimiters) used to detect injection-probe searches |
Example:
Show code
add_filter( 'burst_search_allowed_scripts', function( $scripts, $locales ) {
// Allow Greek search terms in addition to the auto-detected scripts.
$scripts[] = 'Greek';
return $scripts;
}, 10, 2 );
External link tracking
ProAvailable in Burst Pro
External link tracking records which outbound links visitors click so you can see which external destinations are most used.
When external link tracking is enabled (the Pro-only track_external_links option), the JavaScript client adds an external_link_url field to the hit payload whenever a visitor clicks an outbound link. The URL is sanitized server-side in prepare_tracking_data(): only http/https URLs with a valid scheme and host are kept; everything else is discarded.
After the statistic row has been created, shared tracking code fires the burst_track_external_link action with the statistic ID and the clicked URL:
do_action( 'burst_track_external_link', (int) $statistic_id, $external_link_url );
The free build attaches no listener, so it carries no reference to any Pro class. Pro registers a handler (Burst\Pro\Frontend\Tracking\Tracking_Pro::track_external_link()) that applies the feature gating and, when it passes, calls Burst\Pro\Frontend\External_Link\External_Link_Frontend::track_external_link() to write the relation between the statistic row and the external link.
Pro handler signature:
public function track_external_link( int $statistic_id, string $url ): void
Writer signature:
public static function track_external_link( int $statistic_id, string $url ): void
Parameters:
| Parameter | Type | Description |
|---|---|---|
$statistic_id | int | ID of the statistic row the click is attributed to |
$url | string | Clicked external URL, normalized before lookup |
Collection is gated twice. The track_external_links option must be enabled, and the clicked URL must already exist in the burst_external_links lookup table. The lookup table is populated by a background scraper that walks the homepage and published posts; URLs that are not already present are ignored — the tracker never inserts new lookup rows. At most one external link is recorded per statistic row.
Before lookup, the URL is normalized to match the scraper's stored form: the host is lowercased, the fragment is stripped, and a trailing slash is added when there is no query string and the final path segment has no file extension.
Settings reference
The following plugin settings directly affect tracking behaviour:
| Option key | Type | Description |
|---|---|---|
privacy_level | string | How visitors are recognized: cookie (default, first-party cookie), private_mode (cookieless, rotating-salt hash, no cookie or fingerprint), or fingerprint (cookieless, device fingerprint) |
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) |
track_external_links | bool | Collect outbound link clicks and store the statistic-to-external-link relation (Pro) |
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_locations | Lookup table mapping city_code to country, region/state and city. Seeded with one negative-city_code row per country. |
{prefix}_burst_goal_statistics | Many-to-many join between statistics rows and completed goals. |
{prefix}_burst_external_links | Lookup table of external (outbound) link URLs discovered by the scraper. |
{prefix}_burst_statistics_external_links | Many-to-many join between statistics rows and external links. |
{prefix}_burst_searches | Lookup table of distinct on-site search terms. |
{prefix}_burst_statistics_searches | Many-to-many join between statistics rows and search terms, with the result count. |
{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.
The columns browser_id, browser_version_id, platform_id, device_id, first_time_visit, and bounce live on burst_sessions, not burst_statistics. Custom SQL queries and third-party integrations must join burst_sessions to read these columns. Location data is read by joining burst_sessions.city_code to burst_locations.