# Pitfalls: lessons from real use A changelog of mistakes found while shipping real apps and games with storebound, newest first. Each entry says what went wrong, why, and what now guards it. The agent-facing short version is `skills/store-pitfalls/SKILL.md`; keep the two in step. Rule (CONTRIBUTING.md): every bug found in real use adds an entry here and, where possible, a check or a test that fails without the fix. | # | date | lesson | guard | |---|---|---|---| | 14 | 2026-10 | `console fill apple-app-privacy` clicked Next onto a real console page it did not recognise and then waited 8 hours without a single log line. | Every wait of the console engine is bounded: a step watchdog (`--step-timeout`, 30 s) writes a sanitized debug dump to `.storebound/console-debug/` and prints the page's buttons and the manual steps; a line a minute while waiting; no progress for 20 min stops the run with exit 1; `--debug-dump` dumps every step (`cli/console/debug.ts`, `tests/console-flow.test.ts`, which also scans the engine for waits without a deadline). | | 13 | 2026-10 | App Privacy purposes were over-mapped: Play "advertising" became both Third-Party Advertising and Developer's Advertising or Marketing (Device ID, Coarse Location, Product Interaction, Performance Data). | `applePurpose` maps one Play purpose to one Apple purpose (advertising: third-party only); Developer's Advertising only from `appPrivacy.types`; `check` compares `appPrivacy.types` with Info.plist / PrivacyInfo.xcprivacy (`tests/app-privacy.test.ts`). | | 12 | 2026-10 | A new Play app has no API: Play Console's "Create app" is the only way, its page-level button carries the same words as the form's final submit, and Free cannot become Paid later. | `console fill play-app-record`: `createAppAllowed` lets the opener through only on the app list, never in a form or dialog (EN + TR); declarations never ticked; Free / Paid outlined as an owner check (`tests/console-fill.test.ts`). | | 11 | 2026-10 | Google Play has no public search API, so there is no Play keyword rank or Play competitor list to read without scraping. | `aso` reads App Store search only (iTunes Search API); `aso audit` covers Play through the listing text (short description, density); the aso-basics skill says so. | | 10 | 2026-10 | The iTunes Search API order is close to, not equal to, App Store search (no personalisation, no search ads, no in-app event rows) and it has no subtitle field. | `aso rank` / `aso competitors` / `aso audit` label ranks as a trend and show no competitor subtitles; popularity only from Apple Ads, never estimated (`cli/lib/aso-intel.ts`). | | 9 | 2026-10 | A retried or repeated write can duplicate on a live store: a POST create retried after a timeout or 5xx, a re-run during the list lag right after a create, two runs at once, a Play edit redone after an unclear commit, a crash in the middle of a screenshot set. | Every write is an upsert (`cli/lib/upsert.ts`: findOrCreate with a fresh re-check and a journal of recent creates, createOnce treating 409 as found, no blind retry of a create); API clients retry only requests that are safe twice; Play edits are all or nothing; asset sets keep the matching prefix; one lock per app and store (`cli/lib/lock.ts`, `--force-unlock`). Table: `docs/idempotency.md`; `tests/idempotency.test.ts` runs every write twice and asserts zero writes on the second run. | | 8 | 2026-10 | Capacitor plugins in a monorepo live in `apps/native/node_modules`, so `scan` missed them. | `scan` / `privacy-manifest` resolve workspace packages (`cli/lib/scan/project.ts`, `cli/lib/node-packages.ts`). | | 7 | 2026-10 | Console UIs: the App Store Connect sign-in runs in an `idmsa.apple.com` iframe; "User Access" is a plain heading, not a labeled group; Chrome drops session cookies without `--restore-last-session`. | `console` session and fill (`cli/console/`), `tests/console-*.test.ts`. | | 6 | 2026-10 | Screenshot fonts: only part of the glyphs a language needs were available, so Turkish / Polish / Vietnamese characters fell back to another font silently. | `shots fonts` / `shots check` check every language's characters against the fonts (`tests/glyphs.test.ts`). | | 5 | 2026-10 | Account features do not imply email collection; Data safety said "email collected" without evidence. | `scan` marks email only with code evidence, else "ask owner" (`cli/lib/scan/declare.ts`). | | 4 | 2026-10 | The App Store keyword limit (100) counts UTF-8 bytes, not characters. | `check` counts bytes (`cli/lib/copy.ts`). | | 3 | 2026-10 | App Store app names are unique per storefront: "already being used" although the developer owns no such app. | `apple.name` (App Store name with a suffix the owner picks); `app.name` stays the brand. | | 2 | 2026-10 | Dry runs that print no plan ("listing: 20 locales") break "show the dry run to the owner". | `cli/lib/dry.ts` `dryPlan(title, rows)`; `tests/pitfalls.test.ts` scans `cli/**` for `--dry` branches that print only a count (allowlist with TODOs for commands being fixed). | | 1 | 2026-10 | App Store Connect's `/inAppPurchasesV2` list lags right after `apple iap` creates a product; `apple iap-review` then skipped it silently. | `apple iap-review` names config products missing from the store's list and exits 1; `apple iap` and `play products` re-read the list after creating and warn (`cli/lib/store-lag.ts`, `tests/pitfalls.test.ts`). | ## Adding an entry 1. A row above (next number, month, one-sentence lesson, the guard with file or test names). 2. A section in `skills/store-pitfalls/SKILL.md`: symptom → cause → what to do → which command guards it. Then `bun tools/gen-adapters.ts`. 3. Where the code can know it: a check (`check`, `validate`, `scan`, a warning in the command) and a test with recorded store answers that fails without the fix.