← All reports

Weekly report — week ending Thu 1 Oct 2026

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.

Summary of issues

#areaissueversionsevidencerecommended action
1RetentionDay-1 retention fell for installs from 25 Septemberv33 and v34 alikeD1 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.
2UARecent 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.
3Live eventLucky Machine: ad tickets granted without an adAll 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.
4Live eventLucky Machine: check-in reward claimed twicev28, v29, v32, v33, v34The 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).
5Live eventLucky Machine: bulk ticket credit to one accountv29One 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.
6Live eventLucky Machine: spin results and purchased tickets are not loggedAll event builds6,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.
7Live eventLucky Machine: limited reach and conversionv28–v341,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.
8AnalyticsGeneric ad_reward event over-counts rewards on some builds (no rewards are missing an ad)v26.2, v32, v33Every 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.
9Ad monetisationUnbounded ad-load retries when AppLovin cannot be reachedv32, v33, v3417,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.
10AnalyticsLargest group of failed ad loads has no error messagev32, v33, v3439.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).
11StabilityFatal exception rate improving with each buildv32 → v34Share 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).
12IAPNo-ads sale packs removedv32, v33, v340 .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.
13DistributionUnofficial v29 distribution in Chinav29820 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.
14Analytics18-hour gap in event collectionAll buildsEvent 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.
15AnalyticsNew events lack parametersv28, 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.

1 · Day-1 retention

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 periodinstallsD1 %
09-04..101,95825.9
09-11..171,68926.6
09-18..241,12325.6
09-25..3067920.9

Where: non-Tier-1 traffic

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.

tierD1 % 09-04..10D1 % 09-11..17D1 % 09-18..24D1 % 09-25..30installs 09-04..10installs 09-11..17installs 09-18..24installs 09-25..30
Rest25.625.426.219.81,398987702450
Tier 126.828.324.723.1560702421229

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.

campaignD1 % 09-04..10D1 % 09-11..17D1 % 09-18..24D1 % 09-25..30installs 09-04..10installs 09-11..17installs 09-18..24installs 09-25..30
(unattributed)20.821.11816969882501324
FB_TinyKnightfall_A_CPA_Global_1408202632.631.1––18445––
FB_TinyKnightfall_A_ROAS_Tier1_29082026–32.6–––89––
GA_TinyKnightfa_A_ROAS_Global_200720262834.93324.2418312309182
GA_TinyKnightfa_A_ROAS_Tier1_150926–48.430.923.1–6216252
GA_TinyKnightfa_A_ROAS_Tier1_2908202639.834.8––181141––
GA_TinyKnightfa_A_ROAS_Tier1_29092026–––38.2–––34
Organic27.920.930.724.118315815087

Why: install date and traffic, not the build

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.

tierinstall_verinstallsD1 %
Rest338320.5
Rest3432419.8
Tier 1334314
Tier 13417826.4
buildinstallsd1_pctpct_started_stage_1pct_cleared_1_stagepct_cleared_5_stagespct_cleared_10_stagespct_cleared_20_stagesmedian_minutes_24hpct_under_1_minsessions_24hpct_crash_24h
3313818.37170.356.551.436.28.525.41.55.1
3460422.166.661.349.542.225.34.732.81.60.990
Query (first-session funnel by install build, installs 25–30 Sep)
-- 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 build

Stability

The 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_verfirst_installlast_installinstallsd1_eligibled1_pctmedian_d0_minutespct_under_1_min_d0median_d0_stagesinstalls_with_fatal_crash_d0d1pct_with_fatal_crash
2909-1710-0173873127.44.431.4400
3209-2110-0123923725.34.935.13104.2
3309-2310-01328315264.334.8482.4
3409-2510-0171350222.14.433.7370.980
Show the query
-- 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

2 · Weekly overview

week from (Fri)DAUDAU Chinainstalls ex-CNinstalls ChinaIAP $no-ads $.sale $other IAP $ad $total $payers% DAU watching an adeCPM $
08-2896971,943251,5445083631,0361291,673846.650.9
09-0489951,959151,34951332183698.41,448676.750.7
09-1182461,688301,2985153027831461,443567.654.4
09-187761361,11963587814549.5732121999408.353.8
09-2556033926995971310466113710378.657.0
Query (one row per user per day, from 2026-08-01)
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, d

3 · ROAS spikes and the players behind them

Installs 28–30 September: the AppsFlyer Cohort & Retention view

GA 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.

