Integrations

WordPress

A WordPress site gets both halves from one activation. The plugin github.com/mahope/bugbottle-wordpress mounts this panel, adds the route that receives it, and stores what arrives as a private bugbottle_report post type with an admin list, a detail screen and an optional email through whatever SMTP plugin the site already has. There is no route to write, no script tag to add, and no build step: the plugin is PHP and two JavaScript files. It is the only route in this documentation where the receiving end is not yours to write.

It requires WordPress 6.4 or later and PHP 8.1, and is tested to WordPress 7.1. The bundled panel is this package's dist/bugbottle.js, unmodified, so what a visitor downloads is the build documented under "Two builds" — 66 500 bytes, 24.6 kB gzipped. The screenshot renderer is a second file of about 15 kB (6 kB over the wire) and is loaded only while the Screenshots setting is on, because the panel build deliberately carries no renderer.

Installing it #

It is not in the WordPress plugin directory yet, so "Add New → search for Bugbottle" finds nothing. Checked 27 September 2026: api.wordpress.org/plugins/info/1.0/bugbottle.json answers {"error":"Plugin not found."}, and wordpress.org/plugins/bugbottle/ redirects to the search page. The plugin's own readme still leads with the directory, so this section is where the correction lives. Download the zip from the latest release (v1.0.0, which bundles the library at 1.0.0) and install it with Plugins → Add New → Upload Plugin. Updating replaces the plugin directory; reports and settings live in the database and survive it.

Then open Bug reports → Settings. At a minimum set a recipient under Email recipient, or the reports only ever exist in wp-admin.

The settings are the mount options #

Every setting on that screen is one option from "The panel", and the defaults are the ones in this table. It is the shortest mapping in this documentation, because there is nothing between the two: the plugin builds one JSON object from the settings and calls window.bugbottle.mount(options) with it.

Setting What it becomes Default
Enabled Whether the panel is printed at all. Nothing in the settings is read on a site that has it off. on
Language locale. auto is the site's own language, da_DK rewritten to the da-DK the library resolves. auto
Primary colour, Position theme.primary and theme.position. #2563eb, bottom-right
Brand name, Logo URL brand.name and brand.logo. Empty keys are left out of the object rather than sent as empty strings. empty
Trigger selector trigger — a CSS selector for your own button. Empty keeps the floating one. none
Keyboard shortcut shortcut. An empty field is no shortcut, not the default, and false is how the library is told so. mod+shift+b
Open the panel on an uncaught error openOnError. off
Contact field contact, in three states: off, optional, and required — false, true and "required". Personal data you asked for, so it is off. off
Scrub scrub, handed in as this library's own scrubReport. on
Offline queue queue, handed in as createQueue with the endpoint and headers, and the signer when you have a key. on
Network log network, handed in as initNetwork. Your own endpoint is passed to it so the panel does not log its own POST. on
Breadcrumbs breadcrumbs, handed in as initBreadcrumbs. on
Timings and storage snapshot perf, handed in as initPerf. Both halves are simplifications and the library says which. off
Shake to report shake, handed in as onShake. On iOS nothing arrives until your own button calls requestShakePermission(), and the plugin never puts that prompt up for you. off
Screenshots screenshot, handed in from the second script, and what puts the picture editor under the preview. off
Signing key(s) signKey → createSigner. Only the first key is sent to the browser. empty
Only for logged-in users, Accept reports from visitors who are not logged in Whether the panel is printed for a visitor with no session. Both false is the default, and it is the safe one: a visitor who gets no panel cannot send an anonymous screenshot nobody can be asked about later. both off
Email recipient, Email on submit wp_mail with the report as Markdown and a link into wp-admin. off

No data-* attributes are read. If you have read "One script tag", this is the part that differs: the plugin assembles one config object and calls mount() explicitly, because the settings carry nested theme and brand shapes that would have to be squeezed through attributes and parsed straight back out again. The data-* attributes appear in one place only, and they are the ones you put on your own markup — see the screenshots below.

The route #

POST /wp-json/bugbottle/v1/report takes the ordinary JSON body, so your own form can post to it with the panel switched off. Anonymous reports are rate limited to ten an hour per IP address. With a signing key set, the route requires the library's X-Bugbottle-Signature header, verified over the raw body within five minutes of the server clock in either direction, in constant time, and refused if that digest has already been accepted inside the window — missing, malformed, wrong, expired and replayed all answer 401 alike. One key per line is how a key is rotated: add the new one, wait for cached pages carrying the old one to expire, then remove the old one.

A key that is sent to a browser is public. Signing raises the cost of posting junk from a script that never read your page; it is spam deterrence beside the rate limit, not authentication.

Screenshots #

Off until you turn it on, and with it off the renderer is not loaded at all, so no report can carry a picture — that is a complete answer rather than a half-measure. When it is on:

Deleting a report deletes the row and deliberately leaves the file on disk: an accidental delete is recoverable, and a directory nobody can reach over HTTP is a smaller problem than an unrecoverable one. Clear the directory yourself when you mean it.

Before you turn it on, read A privacy checklist — the four questions to answer first, and the two places a screenshot is most likely to carry somebody else's name, an order or a half-written message.

What arrives, and what is deliberately dropped #

A session replay — the rrweb recording of the seconds before the report — is stored without it and never refused for carrying one. Post meta is the wrong place for a megabyte of nested JSON, and nothing in wp-admin can play a replay back, so keeping it would be storage nobody can use.

The notes lines the library writes about a report itself are kept and shown above the evidence, so a reader who sees no picture is told why before they go looking for one. Today there is one: a report the browser had to keep while it was offline, and could not keep in full, is stored without its picture and says so.

Hooks #

Two filters and one action, which is the whole extension surface:

php
// Hide the panel on the front end of a page type nobody reports from.
add_filter( 'bugbottle_show_panel', function ( bool $show ): bool {
  return $show && ! is_checkout();
} );

// Add to the configuration handed to mount().
add_filter( 'bugbottle_panel_config', function ( array $config ): array {
  $config['extra'] = [ 'plan' => current_user_can( 'manage_options' ) ? 'admin' : 'free' ];
  return $config;
} );

// Fires after a report is stored, with the post id and the report.
add_action( 'bugbottle_report_stored', function ( int $id, array $report ): void {
  error_log( sprintf( 'bugbottle report %d: %s', $id, $report['type'] ) );
}, 10, 2 );

bugbottle_panel_config is filtered after the plugin has built the object and before it is JSON-encoded, so anything you add there reaches the browser without a line of escaping.

Edit this page on GitHub