A real run (sanitized debug dump) showed why the walk stopped after Next:
the data types step is the same "Data Collection" dialog (URL unchanged),
and each checkbox's accessible name is its label run together with its
help text ("NameIncluding first or last name"). Some labels also differ
from Apple's help page ("Purchases", "Other Data").
- labelNames: the exact name, then the label as a prefix that ends where
the help text starts (no lowercase letter follows), so "Name" never
matches "Phone Number...", "Head" never "Health...", "Contact" never
"Contacts...". Used for the data types and the purposes.
TYPE_ALIASES maps Purchase History / Other Data Types to the console's
labels.
- Dialog per step: a section's dialog is also found as the one visible
dialog with a heading of that name.
- Every click and check is bounded (ACTION_MS, 10 s; the scan test now
also rejects clicks without a timeout).
- The flow fixture mirrors the real dialog's structure (dialog/h2 title,
group h3s, input name = Apple's enum, <label> with label + help <p>s,
Save disabled until a type is ticked); no data from the dump. A test
checks every Apple type matches exactly one real-style checkbox.
- Pitfall 15.
Publish is still never clicked; unknown steps still get the watchdog dump.
6.2 KiB
6.2 KiB
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 |
|---|---|---|---|
| 15 | 2026-10 | The real App Privacy "Data Collection" dialog names each data type checkbox by its label run together with its help text ("NameIncluding first or last name"), and labels some types differently from Apple's help page ("Purchases", "Other Data"): exact-name matching found none. | labelNames (exact name, then the label as a prefix that ends where the help text starts) and TYPE_ALIASES in cli/lib/console-forms.ts; the flow fixture mirrors the real dialog's structure (tests/console-flow.test.ts). |
| 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
- A row above (next number, month, one-sentence lesson, the guard with file or test names).
- A section in
skills/store-pitfalls/SKILL.md: symptom → cause → what to do → which command guards it. Thenbun tools/gen-adapters.ts. - 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.