Skip to main content

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:

KeyTypeDescription
statusstringActive transport identifier (for example rest, beacon, or error)
last_testintUnix 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:

FieldTypeDescription
urlstringFull URL of the current page, including query string
referrer_urlstringHTTP referrer URL
external_link_urlstringURL of an outbound link the visitor clicked (collected only when external link tracking is enabled)
user_agentstringBrowser user-agent string
uidstringUnique visitor identifier: the cookie value in cookie mode, a rotating-salt hash in private mode, or the fingerprint in fingerprint mode
fingerprintstringCanvas/audio fingerprint (fingerprint mode); null in private mode
time_on_pageintSeconds spent on the current page
completed_goalsarrayArray of goal IDs completed client-side
page_idintWordPress post/term ID of the tracked page (0 when unknown). Used for server-side visits-goal matching
page_typestringPage type identifier (see Page Type Identifiers)
search_termstringSearch term the visitor used to reach the page, sanitized with sanitize_text_field on input
should_load_ecommerceboolWhether 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.

ConditionResult
Last row found, all device IDs = 0update — accumulate time_on_page
Last row found, device IDs presentcreate — new pageview row
No last row found, all device IDs = 0Error — returns empty (no action)
No last row found, device IDs presentcreate — 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

ColumnTypeDescription
IDintAuto-increment primary key
first_visited_urlTEXTFirst page URL in the session
last_visited_urlTEXTMost recently visited URL
hostvarchar(255)Site host (populated when domain filtering is enabled)
referrervarchar(255)Traffic source referrer
goal_idintAssociated goal ID
city_codeintGeoIP location lookup ID; joins burst_locations
browser_idintBrowser lookup table ID
browser_version_idintBrowser version lookup ID
platform_idintOS/platform lookup ID
device_idintDevice type lookup ID
first_time_visittinyint1 if this is the visitor's first visit
bouncetinyint1 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:

PriorityHeaderSource
1HTTP_CF_CONNECTING_IPCloudflare real client IP
2HTTP_TRUE_CLIENT_IPAkamai / Cloudflare Enterprise
3HTTP_X_FORWARDED_FORReverse proxies (leftmost public IP used)
4HTTP_X_REAL_IPnginx proxy
5HTTP_X_CLUSTER_CLIENT_IPCluster environments
6HTTP_CLIENT_IPGeneral proxy header
7REMOTE_ADDRDirect 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:

FilterParametersDescription
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_code row keyed by the MaxMind geoname ID
  • country-only (free): a negative city_code placeholder 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.

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.

caution

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:

ValueDescription
front-pageStatic homepage
blog-indexPosts homepage
date-archiveDate archive
404Not found page
archive-genericGeneric archive
wc-shopWooCommerce shop page
tagTag archive
taxCustom taxonomy archive
authorAuthor archive
searchSearch results
categoryCategory archive
Any public post type sluge.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:

ValueEffect
404Only 404 hits
200Everything except 404 hits
allNo 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:

ParameterTypeDescription
$rolesarrayArray 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:

ParameterTypeDescription
$sanitized_dataarraySanitized tracking data (see payload fields)
$hit_typestring'create' or 'update'
$previous_hitarrayPrevious 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:

ParameterTypeDescription
$statisticarrayStatistic 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:

ParameterTypeDescription
$insert_idintID of the newly created statistic row
$statisticarrayStatistic 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 );

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:

ParameterTypeDescription
$statistic_idintID of the statistic row the click is attributed to
$urlstringClicked 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:

ParameterTypeDescription
$handlerstringFully-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:

ParameterTypeDescription
$optionsarrayFull 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,
],
]

Filter the number of days the visitor UID cookie is retained.

Parameters:

ParameterTypeDescription
$daysintCookie 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:

ParameterTypeDescription
$urlstringFull 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:

ParameterTypeDescription
$depsarrayArray 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:

ParameterTypeDescription
$blocked_ipsarrayArray 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:

ParameterTypeDescription
$ipstringResolved 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:

ParameterTypeDescription
$enabledboolWhether 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:

ParameterTypeDefaultDescription
$hook_namestringName of the hook to record as a completed goal
$uidstring''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_field and 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_length limit (default 30)
  • 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:

ParameterTypeDescription
$statistic_idintID of the newly created statistic row
$statisticarrayStatistic 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

FilterDefaultDescription
burst_search_max_unspaced_length30Maximum length of a single unspaced token before it is treated as spam
burst_search_merge_window120Seconds within which a longer typing-style search merges into the previous one
burst_search_allowed_scriptsderived from site localesPCRE Unicode script names allowed in search terms
burst_search_injection_patternsbuilt-in listRegex 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 );

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:

ParameterTypeDescription
$statistic_idintID of the statistic row the click is attributed to
$urlstringClicked 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 keyTypeDescription
privacy_levelstringHow 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_trackboolHonour the browser's DNT: 1 header
enable_turbo_modeboolLoad tracking script with defer (in footer) instead of async
track_url_changeboolTrack SPA URL changes (query string included in URL comparison)
track_external_linksboolCollect outbound link clicks and store the statistic-to-external-link relation (Pro)
filtering_by_domainboolStore and filter by host in sessions
custom_block_rulesstringNewline-separated block rules (strings or regex)
ip_blockliststringNewline-separated IP addresses or CIDR ranges
user_role_blocklistarrayWordPress user roles excluded from tracking
ghost_modeboolObfuscate tracking asset filenames
headlessboolDisable script enqueueing (API-only mode)
combine_vars_and_scriptboolInline 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_error AJAX action and written to the error log
  • The debug: 1 flag is included in the JavaScript burst options object

Database tables

TablePurpose
{prefix}_burst_statisticsOne row per pageview. Foreign key to burst_sessions.
{prefix}_burst_sessionsOne row per visitor session.
{prefix}_burst_locationsLookup table mapping city_code to country, region/state and city. Seeded with one negative-city_code row per country.
{prefix}_burst_goal_statisticsMany-to-many join between statistics rows and completed goals.
{prefix}_burst_external_linksLookup table of external (outbound) link URLs discovered by the scraper.
{prefix}_burst_statistics_external_linksMany-to-many join between statistics rows and external links.
{prefix}_burst_searchesLookup table of distinct on-site search terms.
{prefix}_burst_statistics_searchesMany-to-many join between statistics rows and search terms, with the result count.
{prefix}_burst_browsersBrowser name lookup table.
{prefix}_burst_browser_versionsBrowser version lookup table.
{prefix}_burst_platformsOperating system lookup table.
{prefix}_burst_devicesDevice 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.