.sale activity on v32–v36. This page is the 18 Sep snapshot of v26.2–v29. See the 10 Oct revert decision document →.sale packs are still being soldanalytics_538412813 · generated 2026-10-10On v26.2, v28 and v29 the .sale ad-removal packs are tapped, paid
for, and the reward is handed out. 24 completed purchases and
12 in-game reward payouts across those builds, the most recent on
2026-09-18.
It happens for players who installed on that version and for players who installed on an earlier one — the split is in the first table.
tapped = purchase_click, the player opening the offer.
paid = in_app_purchase, Play billing them.
rewarded = earn, the game handing over the entitlement.
Any one of them being non-zero means the pack exists on that build; all three means it
works end to end.
| Build | Player installed… | Tapped | Paid | Rewarded | Users | Last seen |
|---|---|---|---|---|---|---|
| v26.2 | install version unknown | 0 | 2 | 0 | 2 | 2026-09-16 |
| v26.2 | installed ON this version | 14 | 16 | 8 | 21 | 2026-09-15 |
| v26.2 | installed on an EARLIER version | 1 | 2 | 1 | 2 | 2026-09-11 |
| v28 | install version unknown | 0 | 1 | 0 | 1 | 2026-09-16 |
| v28 | installed ON this version | 5 | 2 | 1 | 6 | 2026-09-17 |
| v29 | installed ON this version | 1 | 1 | 2 | 3 | 2026-09-18 |
v27.2 returns no rows at all — that is not evidence of removal. That build only ever reached 7 devices, for a single day; there is no sample to find anything in. Treat v28 and v29 as the releases that matter.
On v28 and v29 the buyers are players who installed on those builds. That is expected rather than surprising: these packs only ever sell in a player's first few days, so a long-standing player would not buy one on any version. v26.2 is where both cohorts show up.
The query that produced the table above — copy and re-run it:
WITH fo AS ( -- which version did each player install on?
SELECT user_pseudo_id uid, ANY_VALUE(app_info.version) install_version
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE event_name = 'first_open' GROUP BY uid
),
sale AS (
SELECT app_info.version app_version, user_pseudo_id uid, event_name,
PARSE_DATE('%Y%m%d', event_date) day,
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_*`
)
SELECT s.app_version,
CASE WHEN f.install_version IS NULL THEN 'install version unknown'
WHEN f.install_version = s.app_version THEN 'installed ON this version'
ELSE 'installed on an EARLIER version' END cohort,
COUNTIF(s.event_name='purchase_click') tapped,
COUNTIF(s.event_name='in_app_purchase') paid,
COUNTIF(s.event_name='earn') rewarded,
COUNT(DISTINCT s.uid) users,
MAX(s.day) last_seen
FROM sale s LEFT JOIN fo f ON f.uid = s.uid
WHERE s.sku LIKE '%.sale'
AND s.app_version IN ('26.2','27.2','28','29')
GROUP BY app_version, cohort
ORDER BY app_version, cohort
GA4 only ever sees the client. The other half of "is it removed" is the storefront
itself, so this was pulled straight from the Play Developer API
(androidpublisher v3, monetization.onetimeproducts.list) rather
than read off a dashboard. All 95 products came back, 16,530
product×region price rows, and every one of the three .sale packs
is ACTIVE and AVAILABLE with its price unchanged:
| Product ID | Purchase option | US price | State | US availability | Regions available |
|---|---|---|---|---|---|
| skip.ad.30days.sale | buy-no-ads-30-days-sale | USD 4.99 | ACTIVE | AVAILABLE | 174 |
| skip.ad.annual.sale | buy-no-ads-annual-sale | USD 4.99 | ACTIVE | AVAILABLE | 174 |
| skip.ad.permanent.sale | buy-no-ads-permanent-sale | USD 10.99 | ACTIVE | AVAILABLE | 174 |
| skip.ad.10days | buy-no-ads-10-days | USD 4.99 | ACTIVE | AVAILABLE | 174 |
| skip.ad.30days | buy-no-ads-30-days | USD 10.99 | ACTIVE | AVAILABLE | 174 |
| skip.ad.3days | buy-no-ads-3-days | USD 2.99 | ACTIVE | AVAILABLE | 174 |
| skip.ad.annual | buy-no-ads-annual | USD 10.99 | ACTIVE | AVAILABLE | 174 |
| skip.ad.permanent | buy-no-ads-permanent | USD 29.99 | ACTIVE | AVAILABLE | 174 |
The pull was also diffed against the copy committed on 2026-09-16, field by field across all 16,530 rows: zero products added, zero removed, zero price changes, zero state changes. Nothing has been deactivated or unpublished on the store side.
Re-pull the live catalogue yourself:
uv run --with google-api-python-client --with google-auth --with pyyaml \\
python dashboard/pull_play_catalogue.py \\
--key ~/.config/play-sa.json --out schema/play_catalogue.yaml
Worth noting skip.ad.annual.sale in that table: it is ACTIVE and priced on
the store, but it has never once been tapped or bought in the entire GA4 history — the
client has never surfaced it. Being live on Play and being live in the game are two
different things, which is exactly why both were checked.
Not a sample and not an aggregate. This is the complete list, 15 events, nothing filtered out.
| When (UTC) | Running | Installed on | Event | SKU | USD | Country |
|---|---|---|---|---|---|---|
| 2026-09-16 13:25 | v28 | v28 | purchase_click | skip.ad.permanent.sale | Switzerland | |
| 2026-09-16 13:26 | v28 | v28 | earn | skip.ad.permanent.sale | Switzerland | |
| 2026-09-16 13:26 | v28 | v28 | iap_purchase_success | skip.ad.permanent.sale | Switzerland | |
| 2026-09-16 13:26 | v28 | v28 | first_purchase | skip.ad.permanent.sale | Switzerland | |
| 2026-09-16 13:52 | v28 | v28 | in_app_purchase | skip.ad.permanent.sale | $10.89 | Switzerland |
| 2026-09-16 19:21 | v28 | v28 | purchase_click | skip.ad.permanent.sale | Hong Kong | |
| 2026-09-16 22:40 | v28 | ? | in_app_purchase | skip.ad.permanent.sale | $10.99 | United States |
| 2026-09-17 05:17 | v28 | v28 | purchase_click | skip.ad.permanent.sale | United States | |
| 2026-09-17 06:16 | v28 | v28 | purchase_click | skip.ad.permanent.sale | South Korea | |
| 2026-09-17 13:35 | v28 | v28 | purchase_click | skip.ad.30days.sale | Indonesia | |
| 2026-09-17 13:38 | v28 | v28 | in_app_purchase | skip.ad.permanent.sale | $13.84 | Spain |
| 2026-09-17 21:00 | v29 | v29 | in_app_purchase | skip.ad.permanent.sale | $13.29 | Poland |
| 2026-09-17 22:16 | v29 | v29 | earn | skip.ad.permanent.sale | Australia | |
| 2026-09-17 22:16 | v29 | v29 | earn | skip.ad.30days.sale | Australia | |
| 2026-09-18 10:13 | v29 | v29 | purchase_click | skip.ad.permanent.sale | Germany |
Only in_app_purchase carries a USD value; the client-side events
(purchase_click, earn, iap_purchase_success,
first_purchase) have no money field at all, which is why that column is blank
on those rows.
The query that produced the event log above:
WITH fo AS (
SELECT user_pseudo_id uid, ANY_VALUE(app_info.version) install_version
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*`
WHERE event_name = 'first_open' GROUP BY uid
)
SELECT FORMAT_TIMESTAMP('%Y-%m-%d %H:%M', TIMESTAMP_MICROS(e.event_timestamp)) ts,
e.app_info.version app_version,
IFNULL(f.install_version,'?') install_version,
e.event_name,
REPLACE(COALESCE(
(SELECT value.string_value FROM UNNEST(e.event_params) WHERE key='product_id'),
(SELECT value.string_value FROM UNNEST(e.event_params) WHERE key='position')
),'com.idle.tiny.','') sku,
ROUND(e.event_value_in_usd,2) usd, e.geo.country country
FROM `tiny-knightfall-idle-rpg-game.analytics_538412813.events_intraday_*` e
LEFT JOIN fo f ON f.uid = e.user_pseudo_id
WHERE e.app_info.version IN ('27.2','28','29')
AND COALESCE(
(SELECT value.string_value FROM UNNEST(e.event_params) WHERE key='product_id'),
(SELECT value.string_value FROM UNNEST(e.event_params) WHERE key='position')
) LIKE '%.sale'
ORDER BY ts
The two queries above scan the whole export (~2.5 GiB each). This one filters on
_TABLE_SUFFIX first, so it prunes to the recent shards and reads
about 60 MB — cheap enough to re-run whenever you want to check.
WITH e AS (
SELECT app_info.version app_version, event_name, user_pseudo_id,
PARSE_DATE('%Y%m%d', event_date) day,
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 >= '20260916' -- v28 launch day; widen to look further back
)
SELECT app_version, sku, event_name,
COUNT(*) events, COUNT(DISTINCT user_pseudo_id) users, MAX(day) last_seen
FROM e
WHERE sku LIKE '%.sale'
GROUP BY app_version, sku, event_name
ORDER BY app_version, sku, event_name
app_info.version is a STRING, not a number.
IN (28, 29) returns nothing, and so does any
SAFE_CAST(app_info.version AS INT64) — because v26 stamps itself
26.2 and casting it yields NULL. Always quote:
IN ('28','29').earn stores the SKU in position, not
product_id. Filter only on product_id and every reward
payout disappears from the results — which is exactly the signal that proves the pack is
actually being delivered. Hence the COALESCE in all three queries.