Helping Organisations get organised!

Using LH Beacon

A consuming plugin does two things: queues a payload in the browser, and listens for it on the server. LH Beacon carries the payload between them unread.

In the browser

Declare lh-beacon as a dependency of your script. That is what loads it: LH Beacon registers its script but never enqueues it, so it only appears on pages that need it.

wp_enqueue_script( 'my-plugin', $src, array( 'lh-beacon' ), $version, array( 'in_footer' => true, 'strategy' => 'defer' ) );

Then queue a payload under a key. The key is a lowercase slug (letters, digits, hyphens and underscores, up to 64 characters), normally your plugin’s text domain. The payload is any JSON-serialisable value, or a function returning one, which is called at send time so it can report the latest state.

lhBeacon.set( 'my-plugin', { view_id: 123 } );
lhBeacon.set( 'my-plugin', () => ( { scrolled: maxScroll } ) );
lhBeacon.remove( 'my-plugin' );

When the page is hidden (visibilitychange) or unloaded (pagehide), everything queued goes out in one navigator.sendBeacon() request, falling back to fetch with keepalive. It is sent once per hide, even when both events fire. Payloads stay queued until removed, so a visitor who switches tabs and comes back sends again on the next hide: listeners should treat repeats as normal. A page restored from the back/forward cache carries on as before.

To send something only once, such as a list of events, queue a function that empties your list as it returns it and returns null when there is nothing new. Nothing is sent for a key whose function returns null. LH JavaScript Error Log works this way.

Keep your payload small. The whole request, every plugin’s payload together, is refused if it is over the size limit (8192 bytes by default), so one oversized payload loses everyone’s. Budget a share and trim to it in the browser.

On the server

Listen on the action for your key. The payload arrives exactly as the browser sent it, decoded from JSON and otherwise untouched: sanitise and validate it yourself. The request is passed too, for headers, and so is the visitor’s user ID.

add_action( 'lh_beacon_received_my-plugin', function ( $payload, WP_REST_Request $request, $user_id ) {
	// Validate, then store. $user_id is 0 for a logged-out visitor.
}, 10, 3 );

lh_beacon_received also fires for every key, with the key as its first argument and the same payload, request and user ID after it, for logging or debugging. During dispatch the user ID is also available from LH_Beacon_plugin::return_current_user_id().

Who sent it

LH Beacon prints a nonce into each page with its settings and sends it with every beacon. On the server it identifies a logged-in visitor from their WordPress login cookie and checks the nonce against that user, or against user 0 for a logged-out visitor. A beacon whose nonce is missing or expired is refused before any listener runs (see Limits and filters). You do not need a nonce of your own.

The user ID is an identity only. The current user is switched just for the nonce check and switched back, so get_current_user_id() and capability checks inside a listener see a logged-out request. Use the ID to attribute data, never to authorise anything. The lh_beacon_get_user_id filter can change it, or return 0 to anonymise every beacon.

Beacons go to /wp-json/lh-beacon/v1/collect on the same site as the page. On a site that blocks the REST API for logged-out visitors, that route must be allowed. On LocalHero, LH Disable REST API for anonymous users whitelists it from 1.4.5.

If you like this plugin please consider making a donation to further development