Metrics
What every number in Earnback means, where it comes from and what it can’t show. The code is in src/shared/metrics/ and src/shared/plans.ts.
Sources of truth
| Question | Answered by | Never by |
|---|---|---|
| What did I spend? | the ad platforms (and your CSV files) | |
| How many players did I get, and from where? | your analytics (GA4 first-user source) | the platforms’ own install or conversion counts |
| Do they play and come back? | your analytics (funnel events, active users) | |
| What did they earn? | AdMob (totals), GA4 (per first-open cohort) |
The platforms’ own conversion counts are shown in grey on the Campaigns page, for reference within one platform. Each platform counts differently (modelled, view-through, its own attribution window), so they are never added up or compared across channels.
Filters
Period, app, channel, campaign, country, and day or week buckets. Not every dataset knows every dimension:
| Dataset | App | Channel | Campaign | Country |
|---|---|---|---|---|
| Spend (Google Ads) | ✓ | ✓ | ✓ | – |
| Spend (Meta, TikTok, CSV with a country column) | ✓ | ✓ | ✓ | ✓ |
| Creatives, placements | ✓ | ✓ | ✓ | – |
| Players, funnel, retention, cohort revenue (GA4) | ✓ | ✓ | – | ✓ |
| Ad network revenue (AdMob) | ✓ | – | – | ✓ |
When a filter can’t be honoured, a total ignores it and says so (“can’t be split by campaign”), and a ratio of two numbers shows “–” with the reason. Earnback never divides a filtered number by an unfiltered one. Analytics can’t see campaigns, so cost per player per campaign isn’t available; per channel it is.
Spend and delivery
- Spend: what the platform charged, in the account’s currency. Currencies are not converted.
- Impressions, taps: as the platform counts them. Taps are clicks on Google, link clicks on Meta, clicks on TikTok.
- Complete through: the last day a source considers final. Spend platforms are complete through yesterday; GA4 through two days ago. Today is always partial.
Players
- New players: players whose first open falls in the period (GA4
first_open, counted bytotalUsers), by first-open day. - Channel: the player’s first-user source / medium, mapped by the channel rules (ga4.md). Unattributable players go to Unknown, unmapped sources to Other, and the report lists them.
- Funnel step rate: players of the period’s cohorts who logged the step’s event (at any time since their first open), ÷ new players. Recent cohorts still grow.
- Retention day N: players active again exactly N days after their first open, ÷ the cohort’s players. A cohort counts only once day N has passed.
- Players from: if your analytics started later than your ads (a new SDK, a switch of provider), spend before its first day with players has nobody to set against it. The Overview says so and offers to start the range on that day; the digest leaves those days out of cost per player and tracking alerts.
Cost per new player
Players come from analytics, which misses players who decline consent and can’t attribute every install. So cost per player is given as a range:
- Low (floor): spend ÷ every new player in the period, paid or not. The real cost can’t be lower.
- High (ceiling): spend ÷ players credited to paid channels. The real cost can’t be higher, unless analytics misses paid players entirely.
When no player is credited to a paid channel yet, the tile shows “at least” the floor. With a channel filter it is that channel’s spend ÷ that channel’s players, an upper bound for the same reason.
Revenue
- Ad revenue (network): what AdMob reports as estimated earnings, per day. Covers every player; can’t be split by channel or campaign. Finalised by AdMob after the month ends.
- Cohort revenue: ad revenue (impression-level,
totalAdRevenue) and purchase revenue (purchaseRevenue) from GA4, assigned to the player’s first-open day and channel. This is what payback is built from. It needs impression-level ad revenue linked to Firebase. - Revenue per player: cohort revenue ÷ new players of those cohorts.
Earned back and payback
- Earned back: cohort revenue so far from players who started in the period ÷ spend in the period. On the Overview without a channel filter, it counts paid channels’ players only. It keeps growing while those players play; read it together with the cohort’s age.
- Payback curve: cumulative revenue per player at each day since first open (day 0, 1, …), over the cohorts in the period. A cohort contributes to day N only once day N has passed, so the right end of the curve rests on fewer, older cohorts. The Revenue page draws each paid channel’s cost per player as a dashed line; where the curve crosses it, that channel’s players have paid for themselves.
Creatives
- Hook rate: video views ÷ impressions. Meta counts 3-second plays, TikTok 2-second views. Compare within one channel.
- Hold rate: completions ÷ video views. Meta counts ThruPlays (to the end, or 15 seconds on longer videos), TikTok plays to 100%.
- Tap rate: taps ÷ impressions.
- Google App campaign assets carry Google’s performance label (Best, Good, Low, Learning) instead of view counts. One impression counts for a video, a headline and a description at once, so compare assets within a kind.
Plans
A plan is a test with campaigns, a period, budget limits, pass marks and review dates. Everything is evaluated over the plan’s period up to today, and filters don’t apply.
Budgets: spend of each campaign inside the period, against expected (where it should land), hardLimit (the platform’s own cap: lifetime budget, or twice the daily budget on Google) and approved (what you said yes to). An unset limit never alerts.
Pass marks, per channel:
| Metric | Value | Waits for |
|---|---|---|
costPerPlayer |
spend ÷ players credited to the channel | minPlayers |
stepRate |
players reaching step ÷ new players |
minPlayers |
retention |
back on day day ÷ players |
minPlayers (counted over cohorts whose day has passed) |
earnedBack |
cohort revenue ÷ spend | minPlayers |
hookRate, holdRate, tapRate |
as above, from the channel’s creatives | minImpressions |
Verdicts: too early until the sample is big enough; on track within min / max; below outside them; stop past stopAbove / stopBelow. A stop verdict or a budget over its limit is an alert (Overview, digest, and exit code 1 from earnback check).
Time zones
Each platform reports days in its account’s time zone; the config’s timeZone decides “today”. Earnback doesn’t shift platform days, so a Google day and a GA4 day can be offset by a few hours if their time zones differ. Set your ad accounts and analytics property to the same time zone where you can.