Skip to main content

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_id and the running active_seconds total, so the latest beat for an event id supersedes earlier ones.
  • Beats are sent with keepalive so 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_view fires 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_view event already records the load. So every queued heartbeat has active_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.

FieldTypeDescription
event_idstringUUID of the page_view event this heartbeat belongs to. Repeated across beats; the latest wins.
active_secondsintegerCumulative active time on the page in whole seconds. Excludes time the page was backgrounded.
received_atstringISO 8601 timestamp stamped server-side when the beat was received.
Latest beat wins

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:

ParameterTypeRequiredDescription
countinteger ≥ 1YesNumber of heartbeats to remove. Use the count value from the GET response.

Response 204 No Content — heartbeats removed.

Response 422 Unprocessable Contentcount is missing or invalid.


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 keyDefaultDescription
eventqueue.redis_connectiondefaultLaravel Redis connection to use for the heartbeat queue.