Campaign report
Click any campaign in the list. The report is the same for wizard campaigns, transactional sends and A/B tests; the A/B table simply appears when there is one.

Header
Name, status badge, sent time, a one-line preview of the default locale's copy, and Download events CSV — see Export.
While the status is sending the page polls every few seconds; the counters move as the workers report.
Stat tiles
| Tile | Definition |
|---|---|
| Sent | Pushes the transport accepted. Underneath: how many were held back by a delivery rule. |
| Delivered | Devices that reported delivered via the SDK. Underneath: delivery rate = delivered / sent. |
| Opens (open rate) | opened / delivered — not / sent. A push that never arrived cannot be opened; dividing by sent understates every campaign. |
| Clicks (click rate) | clicked / delivered. clicked means an action button; a tap on the body is opened. |
Funnel
Sent → delivered → opened → clicked as bars, with the count and the rate against the previous stage. A line under it counts failures.
Hourly
Events per hour in UTC across the campaign's life. Sends cluster in the first minutes; deliveries trail as devices come online; opens spread over hours. For a user's local time campaign the send line is spread across the day, one bump per timezone.
A/B test table

One row per variant: sent, delivered (+ rate), open rate, click rate. The row marked winner is the one you picked. The header says how much of the audience was in the test (sampleRate) and, if a remainder was held back, offers Send to the rest — one click sends the winning copy to exactly the subscribers who were not in the sample, as a new campaign linked from here. Details in A/B testing.
Failure reasons
The transport's own codes, counted:
| Code | Platform | Permanent? | Meaning |
|---|---|---|---|
Unregistered, BadDeviceToken, DeviceTokenNotForTopic | iOS | ✅ | Token dead or for another app. Subscription invalidated. |
UNREGISTERED, SENDER_ID_MISMATCH, INVALID_ARGUMENT | Android | ✅ | Token dead / belongs to another Firebase project. Invalidated. |
404, 410 | Web | ✅ | Subscription gone. Invalidated. |
413 | Web | ✅ | Payload over 4 KB for that endpoint. |
ServiceUnavailable, TooManyRequests, UNAVAILABLE, INTERNAL, 429, 503 | any | ❌ | Retried up to PUSH_MAX_ATTEMPTS with backoff; counted here only if all attempts failed. |
InvalidProviderToken, PERMISSION_DENIED, TopicDisallowed | any | credential | Your key/service account is wrong. Whole platform fails at once — re-check Credentials. |
Skip reasons
quiet_hours and frequency_cap counts. With quiet hours set to delay rather than skip, deferred pushes are not skips; they show up as sends later in the hourly chart.
Platform breakdown
Sent / delivered / opened / clicked / failed / skipped and open rate per platform. iOS delivery tracking needs the Notification Service Extension; without it iOS delivered stays at zero while opened still counts — the tell-tale sign of a missing NSE.
Export
Download events CSV streams every event of the campaign: ts, type, platform, userId, subscriptionId, reason, code, invalidated. Tokens and endpoints are never exported — the projection does not even read them. The download is a route handler on the dashboard server, so the admin token stays server-side.
Users and subscriptions have the same export on their own pages.