A real `console fill apple-app-privacy` clicked Next onto a console page the engine did not recognise and logged nothing for 8 hours. - Step watchdog: after a click, or while the form is expected, no known page within --step-timeout seconds (default 30) -> "the page after "Next" is not a page storebound knows ... Buttons there: ...", a debug dump, and the manual steps. It keeps watching, but 20 min without progress closes the window and exits 1. - A heartbeat line every minute while waiting (also during sign-in); the final review wait is bounded (2 h) with a reminder every 10 min. - Debug dump (cli/console/debug.ts) in .storebound/console-debug/ <form>-<time>/: page.json (URL without query, headings, dialogs, every visible control with role and name), page.html (rebuilt from a clone: no scripts, values, hidden inputs or non-whitelisted attributes) and a small screenshot. Emails, phone numbers and the header / account area are replaced; cookies, storage and tokens are never read. --debug-dump writes one at every step for bug reports. - Guardrail tests: an unknown-page fixture (dump sanitized, bounded exit) and a scan of cli/console for while loops without a deadline and Playwright waits without a timeout (one waitForLoadState fixed). - Pitfall 14 (docs/pitfalls.md, store-pitfalls skill); the console-autofill skill says how to send a dump.
12 KiB
12 KiB
| name | description |
|---|---|
| store-pitfalls | Known pitfalls with App Store Connect, Google Play and storebound itself, learned from real releases, and what to do about each - store lists that lag right after a create, dry runs that print a count instead of a plan, App Store names taken on a storefront, keyword limits in bytes, Data safety answers without code evidence, missing font glyphs in screenshots, console sign-in and sessions, Capacitor monorepo plugins, duplicates from retries, re-runs and parallel runs, App Store search ranks that are only a trend, Play without a search API, Play's Create app form (no API, opener and submit share a name, Free is final), App Privacy purposes over-mapped (Play advertising is Third-Party Advertising only), an unknown console page (bounded wait and a debug dump). Read it before any store task, and when a store command does something you did not expect. |
Store pitfalls
Each entry: symptom → cause → what to do → what guards it now. The full changelog of lessons with dates is
docs/pitfalls.md in the storebound repository. When you hit a new one, tell the owner and add it there (and here).
1. A product you just created is "not there"
- Symptom:
apple iap-reviewuploads nothing for a productapple iapcreated a minute ago; a list looks short. - Cause: App Store Connect's
/inAppPurchasesV2list (and Play's product list) can lag for a short while after a create. A command that walks the list skips what is not in it yet. - Do: wait a minute and re-run. Never assume a product was handled because the command finished.
- Guard:
apple iap-reviewcompares the config's products with the store's list, names the missing ones ("the store's list can lag a minute after creating; re-run") and exits 1.apple iapandplay productsread the list again after creating and warn about any new product that is not in it yet; their dry runs name products that are not on the store.
2. A dry run that only prints a count
- Symptom:
--drysays "listing: 20 locales" or "3 change(s)", but not which fields would change to what. - Cause: the command logged a summary instead of the plan.
- Do: never ask the owner to approve a count. Show the rows that change (item, action, old → new). If a command
only prints a count, say so and show the owner the diff from
pull/--jsonoutput instead. - Guard:
cli/lib/dry.tsdryPlan(title, rows)prints the standard plan table (apple iap,apple iap-review,play productsuse it);tests/pitfalls.test.tsscanscli/**for--drybranches that print nothing but a count.
3. "The app name you entered is already being used"
- Symptom: creating the App Store record or saving the name fails although you own no app with that name.
- Cause: App Store app names are unique per storefront, across all developers.
- Do: ask the owner for a variant (a short suffix such as "Example Notes: Lists") and set
apple.namein the config; keepapp.namefor Play and the brand. Do not pick the suffix yourself. - Guard:
apple.nameoverrides the App Store name.
4. Keyword field "too long" at fewer than 100 characters
- Symptom: App Store Connect rejects keywords that look short enough, mostly in Turkish, German, CJK.
- Cause: the 100 limit counts UTF-8 bytes, not characters ("ş" is 2 bytes, a CJK character 3).
- Do: shorten by bytes; drop words, not commas.
- Guard:
storebound checkcounts bytes for keywords.
5. Data safety says "email collected" for an app that never asks for one
- Symptom: the proposal marks email (or phone) as collected because the app has accounts.
- Cause: account features do not imply email collection (anonymous ids, Game Center, Play Games sign-in).
- Do: answer only what the code proves; ask the owner for the rest (rule 5).
- Guard:
storebound scanmarks email only with evidence (an email field, an email sign-up call) and otherwise says "ask owner".
6. Boxes or wrong glyphs in localized screenshots
- Symptom: Turkish, Polish or Vietnamese captions show fallback glyphs (ş, ğ, ł, ệ) while the English ones are fine.
- Cause: the font (or the Google Fonts subset that was downloaded) does not cover those characters, and the browser silently draws them with another font.
- Do: check every language's characters before rendering, pick a font that covers them or let the fallback fill in.
- Guard:
storebound shots fonts/shots checkcheck each language's characters against the fonts' glyphs.
7. Console automation stops at sign-in or loses the session
- Symptom:
console fillwaits on a sign-in page, cannot find fields, or is signed out on the next run. - Cause: the App Store Connect sign-in runs inside an
idmsa.apple.comiframe; "User Access" is a plain heading, not a labeled group; Chrome drops session cookies unless the last session is restored. - Do: let the owner sign in in the opened window (never type credentials); re-run after the sign-in.
- Guard: the console session handles the iframe and the plain heading and starts Chrome with
--restore-last-session; a partial miss fills every field found and leaves only the missing ones to the owner.
8. scan misses Capacitor plugins in a monorepo
- Symptom: SDKs or privacy manifests of Capacitor plugins are not found.
- Cause: the plugins live in
apps/native/node_modules(a workspace package), not in the rootnode_modules. - Do: run
scanfrom the project root; check that the stack line lists the plugins. - Guard:
storebound scanandprivacy-manifestresolve packages through workspace folders too.
9. A second run (or a retry) creates a duplicate
- Symptom: two in-app purchases or subscription groups with the same name, a screenshot twice in a set, an offer code campaign or a custom product page twice, after a timeout, a crash, a re-run a minute later, or two runs at once.
- Cause: a POST create retried after a timeout or a 5xx may have been applied the first time; the store's lists lag right after a create, so a re-run does not see what the last run made; two writers race; a Play edit redone after a commit whose answer was lost.
- Do: just run the command again: every write command is an upsert and converges (
docs/idempotency.md). Never retry a create by hand (storebound api) without a GET first. If a run says another storebound run holds the lock, wait for it; use--force-unlockonly when that run is really gone. Never run two write commands for the same app in parallel. - Guard:
cli/lib/upsert.ts(findOrCreate: fresh re-check + journal.storebound/created.json; createOnce: 409 = found, an unclear answer is looked up, never re-sent blindly), retries only for requests that are safe twice, Play edits deleted on every error and never redone after an unclear commit, screenshot / preview sets keep the matching prefix in order,.storebound/locks/per app and store.tests/idempotency.test.ts.
10. "We rank #4 for this keyword" (from the Search API)
- Symptom:
aso ranksays #4, the owner's phone shows the app lower (or higher) in App Store search. - Cause: the iTunes Search API order is close to, not equal to, App Store search: no personalisation, no search ads, no in-app event rows. It has no subtitle field either.
- Do: read ranks as a trend over weeks (
aso auditarrows), never as a promise; never claim a competitor's subtitle (no public API has it); a popularity number exists only from Apple Ads, never estimate one. - Guard:
aso rank,aso competitorsandaso auditsay it in their output;aso suggestshows popularity "unknown" without Apple Ads credentials.
11. "Check our Google Play keyword ranks"
- Symptom: the owner asks for Play ranks or Play competitors per keyword.
- Cause: Google Play has no public search API; reading Play search pages would be scraping.
- Do: say so. Measure Play through the Play Console acquisition reports (search terms) and
analytics snapshot(store listing visitors, acquisitions); improve the listing text withaso audit(short description, density). - Guard:
asocovers App Store search only; the aso-basics skill states the limit.
12. A new Play app: "Create app" is both the opener and the submit
- Symptom: an agent cannot create the Play app through the API, or clicks "Create app" without knowing whether it opens the form or submits it; or an app goes out as Free and the owner later wants it paid.
- Cause: the Play Developer API cannot create apps. Play Console's Home has a "Create app" button that opens the form, and the form's final button has the same words. Free cannot become Paid later on Google Play. The package name is not in this form: the first AAB upload sets it.
- Do:
storebound console fill play-app-record(pick the account withplay.developerIdorplay.developerAccount); tell the owner that Free is final; the owner ticks the declarations and presses Create app; thenstorebound play upload --track internal(orflow run release) sets the package name. - Guard:
createAppAllowedlets the opener through only on the app list, never inside a form or dialog (English and Turkish); agreement checkboxes are never ticked; Free or paid is outlined as an owner check.
13. App Privacy purposes over-mapped
- Symptom: App Privacy says Developer's Advertising or Marketing for Device ID, Coarse Location, Product Interaction or Performance Data, although the app only shows an ad SDK's ads.
- Cause: a Play purpose was turned into two Apple purposes ("advertising" -> Third-Party Advertising AND Developer's Advertising or Marketing). Apple's definitions (developer.apple.com/app-store/app-privacy-details/): third-party ads are Third-Party Advertising; Developer's Advertising is first-party ads or marketing to your users.
- Do: one Apple purpose per Play purpose; put the owner's real answers in
appPrivacy.types(Apple type ids, purposes, linked, tracking) and letstorebound checkcompare them with Info.plist and PrivacyInfo.xcprivacy. - Guard:
applePurpose(cli/lib/declarations.ts),appPrivacy.typesvalidation andappPrivacyMismatches(cli/lib/app-privacy.ts),tests/app-privacy.test.ts.
14. An unknown console page: bounded wait and a debug dump
- Symptom:
console fillclicked Next (or opened the form) and then nothing happened in the log for a long time. - Cause: the console showed a page storebound does not recognise (a changed layout, a new question): no field it can fill and no button it may click.
- Do: read the warning: it names the page ("the page after "Next""), its buttons, and where the debug dump is.
Do that part by hand (the manual steps follow), or stop. To report it, re-run with
--debug-dumpand attach the folder.storebound/console-debug/<form>-<time>/after the owner looked atscreen.jpg(the screenshot shows what the window showed).page.json/page.htmlhold labels and structure only: no values, cookies or tokens; emails, phone numbers and the account area are replaced. - Guard: the watchdog in
fillSession(--step-timeout, default 30 s; a heartbeat line a minute; 20 min without progress ends the run with exit 1),cli/console/debug.ts,tests/console-flow.test.ts(unknown-page fixture and a scan for waits without a deadline).
Adding a lesson
Every bug found in real use gets an entry in docs/pitfalls.md and here, and, where possible, a check or a test
that fails without the fix (see CONTRIBUTING.md).