campaigninstallscost $eCPI $ROAS D0ROAS D1ROAS D2ROAS D3
GA_TinyKnightfa_A_ROAS_Global_200720261832461.37.40%46.66%50.82%50.90%
GA_TinyKnightfa_A_ROAS_Tier1_29092026412656.53.00%6.65%6.79%6.79%
GA_TinyKnightfa_A_ROAS_Tier1_150926302538.417.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 player: identification

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 timeAppsFlyer $
2026-09-29 14:23skip.ad.permanent36.22026-09-29 13:58:2836.0
2026-09-29 22:56package.hero.doom12.02026-09-29 22:51:4611.9
2026-09-29 23:02pass.tiny.guide12.02026-09-29 22:55:0611.9
2026-09-29 23:25membership12.02026-09-29 23:03:2211.9
Query (GA4 events for the players, matched by AppsFlyer ID)
-- 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.ts

Installs 24 September (GA ROAS Tier1 15-09)

A 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 dateinstallsspend $IAP $ad $ROAS to date
09-1814619365.134.60.52x
09-1944121752.712.40.30x
09-2032228777.743.00.42x
09-2120542959.517.60.18x
09-2218536556.925.10.22x
09-231753248.754.10.19x
09-241632671731151.08x
09-2519222635.323.70.26x
09-261312182.015.60.08x
09-2712627760.916.10.28x
09-281312841267.80.47x
09-2914719732.219.90.26x
09-301032825.017.20.08x
10-0111518613.29.70.12x
packpurchasesUSD (GA4)
package.event.theme.1120.0
daily.challenge.key.1713.9
skip.ad.30days111.0
special.loot.storage110.0
package.hero.doom110.0
pass.tiny.guide110.0
auto.blessing15.0
daily.promotion15.0
weekly.sugar.world15.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.

AppsFlyerGA4
Purchases16 rows, $119.8415 purchases, $89.85
Ad revenue107 rows, $88.7416 impressions, $12.54

4 · Live event: Lucky Machine (16–25 September)

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.

metricvalue
Event window16 Sep – 25 Sep 2026 (10 days)
Builds that contain the eventv28, v29, v32, v33, v34
Active users in window (ex-China)3,753
… on a build without the event1,522 (41%)
… on an event build (eligible)2,231
Users who spun at least once213 (9.5% of eligible)
Spins (ex-China)6,878; median 28 per spinner; top 10 users 20% of spins
Tickets earned (logged) vs spent8,379 earned, 6,878 spent, 1,501 unspent at close
Ticket sourcescheck-in 7,915 · ads 242 · spin prizes 222 · purchases not logged
Star milestones137 users claimed slot 0; 7 completed slot 13
Event pack $9.9942 users tapped, 8 purchases, $91.72 gross; 4 of the buyers had purchased before the event
Ticket packs x10 $4.99 / x120 $49.990 purchases
Share of event-window IAP6.3% of $1,457 (ex-China, 16–25 Sep)
Query (one row per active user, 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.u

Revenue: event window against the 10 days before

Revenue 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.

metric10 days before (06–15 Sep)event (16–25 Sep)6 days after (26 Sep–1 Oct)event vs beforechange %
days1010600
DAU875668502-208-23.7
total revenue $1,8121,637536-175-9.7
IAP $1,6291,457443-172-10.6
Lucky Machine pack $091.7091.7–
no-ads $69638887.7-308-44.3
other IAP $93397735644.24.7
ad $18318092.1-2.8-1.5
revenue $ per day18116489.3-17.5-9.7
IAP $ per DAU0.1860.2180.1470.03217.2
other IAP $ per DAU (excl. pack, excl. no-ads)0.1070.1460.1180.04037.3
ad $ per DAU0.0210.0270.0310.00629.1
payers815830-23-28.4
groupusersIAP per user, 10 days before $IAP per user, event $% paying before% paying during eventchange %
did not spin4021.31.163.5-17
spun623.74.117.714.511.9

How ad tickets are granted

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.

regionad tickets per user-dayuser-daysticketsevent ads watchedevent ad rewards loggedmedian seconds, first to last ticket
China177000
China212001.1
China43120085.3
China5103515004.2
rest of world122110
rest of world2244445.5
rest of world326332.7
rest of world54623025264.6
Show the query
-- 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, 2

87% 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).

regionwhat_preceded_the_grantgrantsusersshare %
Chinano ad: load failed, ticket granted anyway861816.1
Chinano ad activity from 60 s before to 30 s after4479683.9
rest of worldevent ad shown (impression on rewareded_mini_event)28711.4
rest of worldan ad was shown, but on another placement210.800
rest of worldad show/reward logged, no impression331.2
rest of worldno ad: load failed, ticket granted anyway1646.5
rest of worldno ad activity from 60 s before to 30 s after1964180
Show the query
-- 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
regiongrantsusersuser_daysgrants_per_user_dayseconds between grants (p25 / median / p75)grants_within_15s_of_previous
China5331131134.70.9 / 1.0 / 1.2412
rest of world24553544.50.9 / 1.2 / 1.6168
Show the query
-- 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

Daily activity

