Tiny Knightfall · source: GA4 export tiny-knightfall-idle-rpg-game.analytics_538412813 ·
weeks run Friday to Thursday · generated 2026-10-02 · revenue is gross (VAT-inclusive outside the US, before refunds) ·
per-version tables cover 09-18 to 10-01 and exclude China unless stated.
Weekly revenue $710, against an average of $1,445 for the weeks of 4 and 11 September (-51%). IAP $597, ads $113. DAU 560 (previously 861); new installs excluding China 926 (previously 1,824).
The decline is driven primarily by lower install volume following the end of several UA campaigns, and secondarily by the removal of the no-ads sale packs. Day-1 retention fell for installs from 25 September, driven by non-Tier-1 campaign traffic rather than the build. The Lucky Machine event (16–25 September) generated $92 in pack sales and shows several reward-grant defects. The table below lists each issue identified, the builds affected, the supporting figures and a recommended action. Each section that follows contains the underlying data and the query used.
| # | area | issue | versions | evidence | recommended action |
|---|---|---|---|---|---|
| 1 | Retention | Day-1 retention fell for installs from 25 September | v33 and v34 alike | D1 20.9% for installs 25–30 Sep against 25.9–26.6% in the three prior weeks. The drop is in non-Tier-1 traffic (26.2% → 19.8%) and in the GA ROAS campaigns; v33 installs on the same dates retain no better than v34 (18.3% vs 22.1%). v34 players do progress less on day 0 (42.2% vs 51.4% reach stage 10). | Review GA campaign targeting and creative changes around 25 September. Re-check v34 day-0 progression next week. |
| 2 | UA | Recent ROAS spikes are individual players | — | GA ROAS Global, installs 28–30 Sep: ROAS 7.40% → 46.66% at D1. One Swedish player (AF 1790634583457-2090850799405754834) accounts for $71.71 of $124.98 (57%), mostly the $29.99 permanent no-ads pack; without that player D3 ROAS is 21.7%. GA ROAS Tier1 15-09, installs 24 Sep: one US player is 93% of the cohort's revenue. | Read campaign ROAS on these cohorts as single-player outcomes, not a campaign change. Reconcile AppsFlyer ad revenue against AppLovin MAX user-level revenue for the 24 Sep player. |
| 3 | Live event | Lucky Machine: ad tickets granted without an ad | All event builds (v28–v34) | Outside China, 212 of 245 ad-ticket grants (87%) have no watch-ad tap, ad load or impression from 60 seconds before to 30 seconds after the grant; 28 follow an impression on the event placement. Grants arrive in bursts of ~4.5 per user-day, a median 1.2 s apart. China (ads unavailable): 533 grants, none preceded by an impression. | Grant the ticket only from the rewarded-ad completion callback, one ticket per completed ad. |
| 4 | Live event | Lucky Machine: check-in reward claimed twice | v28, v29, v32, v33, v34 | The check-in claims every day elapsed so far, so on event day k a player can hold at most k claims. 41 users exceeded that limit by 203 claims (1,015 tickets, 8.7% of all check-in claims). On 8 of the 10 event days the per-user maximum is exactly 2k (day 5: 10, day 8: 16, day 10: 20); duplicate claims arrive within ~2 seconds. | Make the claim idempotent (persist the claimed day before granting; disable the button while the claim is processed). |
| 5 | Live event | Lucky Machine: bulk ticket credit to one account | v29 | One user (Australia, v29, installed 17 Sep) received 222 tickets logged as earn position play_event_lucky_machine in two bursts (80 on 17 Sep, 142 on 18 Sep), with a median of 1 ms between credits; a spin takes several seconds, so these are not spin prizes. The account bought nothing, earned 15 check-in tickets, records no spins, and still reached star milestone 5 (the only other user at that milestone needed 104 spins). | Review the code paths that credit EVENT_LUCKY_MACHINE_ITEM with this position; check the account for a modified client. |
| 6 | Live event | Lucky Machine: spin results and purchased tickets are not logged | All event builds | 6,915 spins (ex-China) produce a spend but no earn for the prize. 8 event-pack purchases produce no earn with the SKU as position, so ticket and reward delivery for paid packs cannot be verified. | Log each prize as earn (position play_event_lucky_machine) and each pack grant with the SKU as position. |
| 7 | Live event | Lucky Machine: limited reach and conversion | v28–v34 | 1,522 of 3,753 active users (41%) were on builds without the event. 213 users spun (9.5% of eligible). Event pack: 8 purchases, $91.72. Ticket packs: 0 purchases. | Gate future events on a forced-update floor; review ticket pack pricing against the free ticket supply. |
| 8 | Analytics | Generic ad_reward event over-counts rewards on some builds (no rewards are missing an ad) | v26.2, v32, v33 | Every placement reward (ad_rewarded_<placement>) was checked against the user's impressions from 120 s before to 30 s after: 4,158 of 4,168 (99.8%) have a matching ad, on every build, placement and country. Placement rewards per impression are 1.00–1.01 on all builds. The generic ad_reward event (fixed AdMob unit id) fires 1.38× (v26.2), 4.67× (v32), 1.68× (v33) per impression, against 1.02× on v34. | No gameplay fix needed. Use ad_rewarded_<placement> as the reward record; treat ad_reward as unreliable for counting rewards on the listed builds. |
| 9 | Ad monetisation | Unbounded ad-load retries when AppLovin cannot be reached | v32, v33, v34 | 17,313 failed loads this week. 32.4% are Unable to resolve host "ms.applvn.com" (88 users, mainly Sudan, Iran and other markets where AppLovin is unreachable); 25.2% resolve to 0.0.0.0, the signature of a DNS ad blocker (14 users, median 151 failed loads each, max 1,251). Rewarded fill itself has improved (59% on v29 → 76% on v34). | After a host-resolution failure, stop requesting ads for the session (or back off exponentially with a cap) and show an "ads unavailable" state instead of a watch-ad button that cannot succeed. |
| 10 | Analytics | Largest group of failed ad loads has no error message | v32, v33, v34 | 39.2% of failed loads (6,788 from 24 users, mainly Iran) are logged with no error value, so their cause cannot be determined. | Always populate error on ad_request_status failures (SDK error code and message). |
| 11 | Stability | Fatal exception rate improving with each build | v32 → v34 | Share of new installs with a fatal app_exception in the first two days: 4.18% (v32) → 2.44% (v33) → 0.98% (v34). Exception logging starts in v30. | No action beyond continued monitoring; attach the exception type to app_exception (see Analytics). |
| 12 | IAP | No-ads sale packs removed | v32, v33, v34 | 0 .sale taps, purchases and grants on v32–v34. Full-price no-ads taps 18 / 47 / 51 resulted in 1 / 3 / 5 purchases. Estimated net impact −$0.07 to −$0.09 per new install excluding the top payer (≈ −$65–85 per week). | No client change required. Pricing decision pending. |
| 13 | Distribution | Unofficial v29 distribution in China | v29 | 820 users, 695 installs, 30.2 min per user. 89 shop taps, 0 purchases, 20 ad impressions ($0.08), rewarded fill 27.8%. | Excluded from KPIs. Consider blocking unofficial builds by installer source or signature check. |
| 14 | Analytics | 18-hour gap in event collection | All builds | Event volume falls to near zero between 09-28 09:00 and 09-29 03:00 UTC (7 users at 10:00, 1 event at 18:00). | Confirm whether a backend outage occurred in this window. |
| 15 | Analytics | New events lack parameters | v28, v29, v30+ | event_slot_claim_stars_0…_13 are 14 separate event names with no parameters. ad_rewarded_rewareded_mini_event has no placement or value, and its name contains a typo. app_exception (70 events) carries only fatal and timestamp. | Use one event with a slot parameter. Add placement and a corrected name to the mini-event ad event. Attach the exception type or Crashlytics issue ID to app_exception. |
Day-1 retention fell to 20.9% for installs from 25–30 September, from 25.6–26.6% in the three prior weeks.
Share of new installs (excluding China) active on the calendar day after install. Installs of 27 September are excluded because their day 1 falls in the 28 September collection gap.
| install period | installs | D1 % |
|---|---|---|
| 09-04..10 | 1,958 | 25.9 |
| 09-11..17 | 1,689 | 26.6 |
| 09-18..24 | 1,123 | 25.6 |
| 09-25..30 | 679 | 20.9 |
The decline is concentrated outside Tier 1: 26.2% → 19.8%, against 24.7% → 23.1% in Tier 1.
Tier 1 = US, Canada, UK, Western/Northern Europe, Japan, South Korea, Taiwan, Hong Kong, Singapore, Australia, New Zealand.
| tier | D1 % 09-04..10 | D1 % 09-11..17 | D1 % 09-18..24 | D1 % 09-25..30 | installs 09-04..10 | installs 09-11..17 | installs 09-18..24 | installs 09-25..30 |
|---|---|---|---|---|---|---|---|---|
| Rest | 25.6 | 25.4 | 26.2 | 19.8 | 1,398 | 987 | 702 | 450 |
| Tier 1 | 26.8 | 28.3 | 24.7 | 23.1 | 560 | 702 | 421 | 229 |
By campaign, the GA ROAS campaigns and unattributed installs account for the decline; organic installs are within their normal range.
Campaign from AppsFlyer attribution (about half of installs resolve to a campaign; the rest are unattributed). Cells with fewer than 25 installs are omitted.
| campaign | D1 % 09-04..10 | D1 % 09-11..17 | D1 % 09-18..24 | D1 % 09-25..30 | installs 09-04..10 | installs 09-11..17 | installs 09-18..24 | installs 09-25..30 |
|---|---|---|---|---|---|---|---|---|
| (unattributed) | 20.8 | 21.1 | 18 | 16 | 969 | 882 | 501 | 324 |
| FB_TinyKnightfall_A_CPA_Global_14082026 | 32.6 | 31.1 | – | – | 184 | 45 | – | – |
| FB_TinyKnightfall_A_ROAS_Tier1_29082026 | – | 32.6 | – | – | – | 89 | – | – |
| GA_TinyKnightfa_A_ROAS_Global_20072026 | 28 | 34.9 | 33 | 24.2 | 418 | 312 | 309 | 182 |
| GA_TinyKnightfa_A_ROAS_Tier1_150926 | – | 48.4 | 30.9 | 23.1 | – | 62 | 162 | 52 |
| GA_TinyKnightfa_A_ROAS_Tier1_29082026 | 39.8 | 34.8 | – | – | 181 | 141 | – | – |
| GA_TinyKnightfa_A_ROAS_Tier1_29092026 | – | – | – | 38.2 | – | – | – | 34 |
| Organic | 27.9 | 20.9 | 30.7 | 24.1 | 183 | 158 | 150 | 87 |
On the same install dates, v33 retains no better than v34 (18.3% vs 22.1%), so the build does not explain the decline.
v34 became the main install build on 25 September, the same day the decline starts, so build and date are confounded in the weekly view. Restricting to installs from 25–30 September removes the date effect. One difference remains in the first session: v34 players progress less on day 0 — 42.2% reach stage 10 against 51.4% on v33, and 32.8% play under one minute against 25.4%. The v33 sample for these dates is 138 installs, so this is a watch item for next week rather than a confirmed regression.
| tier | install_ver | installs | D1 % |
|---|---|---|---|
| Rest | 33 | 83 | 20.5 |
| Rest | 34 | 324 | 19.8 |
| Tier 1 | 33 | 43 | 14 |
| Tier 1 | 34 | 178 | 26.4 |
| build | installs | d1_pct | pct_started_stage_1 | pct_cleared_1_stage | pct_cleared_5_stages | pct_cleared_10_stages | pct_cleared_20_stages | median_minutes_24h | pct_under_1_min | sessions_24h | pct_crash_24h |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 33 | 138 | 18.3 | 71 | 70.3 | 56.5 | 51.4 | 36.2 | 8.5 | 25.4 | 1.5 | 5.1 |
| 34 | 604 | 22.1 | 66.6 | 61.3 | 49.5 | 42.2 | 25.3 | 4.7 | 32.8 | 1.6 | 0.990 |
-- Same question restricted to installs 2026-09-25 .. 09-30, so v33 and v34 are compared on the same dates
-- Installs 2026-09-18 .. 2026-09-30, ex-China. Milestones measured within the first 24 h after first_open;
-- day-1 return = any event on the next calendar day (installs whose next day is 09-28, the collection gap, excluded).
WITH ev AS (
SELECT user_pseudo_id u, event_name e, event_timestamp ts, PARSE_DATE('%Y%m%d', event_date) d,
app_info.version v, geo.country country,
(SELECT value.int_value FROM UNNEST(event_params) WHERE key='level') lvl,
(SELECT value.int_value FROM UNNEST(event_params) WHERE key='engagement_time_msec') eng
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE _TABLE_SUFFIX BETWEEN '20260918' AND '20261001'
),
inst AS (
SELECT u, ARRAY_AGG(STRUCT(ts, d, v, country) ORDER BY ts LIMIT 1)[OFFSET(0)] i
FROM ev WHERE e = 'first_open' GROUP BY u
),
per AS (
SELECT inst.u, inst.i.v build, inst.i.d install_date,
COUNTIF(ev.e = 'session_start' AND ev.ts <= inst.i.ts + 86400000000) sessions_24h,
SUM(IF(ev.e = 'user_engagement' AND ev.ts <= inst.i.ts + 86400000000, ev.eng, 0)) / 60000 minutes_24h,
MIN(IF(ev.e = 'level_start', ev.ts, NULL)) first_level_start_ts,
MAX(IF(ev.e = 'level_win' AND ev.ts <= inst.i.ts + 86400000000, ev.lvl - 1, 0)) stages_cleared_24h,
MAX(IF(ev.e = 'level_start' AND ev.lvl = 1, 1, 0)) started_stage_1,
COUNTIF(ev.e = 'app_exception' AND ev.ts <= inst.i.ts + 86400000000) crashes_24h,
COUNTIF(ev.e = 'purchase_click' AND ev.ts <= inst.i.ts + 86400000000) shop_taps_24h,
COUNTIF(ev.e = 'show_ad_click' AND ev.ts <= inst.i.ts + 86400000000) ad_taps_24h,
MAX(IF(DATE_DIFF(ev.d, inst.i.d, DAY) = 1, 1, 0)) returned_d1
FROM inst JOIN ev USING (u)
WHERE inst.i.country != 'China' AND ev.ts >= inst.i.ts AND inst.i.d BETWEEN '2026-09-25' AND '2026-09-30'
GROUP BY 1, 2, 3
)
SELECT build,
COUNT(*) installs,
ROUND(100 * AVG(IF(install_date <= '2026-09-30' AND install_date != '2026-09-27', returned_d1, NULL)), 1) d1_pct,
ROUND(100 * AVG(started_stage_1), 1) pct_started_stage_1,
ROUND(100 * COUNTIF(stages_cleared_24h >= 1) / COUNT(*), 1) pct_cleared_1_stage,
ROUND(100 * COUNTIF(stages_cleared_24h >= 5) / COUNT(*), 1) pct_cleared_5_stages,
ROUND(100 * COUNTIF(stages_cleared_24h >= 10) / COUNT(*), 1) pct_cleared_10_stages,
ROUND(100 * COUNTIF(stages_cleared_24h >= 20) / COUNT(*), 1) pct_cleared_20_stages,
ROUND(APPROX_QUANTILES(minutes_24h, 2)[OFFSET(1)], 1) median_minutes_24h,
ROUND(100 * COUNTIF(minutes_24h < 1) / COUNT(*), 1) pct_under_1_min,
ROUND(AVG(sessions_24h), 2) sessions_24h,
ROUND(100 * COUNTIF(crashes_24h > 0) / COUNT(*), 2) pct_crash_24h
FROM per
GROUP BY build
HAVING installs >= 50
ORDER BY buildThe fatal exception rate is improving with each build: 4.18% (v32) → 2.44% (v33) → 0.98% (v34) of new installs.
Share of new installs logging a fatal app_exception in their first two days. Exception logging starts in v30, so earlier builds cannot be compared.
| install_ver | first_install | last_install | installs | d1_eligible | d1_pct | median_d0_minutes | pct_under_1_min_d0 | median_d0_stages | installs_with_fatal_crash_d0d1 | pct_with_fatal_crash |
|---|---|---|---|---|---|---|---|---|---|---|
| 29 | 09-17 | 10-01 | 738 | 731 | 27.4 | 4.4 | 31.4 | 4 | 0 | 0 |
| 32 | 09-21 | 10-01 | 239 | 237 | 25.3 | 4.9 | 35.1 | 3 | 10 | 4.2 |
| 33 | 09-23 | 10-01 | 328 | 315 | 26 | 4.3 | 34.8 | 4 | 8 | 2.4 |
| 34 | 09-25 | 10-01 | 713 | 502 | 22.1 | 4.4 | 33.7 | 3 | 7 | 0.980 |
-- New installs since 2026-09-11 by the build they installed on (ex-China):
-- day-1 return, first-day play time and progress, fatal crashes
WITH x AS (
SELECT user_pseudo_id u, event_name ev, event_timestamp ts,
PARSE_DATE('%Y%m%d', event_date) d, app_info.version v, geo.country country,
(SELECT value.int_value FROM UNNEST(event_params) WHERE key='engagement_time_msec') eng,
(SELECT value.int_value FROM UNNEST(event_params) WHERE key='level') lvl,
(SELECT value.int_value FROM UNNEST(event_params) WHERE key='fatal') fatal
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE _TABLE_SUFFIX BETWEEN '20260911' AND '20261001'
),
inst AS (
SELECT u, ARRAY_AGG(STRUCT(d, v, country) ORDER BY ts LIMIT 1)[OFFSET(0)] i
FROM x WHERE ev = 'first_open' GROUP BY u
),
per AS (
SELECT inst.u, inst.i.v install_ver, inst.i.d install_date,
MAX(IF(DATE_DIFF(ev.d, inst.i.d, DAY) = 1, 1, 0)) d1,
SUM(IF(ev.d = inst.i.d AND ev.ev = 'user_engagement', ev.eng, 0)) / 60000 d0_minutes,
MAX(IF(ev.d = inst.i.d AND ev.ev = 'level_win', ev.lvl - 1, 0)) d0_stages_cleared,
COUNTIF(ev.ev = 'app_exception' AND ev.fatal = 1 AND DATE_DIFF(ev.d, inst.i.d, DAY) <= 1) crashes_d0d1
FROM inst JOIN x AS ev USING (u)
WHERE inst.i.country != 'China'
GROUP BY 1, 2, 3
)
SELECT install_ver,
MIN(install_date) first_install, MAX(install_date) last_install,
COUNT(*) installs,
-- D1 only for installs whose D1 is observable and not the 09-28 data gap
COUNTIF(install_date <= '2026-09-30' AND install_date != '2026-09-27') d1_eligible,
ROUND(100 * AVG(IF(install_date <= '2026-09-30' AND install_date != '2026-09-27', d1, NULL)), 1) d1_pct,
ROUND(APPROX_QUANTILES(d0_minutes, 2)[OFFSET(1)], 1) median_d0_minutes,
ROUND(100 * COUNTIF(d0_minutes < 1) / COUNT(*), 1) pct_under_1_min_d0,
ROUND(APPROX_QUANTILES(d0_stages_cleared, 2)[OFFSET(1)], 0) median_d0_stages,
ROUND(100 * COUNTIF(d0_stages_cleared = 0) / COUNT(*), 1) pct_cleared_no_stage_d0,
COUNTIF(crashes_d0d1 > 0) installs_with_fatal_crash_d0d1,
ROUND(100 * COUNTIF(crashes_d0d1 > 0) / COUNT(*), 2) pct_with_fatal_crash
FROM per
GROUP BY install_ver
HAVING installs >= 50
ORDER BY first_install| week from (Fri) | DAU | DAU China | installs ex-CN | installs China | IAP $ | no-ads $ | .sale $ | other IAP $ | ad $ | total $ | payers | % DAU watching an ad | eCPM $ |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 08-28 | 969 | 7 | 1,943 | 25 | 1,544 | 508 | 363 | 1,036 | 129 | 1,673 | 84 | 6.6 | 50.9 |
| 09-04 | 899 | 5 | 1,959 | 15 | 1,349 | 513 | 321 | 836 | 98.4 | 1,448 | 67 | 6.7 | 50.7 |
| 09-11 | 824 | 6 | 1,688 | 30 | 1,298 | 515 | 302 | 783 | 146 | 1,443 | 56 | 7.6 | 54.4 |
| 09-18 | 776 | 136 | 1,119 | 635 | 878 | 145 | 49.5 | 732 | 121 | 999 | 40 | 8.3 | 53.8 |
| 09-25 | 560 | 33 | 926 | 99 | 597 | 131 | 0 | 466 | 113 | 710 | 37 | 8.6 | 57.0 |
WITH ev AS (
SELECT user_pseudo_id u, PARSE_DATE('%Y%m%d', event_date) d, event_name e, event_timestamp ts,
geo.country country, app_info.version v, event_value_in_usd usd,
COALESCE((SELECT value.string_value FROM UNNEST(event_params) WHERE key='product_id'),
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='position')) sku,
(SELECT COALESCE(value.double_value, value.float_value, CAST(value.int_value AS FLOAT64))
FROM UNNEST(event_params) WHERE key='value') val,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='ad_format') ad_format,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='ad_type') ad_type,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='status') status,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='error') err,
(SELECT value.int_value FROM UNNEST(event_params) WHERE key='engagement_time_msec') eng,
(SELECT value.string_value FROM UNNEST(user_properties) WHERE key='appsflyer_id') af
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE _TABLE_SUFFIX >= '20260801'
)
SELECT u, d,
ANY_VALUE(country) country,
ARRAY_AGG(v IGNORE NULLS ORDER BY ts DESC LIMIT 1)[SAFE_OFFSET(0)] v,
MAX(af) af,
COUNTIF(e='first_open') first_opens,
SUM(IF(e='user_engagement', eng, 0)) / 60000 eng_min,
SUM(IF(e='in_app_purchase', usd, 0)) iap_usd,
COUNTIF(e='in_app_purchase') iap_buys,
SUM(IF(e='in_app_purchase' AND sku LIKE '%skip.ad%', usd, 0)) noads_usd,
COUNTIF(e='in_app_purchase' AND sku LIKE '%skip.ad%') noads_buys,
SUM(IF(e='in_app_purchase' AND sku LIKE '%skip.ad%.sale', usd, 0)) sale_usd,
COUNTIF(e='in_app_purchase' AND sku LIKE '%skip.ad%.sale') sale_buys,
COUNTIF(e='purchase_click' AND sku LIKE '%skip.ad%') noads_taps,
COUNTIF(e='purchase_click') shop_taps,
SUM(IF(e='ad_impression_MAX', COALESCE(val, 0), 0)) ad_usd,
COUNTIF(e='ad_impression_MAX') ad_imps,
COUNTIF(e='ad_impression_MAX' AND UPPER(ad_format) LIKE '%REWARD%') rew_imps,
COUNTIF(e='ad_impression_MAX' AND UPPER(ad_format) LIKE '%INTER%' AND UPPER(ad_format) NOT LIKE '%REWARD%') inter_imps,
COUNTIF(e='show_ad_click') ad_clicks,
COUNTIF(e='ad_reward') ad_rewards,
COUNTIF(e='ad_request_status' AND ad_type='rewarded') rew_req,
COUNTIF(e='ad_request_status' AND ad_type='rewarded' AND status='success') rew_ok,
COUNTIF(e='ad_request_status' AND ad_type='inter') inter_req,
COUNTIF(e='ad_request_status' AND ad_type='inter' AND status='success') inter_ok,
COUNTIF(e='ad_request_status' AND status='fail' AND err LIKE '%applvn.com%') dns_fail,
COUNTIF(e='ad_request_status' AND status='fail' AND (err LIKE '%No Fill%' OR err LIKE '%no eligible ads%')) nofill_fail,
COUNTIF(e='ad_rewarded_show_failed') rew_show_fail
FROM ev GROUP BY u, dGA ROAS Global's D1 jump from 7.40% to 46.66% comes from one Swedish player.
The table reproduces AppsFlyer's Cohort & Retention report (User acquisition · Revenue by media source · grouped by campaign · installs 28–30 Sep · ROAS, cumulative) from the same AppsFlyer data, so the figures match the dashboard. Small D2/D3 differences are late-arriving AppsFlyer revenue.
| campaign | installs | cost $ | eCPI $ | ROAS D0 | ROAS D1 | ROAS D2 | ROAS D3 |
|---|---|---|---|---|---|---|---|
| GA_TinyKnightfa_A_ROAS_Global_20072026 | 183 | 246 | 1.3 | 7.40% | 46.66% | 50.82% | 50.90% |
| GA_TinyKnightfa_A_ROAS_Tier1_29092026 | 41 | 265 | 6.5 | 3.00% | 6.65% | 6.79% | 6.79% |
| GA_TinyKnightfa_A_ROAS_Tier1_150926 | 30 | 253 | 8.4 | 17.67% | 17.77% | 17.81% | 25.72% |
Of the 68 Global-campaign users from these installs who generated any revenue, two made purchases. The Swedish player alone accounts for $71.71 of $124.98 (57%). Excluding that player, D3 ROAS would be 21.7% rather than 50.9%.
The same player appears in AppsFlyer and GA4, with matching purchases.
AppsFlyer ID 1790634583457-2090850799405754834, country SE, campaign GA_TinyKnightfa_A_ROAS_Global_20072026, install 2026-09-28 22:29:44 UTC. In GA4 the same AppsFlyer ID is attached to user_pseudo_id 78322a5caaa7ca8da1c1a9d7fafdcc41 (build 34). The install falls inside the 28 September collection gap (09:00 → 03:00 UTC), which is why GA4 has no first_open for this user. All four purchases were made on day 1 (29 September). The largest is the permanent no-ads pack at its full price ($29.99 list; $36 including Swedish VAT) — the price point that applies now that the .sale variant has been removed.
| GA4 purchase time (UTC) | pack (GA4 product_id) | GA4 $ | AppsFlyer event time | AppsFlyer $ |
|---|---|---|---|---|
| 2026-09-29 14:23 | skip.ad.permanent | 36.2 | 2026-09-29 13:58:28 | 36.0 |
| 2026-09-29 22:56 | package.hero.doom | 12.0 | 2026-09-29 22:51:46 | 11.9 |
| 2026-09-29 23:02 | pass.tiny.guide | 12.0 | 2026-09-29 22:55:06 | 11.9 |
| 2026-09-29 23:25 | membership | 12.0 | 2026-09-29 23:03:22 | 11.9 |
-- The players behind the late-September ROAS spikes, matched from AppsFlyer ID to GA4: every purchase and
-- daily ad revenue. AppsFlyer IDs come from dashboard/afdata_local/af_cohort_*_rows.csv.
WITH ids AS (
SELECT * FROM UNNEST([
STRUCT('1790634583457-2090850799405754834' AS af_id, 'Sweden · GA ROAS Global · installed 28 Sep' AS label),
STRUCT('1790717823534-6603760123075115430', 'UAE · GA ROAS Global · installed 29 Sep'),
STRUCT('1790223402902-2556825812026972686', 'US · GA ROAS Tier1 15-09 · installed 24 Sep'),
STRUCT('1790563492688-5948370201236570453', 'US · GA ROAS Tier1 15-09 · installed 28 Sep')])
),
ev AS (
SELECT user_pseudo_id u, event_name e, event_timestamp ts, PARSE_DATE('%Y%m%d', event_date) d,
geo.country country, app_info.version v, event_value_in_usd usd,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='product_id') sku,
(SELECT COALESCE(value.double_value, value.float_value, CAST(value.int_value AS FLOAT64))
FROM UNNEST(event_params) WHERE key='value') ad_value,
(SELECT value.string_value FROM UNNEST(user_properties) WHERE key='appsflyer_id') af
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE _TABLE_SUFFIX >= '20260923'
),
users AS (SELECT DISTINCT ev.u, ids.label, ids.af_id FROM ev JOIN ids ON ev.af = ids.af_id)
SELECT users.label, users.af_id, ev.u AS user_pseudo_id, ev.e AS event_name,
FORMAT_TIMESTAMP('%Y-%m-%d %H:%M', TIMESTAMP_MICROS(ev.ts)) AS event_time_utc,
ev.country, ev.v AS version, ev.sku, ROUND(ev.usd, 2) AS usd, ROUND(ev.ad_value, 4) AS ad_value
FROM users JOIN ev USING (u)
WHERE ev.e IN ('first_open', 'in_app_purchase', 'ad_impression_MAX')
ORDER BY users.label, ev.tsA second spike: one US player generated $209 of the campaign cohort's $225 from 24 September installs.
AppsFlyer cohort ROAS (revenue to date ÷ spend, by install date, all paid campaigns). The 24 September cohort stands out at 1.08x. The player installed on Thursday 24 September, the last day of the previous report week, but most of the spending happened this week: $48.89 of purchases between 25 September and 1 October in GA4 ($78.88 in AppsFlyer), 8% of this week's IAP. Purchases are spread across 9 packs, so no single pack explains the spike.
| install date | installs | spend $ | IAP $ | ad $ | ROAS to date |
|---|---|---|---|---|---|
| 09-18 | 146 | 193 | 65.1 | 34.6 | 0.52x |
| 09-19 | 441 | 217 | 52.7 | 12.4 | 0.30x |
| 09-20 | 322 | 287 | 77.7 | 43.0 | 0.42x |
| 09-21 | 205 | 429 | 59.5 | 17.6 | 0.18x |
| 09-22 | 185 | 365 | 56.9 | 25.1 | 0.22x |
| 09-23 | 175 | 324 | 8.7 | 54.1 | 0.19x |
| 09-24 | 163 | 267 | 173 | 115 | 1.08x |
| 09-25 | 192 | 226 | 35.3 | 23.7 | 0.26x |
| 09-26 | 131 | 218 | 2.0 | 15.6 | 0.08x |
| 09-27 | 126 | 277 | 60.9 | 16.1 | 0.28x |
| 09-28 | 131 | 284 | 126 | 7.8 | 0.47x |
| 09-29 | 147 | 197 | 32.2 | 19.9 | 0.26x |
| 09-30 | 103 | 282 | 5.0 | 17.2 | 0.08x |
| 10-01 | 115 | 186 | 13.2 | 9.7 | 0.12x |
| pack | purchases | USD (GA4) |
|---|---|---|
| package.event.theme.1 | 1 | 20.0 |
| daily.challenge.key.1 | 7 | 13.9 |
| skip.ad.30days | 1 | 11.0 |
| special.loot.storage | 1 | 10.0 |
| package.hero.doom | 1 | 10.0 |
| pass.tiny.guide | 1 | 10.0 |
| auto.blessing | 1 | 5.0 |
| daily.promotion | 1 | 5.0 |
| weekly.sugar.world | 1 | 5.0 |
AppsFlyer and GA4 disagree on this player's ad revenue by roughly 7×.
AppsFlyer receives ad revenue from AppLovin MAX server-side, in daily aggregated rows; GA4 receives it from the client's ad_impression_MAX event. One of the two is incomplete for this player. Until reconciled against AppLovin's user-level revenue report, treat AppsFlyer ad revenue — and therefore ROAS — for this cohort as an upper bound.
| AppsFlyer | GA4 | |
|---|---|---|
| Purchases | 16 rows, $119.84 | 15 purchases, $89.85 |
| Ad revenue | 107 rows, $88.74 | 16 impressions, $12.54 |
The slot-machine event introduced with the 15 September content update. Players earn tickets
(EVENT_LUCKY_MACHINE_ITEM) from a daily check-in and from rewarded ads, spend one ticket per spin, and
claim star milestones (event_slot_claim_stars_0 … _13). Three SKUs support it: the event pack
($9.99) and ticket packs x10 ($4.99) and x120 ($49.99). Figures exclude China unless stated.
| metric | value |
|---|---|
| Event window | 16 Sep – 25 Sep 2026 (10 days) |
| Builds that contain the event | v28, v29, v32, v33, v34 |
| Active users in window (ex-China) | 3,753 |
| … on a build without the event | 1,522 (41%) |
| … on an event build (eligible) | 2,231 |
| Users who spun at least once | 213 (9.5% of eligible) |
| Spins (ex-China) | 6,878; median 28 per spinner; top 10 users 20% of spins |
| Tickets earned (logged) vs spent | 8,379 earned, 6,878 spent, 1,501 unspent at close |
| Ticket sources | check-in 7,915 · ads 242 · spin prizes 222 · purchases not logged |
| Star milestones | 137 users claimed slot 0; 7 completed slot 13 |
| Event pack $9.99 | 42 users tapped, 8 purchases, $91.72 gross; 4 of the buyers had purchased before the event |
| Ticket packs x10 $4.99 / x120 $49.99 | 0 purchases |
| Share of event-window IAP | 6.3% of $1,457 (ex-China, 16–25 Sep) |
-- One row per non-China user active during the Lucky Machine window (2026-09-16 .. 09-25),
-- with event activity, spend before/during the event and activity after it.
WITH x AS (
SELECT PARSE_DATE('%Y%m%d', event_date) d, user_pseudo_id u, event_name ev, event_timestamp ts,
app_info.version v, geo.country country, event_value_in_usd usd,
COALESCE((SELECT value.string_value FROM UNNEST(event_params) WHERE key='position'),
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='product_id')) pos,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='currency') cur,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='ad_placement') placement,
(SELECT COALESCE(CAST(value.int_value AS FLOAT64), value.double_value) FROM UNNEST(event_params) WHERE key='volume') vol
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE _TABLE_SUFFIX BETWEEN '20260902' AND '20261001'
),
active AS (
SELECT u FROM x WHERE d BETWEEN '2026-09-16' AND '2026-09-25' GROUP BY u
HAVING LOGICAL_AND(country != 'China')
)
SELECT x.u,
ARRAY_AGG(IF(d BETWEEN '2026-09-16' AND '2026-09-25', v, NULL) IGNORE NULLS ORDER BY ts DESC LIMIT 1)[SAFE_OFFSET(0)] event_version,
ANY_VALUE(country) country,
MIN(IF(ev = 'first_open', d, NULL)) install_date,
COUNT(DISTINCT IF(d BETWEEN '2026-09-16' AND '2026-09-25', d, NULL)) event_days_active,
COUNTIF(ev = 'spend' AND pos = 'play_event_lucky_machine') spins,
COUNT(DISTINCT IF(ev = 'spend' AND pos = 'play_event_lucky_machine', d, NULL)) spin_days,
SUM(IF(ev = 'earn' AND cur = 'EVENT_LUCKY_MACHINE_ITEM' AND pos = 'check_in_event', vol, 0)) tickets_check_in,
SUM(IF(ev = 'earn' AND cur = 'EVENT_LUCKY_MACHINE_ITEM' AND pos = 'ads_lucky_machine_ticket', vol, 0)) tickets_ads,
COUNTIF(ev = 'earn' AND cur = 'EVENT_LUCKY_MACHINE_ITEM' AND pos = 'ads_lucky_machine_ticket') ad_ticket_grants,
SUM(IF(ev = 'earn' AND cur = 'EVENT_LUCKY_MACHINE_ITEM' AND pos = 'play_event_lucky_machine', vol, 0)) tickets_from_spins,
SUM(IF(ev = 'earn' AND cur = 'EVENT_LUCKY_MACHINE_ITEM' AND pos LIKE '%lucky.machine%', vol, 0)) tickets_purchased,
COUNTIF(ev = 'ad_impression_MAX' AND placement = 'rewareded_mini_event') event_ad_impressions,
COUNTIF(ev = 'ad_impression_MAX' AND d BETWEEN '2026-09-16' AND '2026-09-25') all_ad_impressions_in_window,
COUNTIF(ev = 'show_ad_click' AND d BETWEEN '2026-09-16' AND '2026-09-25') watch_ad_taps_in_window,
MAX(IF(ev LIKE 'event_slot_claim_stars_%', SAFE_CAST(REGEXP_EXTRACT(ev, '([0-9]+)$') AS INT64), NULL)) max_star_slot,
COUNTIF(ev = 'purchase_click' AND pos LIKE '%lucky.machine%') pack_taps,
COUNTIF(ev = 'in_app_purchase' AND pos LIKE '%lucky.machine%') pack_purchases,
SUM(IF(ev = 'in_app_purchase' AND pos LIKE '%lucky.machine%', usd, 0)) pack_usd,
SUM(IF(ev = 'in_app_purchase' AND d BETWEEN '2026-09-02' AND '2026-09-15', usd, 0)) iap_before_usd,
SUM(IF(ev = 'in_app_purchase' AND d BETWEEN '2026-09-16' AND '2026-09-25', usd, 0)) iap_during_usd,
COUNT(DISTINCT IF(d BETWEEN '2026-09-26' AND '2026-10-01', d, NULL)) days_active_after
FROM x JOIN active USING (u)
GROUP BY x.uRevenue per player rose during the event; total revenue fell 10% because DAU fell 24% and the no-ads sale packs were removed on 21 September.
Excluding China. The Lucky Machine pack added $91.72. Other IAP (excluding the pack and no-ads SKUs) rose from $933 to $977 despite the lower audience, i.e. +37% per DAU, and fell back to $0.118 per DAU after the event. Ad revenue per DAU continued its existing upward trend after the event, so it is not attributed to the event. Within users active in both windows on event builds, those who spun increased IAP by +12% while those who did not spin changed by -17%; the spinner group is 62 users and 9 payers, so this is indicative only.
| metric | 10 days before (06–15 Sep) | event (16–25 Sep) | 6 days after (26 Sep–1 Oct) | event vs before | change % |
|---|---|---|---|---|---|
| days | 10 | 10 | 6 | 0 | 0 |
| DAU | 875 | 668 | 502 | -208 | -23.7 |
| total revenue $ | 1,812 | 1,637 | 536 | -175 | -9.7 |
| IAP $ | 1,629 | 1,457 | 443 | -172 | -10.6 |
| Lucky Machine pack $ | 0 | 91.7 | 0 | 91.7 | – |
| no-ads $ | 696 | 388 | 87.7 | -308 | -44.3 |
| other IAP $ | 933 | 977 | 356 | 44.2 | 4.7 |
| ad $ | 183 | 180 | 92.1 | -2.8 | -1.5 |
| revenue $ per day | 181 | 164 | 89.3 | -17.5 | -9.7 |
| IAP $ per DAU | 0.186 | 0.218 | 0.147 | 0.032 | 17.2 |
| other IAP $ per DAU (excl. pack, excl. no-ads) | 0.107 | 0.146 | 0.118 | 0.040 | 37.3 |
| ad $ per DAU | 0.021 | 0.027 | 0.031 | 0.006 | 29.1 |
| payers | 81 | 58 | 30 | -23 | -28.4 |
| group | users | IAP per user, 10 days before $ | IAP per user, event $ | % paying before | % paying during event | change % |
|---|---|---|---|---|---|---|
| did not spin | 402 | 1.3 | 1.1 | 6 | 3.5 | -17 |
| spun | 62 | 3.7 | 4.1 | 17.7 | 14.5 | 11.9 |
Normal: one event ad → one ticket. Actual: the daily cap of five tickets is granted in one burst, mostly without ads.
Per user × day: ad tickets (earn · ads_lucky_machine_ticket) against event ads watched (impressions on rewareded_mini_event) and the game's own ad_rewarded_rewareded_mini_event. Users with 1–3 tickets match their ads; users with 5 tickets received them a few seconds apart with few or no ads — in China, where ads cannot load, with none at all.
| region | ad tickets per user-day | user-days | tickets | event ads watched | event ad rewards logged | median seconds, first to last ticket |
|---|---|---|---|---|---|---|
| China | 1 | 7 | 7 | 0 | 0 | 0 |
| China | 2 | 1 | 2 | 0 | 0 | 1.1 |
| China | 4 | 3 | 12 | 0 | 0 | 85.3 |
| China | 5 | 103 | 515 | 0 | 0 | 4.2 |
| rest of world | 1 | 2 | 2 | 1 | 1 | 0 |
| rest of world | 2 | 2 | 4 | 4 | 4 | 45.5 |
| rest of world | 3 | 2 | 6 | 3 | 3 | 2.7 |
| rest of world | 5 | 46 | 230 | 25 | 26 | 4.6 |
-- Lucky Machine ad tickets, time-free check, 2026-09-16 .. 2026-09-25: per user × day, tickets granted from ads
-- vs event ads actually watched (impressions on rewareded_mini_event and the game's own ad_rewarded_rewareded_mini_event).
WITH x AS (
SELECT user_pseudo_id u, event_date d, event_name ev, event_timestamp ts, geo.country = 'China' cn,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='position') pos,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='ad_placement') placement
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE _TABLE_SUFFIX BETWEEN '20260916' AND '20260925'
AND event_name IN ('earn', 'ad_impression_MAX', 'ad_rewarded_rewareded_mini_event')
),
c AS (
SELECT u, d, LOGICAL_OR(cn) cn,
COUNTIF(ev = 'earn' AND pos = 'ads_lucky_machine_ticket') tickets,
COUNTIF(ev = 'ad_impression_MAX' AND placement = 'rewareded_mini_event') event_ads,
COUNTIF(ev = 'ad_rewarded_rewareded_mini_event') event_ad_rewards,
(MAX(IF(ev = 'earn' AND pos = 'ads_lucky_machine_ticket', ts, NULL))
- MIN(IF(ev = 'earn' AND pos = 'ads_lucky_machine_ticket', ts, NULL))) / 1e6 span_s
FROM x GROUP BY 1, 2
)
SELECT IF(cn, 'China', 'rest of world') region, tickets AS tickets_per_user_day,
COUNT(*) user_days, SUM(tickets) tickets, SUM(event_ads) event_ads_watched, SUM(event_ad_rewards) event_ad_rewards,
ROUND(APPROX_QUANTILES(span_s, 2)[OFFSET(1)], 1) median_seconds_first_to_last_ticket
FROM c WHERE tickets > 0
GROUP BY 1, 2 ORDER BY 1, 287% of ad tickets outside China are granted with no ad activity beforehand.
For each ads_lucky_machine_ticket grant, the same user's ad events from 60 seconds before to 30 seconds after are classified: an impression on the event placement (expected path), an impression elsewhere, an ad show or reward without impression, a failed load, a tap only, or nothing. Grants are issued in bursts roughly one second apart, approximately the full daily allowance at once, which is not compatible with one rewarded ad per ticket (a rewarded ad lasts 15–30 seconds).
| region | what_preceded_the_grant | grants | users | share % |
|---|---|---|---|---|
| China | no ad: load failed, ticket granted anyway | 86 | 18 | 16.1 |
| China | no ad activity from 60 s before to 30 s after | 447 | 96 | 83.9 |
| rest of world | event ad shown (impression on rewareded_mini_event) | 28 | 7 | 11.4 |
| rest of world | an ad was shown, but on another placement | 2 | 1 | 0.800 |
| rest of world | ad show/reward logged, no impression | 3 | 3 | 1.2 |
| rest of world | no ad: load failed, ticket granted anyway | 16 | 4 | 6.5 |
| rest of world | no ad activity from 60 s before to 30 s after | 196 | 41 | 80 |
-- For every Lucky Machine ad-ticket grant: what ad activity did the same user log from 60 s before to 30 s after it?
-- (MAX logs ad_impression_MAX 0.02-17 s AFTER the reward callback, so the window must extend past the grant.)
-- Distinguishes "ad shown but impression not logged" from "ticket granted with no ad at all".
WITH x AS (
SELECT user_pseudo_id u, event_name ev, event_timestamp ts, geo.country = 'China' cn, app_info.version v,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='position') pos,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='ad_type') ad_type,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='status') status,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='error') err,
COALESCE((SELECT value.string_value FROM UNNEST(event_params) WHERE key='ad_placement'),
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='placement')) placement
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE _TABLE_SUFFIX BETWEEN '20260916' AND '20260925'
AND event_name IN ('earn', 'show_ad_click', 'ad_request', 'ad_request_status', 'ad_show',
'ad_impression_MAX', 'ad_reward', 'ad_rewarded_show_failed', 'ad_rewarded_rewareded_mini_event')
),
grants AS (SELECT u, ts, cn, v FROM x WHERE ev = 'earn' AND pos = 'ads_lucky_machine_ticket'),
ctx AS (
SELECT g.u, g.ts, g.cn, g.v,
COUNTIF(a.ev = 'show_ad_click') taps,
COUNTIF(a.ev = 'ad_request_status' AND a.ad_type = 'rewarded' AND a.status = 'success') rewarded_loaded,
COUNTIF(a.ev = 'ad_request_status' AND a.status = 'fail') load_failures,
COUNTIF(a.ev = 'ad_show') ad_shows,
COUNTIF(a.ev = 'ad_impression_MAX') impressions,
COUNTIF(a.ev = 'ad_impression_MAX' AND a.placement = 'rewareded_mini_event') event_impressions,
COUNTIF(a.ev = 'ad_reward') ad_rewards,
COUNTIF(a.ev = 'ad_rewarded_rewareded_mini_event') mini_event_ad_logs
FROM grants g LEFT JOIN x a
ON a.u = g.u AND a.ev != 'earn' AND a.ts BETWEEN g.ts - 60000000 AND g.ts + 30000000
GROUP BY 1, 2, 3, 4
)
SELECT IF(cn, 'China', 'rest of world') region,
CASE
WHEN event_impressions > 0 THEN '1 event ad shown (impression on rewareded_mini_event)'
WHEN impressions > 0 THEN '2 an ad was shown, but on another placement'
WHEN ad_shows > 0 OR ad_rewards > 0 THEN '3 ad show/reward logged, no impression'
WHEN load_failures > 0 THEN '4 no ad: load failed, ticket granted anyway'
WHEN taps > 0 THEN '5 no ad: watch-ad tap only'
ELSE '6 no ad activity from 60 s before to 30 s after'
END AS what_preceded_the_grant,
COUNT(*) grants, COUNT(DISTINCT u) users
FROM ctx GROUP BY 1, 2 ORDER BY 1, 2| region | grants | users | user_days | grants_per_user_day | seconds between grants (p25 / median / p75) | grants_within_15s_of_previous |
|---|---|---|---|---|---|---|
| China | 533 | 113 | 113 | 4.7 | 0.9 / 1.0 / 1.2 | 412 |
| rest of world | 245 | 53 | 54 | 4.5 | 0.9 / 1.2 / 1.6 | 168 |
-- Ad-ticket grants per user per day and the spacing between consecutive grants (all countries)
WITH g AS (
SELECT user_pseudo_id u, PARSE_DATE('%Y%m%d', event_date) d, event_timestamp ts, geo.country = 'China' cn
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE _TABLE_SUFFIX BETWEEN '20260916' AND '20260925' AND event_name = 'earn'
AND (SELECT value.string_value FROM UNNEST(event_params) WHERE key='position') = 'ads_lucky_machine_ticket'
),
s AS (SELECT *, (ts - LAG(ts) OVER (PARTITION BY u ORDER BY ts)) / 1e6 AS gap_s FROM g)
SELECT IF(cn, 'China', 'rest of world') region,
COUNT(*) grants, COUNT(DISTINCT u) users, COUNT(DISTINCT CONCAT(u, CAST(d AS STRING))) user_days,
ROUND(COUNT(*) / COUNT(DISTINCT CONCAT(u, CAST(d AS STRING))), 2) grants_per_user_day,
APPROX_QUANTILES(gap_s, 4) gap_seconds_quartiles,
COUNTIF(gap_s < 15) grants_within_15s_of_previous
FROM s GROUP BY 1| d | dau_ex_cn | spinners | % DAU spinning | spins | tickets_check_in | tickets_ads | tickets_from_spins | event_ad_impressions | star_slot0_claims | pack_taps | pack_purchases | pack_usd | china_spinners |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 09-16 | 771 | 10 | 1.3 | 174 | 60 | 30 | 0 | 0 | 3 | 5 | 0 | 0 | 0 |
| 09-17 | 745 | 26 | 3.5 | 378 | 305 | 25 | 80 | 5 | 8 | 9 | 0 | 0 | 0 |
| 09-18 | 711 | 12 | 1.7 | 206 | 310 | 20 | 142 | 0 | 7 | 8 | 0 | 0 | 0 |
| 09-19 | 623 | 26 | 4.2 | 440 | 505 | 27 | 0 | 2 | 10 | 11 | 2 | 23.4 | 63 |
| 09-20 | 635 | 38 | 6 | 944 | 1,005 | 45 | 0 | 11 | 21 | 11 | 1 | 10.0 | 46 |
| 09-21 | 601 | 25 | 4.2 | 608 | 590 | 27 | 0 | 5 | 12 | 5 | 1 | 10.0 | 22 |
| 09-22 | 635 | 20 | 3.1 | 765 | 870 | 12 | 0 | 7 | 14 | 1 | 2 | 23.1 | 12 |
| 09-23 | 654 | 35 | 5.4 | 1,224 | 1,445 | 11 | 0 | 2 | 21 | 3 | 1 | 12.6 | 8 |
| 09-24 | 634 | 28 | 4.4 | 891 | 1,260 | 20 | 0 | 1 | 18 | 3 | 1 | 12.6 | 4 |
| 09-25 | 680 | 37 | 5.4 | 1,285 | 1,615 | 28 | 0 | 3 | 21 | 6 | 0 | 0 | 3 |
-- Lucky Machine (slot machine) event, daily, 2026-09-14 .. 2026-09-27. China reported separately.
WITH x AS (
SELECT PARSE_DATE('%Y%m%d', event_date) d, user_pseudo_id u, event_name ev, geo.country = 'China' cn,
event_value_in_usd usd,
COALESCE((SELECT value.string_value FROM UNNEST(event_params) WHERE key='position'),
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='product_id')) pos,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='currency') cur,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='ad_placement') placement,
(SELECT COALESCE(CAST(value.int_value AS FLOAT64), value.double_value) FROM UNNEST(event_params) WHERE key='volume') vol
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE _TABLE_SUFFIX BETWEEN '20260914' AND '20260927'
)
SELECT d,
COUNT(DISTINCT IF(NOT cn, u, NULL)) dau_ex_cn,
COUNT(DISTINCT IF(NOT cn AND cur='EVENT_LUCKY_MACHINE_ITEM', u, NULL)) ticket_holders,
COUNT(DISTINCT IF(NOT cn AND ev='spend' AND pos='play_event_lucky_machine', u, NULL)) spinners,
COUNTIF(NOT cn AND ev='spend' AND pos='play_event_lucky_machine') spins,
SUM(IF(NOT cn AND ev='earn' AND cur='EVENT_LUCKY_MACHINE_ITEM' AND pos='check_in_event', vol, 0)) tickets_check_in,
SUM(IF(NOT cn AND ev='earn' AND cur='EVENT_LUCKY_MACHINE_ITEM' AND pos='ads_lucky_machine_ticket', vol, 0)) tickets_ads,
SUM(IF(NOT cn AND ev='earn' AND cur='EVENT_LUCKY_MACHINE_ITEM' AND pos='play_event_lucky_machine', vol, 0)) tickets_from_spins,
SUM(IF(NOT cn AND ev='earn' AND cur='EVENT_LUCKY_MACHINE_ITEM' AND pos LIKE '%lucky.machine%', vol, 0)) tickets_purchased,
COUNTIF(NOT cn AND ev='ad_impression_MAX' AND placement='rewareded_mini_event') event_ad_impressions,
COUNTIF(NOT cn AND ev='event_slot_claim_stars_0') star_slot0_claims,
COUNTIF(NOT cn AND ev='purchase_click' AND pos LIKE '%lucky.machine%') pack_taps,
COUNTIF(NOT cn AND ev='in_app_purchase' AND pos LIKE '%lucky.machine%') pack_purchases,
ROUND(SUM(IF(NOT cn AND ev='in_app_purchase' AND pos LIKE '%lucky.machine%', usd, 0)), 2) pack_usd,
ROUND(SUM(IF(NOT cn AND ev='in_app_purchase', usd, 0)), 2) all_iap_usd,
COUNT(DISTINCT IF(cn AND ev='spend' AND pos='play_event_lucky_machine', u, NULL)) china_spinners
FROM x GROUP BY d ORDER BY dThe event is only present on v28 and later; 1,522 active users (41%) could not access it.
v26.2 and older builds record no ticket, spin or star events. Among eligible users, participation is highest on v33.
| version | active users | % of users spinning | spins per spinner | check-in tickets per spinner | ad tickets per 1k users | event ad impressions per 1k users | pack taps per 1k users | pack purchases per 1k users | pack ARPU $ |
|---|---|---|---|---|---|---|---|---|---|
| 28 | 220 | 5.5 | 16.7 | 10.8 | 114 | 0 | 27.3 | 0 | 0 |
| 29 | 1,042 | 8 | 24.3 | 29.2 | 108 | 24 | 30.7 | 0.960 | 0.010 |
| 32 | 372 | 10.8 | 33.9 | 42 | 94.1 | 16.1 | 16.1 | 5.4 | 0.061 |
| 33 | 453 | 13.9 | 44.5 | 51.3 | 132 | 11 | 30.9 | 8.8 | 0.105 |
| 34 | 144 | 10.4 | 33.5 | 30.3 | 69.4 | 0 | 6.9 | 6.9 | 0.079 |
778 ad-ticket grants against 33 event-placement impressions; 676 grants on user-days with no impression at all.
Each ad-ticket grant should follow one rewarded impression on rewareded_mini_event. The table compares grants with impressions logged by the same user on the same day; version is the build recorded on that user-day, so a user who updated that day can appear under the earlier build. In China, where ads do not load, grants continue without impressions, which indicates the ticket is awarded independently of ad completion.
| region | version | ad-ticket grants | event ad impressions per 100 grants | % of grants on days with no ad impression |
|---|---|---|---|---|
| China | 29 | 536 | 0 | 95.5 |
| rest of world | 26.2 | 15 | 0 | 66.7 |
| rest of world | 28 | 45 | 0 | 77.8 |
| rest of world | 29 | 124 | 20.2 | 59.7 |
| rest of world | 32 | 25 | 20 | 80 |
| rest of world | 33 | 28 | 10.7 | 89.3 |
| rest of world | 34 | 5 | 0 | 0 |
-- Lucky Machine ticket grants per user per day (all countries), with the ad impressions the same user
-- generated that day. Check-in is expected at most once per day; an ad ticket is expected to follow an ad.
WITH x AS (
SELECT PARSE_DATE('%Y%m%d', event_date) d, user_pseudo_id u, event_name ev, event_timestamp ts,
app_info.version v, geo.country = 'China' cn,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='position') pos,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='currency') cur,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='ad_placement') placement
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE _TABLE_SUFFIX BETWEEN '20260916' AND '20260925'
AND event_name IN ('earn', 'ad_impression_MAX', 'show_ad_click', 'ad_reward', 'session_start')
)
SELECT d, u, ANY_VALUE(v) version, LOGICAL_OR(cn) china,
COUNTIF(ev = 'earn' AND cur = 'EVENT_LUCKY_MACHINE_ITEM' AND pos = 'check_in_event') check_in_grants,
COUNTIF(ev = 'earn' AND cur = 'EVENT_LUCKY_MACHINE_ITEM' AND pos = 'ads_lucky_machine_ticket') ad_ticket_grants,
COUNTIF(ev = 'ad_impression_MAX' AND placement = 'rewareded_mini_event') event_ad_impressions,
COUNTIF(ev = 'ad_impression_MAX') all_ad_impressions,
COUNTIF(ev = 'ad_reward') ad_rewards,
COUNTIF(ev = 'session_start') sessions,
-- seconds between consecutive check-in grants for this user-day
MIN(IF(ev = 'earn' AND pos = 'check_in_event', ts, NULL)) first_check_in_ts,
MAX(IF(ev = 'earn' AND pos = 'check_in_event', ts, NULL)) last_check_in_ts
FROM x
GROUP BY d, u
HAVING check_in_grants > 0 OR ad_ticket_grants > 041 users received 203 check-in claims beyond the number of elapsed event days (1,015 tickets).
The check-in grants one claim per elapsed event day, so on day k at most k claims can exist. Maximum claims per user-day reach exactly twice the allowed number on 8 of the 10 days, consistent with the full backlog being granted twice in one action.
| event_day | user_days | mean_grants | allowed max (= event day) | max_grants | over_limit_user_days |
|---|---|---|---|---|---|
| 1 | 12 | 1 | 1 | 1 | 0 |
| 2 | 32 | 2.0 | 2 | 2 | 0 |
| 3 | 21 | 3.0 | 3 | 6 | 1 |
| 4 | 84 | 4.6 | 4 | 8 | 15 |
| 5 | 84 | 4.6 | 5 | 10 | 7 |
| 6 | 47 | 5.1 | 6 | 12 | 4 |
| 7 | 43 | 5.8 | 7 | 7 | 0 |
| 8 | 47 | 6.3 | 8 | 16 | 2 |
| 9 | 43 | 6.7 | 9 | 18 | 2 |
| 10 | 48 | 7.1 | 10 | 20 | 2 |
| region | version | check-in users | % of check-in users over-granted | extra tickets per affected user |
|---|---|---|---|---|
| China | 29 | 145 | 15.9 | 21.5 |
| rest of world | 28 | 34 | 2.9 | 5 |
| rest of world | 29 | 128 | 7.8 | 21 |
| rest of world | 32 | 48 | 2.1 | 35 |
| rest of world | 33 | 45 | 11.1 | 44 |
| rest of world | 34 | 8 | 12.5 | 50 |
Spinners spend more and return more often, at every engagement level.
Users on event builds, grouped by the number of days they were active during the event window, so that highly engaged players are compared with each other. This is an association: players who choose to spin are likely to be more engaged in ways the grouping does not capture, so the gap is an upper bound on the event's effect.
| engagement | group | users | iap_usd_during | active_after_event_pct | days_active_after |
|---|---|---|---|---|---|
| 1 day | did not spin | 1,328 | 0.061 | 12.0 | 0.172 |
| 1 day | spun | 52 | 1.7 | 25 | 0.481 |
| 2–3 days | did not spin | 485 | 0.335 | 20.8 | 0.363 |
| 2–3 days | spun | 85 | 1.3 | 27.1 | 0.494 |
| 4–6 days | did not spin | 104 | 2.8 | 43.3 | 0.990 |
| 4–6 days | spun | 36 | 3.6 | 61.1 | 1.9 |
| 7–10 days | did not spin | 101 | 2.7 | 87.1 | 3.3 |
| 7–10 days | spun | 40 | 6.8 | 95 | 4.7 |
-- What the Lucky Machine SKUs grant: earn events whose position is the SKU id
SELECT (SELECT value.string_value FROM UNNEST(event_params) WHERE key='position') sku,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='currency') currency,
COUNT(*) grants, COUNT(DISTINCT user_pseudo_id) users,
APPROX_QUANTILES((SELECT COALESCE(CAST(value.int_value AS FLOAT64), value.double_value)
FROM UNNEST(event_params) WHERE key='volume'), 2)[OFFSET(1)] median_volume
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE _TABLE_SUFFIX BETWEEN '20260916' AND '20260926' AND event_name = 'earn'
AND (SELECT value.string_value FROM UNNEST(event_params) WHERE key='position') LIKE '%lucky.machine%'
GROUP BY 1, 2 ORDER BY grants DESCNo rewarded-ad rewards are granted without an ad: 99.8% of 4,168 rewards have a matching impression.
Every reward the game grants for a rewarded ad is logged as ad_rewarded_<placement>. For each one, the same user's ad events from 120 seconds before to 30 seconds after were checked (AppLovin MAX logs the impression 0.02–17 seconds after the reward callback, median 0.25 s, so the window must extend past the reward). 18 September – 1 October, excluding China.
| ad activity around the reward (−120 s … +30 s) | rewards | share % |
|---|---|---|
| rewarded ad shown (same placement) | 4,153 | 99.6 |
| backup interstitial shown (same placement) | 2 | 0.050 |
| ad shown on another placement | 3 | 0.070 |
| ad_show logged, no impression | 2 | 0.050 |
| no ad: watch-ad tap only | 8 | 0.190 |
Proof 1 — timing. No id links an impression to a reward, so rewards are paired on user + placement + time. Gap from each reward to the next rewarded impression on the same placement, searching ±1 hour:
| gap_bucket | rewards | max_gap_s | also_impression_within_17s_before |
|---|---|---|---|
| next impression 0–1 s after | 3,732 | 0.998 | 25 |
| 1–5 s after | 383 | 4.8 | 2 |
| 5–17 s after | 28 | 16.6 | 0 |
| 17–60 s after | 6 | 59.0 | 0 |
| 1–10 min after | 4 | 110 | 1 |
| no impression within 1 h after | 15 | – | 0 |
Proof 2 — counts, no timestamps. Per user × placement × day, rewards vs rewarded impressions. Never more impressions than rewards; the excess cells each hold exactly one reward:
| comparison | user_placement_days | rewards | impressions | excess_rewards |
|---|---|---|---|---|
| equal | 1,923 | 4,088 | 4,088 | 0 |
| more rewards than impressions | 26 | 80 | 54 | 26 |
-- Proof of the reward ↔ impression pairing, 2026-09-18 .. 2026-10-01, ex-China.
-- No shared id exists, so pair on user + placement + time. For every reward (ad_rewarded_<placement>) find the
-- nearest REWARDED impression on the same placement AFTER it and BEFORE it, searching ±1 hour, and bucket the gaps.
WITH x AS (
SELECT user_pseudo_id u, event_name ev, event_timestamp ts,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='ad_placement') placement,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='ad_format') fmt
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE _TABLE_SUFFIX BETWEEN '20260918' AND '20261001' AND geo.country != 'China'
AND (event_name LIKE 'ad\\_rewarded\\_%' OR event_name = 'ad_impression_MAX')
),
r AS (SELECT u, ts, REGEXP_REPLACE(ev, r'^ad_rewarded_', '') placement
FROM x WHERE ev LIKE 'ad\\_rewarded\\_%' AND ev != 'ad_rewarded_show_failed'),
imp AS (SELECT u, ts, placement FROM x WHERE ev = 'ad_impression_MAX' AND fmt = 'REWARDED'),
pair AS (
SELECT r.u, r.ts, r.placement,
MIN(IF(i.ts >= r.ts, (i.ts - r.ts) / 1e6, NULL)) next_s, -- seconds to the next same-placement impression
MIN(IF(i.ts < r.ts, (r.ts - i.ts) / 1e6, NULL)) prev_s -- seconds since the previous one
FROM r LEFT JOIN imp i ON i.u = r.u AND i.placement = r.placement AND ABS(i.ts - r.ts) <= 3600000000
GROUP BY 1, 2, 3
)
SELECT
CASE
WHEN next_s IS NULL THEN '9 no impression within 1 h after'
WHEN next_s < 1 THEN '1 next impression 0–1 s after'
WHEN next_s < 5 THEN '2 1–5 s after'
WHEN next_s < 17 THEN '3 5–17 s after'
WHEN next_s < 60 THEN '4 17–60 s after'
WHEN next_s < 600 THEN '5 1–10 min after'
ELSE '6 10–60 min after'
END AS gap_bucket,
COUNT(*) rewards,
ROUND(MAX(next_s), 3) max_gap_s,
COUNTIF(prev_s < 17) also_impression_within_17s_before
FROM pair GROUP BY 1 ORDER BY 1-- Time-free check: per user × placement × day, number of rewards vs number of rewarded impressions.
-- If every reward has exactly one impression, the counts are equal regardless of timestamps.
WITH x AS (
SELECT user_pseudo_id u, event_date d, event_name ev,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='ad_placement') placement,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='ad_format') fmt
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE _TABLE_SUFFIX BETWEEN '20260918' AND '20261001' AND geo.country != 'China'
AND (event_name LIKE 'ad\\_rewarded\\_%' OR event_name = 'ad_impression_MAX')
),
c AS (
SELECT u, d, COALESCE(IF(ev = 'ad_impression_MAX', placement, NULL), REGEXP_REPLACE(ev, r'^ad_rewarded_', '')) placement,
COUNTIF(ev LIKE 'ad\\_rewarded\\_%' AND ev != 'ad_rewarded_show_failed') rewards,
COUNTIF(ev = 'ad_impression_MAX' AND fmt = 'REWARDED') impressions
FROM x GROUP BY 1, 2, 3
)
SELECT
CASE WHEN rewards = impressions THEN '1 equal'
WHEN rewards > impressions THEN '2 more rewards than impressions'
ELSE '3 more impressions than rewards' END AS comparison,
COUNT(*) user_placement_days, SUM(rewards) rewards, SUM(impressions) impressions,
SUM(GREATEST(rewards - impressions, 0)) excess_rewards
FROM c WHERE rewards > 0 OR impressions > 0
GROUP BY 1 ORDER BY 1No segment shows a systematic gap: the 10 unmatched rewards are spread across builds, placements and countries, one or two each. Owning a no-ads SKU or membership does not change the result.
| build | rewards | users | without ad | % with matching ad |
|---|---|---|---|---|
| 29 | 1,652 | 196 | 5 | 99.7 |
| 34 | 1,599 | 210 | 2 | 99.9 |
| 32 | 391 | 73 | 1 | 99.7 |
| 33 | 310 | 64 | 1 | 99.7 |
| 26.2 | 206 | 21 | 1 | 99.5 |
| placement | rewards | users | without ad | % with matching ad |
|---|---|---|---|---|
| acquire_hero | 1,026 | 266 | 1 | 99.9 |
| daily_quest | 341 | 94 | 1 | 99.7 |
| key_challenge_BossRush | 279 | 139 | 0 | 100 |
| flybox_gold | 245 | 75 | 0 | 100 |
| key_challenge_GoldRush | 243 | 125 | 2 | 99.2 |
| summon_gear | 237 | 85 | 1 | 99.6 |
| lucky_spin | 229 | 64 | 1 | 99.6 |
| summon_skill | 183 | 55 | 0 | 100 |
| summon_companion | 158 | 48 | 1 | 99.4 |
| blessing_attack | 123 | 40 | 0 | 100 |
| welcome_back | 122 | 48 | 0 | 100 |
| key_challenge_HoneyWar | 95 | 33 | 1 | 98.9 |
| leagueboss_attack_ad | 87 | 27 | 0 | 100 |
| skip_time_research | 84 | 15 | 0 | 100 |
| game_speed_x2 | 80 | 53 | 2 | 97.5 |
| country | rewards | users | without ad | % with matching ad |
|---|---|---|---|---|
| Indonesia | 439 | 24 | 0 | 100 |
| Brazil | 401 | 30 | 1 | 99.8 |
| Spain | 310 | 18 | 1 | 99.7 |
| United States | 295 | 42 | 0 | 100 |
| Germany | 212 | 30 | 0 | 100 |
| Thailand | 189 | 26 | 0 | 100 |
| South Korea | 142 | 23 | 0 | 100 |
| Italy | 135 | 8 | 0 | 100 |
| Canada | 111 | 27 | 0 | 100 |
| Poland | 111 | 6 | 0 | 100 |
| Taiwan | 110 | 8 | 0 | 100 |
| Romania | 102 | 3 | 0 | 100 |
| entitlement | rewards | users | without ad | % with matching ad |
|---|---|---|---|---|
| neither | 4,147 | 509 | 10 | 99.8 |
| owns membership | 11 | 2 | 0 | 100 |
| owns a no-ads SKU | 10 | 2 | 0 | 100 |
-- Rewarded-ad grants without an ad: one row per reward (ad_rewarded_<placement>), 2026-09-18 .. 2026-10-01, ex-China.
-- For each reward, the same user's ad activity from 120 s before to 30 s after it (MAX logs the impression
-- 0.02-17 s AFTER the reward callback, median 0.25 s), plus build, country and whether the user had
-- bought a no-ads SKU or membership before the reward (lifetime, from the whole export).
WITH x AS (
SELECT user_pseudo_id u, event_name ev, event_timestamp ts, app_info.version v, geo.country country,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='ad_format') fmt,
COALESCE((SELECT value.string_value FROM UNNEST(event_params) WHERE key='ad_placement'),
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='placement')) placement,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='ad_source') ad_source,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='ad_type') ad_type,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='status') status
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE _TABLE_SUFFIX BETWEEN '20260918' AND '20261001' AND geo.country != 'China'
AND (event_name LIKE 'ad\\_rewarded\\_%' OR event_name IN ('ad_impression_MAX', 'ad_show', 'show_ad_click',
'ad_request_status', 'ad_reward'))
),
owners AS ( -- lifetime: first purchase time of each entitlement type
SELECT user_pseudo_id u,
MIN(IF((SELECT value.string_value FROM UNNEST(event_params) WHERE key='product_id') LIKE '%skip.ad%', event_timestamp, NULL)) noads_ts,
MIN(IF((SELECT value.string_value FROM UNNEST(event_params) WHERE key='product_id') LIKE '%membership%', event_timestamp, NULL)) membership_ts
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE event_name = 'in_app_purchase'
GROUP BY u
),
rewards AS (
SELECT u, ts, v, country, REGEXP_REPLACE(ev, r'^ad_rewarded_', '') placement
FROM x WHERE ev LIKE 'ad\\_rewarded\\_%' AND ev != 'ad_rewarded_show_failed'
),
ctx AS (
SELECT r.u, r.ts, r.v, r.country, r.placement,
COUNTIF(a.ev = 'ad_impression_MAX' AND a.fmt = 'REWARDED' AND a.placement = r.placement) rew_imp_same,
COUNTIF(a.ev = 'ad_impression_MAX' AND a.fmt = 'INTER' AND a.placement = CONCAT('interstitial_backup_', r.placement)) backup_imp_same,
COUNTIF(a.ev = 'ad_impression_MAX') any_imp,
COUNTIF(a.ev = 'ad_show') ad_show,
COUNTIF(a.ev = 'show_ad_click') taps,
COUNTIF(a.ev = 'ad_request_status' AND a.ad_type = 'rewarded' AND a.status = 'fail') rew_load_fail,
COUNTIF(a.ev = 'ad_request_status' AND a.ad_type = 'rewarded' AND a.status = 'success') rew_load_ok,
ARRAY_AGG(IF(a.ev = 'ad_impression_MAX', a.ad_source, NULL) IGNORE NULLS ORDER BY a.ts DESC LIMIT 1)[SAFE_OFFSET(0)] ad_source,
COUNTIF(a.ev LIKE 'ad\\_rewarded\\_%' AND a.ev != 'ad_rewarded_show_failed' AND a.ts < r.ts AND a.ts != r.ts) prior_rewards_120s
FROM rewards r
LEFT JOIN x a ON a.u = r.u AND a.ts BETWEEN r.ts - 120000000 AND r.ts + 30000000
GROUP BY 1, 2, 3, 4, 5
)
SELECT ctx.*,
CASE
WHEN rew_imp_same > 0 THEN '1 rewarded ad shown (same placement)'
WHEN backup_imp_same > 0 THEN '2 backup interstitial shown (same placement)'
WHEN any_imp > 0 THEN '3 ad shown on another placement'
WHEN ad_show > 0 THEN '4 ad_show logged, no impression'
WHEN rew_load_fail > 0 THEN '5 no ad: load failed'
WHEN taps > 0 THEN '6 no ad: watch-ad tap only'
ELSE '7 no ad activity at all'
END AS what_preceded,
owners.noads_ts IS NOT NULL AND owners.noads_ts < ctx.ts AS owned_noads,
owners.membership_ts IS NOT NULL AND owners.membership_ts < ctx.ts AS owned_membership
FROM ctx LEFT JOIN owners USING (u)The earlier rewards-per-tap gap came from the generic ad_reward event, which over-fires on v26.2, v32, v33.
ad_reward carries a single fixed AdMob unit id whatever network served the ad, and is not time-aligned with the impression. Placement rewards track impressions 1:1 on every build, while ad_reward runs well above them on the listed builds — a logging difference, not rewards granted for free. This week, per 1,000 users:
| build | users | watch-ad taps per 1k users | rewarded impressions per 1k users | placement rewards per 1k users | placement rewards per impression | generic ad_reward per impression |
|---|---|---|---|---|---|---|
| 26.2 | 99 | 535 | 263 | 263 | 1 | 1.4 |
| 29 | 167 | 1,162 | 1,138 | 1,144 | 1.0 | 1.0 |
| 32 | 84 | 190 | 179 | 179 | 1 | 4.7 |
| 33 | 395 | 291 | 276 | 278 | 1.0 | 1.7 |
| 34 | 1,257 | 1,457 | 1,267 | 1,272 | 1 | 1.0 |
-- Rewarded-ad event counts per build, this week (2026-09-25 .. 2026-10-01), ex-China, per 1,000 users.
-- ad_reward = generic reward log (fixed AdMob unit); ad_rewarded_<placement> = per-placement reward log.
WITH x AS (
SELECT app_info.version v, user_pseudo_id u, event_name ev,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='ad_format') fmt
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE _TABLE_SUFFIX BETWEEN '20260925' AND '20261001' AND geo.country != 'China'
),
n AS (SELECT v, COUNT(DISTINCT u) users FROM x GROUP BY v)
SELECT x.v AS version, ANY_VALUE(n.users) users,
COUNTIF(ev = 'show_ad_click') watch_ad_taps,
COUNTIF(ev = 'ad_impression_MAX' AND fmt = 'REWARDED') rewarded_impressions,
COUNTIF(ev = 'ad_show') ad_show,
COUNTIF(ev LIKE 'ad\\_rewarded\\_%' AND ev != 'ad_rewarded_show_failed') placement_rewards,
COUNTIF(ev = 'ad_reward') generic_ad_reward
FROM x JOIN n USING (v)
GROUP BY x.v
HAVING users >= 50 AND version IN ('26.2', '29', '32', '33', '34')
ORDER BY versionRewarded fill improved from 59% on v29 to 76% on v34; load retries on DNS failure remain high for affected devices.
This week (25 Sep – 1 Oct), excluding China, builds with at least 50 users. All counts are per 1,000 users of that build so builds of different size compare directly. Rewarded fill = successful rewarded loads ÷ attempts; failed loads per affected user counts failed loads to ms.applvn.com among users who had at least one.
| version | users | watch-ad taps per 1k users | rewarded impressions per 1k users | ad ARPU $ | rewarded fill % | % users with DNS failures | failed loads per affected user |
|---|---|---|---|---|---|---|---|
| 26.2 | 51 | 1,039 | 510 | 0.010 | 96.2 | 2 | 4 |
| 29 | 110 | 1,764 | 1,727 | 0.046 | 58.7 | 8.2 | 8 |
| 32 | 62 | 258 | 242 | 0.005 | 30.9 | 4.8 | 82 |
| 33 | 320 | 359 | 341 | 0.024 | 73.6 | 2.5 | 190 |
| 34 | 1,050 | 1,744 | 1,517 | 0.095 | 76.4 | 4.7 | 158 |
-- Ads health by app version, this week 2026-09-25 .. 2026-10-01, all countries except China
WITH e AS (
SELECT user_pseudo_id u, app_info.version v, event_name ev,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='ad_format') fmt,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='ad_placement') placement,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='ad_type') ad_type,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='status') status,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='error') err,
(SELECT COALESCE(value.double_value, value.float_value, CAST(value.int_value AS FLOAT64))
FROM UNNEST(event_params) WHERE key='value') val
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE _TABLE_SUFFIX BETWEEN '20260925' AND '20261001'
AND geo.country != 'China'
AND event_name IN ('ad_impression_MAX','ad_reward','show_ad_click','ad_request_status','user_engagement')
),
per_user AS (
SELECT v, u,
COUNTIF(ev='ad_impression_MAX' AND fmt='REWARDED') rew_imps,
COUNTIF(ev='ad_impression_MAX' AND fmt='INTER' AND placement LIKE 'interstitial_backup%') backup_inter_imps,
COUNTIF(ev='ad_impression_MAX' AND fmt='INTER' AND placement NOT LIKE 'interstitial_backup%') regular_inter_imps,
SUM(IF(ev='ad_impression_MAX', val, 0)) ad_usd,
COUNTIF(ev='ad_reward') rewards,
COUNTIF(ev='show_ad_click') ad_clicks,
COUNTIF(ev='ad_request_status' AND ad_type='rewarded') rew_loads,
COUNTIF(ev='ad_request_status' AND ad_type='rewarded' AND status='success') rew_loads_ok,
COUNTIF(ev='ad_request_status' AND ad_type='inter') inter_loads,
COUNTIF(ev='ad_request_status' AND ad_type='inter' AND status='success') inter_loads_ok,
COUNTIF(ev='ad_request_status' AND status='fail' AND err LIKE '%applvn.com%') dns_fails
FROM e GROUP BY v, u
)
SELECT v AS version,
COUNT(*) users,
SUM(rew_imps) rewarded_impressions,
SUM(regular_inter_imps) regular_interstitial_impressions,
SUM(backup_inter_imps) backup_interstitial_impressions,
ROUND(SUM(ad_usd), 2) ad_usd,
SUM(ad_clicks) watch_ad_taps,
SUM(rewards) rewards_granted,
SUM(rewards) - SUM(rew_imps) rewards_minus_impressions,
COUNTIF(rewards > 0 AND rew_imps = 0) users_rewarded_never_saw_ad,
SUM(IF(rew_imps = 0, rewards, 0)) rewards_to_those_users,
SUM(rew_loads) rewarded_load_attempts,
ROUND(100 * SAFE_DIVIDE(SUM(rew_loads_ok), SUM(rew_loads)), 1) rewarded_fill_pct,
SUM(inter_loads) interstitial_load_attempts,
SUM(inter_loads_ok) interstitial_loads_ok,
SUM(dns_fails) dns_fail_attempts,
COUNTIF(dns_fails > 0) users_with_dns_fail,
ROUND(SAFE_DIVIDE(SUM(dns_fails), COUNTIF(dns_fails > 0)), 0) dns_fails_per_affected_user
FROM per_user
GROUP BY v
HAVING users >= 50 AND v IN ('26.2', '28', '29', '32', '33', '34')
ORDER BY users DESCWhy ad loads fail: 58% of failed loads are the device being unable to reach AppLovin's server; genuine no-fill is 0.1%.
Every failed load logs ad_request_status with status = fail and the SDK's raw error message. Before requesting an ad, the device looks up the address of AppLovin's mediation server ms.applvn.com (DNS). Two failure messages show that lookup failing:
Unable to resolve host "ms.applvn.com": No address associated with hostname — the lookup returns nothing. Users are concentrated in markets where AppLovin is unreachable (Sudan, Iran, Yemen, Syria, Cuba, Libya), plus a few individual devices elsewhere with very high counts.Failed to connect to ms.applvn.com/0.0.0.0:443 (or [::]:443) — the lookup returns 0.0.0.0, a non-routable address. This is how DNS-based ad blockers (AdGuard, NextDNS, Pi-hole, private DNS) block ad domains. Few users, very high retry counts.Neither group can be served ads, so the revenue at stake is small. The defect is that the client keeps retrying: the maximum per user is over a thousand failed loads in one week. Group C carries no error message and cannot be classified.
| failure_type | users | failed_loads | failed_loads_per_user | median_per_user | max_per_user | pct_of_failed_loads | top_countries_by_users |
|---|---|---|---|---|---|---|---|
| A · lookup of ms.applvn.com returns no address | 88 | 5,613 | 64 | 6 | 1,152 | 32.4 | Sudan 11, Indonesia 7, Iran 7, Brazil 5, Bangladesh 4, United States 4 |
| B · lookup returns 0.0.0.0 (DNS ad blocker) | 14 | 4,355 | 311 | 151 | 1,251 | 25.2 | Indonesia 5, Brazil 2, United States 2, Italy 1, Latvia 1, Russia 1 |
| C · no error message logged | 24 | 6,788 | 283 | 11 | 2,497 | 39.2 | Iran 10, Ukraine 4, Indonesia 2, Colombia 1, Cuba 1, Ethiopia 1 |
| D · no fill (no ad available) | 3 | 11 | 4 | 2 | 8 | 0.100 | Canada 1, Indonesia 1, Singapore 1 |
| E · other (connection reset, TLS, timeout …) | 16 | 546 | 34 | 2 | 310 | 3.2 | Iran 4, Congo - Kinshasa 2, Brazil 1, Cuba 1, Ethiopia 1, Indonesia 1 |
-- Why ad loads fail this week (2026-09-25 .. 2026-10-01, ex-China): the raw SDK error on ad_request_status,
-- grouped into failure types, with per-user retry intensity and the main countries.
WITH f AS (
SELECT user_pseudo_id u, geo.country country,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='error') err
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE _TABLE_SUFFIX BETWEEN '20260925' AND '20261001' AND geo.country != 'China'
AND event_name = 'ad_request_status'
AND (SELECT value.string_value FROM UNNEST(event_params) WHERE key='status') = 'fail'
),
k AS (
SELECT u, country,
CASE
WHEN err LIKE 'Unable to resolve host%applvn%' THEN 'A · lookup of ms.applvn.com returns no address'
WHEN err LIKE '%applvn.com/0.0.0.0%' OR err LIKE '%applvn.com/[::]%' THEN 'B · lookup returns 0.0.0.0 (DNS ad blocker)'
WHEN err IS NULL THEN 'C · no error message logged'
WHEN err LIKE '%No Fill%' OR err LIKE '%no eligible ads%' THEN 'D · no fill (no ad available)'
ELSE 'E · other (connection reset, TLS, timeout …)'
END kind
FROM f
),
per_user AS (SELECT kind, u, COUNT(*) n FROM k GROUP BY 1, 2),
by_country AS (
SELECT kind, STRING_AGG(CONCAT(country, ' ', CAST(cu AS STRING)), ', ' ORDER BY cu DESC, country LIMIT 6) top_countries
FROM (SELECT kind, country, COUNT(DISTINCT u) cu FROM k GROUP BY 1, 2)
GROUP BY kind
),
agg AS (
SELECT kind, COUNT(*) users, SUM(n) failed_loads, APPROX_QUANTILES(n, 2)[OFFSET(1)] median_per_user, MAX(n) max_per_user
FROM per_user GROUP BY kind
)
SELECT agg.kind AS failure_type, users, failed_loads,
ROUND(failed_loads / users, 0) failed_loads_per_user,
median_per_user, max_per_user,
ROUND(100 * failed_loads / SUM(failed_loads) OVER (), 1) pct_of_failed_loads,
top_countries AS top_countries_by_users
FROM agg JOIN by_country USING (kind)
ORDER BY failure_typeThe sale packs are absent from v32, v33 and v34.
No .sale taps, purchases or reward grants are recorded on v32 or later. Taps on the full-price no-ads offer increased on v33 and v34, with a low conversion rate at the $29.99 price point. v29 continued to sell .sale until 09-21; its grant count reflects the known repeated emission of the 30days.sale grant, not purchase volume.
| version | users | .sale taps per 1k users | .sale purchases per 1k users | full-price no-ads taps per 1k users | full-price no-ads purchases per 1k users | full-price tap → purchase % |
|---|---|---|---|---|---|---|
| 28 | 354 | 16.9 | 8.5 | 25.4 | 8.5 | 33.3 |
| 26.2 | 862 | 1.2 | 1.2 | 4.6 | 0 | 0 |
| 29 | 2,129 | 2.8 | 2.3 | 8.5 | 1.4 | 16.7 |
| 32 | 483 | 0 | 0 | 37.3 | 2.1 | 5.6 |
| 33 | 577 | 0 | 0 | 81.5 | 5.2 | 6.4 |
| 34 | 1,263 | 0 | 0 | 40.4 | 4 | 9.8 |
-- Every no-ads .sale signal by build, 2026-09-16 .. 2026-10-01
WITH e AS (
SELECT app_info.version v, event_name, user_pseudo_id u, PARSE_DATE('%Y%m%d', event_date) d,
COALESCE((SELECT value.string_value FROM UNNEST(event_params) WHERE key='product_id'),
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='position')) sku
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE _TABLE_SUFFIX BETWEEN '20260916' AND '20261001'
)
SELECT v AS version, MIN(d) first_day, COUNT(DISTINCT u) users,
COUNTIF(sku LIKE '%skip.ad%.sale' AND event_name='purchase_click') sale_taps,
COUNTIF(sku LIKE '%skip.ad%.sale' AND event_name='in_app_purchase') sale_purchases,
COUNTIF(sku LIKE '%skip.ad%.sale' AND event_name='earn') sale_reward_grants,
COUNTIF(sku LIKE '%skip.ad%' AND sku NOT LIKE '%.sale' AND event_name='purchase_click') full_price_noads_taps,
COUNTIF(sku LIKE '%skip.ad%' AND sku NOT LIKE '%.sale' AND event_name='in_app_purchase') full_price_noads_purchases,
MAX(IF(sku LIKE '%skip.ad%.sale' AND event_name='in_app_purchase', d, NULL)) last_sale_purchase
FROM e
GROUP BY v
HAVING users >= 20
ORDER BY first_dayEstimated net effect per new install (excluding China): −$0.07 to −$0.09, excluding the top payer.
No-ads revenue per install decreased significantly; other IAP and ad revenue partially offset the loss, though the offset is not statistically significant. The D0–D6 window includes installs affected by the 09-28 collection gap, which biases the post-removal group slightly downward. Bootstrap confidence intervals are provided in notebook 15, section 1; all intervals for the net change include zero.
| window | arm | installs | no-ads buyers /1k | no-ads taps /1k | no-ads $/install | other IAP $/install | ad $/install | total $/install | total excl. top payer |
|---|---|---|---|---|---|---|---|---|---|
| D0–D2 | offer present (installs 09-10..20) | 2,445 | 13.1 | 26.6 | 0.213 | 0.173 | 0.075 | 0.461 | 0.415 |
| D0–D2 | offer absent (installed on v32+) | 762 | 5.2 | 61.7 | 0.061 | 0.269 | 0.106 | 0.436 | 0.344 |
| D0–D6 | offer present (installs 09-10..20) | 2,445 | 13.1 | 27.4 | 0.213 | 0.201 | 0.078 | 0.491 | 0.445 |
| D0–D6 | offer absent (installed on v32+) | 625 | 6.4 | 73.6 | 0.074 | 0.343 | 0.114 | 0.531 | 0.371 |
China traffic consists of 820 users on v29, generating $0 IAP and $0.08 in ad revenue.
Engagement is comparable to other markets (30.2 min per user, against 33.4 for non-China v29 users), and users opened the shop 89 times. However, 0 purchases and 20 ad impressions were recorded; Google Play Billing and the mediated ad networks are not available in this market (rewarded fill 27.8%). v29 is no longer the production build on Google Play, indicating distribution through an unofficial channel. The traffic added approximately 136 DAU per day in the week of 18 September and is excluded from KPI comparisons.
| region | version | users | minutes per user | shop taps per 1k users | purchases per 1k users | IAP ARPU $ | ad impressions per user | ad ARPU $ | rewarded fill % |
|---|---|---|---|---|---|---|---|---|---|
| China | 29 | 820 | 30.2 | 108 | 0 | 0 | 0.020 | 0.000 | 27.8 |
| rest of world | 29 | 1,281 | 33.4 | 112 | 50.7 | 0.408 | 1.3 | 0.057 | 49.9 |
| rest of world | 34 | 1,257 | 26 | 119 | 39.8 | 0.343 | 1.3 | 0.079 | 76.4 |
| rest of world | 33 | 567 | 27.3 | 252 | 58.2 | 0.479 | 0.540 | 0.062 | 72.4 |
| rest of world | 32 | 480 | 26 | 142 | 39.6 | 0.345 | 0.820 | 0.046 | 69.7 |
-- China vs rest, 2026-09-18 .. 2026-10-01, by app version: who they are and what they earn
WITH e AS (
SELECT user_pseudo_id u, app_info.version v, geo.country = 'China' AS china, event_name ev, event_value_in_usd usd,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='ad_type') ad_type,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key='status') status,
(SELECT COALESCE(value.double_value, value.float_value, CAST(value.int_value AS FLOAT64))
FROM UNNEST(event_params) WHERE key='value') val,
(SELECT value.int_value FROM UNNEST(event_params) WHERE key='engagement_time_msec') eng
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE _TABLE_SUFFIX BETWEEN '20260918' AND '20261001'
)
SELECT IF(china, 'China', 'rest of world') AS region, v AS version,
COUNT(DISTINCT u) users,
COUNT(DISTINCT IF(ev='first_open', u, NULL)) installs,
ROUND(SUM(IF(ev='user_engagement', eng, 0)) / 60000 / COUNT(DISTINCT u), 1) minutes_per_user,
COUNTIF(ev='purchase_click') shop_taps,
COUNTIF(ev='in_app_purchase') purchases,
ROUND(SUM(IF(ev='in_app_purchase', usd, 0)), 2) iap_usd,
COUNTIF(ev='ad_impression_MAX') ad_impressions,
ROUND(SUM(IF(ev='ad_impression_MAX', val, 0)), 2) ad_usd,
ROUND(100 * SAFE_DIVIDE(COUNTIF(ev='ad_request_status' AND ad_type='rewarded' AND status='success'),
COUNTIF(ev='ad_request_status' AND ad_type='rewarded')), 1) rewarded_fill_pct
FROM e
GROUP BY region, version
HAVING users >= 30
ORDER BY region, users DESCThree event families were introduced between v28 and v30.
All event names first observed on or after 14 September. No further event names were introduced in v32–v34.
| event_name | first_day | first_version | versions_seen | events | users |
|---|---|---|---|---|---|
| event_slot_claim_stars_0 … _13 (14 events) | 09-16 | 28 | 28,29,32,33,34 | 495 | 179 |
| ad_rewarded_rewareded_mini_event | 09-17 | 29 | 29,32,33 | 37 | 11 |
| app_exception | 09-21 | 30 | 30,31,32,33,34 | 70 | 39 |
-- Event names by the build they first appear in (whole export); keep those first seen on v28 or later
WITH e AS (
SELECT event_name, app_info.version AS v, PARSE_DATE('%Y%m%d', event_date) AS d, user_pseudo_id AS u
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
),
first_seen AS (
SELECT event_name, MIN(d) AS first_day,
ARRAY_AGG(v ORDER BY d LIMIT 1)[OFFSET(0)] AS first_version
FROM e GROUP BY event_name
)
SELECT f.event_name, f.first_day, f.first_version,
STRING_AGG(DISTINCT e.v ORDER BY e.v) AS versions_seen,
COUNT(*) AS events, COUNT(DISTINCT e.u) AS users
FROM first_seen f JOIN e USING (event_name)
WHERE f.first_day >= '2026-09-14'
GROUP BY 1, 2, 3
ORDER BY f.first_day, events DESCLucky Machine star milestones (event_slot_claim_stars_0 … _13): introduced in v28, ended 25 September (see section 4).
179 users claimed slot 0 and 8 reached slot 13 (4.5% of slot-0 users). Events stop on all builds on 25 September, consistent with the feature's event window closing rather than a client regression. Each slot is logged as a separate event name without parameters, which consumes 14 entries of the property's event-name quota and prevents analysis by player state.
| event_name | slot | events | users | % of slot-0 users | versions_seen |
|---|---|---|---|---|---|
| event_slot_claim_stars_0 | 0 | 188 | 179 | 100 | 28,29,32,33,34 |
| event_slot_claim_stars_1 | 1 | 96 | 93 | 52 | 28,29,32,33,34 |
| event_slot_claim_stars_2 | 2 | 44 | 43 | 24 | 28,29,32,33 |
| event_slot_claim_stars_3 | 3 | 28 | 27 | 15.1 | 28,29,32,33 |
| event_slot_claim_stars_4 | 4 | 25 | 24 | 13.4 | 28,29,32,33 |
| event_slot_claim_stars_5 | 5 | 23 | 22 | 12.3 | 28,29,32,33 |
| event_slot_claim_stars_6 | 6 | 18 | 17 | 9.5 | 28,29,32,33 |
| event_slot_claim_stars_7 | 7 | 17 | 16 | 8.9 | 28,29,32,33 |
| event_slot_claim_stars_8 | 8 | 13 | 12 | 6.7 | 28,29,32,33 |
| event_slot_claim_stars_9 | 9 | 11 | 10 | 5.6 | 28,29,32,33 |
| event_slot_claim_stars_10 | 10 | 9 | 8 | 4.5 | 28,29,32 |
| event_slot_claim_stars_11 | 11 | 8 | 7 | 3.9 | 28,29,32 |
| event_slot_claim_stars_12 | 12 | 7 | 7 | 3.9 | 28,29,32 |
| event_slot_claim_stars_13 | 13 | 8 | 8 | 4.5 | 28,29,32 |
Parameters on the new events are insufficient for diagnosis.
app_exception records only fatal and timestamp, with no exception type, message or screen. Non-fatal exceptions are not logged (70 events in total, all fatal). ad_rewarded_rewareded_mini_event (37 events, 11 users) carries no placement or reward value, and the same typo appears in the ad placement name rewareded_mini_event.
| event | key | occurrences | distinct_values | sample_values |
|---|---|---|---|---|
| ad_rewarded_rewareded_mini_event | firebase_screen | 2 | 1 | Lobby |
| app_exception | engagement_time_msec | 20 | 20 | 46,115,218,126,40,44 |
| app_exception | fatal | 70 | 1 | 1 |
| app_exception | timestamp | 70 | 70 | 1790199152266,1790529189672,1790461233038,1790148190110,1790469773585,1790148178987 |
| event_slot_claim_stars_* | firebase_screen | 213 | 1 | Lobby |
-- Parameters carried by the events introduced in v28-v30
SELECT event_name, p.key,
COUNT(*) n,
COUNT(DISTINCT COALESCE(p.value.string_value, CAST(p.value.int_value AS STRING),
CAST(p.value.double_value AS STRING))) distinct_values,
STRING_AGG(DISTINCT COALESCE(p.value.string_value, CAST(p.value.int_value AS STRING)) LIMIT 6) sample_values
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`, UNNEST(event_params) p
WHERE _TABLE_SUFFIX >= '20260916'
AND (event_name LIKE 'event_slot_claim_stars_%' OR event_name IN ('ad_rewarded_rewareded_mini_event', 'app_exception'))
AND p.key NOT IN ('firebase_screen_class','firebase_screen_id','firebase_event_origin','ga_session_id',
'ga_session_number','engaged_session_event','firebase_conversion')
GROUP BY 1, 2
ORDER BY 1, 2Event coverage is unchanged between v29 and v34.
Of all events logged by at least 20 v29 users, only event_slot_claim_stars_0 falls below a quarter of its v29 per-user rate on v34, which is explained by the feature window above. No tracking was lost in v32–v34.
| event_name | users_v29 | users_v32 | users_v33 | users_v34 | pct_users_v29 | pct_users_v34 |
|---|---|---|---|---|---|---|
| event_slot_claim_stars_0 | 58 | 31 | 24 | 5 | 4.5 | 0.400 |
-- Events logged on v29 but missing (or nearly missing) on v32-v34, 2026-09-18 .. 2026-10-01, ex-China.
-- Per-user rates so builds with different audience sizes compare fairly.
WITH e AS (
SELECT event_name, app_info.version v, user_pseudo_id u
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE _TABLE_SUFFIX BETWEEN '20260918' AND '20261001' AND geo.country != 'China'
AND app_info.version IN ('29', '32', '33', '34')
),
users AS (SELECT v, COUNT(DISTINCT u) n FROM e GROUP BY v),
ev AS (SELECT event_name, v, COUNT(*) events, COUNT(DISTINCT u) users FROM e GROUP BY 1, 2)
SELECT event_name,
MAX(IF(ev.v='29', ev.users, 0)) users_v29, MAX(IF(ev.v='32', ev.users, 0)) users_v32,
MAX(IF(ev.v='33', ev.users, 0)) users_v33, MAX(IF(ev.v='34', ev.users, 0)) users_v34,
ROUND(100 * MAX(IF(ev.v='29', ev.users / users.n, 0)), 1) pct_users_v29,
ROUND(100 * MAX(IF(ev.v='34', ev.users / users.n, 0)), 1) pct_users_v34
FROM ev JOIN users USING (v)
GROUP BY event_name
HAVING users_v29 >= 20 AND pct_users_v34 < 0.25 * pct_users_v29
ORDER BY users_v29 DESC| 09-28 06 | 09-28 07 | 09-28 08 | 09-28 09 | 09-28 10 | 09-28 11 | 09-28 18 | 09-28 23 | 09-29 00 | 09-29 01 | 09-29 02 | 09-29 03 | 09-29 04 | 09-29 05 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| events | 2,256 | 1,849 | 3,486 | 239 | 73 | 510 | 1 | 24 | 8 | 143 | 163 | 2,529 | 3,260 | 3,045 |
| users | 67 | 55 | 59 | 30 | 7 | 5 | 1 | 6 | 5 | 5 | 11 | 38 | 48 | 54 |
Hourly event volume falls from approximately 3,000 to near zero for about 18 hours. All 28 September figures across the dashboards are understated, and retention cells falling on that date are excluded from this report.