Heartbeat Queue
The heartbeat queue is a Redis List-based pipeline for delivering page engagement signals from Allegro to external consumers. When enabled, the SDK tracks how long each page is actively open and periodically POSTs that running total to the platform, which pushes it onto a per-tenant Redis List. External consumers poll the queue to read heartbeats, then issue a separate delete call to remove them.
It mirrors the Event Queue — same per-tenant FIFO List, same read-then-delete consumer contract — but carries engagement heartbeats rather than tracked events.
How It Works
Feature Flag
The queue is opt-in per tenant via the SdkHeartbeat Pennant feature flag. The
same flag also surfaces the heartbeat configuration to the SDK, so enabling it
both starts the client loop and opens the queue.
Active Time Tracking
When the SDK loads, it fires a page_view event and starts a timer. While the
page is visible it measures the cumulative active time — the time the page
is in the foreground, excluding any stretch it spends backgrounded — and POSTs
the running total (whole seconds) to /api/heartbeat every few seconds.
- Each beat carries the page_view
event_idand the runningactive_secondstotal, so the latest beat for an event id supersedes earlier ones. - Beats are sent with
keepaliveso the final beat survives the page being backgrounded or unloaded. - When the page is hidden (backgrounded or unloading), the SDK flushes one final beat with the accumulated time and pauses. Time spent hidden is not counted.
- When the page becomes visible again, the timer resumes and active time keeps accruing from where it left off.
- On SPA navigation a new
page_viewfires and the active-time counter resets for the new page. - Beats that round to 0 active seconds are not sent — a sub-second visit (a
quick reload, a prefetch/bot, or a tab opened and immediately hidden) carries
no engagement signal, and the
page_viewevent already records the load. So every queued heartbeat hasactive_seconds≥ 1.
Heartbeat Ingestion
Each accepted heartbeat is pushed onto the tenant's heartbeat queue with a server-stamped receipt timestamp. Each tenant has its own isolated queue.
Reading Heartbeats (get)
Consumers call GET /api/v1/platform/heartbeats to read up to 10,000 heartbeats
from the front of the queue. This is a non-destructive read — heartbeats
remain in the queue until you explicitly delete them.
Deleting Heartbeats (delete)
After processing the heartbeats, call
DELETE /api/v1/platform/heartbeats?count=N where N is the count value
returned by the preceding GET. Passing the exact count prevents a race
condition: any heartbeats that arrived between the GET and the DELETE are
left untouched and will be returned on the next GET.
API Endpoints
All endpoints are under the /api/v1/platform prefix and require Sanctum
authentication (auth:sanctum).
GET /api/v1/platform/heartbeats
Read up to 10,000 heartbeats from the front of the queue. Non-destructive —
heartbeats are not removed until DELETE is called.
If the SdkHeartbeat feature flag is not active for the current tenant, returns
an empty result.
Response 200 OK:
{
"data": {
"count": 2,
"heartbeats": [
{
"event_id": "9b5c1f2e-9c3a-4f1b-8a2d-1e2f3a4b5c6d",
"active_seconds": 5,
"received_at": "2026-02-25T18:00:05+00:00"
},
{
"event_id": "9b5c1f2e-9c3a-4f1b-8a2d-1e2f3a4b5c6d",
"active_seconds": 10,
"received_at": "2026-02-25T18:00:10+00:00"
}
]
}
}
Empty queue response:
{
"data": {
"count": 0,
"heartbeats": []
}
}
Heartbeat Payload
Each entry in the queue is the exact object stored at ingestion time — there is no enrichment or lookup.
| Field | Type | Description |
|---|---|---|
event_id | string | UUID of the page_view event this heartbeat belongs to. Repeated across beats; the latest wins. |
active_seconds | integer | Cumulative active time on the page in whole seconds. Excludes time the page was backgrounded. |
received_at | string | ISO 8601 timestamp stamped server-side when the beat was received. |
Multiple beats share the same event_id as active time grows. To compute final
engagement per page view, keep the largest active_seconds (equivalently,
the latest received_at) for each event_id.
DELETE /api/v1/platform/heartbeats
Remove exactly count heartbeats from the front of the queue (oldest first).
The count parameter is required and must match the count returned by the
preceding GET call — this prevents accidentally deleting heartbeats that
arrived after the read.
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
count | integer ≥ 1 | Yes | Number of heartbeats to remove. Use the count value from the GET response. |
Response 204 No Content — heartbeats removed.
Response 422 Unprocessable Content — count is missing or invalid.
Recommended Consumer Pattern
loop:
GET /api/v1/platform/heartbeats
if count == 0 → sleep, continue
process each heartbeat (keep the largest active_seconds per event_id)
DELETE /api/v1/platform/heartbeats?count={count}
Read all heartbeats first, process them, then delete — passing the exact count
from the GET response. Because GET is non-destructive, heartbeats are safe
to re-read if your consumer needs to retry before issuing the DELETE. Using
the explicit count ensures heartbeats that arrived between your GET and
DELETE are never accidentally removed.
Configuration
| Config key | Default | Description |
|---|---|---|
eventqueue.redis_connection | default | Laravel Redis connection to use for the heartbeat queue. |