Skip to main content

Analytics

Events

EventWritten byWhen
sentworkerthe transport accepted the push (APNs 200, FCM 200, push service 201)
failedworkerthe transport rejected it after all retries; meta.code = provider code, meta.invalidated = whether the subscription was killed
skippedworkera delivery rule held it back; meta.reason = frequency_cap / quiet_hours
deliveredSDKiOS NSE / Android handler / service worker push
openedSDKbody tapped
clickedSDKaction button tapped
dismissedSDKswiped away — Android and web only; iOS does not report it

Events are stored in a MongoDB time-series collection (ts, campaignId meta, seconds granularity) with a TTL of EVENT_RETENTION_DAYS (180). Each event also increments the campaign's stats counters (and stats.variants.<id> for A/B), so reports never scan the collection for totals.

How the SDK pings arrive

Every push carries a tracking id:

base64url("appId|campaignId|userId|subscriptionId|platform|variant") . hmac16

POST /v1/e/<d|o|c|x>/<msgId> verifies the HMAC, writes the event, bumps the counters and emits a webhook — no database lookup, no API key. Rate-limited per IP.

Rates

RateFormulaWhy
Delivery ratedelivered / sentOf what the provider accepted, how much reached a device.
Open rateopened / deliveredA push that never arrived cannot be opened. Dividing by sent — as many tools do — understates every campaign and hides delivery problems inside engagement numbers.
Click rateclicked / deliveredButtons only.

null (shown as ) when the denominator is zero.

Where the numbers show

PlaceContent
App overviewactive subscribers by platform, 7-day sent/delivered/failed, 30-day growth, recent campaigns
Campaign reporttiles, funnel, hourly series, A/B table, failure codes, skip reasons, platform split
GET /v1/campaigns/:id/statsstats + rates for your own dashboards
Webhooksevery event in near real time, to your warehouse

Platform caveats

PlatformdelivereddismissedNotes
iOSneeds the Notification Service ExtensionWithout the NSE, iOS delivery rate reads 0 % while opens still count.
AndroidData-only messages make this possible; a force-stopped app cannot report.
WebThe service worker reports both. Safari has no buttons, so no clicked.

Export

  • Per campaign: Download events CSVts, type, platform, userId, subscriptionId, reason, code, invalidated.
  • Users and subscriptions: CSV from their list pages, honouring the current filter.
  • Tokens and endpoints are never in any export.

For everything else, the events collection is yours to query ({ appId, campaignId, type, ts } is the shape); or stream it out with webhooks as it happens.