ddau_ex_cnspinners% DAU spinningspinstickets_check_intickets_adstickets_from_spinsevent_ad_impressionsstar_slot0_claimspack_tapspack_purchasespack_usdchina_spinners
09-16771101.317460300035000
09-17745263.53783052580589000
09-18711121.720631020142078000
09-19623264.244050527021011223.463
09-206353869441,005450112111110.046
09-21601254.26085902705125110.022
09-22635203.17658701207141223.112
09-23654355.41,2241,4451102213112.68
09-24634284.48911,2602001183112.64
09-25680375.41,2851,6152803216003
Show the query
-- 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 d

By build

The 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.

versionactive users% of users spinningspins per spinnercheck-in tickets per spinnerad tickets per 1k usersevent ad impressions per 1k userspack taps per 1k userspack purchases per 1k userspack ARPU $
282205.516.710.8114027.300
291,042824.329.21082430.70.9600.010
3237210.833.94294.116.116.15.40.061
3345313.944.551.31321130.98.80.105
3414410.433.530.369.406.96.90.079

Defect: ad tickets granted without an ad

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.

regionversionad-ticket grantsevent ad impressions per 100 grants% of grants on days with no ad impression
China29536095.5
rest of world26.215066.7
rest of world2845077.8
rest of world2912420.259.7
rest of world32252080
rest of world332810.789.3
rest of world34500
Show the query
-- 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 > 0

Defect: check-in reward claimed twice

41 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_dayuser_daysmean_grantsallowed max (= event day)max_grantsover_limit_user_days
1121110
2322.0220
3213.0361
4844.64815
5844.65107
6475.16124
7435.8770
8476.38162
9436.79182
10487.110202
regionversioncheck-in users% of check-in users over-grantedextra tickets per affected user
China2914515.921.5
rest of world28342.95
rest of world291287.821
rest of world32482.135
rest of world334511.144
rest of world34812.550

Spending and retention of participants

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.

engagementgroupusersiap_usd_duringactive_after_event_pctdays_active_after
1 daydid not spin1,3280.06112.00.172
1 dayspun521.7250.481
2–3 daysdid not spin4850.33520.80.363
2–3 daysspun851.327.10.494
4–6 daysdid not spin1042.843.30.990
4–6 daysspun363.661.11.9
7–10 daysdid not spin1012.787.13.3
7–10 daysspun406.8954.7
Query (pack grants by SKU position — returns no rows)
-- 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 DESC

5 · Ad monetisation

No 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)rewardsshare %
rewarded ad shown (same placement)4,15399.6
backup interstitial shown (same placement)20.050
ad shown on another placement30.070
ad_show logged, no impression20.050
no ad: watch-ad tap only80.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_bucketrewardsmax_gap_salso_impression_within_17s_before
next impression 0–1 s after3,7320.99825
1–5 s after3834.82
5–17 s after2816.60
17–60 s after659.00
1–10 min after41101
no impression within 1 h after15–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:

comparisonuser_placement_daysrewardsimpressionsexcess_rewards
equal1,9234,0884,0880
more rewards than impressions26805426
Query (proof 1)
-- 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
Query (proof 2)
-- 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 1

By build, placement, country and entitlement

No 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.

buildrewardsuserswithout ad% with matching ad
291,652196599.7
341,599210299.9
3239173199.7
3331064199.7
26.220621199.5
placementrewardsuserswithout ad% with matching ad
acquire_hero1,026266199.9
daily_quest34194199.7
key_challenge_BossRush2791390100
flybox_gold245750100
key_challenge_GoldRush243125299.2
summon_gear23785199.6
lucky_spin22964199.6
summon_skill183550100
summon_companion15848199.4
blessing_attack123400100
welcome_back122480100
key_challenge_HoneyWar9533198.9
leagueboss_attack_ad87270100
skip_time_research84150100
game_speed_x28053297.5
countryrewardsuserswithout ad% with matching ad
Indonesia439240100
Brazil40130199.8
Spain31018199.7
United States295420100
Germany212300100
Thailand189260100
South Korea142230100
Italy13580100
Canada111270100
Poland11160100
Taiwan11080100
Romania10230100
entitlementrewardsuserswithout ad% with matching ad
neither4,1475091099.8
owns membership1120100
owns a no-ads SKU1020100
Show the query
-- 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:

builduserswatch-ad taps per 1k usersrewarded impressions per 1k usersplacement rewards per 1k usersplacement rewards per impressiongeneric ad_reward per impression
26.29953526326311.4
291671,1621,1381,1441.01.0
328419017917914.7
333952912762781.01.7
341,2571,4571,2671,27211.0
Show the query
-- 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 version

Rewarded 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.

versionuserswatch-ad taps per 1k usersrewarded impressions per 1k usersad ARPU $rewarded fill %% users with DNS failuresfailed loads per affected user
26.2511,0395100.01096.224
291101,7641,7270.04658.78.28
32622582420.00530.94.882
333203593410.02473.62.5190
341,0501,7441,5170.09576.44.7158
Show the query
-- 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 DESC

