Reports
The reporting system in Burst Statistics allows you to create, schedule, and send analytics reports via email. Reports can be sent on a daily, weekly, or monthly schedule, and support two formats: a classic email with data tables, and a "story" format with a link to a shareable dashboard.
Overview
Reports are stored in the {prefix}burst_reports database table. Each report has a set of content blocks, a list of email recipients, a frequency, and a delivery schedule. When a scheduled report is due, it is dispatched via WP-Cron using a batched email queue, with delivery status tracked in a separate {prefix}burst_report_logs table.
Pro - CreatorAvailable in the Creator tier
The full reporting system (scheduled reports, multiple formats, content blocks, delivery logs) is a Pro feature.
Database tables
{prefix}burst_reports
Stores report configuration.
| Column | Type | Description |
|---|---|---|
ID | int unsigned | Auto-incremented primary key |
name | varchar(255) | Report display name |
date_range | varchar(255) | The date range used for report data |
format | varchar(32) | Report format (classic or story) |
frequency | varchar(16) | Send frequency (daily, weekly, monthly) |
fixed_end_date | varchar(16) | Fixed end date (Y-m-d) used as the data range end |
day_of_week | varchar(16) | Day of week for weekly/monthly reports (e.g. monday) |
week_of_month | int | Occurrence of the weekday in the month (1–4, or -1 for last) |
send_time | varchar(16) | Time to send the report in HH:MM format (site timezone) |
last_edit | int unsigned | Unix timestamp of last modification |
enabled | tinyint(1) | Whether the report is active |
scheduled | tinyint(1) | Whether the report is sent automatically on schedule |
content | longtext | JSON-encoded array of content block definitions |
recipients | longtext | JSON-encoded array of recipient email addresses |
{prefix}burst_report_logs
Tracks report delivery history.
| Column | Type | Description |
|---|---|---|
ID | bigint unsigned | Auto-incremented primary key |
report_id | bigint unsigned | Associated report ID |
queue_id | varchar(32) | Date-based queue identifier (Y-m-d, or test-Y-m-d-{timestamp} for test sends) |
batch_id | int unsigned | Batch number (null for parent/queue-level log entries) |
status | varchar(32) | Delivery status string |
message | text | Human-readable status message |
time | int unsigned | Unix timestamp of the log entry |
date | varchar(10) | Date string extracted from queue ID (Y-m-d) |
Report formats
The format field on a report controls how the email is constructed.
| Value | Description |
|---|---|
classic | Full HTML email with data tables for each content block |
story | Short teaser email with a button linking to a shareable story dashboard |
Report frequency
The frequency field controls how often a scheduled report is sent.
| Value | Description |
|---|---|
daily | Sent every day at the configured send_time |
weekly | Sent on the configured day_of_week each week |
monthly | Sent on the nth occurrence of day_of_week within each month, determined by week_of_month |
For monthly reports, week_of_month accepts 1–4 for the first through fourth occurrence of the weekday, or -1 for the last occurrence.
Changed in v3.4.0: Weekly and monthly schedule matching now uses wp_date() in the site timezone instead of gmdate() in UTC. Reports configured for a specific weekday are now evaluated against the local calendar day, which prevents off-by-one mismatches near midnight UTC for sites with non-UTC timezones.
Report date ranges
The date_range (or reportDateRange) field on a report defines which period the report data covers.
Accepted values are defined by the Report_Date_Range domain type. The default when creating a report from onboarding is last_week.
Content blocks
Each report contains a content array of block definitions. For the classic format, these blocks map to data table sections rendered in the email.
Block objects stored in the content field have the following structure:
| Property | Type | Description |
|---|---|---|
id | string | Block identifier (must be a valid Report_Content_Block ID) |
filters | array | Query filters applied to this block's data |
content | string | Optional custom text content |
date_range | string | Optional per-block date range override |
date_range_enabled | bool | Whether the per-block date range override is active |
fixed_end_date | string | Per-block fixed end date (Y-m-d) |
comment_title | string | Optional comment block title |
comment_text | string | Optional comment block body text |
The compare block (identified by Report_Content_Block::COMPARE) is handled specially: it renders a summary table comparing pageviews, sessions, visitors, and bounce rate against the previous period.
When report block filters are sanitized through Query_Data, the call now uses an explicit query id (report_sanitize_content_filters) so that slow-query stats can attribute the work back to report sanitization.
Changed in v3.4.0: Query_Data instances must be constructed with an explicit string id as the first argument (e.g. new Query_Data( 'report_block_pages', $args )). Reports internally generate per-block ids in the form report_block_<key> when fetching block data.
Reporting field configuration
Reporting fields exposed to the admin UI are processed by Reporting_Fields::get(). Each field can declare optional notice, pro, and context sub-objects, which support a url key used by the UI to render help, upgrade, and context links.
Scheduling
The burst_every_hour cron hook triggers Reports::maybe_send_report(), which checks all enabled scheduled reports. A report is eligible to send when the current time falls within the report's scheduled send window for the day (within a 24-hour grace window after the scheduled timestamp). Eligible reports are dispatched via wp_schedule_single_event using the burst_send_email_batch hook, scheduled 5 minutes in the future.
Actual email sending happens in Reports::handle_email_batch(), which processes one batch of up to 10 recipients per cron run. If a report has more recipients than the batch size, additional batches are automatically scheduled 5 minutes apart.
Changed in v3.4.0: Reports::maybe_send_report() now exits early if the {prefix}burst_reports table does not exist. This prevents fatal errors on freshly installed sites where table creation has not yet completed when the cron fires.
Changed in v3.4.0: Each eligible report is now guarded by a per-report transient (burst_report_sent_{report_id}, 24-hour TTL) that is set before dispatching. If two burst_every_hour cron events fire in parallel for the same report (for example on sites with overlapping cron runs or external pingers), only the first one will dispatch the report — the second will skip it. This prevents duplicate sends to recipients.
REST API endpoints
GET /wp-json/burst/v1/reports
Returns all reports, ordered by last edit date. Requires manage-level access and a valid burst_nonce nonce parameter.
Changed in v3.4.0: This endpoint now performs an explicit manage_burst_statistics capability check before validating the nonce. Requests from users without manage permission receive a JSON error response instead of report data.
Response:
Show code
{
"request_success": true,
"data": {
"reports": [ { ... } ]
}
}
POST /wp-json/burst/v1/do_action/report/{action}
Performs a write action on a report. Requires admin access. The {action} segment maps to one of the following actions (prefixed with report- internally):
| Action path | Description |
|---|---|
create | Create a new report |
delete | Delete an existing report |
update | Update an existing report |
send-test-report | Send a test email immediately |
send-report-now | Send the report immediately and schedule the queue |
preview | Get a preview HTML render of the report |
Changed in v3.4.0: When send-test-report is used, a parent log entry with status processing is now inserted into {prefix}burst_report_logs (using the test-Y-m-d-{timestamp} queue id) before the test batch is handled, mirroring the logging behavior of scheduled sends. This ensures test send queues appear in the delivery log immediately, with the same parent/child structure used for resolving final queue status.
GET /wp-json/burst/v1/report/logs
Returns aggregated delivery logs for the past 30 days. Requires manage-level access and a valid burst_nonce nonce parameter.
Response:
Show code
{
"request_success": true,
"data": {
"logs": [ { ... } ]
}
}
POST /wp-json/burst/v1/get_action/story-report-data
Returns the report payload used to render a shareable story page. The request must include a valid share token in the action data; the handler validates the token against the stored share-token store before returning anything.
:::caution Breaking change
Changed in v3.4.2: The internal action handler was renamed from report-data to story-report-data, and the token is now validated via Share::validate_share_token() before any data is returned. The handler also resolves share links exclusively through Share->tokens->get_share_links() and constructs the underlying Report with the shared-link flag (new Report( $report_id, true )), so scheduling and recipient fields are always omitted from story responses.
Any external consumer calling the previous report-data action must update its action name to story-report-data and supply the share token.
:::
Request data:
| Field | Type | Description |
|---|---|---|
token | string | A valid 32-character hex share token bound to a report |
Response: A Report::to_array() payload built with $is_shared_link = true, or an empty array when the token is missing or invalid.
Story share URLs
Story share URLs are generated by Reports::get_story_url() from the share token associated with the report.
Changed in v3.4.2: Story URLs are now first-class path routes under /burst-dashboard/story/ instead of hash-based routes on /burst-dashboard/. The token is still passed as the burst_share_token query parameter.
| Version | URL format |
|---|---|
| Before v3.4.2 | https://example.com/burst-dashboard/?burst_share_token={token}#/story |
| v3.4.2 and later | https://example.com/burst-dashboard/story/?burst_share_token={token} |
:::caution Breaking change
Changed in v3.4.2: Any external system that constructs or matches story URLs by the old hash-based path must be updated to use the path-based format. The scheme is still normalized against BURST_URL to keep links consistent when generated from cron.
:::
Internally, story share links are now retrieved through the Share->tokens->get_share_links() service rather than the legacy Share::get_share_links() method. Code that consumes the share-link API from outside the Reports module should call the new service method directly.
Delivery status values
The following status values appear in the burst_report_logs table and are returned in the lastSendStatus field of a report object.
| Status | Description |
|---|---|
processing | Queue has been created and email sending is scheduled |
sending_successful | All recipients received the email |
sending_failed | All emails in the batch failed to send |
partly_sent | Some emails were sent, some failed |
cron_miss | The scheduled cron event did not fire within the expected window |
scheduled | Report is enabled and scheduled but has not sent yet |
ready_to_share | Report is not scheduled; the shareable story link is ready |
concept | Report is scheduled but currently disabled |
:::caution Breaking change
Changed in v3.3.0: The status value ready_to_send has been replaced by ready_to_share. The condition that triggers this status has also changed: previously it was returned only for reports with format = story; it is now returned for any report where scheduled = false, regardless of format.
Any consumer of the reports REST response that checks for "lastSendStatus": "ready_to_send" must be updated to check for "ready_to_share" instead.
:::
Hooks and filters
burst_email_blocks
Filters the list of available content blocks used to populate the classic email report.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$blocks | array | Array of block definitions loaded from includes/Admin/Mailer/config/blocks.php |
Example:
Show code
add_filter( 'burst_email_blocks', function( array $blocks ): array {
// Add a custom block definition.
$blocks['my_custom_block'] = [
'title' => __( 'Top Custom Metric', 'my-plugin' ),
'header' => [ __( 'Label', 'my-plugin' ), __( 'Value', 'my-plugin' ) ],
'url' => '#/statistics',
'query_args' => [
'metrics' => [ 'my_metric' ],
'type' => 'my_type',
],
];
return $blocks;
} );
burst_mail_report_limit
Filters the maximum number of rows returned per content block in a classic email report.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$limit | int | Number of rows. Default: 5 |
Example:
add_filter( 'burst_mail_report_limit', function( int $limit ): int {
return 10;
} );
burst_mail_report_results
Filters the raw query results for a content block before they are processed into the email table.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$raw_results | array | Array of result rows from the statistics query |
$qd | Query_Data | The query data object used for the block |
$start_date | int | Unix timestamp of the report period start |
$end_date | int | Unix timestamp of the report period end |
Example:
Show code
add_filter( 'burst_mail_report_results', function( array $results, $qd, int $start, int $end ): array {
// Remove rows where the first column is empty.
return array_filter( $results, fn( $row ) => ! empty( reset( $row ) ) );
}, 10, 4 );
burst_mail_reports_blocks
Filters the fully built blocks array before it is passed to the Mailer for rendering in a classic report. This runs after each block's data has been fetched and formatted.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$blocks | array | Associative array of built block data keyed by block ID |
$date_start | int | Unix timestamp of the report period start |
$date_end | int | Unix timestamp of the report period end |
Example:
Show code
add_filter( 'burst_mail_reports_blocks', function( array $blocks, int $start, int $end ): array {
// Remove the compare block from all reports.
unset( $blocks['compare'] );
return $blocks;
}, 10, 3 );
burst_email_template
Filters the path to the main HTML email template file.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$path | string | Absolute path to the email template file |
Example:
add_filter( 'burst_email_template', function( string $path ): string {
return get_stylesheet_directory() . '/burst/email.html';
} );
burst_email_block_template
Filters the path to the HTML template used to render each content block within the email.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$path | string | Absolute path to the block template file |
Example:
add_filter( 'burst_email_block_template', function( string $path ): string {
return get_stylesheet_directory() . '/burst/block.html';
} );
burst_email_readmore_template
Filters the path to the HTML template used to render the "read more" section at the bottom of the email.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$path | string | Absolute path to the read-more template file |
Example:
add_filter( 'burst_email_readmore_template', function( string $path ): string {
return get_stylesheet_directory() . '/burst/read-more.html';
} );
burst_report_log_delete_threshold
Filters how many days of report delivery logs are retained before old entries are purged.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$days | int | Number of days to retain logs. Default: 30 |
Example:
add_filter( 'burst_report_log_delete_threshold', function( int $days ): int {
return 90;
} );
burst_report_formats
Filters the list of valid report format identifiers.
Burst ships with classic by default. Pro features may register additional formats such as story by hooking into this filter.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$formats | string[] | Allowed report format slugs. |
Example:
Show code
add_filter( 'burst_report_formats', function( array $formats ): array {
$formats[] = 'my_custom_format';
return array_values( array_unique( $formats ) );
} );
burst_create_report_from_onboarding (action)
Fired during onboarding to automatically create a default weekly summary report for a given email address.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$email | string | The recipient email address for the onboarding report |
The default report created by this action has the following settings:
| Setting | Default value |
|---|---|
| Name | "Weekly Summary" |
| Format | classic |
| Frequency | weekly |
| Day of week | monday |
| Send time | 09:00 |
| Date range | last_week |
| Enabled | true |
| Scheduled | true |
burst_send_email_batch (action)
WP-Cron hook that fires to process a single batch of emails for a report queue.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$report_id | int | The report ID |
$queue_id | string | The queue identifier (date string or test queue ID) |
$batch_id | int|null | The batch number (1-based) |
Do not call or schedule burst_send_email_batch directly. Use the report-send-report-now REST action or the admin UI instead. Manually scheduling duplicate events for the same [$report_id, $queue_id, $batch_id] combination is a no-op because the system checks for existing scheduled events and already-processed queue entries before sending.
Email template variables
The main email template (email.html) supports the following placeholder tokens:
| Token | Description |
|---|---|
{base} | HTML <base> tag pointing to the site home URL |
{title} | Email heading (HTML allowed) |
{logo} | Light-mode logo image URL |
{logo_dark} | Dark-mode logo image URL |
{message} | Introductory message text (e.g. date range coverage) |
{blocks} | Rendered HTML for all content blocks |
{read_more} | Rendered HTML for the "read more" section |
{sent_by_text} | Footer unsubscribe / sender attribution text |
{domain} | Site home URL |
Each block within the block.html template supports:
| Token | Description |
|---|---|
{title} | Block heading |
{subtitle} | Block subheading (e.g. "vs. previous week") |
{table} | Rendered HTML table rows |
{url} | "Learn more" link URL |
{learn-more} | "Learn more" button text |
The read-more.html template supports:
| Token | Description |
|---|---|
{title} | Section heading |
{message} | Teaser text |
{read_more_url} | Call-to-action button URL |
{read_more_text} | Call-to-action button label |
Report object fields (API)
The to_array() method on a Report instance returns the following fields, as used in REST API responses.
Changed in v3.3.0: The Report constructor now accepts a second parameter bool $is_shared_link = false. When true, scheduling and recipient fields are replaced with safe defaults in the returned array, and if the report is disabled to_array() returns an empty array [] instead.
Changed in v3.4.2: The story REST handler (get_action/story-report-data) now always constructs the Report with $is_shared_link = true, so story responses are guaranteed to omit scheduling and recipient fields regardless of caller.
Show code
// Instantiate a report for a shared-link context (omits scheduling/recipient data).
$report = new Report( $report_id, true );
$data = $report->to_array(); // Returns [] if report is disabled.
// Standard instantiation (full data, used in admin/REST contexts).
$report = new Report( $report_id );
$data = $report->to_array();
| Field | Type | Description |
|---|---|---|
id | int|null | Report ID |
name | string | Report name |
format | string | Report format (classic or story) |
enabled | bool | Whether the report is active |
content | array | Array of content block definitions |
reportDateRange | string | Date range identifier |
fixedEndDate | string | Fixed end date (Y-m-d) |
frequency | string | Report frequency (omitted for shared-link requests; defaults to Report_Frequency::DEFAULT) |
weekOfMonth | int|null | Week of month for monthly reports (omitted for shared-link requests) |
dayOfWeek | string|null | Day of week for weekly/monthly reports (omitted for shared-link requests) |
sendTime | string | Scheduled send time (HH:MM) (omitted for shared-link requests) |
lastEdit | int | Unix timestamp of last modification (omitted for shared-link requests) |
scheduled | bool | Whether the report sends automatically (omitted for shared-link requests) |
recipients | array | Array of recipient email addresses (omitted for shared-link requests) |
lastSendStatus | string | Status string from the most recent delivery log (omitted for shared-link requests) |
lastSendMessage | string | Human-readable message for lastSendStatus (omitted for shared-link requests) |
Changed in v3.3.0: For shared-link requests (
$is_shared_link = true), the fields marked above are replaced with empty/default values and are not populated from the database. If the report'senabledflag isfalse,to_array()returns[].
Email delivery details
- Emails are sent using WordPress's
wp_mail()function withContent-Type: text/html; charset=UTF-8. - Before sending, the recipient domain is validated via DNS MX/A record lookup. Emails to domains with no valid DNS records are skipped and logged as failures.
- Recipients are processed in batches of 10 (default). Each batch is scheduled as a separate
burst_send_email_batchcron event, 5 minutes apart. - Duplicate batch processing is prevented: the system checks both existing cron events and the
burst_report_logstable before scheduling or sending. - The
fixed_end_dateon a scheduled report is automatically updated to yesterday each time the report is sent, anchoring the data range end date. - Test reports (sent via the "Send test" button) are delivered immediately without scheduling a cron event, and their queue IDs use the format
test-Y-m-d-{timestamp}to distinguish them from scheduled sends in the logs. Since v3.4.0, aprocessingparent log entry is inserted before the test batch is handled, so test sends produce the same parent/child log structure as scheduled sends.
Report delivery depends on WP-Cron. On sites with infrequent traffic or disabled WP-Cron, scheduled reports may be delayed or missed entirely. Missed sends are recorded in the logs with the cron_miss status. Consider configuring a real system cron job to trigger wp-cron.php on a regular interval.
Database indexes
Changed in v3.4.0: The internal add_index() helper used by Report_Logs and Reports now accepts a bare table suffix (e.g. 'burst_report_logs', 'burst_reports') instead of a fully-prefixed table name. The helper resolves the WordPress table prefix internally and validates the suffix against the plugin's known table list. If you are extending Burst and calling $this->add_index() from your own code, drop the $wpdb->prefix from the first argument.