Why 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:

  • A · 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.
  • B · 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_typeusersfailed_loadsfailed_loads_per_usermedian_per_usermax_per_userpct_of_failed_loadstop_countries_by_users
A · lookup of ms.applvn.com returns no address885,6136461,15232.4Sudan 11, Indonesia 7, Iran 7, Brazil 5, Bangladesh 4, United States 4
B · lookup returns 0.0.0.0 (DNS ad blocker)144,3553111511,25125.2Indonesia 5, Brazil 2, United States 2, Italy 1, Latvia 1, Russia 1
C · no error message logged246,788283112,49739.2Iran 10, Ukraine 4, Indonesia 2, Colombia 1, Cuba 1, Ethiopia 1
D · no fill (no ad available)3114280.100Canada 1, Indonesia 1, Singapore 1
E · other (connection reset, TLS, timeout …)165463423103.2Iran 4, Congo - Kinshasa 2, Brazil 1, Cuba 1, Ethiopia 1, Indonesia 1
Show the query
-- 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_type

6 · No-ads sale pack removal

The 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.

versionusers.sale taps per 1k users.sale purchases per 1k usersfull-price no-ads taps per 1k usersfull-price no-ads purchases per 1k usersfull-price tap → purchase %
2835416.98.525.48.533.3
26.28621.21.24.600
292,1292.82.38.51.416.7
324830037.32.15.6
335770081.55.26.4
341,2630040.449.8
Show the query
-- 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_day

Estimated 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.

windowarminstallsno-ads buyers /1kno-ads taps /1kno-ads $/installother IAP $/installad $/installtotal $/installtotal excl. top payer
D0–D2offer present (installs 09-10..20)2,44513.126.60.2130.1730.0750.4610.415
D0–D2offer absent (installed on v32+)7625.261.70.0610.2690.1060.4360.344
D0–D6offer present (installs 09-10..20)2,44513.127.40.2130.2010.0780.4910.445
D0–D6offer absent (installed on v32+)6256.473.60.0740.3430.1140.5310.371

7 · China

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.

regionversionusersminutes per usershop taps per 1k userspurchases per 1k usersIAP ARPU $ad impressions per userad ARPU $rewarded fill %
China2982030.2108000.0200.00027.8
rest of world291,28133.411250.70.4081.30.05749.9
rest of world341,2572611939.80.3431.30.07976.4
rest of world3356727.325258.20.4790.5400.06272.4
rest of world324802614239.60.3450.8200.04669.7
Show the query
-- 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 DESC

8 · Instrumentation: new events and coverage changes

Three 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_namefirst_dayfirst_versionversions_seeneventsusers
event_slot_claim_stars_0 … _13 (14 events)09-162828,29,32,33,34495179
ad_rewarded_rewareded_mini_event09-172929,32,333711
app_exception09-213030,31,32,33,347039
Show the query
-- 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 DESC

Lucky 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_namesloteventsusers% of slot-0 usersversions_seen
event_slot_claim_stars_0018817910028,29,32,33,34
event_slot_claim_stars_1196935228,29,32,33,34
event_slot_claim_stars_2244432428,29,32,33
event_slot_claim_stars_33282715.128,29,32,33
event_slot_claim_stars_44252413.428,29,32,33
event_slot_claim_stars_55232212.328,29,32,33
event_slot_claim_stars_6618179.528,29,32,33
event_slot_claim_stars_7717168.928,29,32,33
event_slot_claim_stars_8813126.728,29,32,33
event_slot_claim_stars_9911105.628,29,32,33
event_slot_claim_stars_1010984.528,29,32
event_slot_claim_stars_1111873.928,29,32
event_slot_claim_stars_1212773.928,29,32
event_slot_claim_stars_1313884.528,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.

eventkeyoccurrencesdistinct_valuessample_values
ad_rewarded_rewareded_mini_eventfirebase_screen21Lobby
app_exceptionengagement_time_msec202046,115,218,126,40,44
app_exceptionfatal7011
app_exceptiontimestamp70701790199152266,1790529189672,1790461233038,1790148190110,1790469773585,1790148178987
event_slot_claim_stars_*firebase_screen2131Lobby
Show the query
-- 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, 2

Event 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_nameusers_v29users_v32users_v33users_v34pct_users_v29pct_users_v34
event_slot_claim_stars_058312454.50.400
Show the query
-- 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

9 · Event collection gap on 28 September

09-28 0609-28 0709-28 0809-28 0909-28 1009-28 1109-28 1809-28 2309-29 0009-29 0109-29 0209-29 0309-29 0409-29 05
events2,2561,8493,4862397351012481431632,5293,2603,045
users6755593075165511384854

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.