--- name: analyze-unrecognized-apps description: Pull unrecognized host app bundle IDs from Google Analytics via gog, diff them against the mapping tables and add URL scheme mappings disable-model-invocation: true --- # Analyze Unrecognized Host Apps **Last run:** 2026-10-03 (weekly GA batch, automated) - 15 new bundle IDs in the 28-day window (33 events). Added 8 return-URL mappings and 7 `knownNoSchemeHosts` entries; the script reports "nothing new" afterwards. Scheme from the app's own source: Standard Notes registers `$(PRODUCT_BUNDLE_IDENTIFIER)` as its only URL scheme, so `com.standardnotes.standardnotes://`. AASA at the root or a catch-all: `com.microsoft.officemobile` (now Microsoft 365 Copilot, `https://m365.cloud.microsoft/`), Skout, Carousell (`www.carousell.sg`; `www.carousell.com` answers with a Cloudflare challenge), Apple Support (`https://getsupport.apple.com/`, catch-all `/*`). Weaker block: Muse from Meta (`com.facebook.hatch`, `https://muse.ai/open_hatch`, AASA has no root path), Taskrabbit (`/dashboard`), Photos (`photos-redirect://`, community lists only, not read from a simulator runtime). noScheme: Satisfyer Connect, Yandex Telemost (AASA covers meeting links only), Bitrix24 (per-portal deep paths only), Grindr (blog paths only), FlickNote, Presence, and `com.museai.asst.mobile` (not on the App Store). Previous run 2026-09-28 (weekly GA batch, automated) - one new bundle ID in the 28-day window (`com.omachala.happy`, 2 events): a personal fork of Happy whose `app.config.js` registers the same `happy://` scheme as the already-mapped `com.ex3ndr.happy`, so it went into `knownNoSchemeHosts` (shared scheme, never map). No return-URL mappings added. The script reports "nothing new" at the default settings. Follow-up the same day, after Anton reported that Orca never returned: a `gh search code` sweep over every `knownNoSchemeHosts` entry moved four open-source apps to `knownURLs` with schemes read from their own repos (`com.stably.orca.mobile` `orca://`, `com.t3tools.t3code` `t3code://`, `h3p.Neon-Vision-Editor` `neonvisioneditor://`, `xyz.block.buzz.mobile` `buzz://`), and mapped the newly seen `com.google.Drive` to `https://drive.google.com/drive/my-drive` (AASA path `*/drive/*my-drive`). `com.t3tools.t3code.swiftui` stays noScheme: it is a third-party fork build with no verifiable mapping. Previous run 2026-09-27 (second pass) - cleared the 35 single-event bundle IDs left over from the first pass, plus Bear, which arrived since. Added 24 return-URL mappings (9 schemes read from the app's own source or the simulator runtime, 5 AASA-confirmed universal links, 10 under the weaker block) and 12 `knownNoSchemeHosts` entries. The script now reports "nothing new" at the default settings. Also rewrote the Prerequisites: the documented `gog auth add --services analytics` command both narrows the token and is refused by Google because of stale YouTube grants on gog's OAuth client, so the remote flow with `include_granted_scopes` stripped is the working recipe. Findings worth keeping: `ru.yandex.mobile.search` is Yandex **Browser**, not the Yandex app (`ru.yandex.mobile`); X Chat and QuickPaste have no way back at all; Brave and Firefox also claim `http`/`https`, so map only `brave://` / `firefox://`; Apple's Translate and Image Playground are only localization stubs in the iOS 27 simulator runtime, so their schemes cannot be read there. Previous run 2026-09-27 (first pass) - first run on the gog pipeline (`scripts/unrecognized_host_apps.py`, 28-day window, 132 bundle IDs / 528 events, 29 rows with two or more events). Added 17 return-URL mappings and 12 `knownNoSchemeHosts` entries; after the edit the script reports "nothing new" at `--min-events 2`. Twelve mappings are AASA-confirmed at the root or a catch-all path, five went in under a weaker block (AASA matches only a specific page, or the scheme comes from vendor docs). Identity checks via `itunes.apple.com/lookup?bundleId=` paid off: `co.anysphere.sand` is Grok Bot by X, not Cursor (its x.ai AASA matches only concrete share ids, so noScheme); `com.jiangjia.gif` is Kuaishou; `com.supersethealth.superset` is now Bevel; `ru.yandex.uber` now belongs to Fasten. VK's main client went in as `https://vk.com/feed`, not `vk://`, because several VK apps share the team. `com.apple.Preferences` went to noScheme: its only route is the private `App-prefs://`. The 34 single-event bundle IDs were left for a later batch. Previous run 2026-09-12 (19 mappings, 16 noScheme) established: the AASA is often the fastest primary source even for apps that do have a scheme; an app's build manifest beats its Info.plist when the scheme is a build variable; Telegram forks must never be mapped to `tg://`; share extensions go to noScheme, editor extensions whose content lives in the parent app are aliased to it. You are given the following context: $ARGUMENTS ## Task Pull the `unrecognized_host_app` bundle IDs from Google Analytics, work out which ones need a return URL, and add the mappings to the app. ## Prerequisites (one-time) The data comes from the GA4 Analytics Data API through the `gog` CLI (`brew install gog` if missing). The stored gog token for `anton.novoselov@gmail.com` must carry the `analytics.readonly` scope; the default services do not include it. Check with `gog auth list -p`: the services column must include `analytics`. The Mac token has had it since 2026-09-27, so this is only needed after a token reset. **Do not run a bare `gog auth add ... --services analytics --force-consent`.** It fails in two ways: - `--services` *replaces* the token's service set, so passing only `analytics` would leave a token with nothing else. Always pass the full current list from `gog auth list -p`, plus `analytics`. - gog's OAuth client once received YouTube grants, and gog adds `include_granted_scopes=true` to every consent URL. Google merges those old grants into the request and refuses it: "Access blocked: Authorization Error ... scopes that cannot be requested together (youtube.force-ssl, drive.file, youtube, ...)", Error 400 `invalid_request`. What works is the remote flow, with that flag stripped from the URL: ```bash SERVICES=appscript,calendar,chat,classroom,contacts,docs,drive,forms,gmail,people,sheets,slides,tasks,analytics # current list + analytics # 1. print the consent URL, drop the scope-merge flag, open it gog auth add anton.novoselov@gmail.com --services $SERVICES --force-consent --remote --step 1 \ | awk -F'\t' '/^auth_url/{print $2}' | sed 's/&include_granted_scopes=true//' | xargs open # 2. approve; the browser lands on an unreachable http://127.0.0.1:/oauth2/callback?... page. That is expected. # Copy that full URL, then exchange it (within ~5 minutes of step 1): gog auth add anton.novoselov@gmail.com --services $SERVICES --force-consent --remote --step 2 --auth-url '' ``` An agent can run both steps itself: step 1, hand the user the link, then run step 2 with the callback URL they paste back. Verify: ```bash gog ga report "$(cat internal/ga_property_id)" --dimensions eventName --metrics eventCount --from 7daysAgo --max 2 -p ``` A 403 `insufficientPermissions` means the scope is still missing; the script points back to this section. The GA4 property ID (the property behind the Firebase project `vivadicta`) is private: it lives in the gitignored `internal/ga_property_id`, which the script reads (`$GA_PROPERTY_ID` overrides it). The account ID, the Explore report URL and the OAuth client details are in `internal/firebase-analytics-events.md`. On a fresh clone, create `internal/ga_property_id` first. The event is `unrecognized_host_app`, the bundle ID lives in the custom event dimension `bundle_id` (registered 2026-02-24; older hits show as `(not set)`). ## Instructions 1. **Read the current mappings** from `VivaDicta/VivaDictaApp.swift` — find the `knownURLs` dictionary inside `returnURL(forHostId:)`. The `knownNoSchemeHosts` set lives just above it, in `attemptReturnToHost(hostId:)`. 2. **Pull and diff the analytics data** with the repo script, which runs `gog ga report`, filters the event client-side (the CLI has no filter flag) and compares every bundle ID against both tables in the local Swift file: ```bash scripts/unrecognized_host_apps.py # last 28 days, actionable rows only scripts/unrecognized_host_apps.py --days 90 --all # longer window, include mapped/noScheme rows scripts/unrecognized_host_apps.py --json /tmp/unrecognized.json # machine-readable copy ``` The default window is 28 days, which is what the weekly run wants: old app versions keep reporting apps that are already mapped, and the script hides those. Exit code 3 means nothing actionable - stop and say so. Use `--min-events 2` to skip singletons when the list is long; singletons still deserve a look when they are obviously popular apps. **Fallback only if the API is unavailable:** the user can paste a screenshot or list from the GA Explore exploration "Unrecognized Host Apps" (URL in `internal/firebase-analytics-events.md`; also reachable from Firebase Console → Analytics → "View more in Google Analytics"). Then do the cross-reference from step 3 by hand. 3. **Review the script's categories** — it prints one of these per bundle ID; check the automatic verdicts rather than re-deriving them: **`mapped`** — already in `knownURLs` (hidden unless `--all`; it shows up from app versions that predate the mapping) **`noscheme`** — already in `knownNoSchemeHosts` (hidden unless `--all`) **`variant:`** — same app as a mapped `` under a different bundle ID: a team-ID prefix (`4GU63N96WE.com.p5sys.jumpdesktop`), a regional build (`com.amazon.AmazonDE`), or an app extension (`net.whatsapp.WhatsApp.ShareExtension`). Aliasing a prefixed or regional build to the same URL is safe. An *extension* is a judgement call — returning to the parent app is not the surface the user was in. The script only detects prefix/suffix variants; spot regional builds (`AmazonUK` vs `Amazon`) yourself. **`apple-new`** — `com.apple.*` not in either table. Usually a system service that cannot be returned to (`com.apple.SafariViewService`, `com.apple.springboard`, `com.apple.siri`) and belongs in `knownNoSchemeHosts`; some Apple apps do have schemes (`shortcuts://`, `mobilenotes://`, `maps://`), check the simulator runtime binary before deciding. **`new`** — real third-party app not yet in either table. These are the research queue. **`not-actionable`** — `(not set)`, pre-custom-dimension data. Ignore. 4. **Research a way back** for each `new` app. Search for: - "[app name] iOS URL scheme" - "[app name] deep link" - "[bundle id] URL scheme" - Known URL scheme databases and GitHub repos Two sources beat any blog list, and are worth the extra step for high-volume apps: - **The app's own registration** — its open-source `Info.plist` or build file (`CFBundleURLSchemes`), or, for Apple apps, the binary in a simulator runtime under `/Library/Developer/CoreSimulator/.../RuntimeRoot/Applications/`. - **The app's AASA file** — `https:///.well-known/apple-app-site-association`. If it lists the bundle ID, the matching `https://` URL is a valid fallback for an app that registers no custom scheme. This works *only* on this code path, because the lookup runs solely for the app the keyboard was just typing into, so it is installed by definition. Before filing an app as noScheme, run `gh search code '""'` across GitHub. "No AASA" says nothing about a custom scheme: Orca, T3 Code, Neon Vision Editor and Buzz all sat in `knownNoSchemeHosts` while their own repos registered `orca`, `t3code`, `neonvisioneditor` and `buzz`. Expo apps declare it as `"scheme"` in `app.json` / `app.config.{js,ts}`; Flutter apps keep the bundle ID in `ios/Flutter/*.xcconfig` and the scheme in `ios/Runner/Info.plist`. Ignore hits in `getdictus/dictus-ios` `KnownAppSchemes.swift`: it is a port of this table, not independent evidence. Watch for schemes shared between apps. Swiftgram registers `tg://` *and* `telegram://` alongside official Telegram; iOS picks between claimants unpredictably, so map only a scheme the app owns outright (`sg://`). 5. **Output a summary table** with: - Bundle ID - Event count - Category (mapped / variant / apple-new / new / not actionable) - Return URL (if found), confidence level and the source it came from 6. **After user confirms** which entries to add, update `VivaDicta/VivaDictaApp.swift`: - Apps with a way back → the `knownURLs` dictionary in `returnURL(forHostId:)` - Apps with none → the `knownNoSchemeHosts` set, so they stop being reported as unrecognized Re-run `scripts/unrecognized_host_apps.py` afterwards: every row you handled must now come back as `mapped` or `noscheme` (visible with `--all`), which also proves the Swift edit parses. **There is no plist step, and adding one is a regression.** `LSApplicationQueriesSchemes` was deleted on 2026-08-29 along with the `canOpenURL` gate it existed to permit. Apple caps that array at 50 entries, and past the cap `canOpenURL` returns false whether or not the app is installed — which silently killed the newest mappings. `UIApplication.open` needs no declaration and reports failure through its own result, so the table can now grow without limit. Do not reintroduce either. 7. **Update the "Last run" line** at the top of this file with the date and what changed. - Do NOT add URL schemes you are not confident about without user confirmation - Low-confidence schemes should be flagged — the most reliable verification is the app's own `CFBundleURLSchemes`, from its source, its shipped binary, or an installed copy - Some bundle IDs (Apple system services, embedded browser views) are not actionable and should be called out as such - Treat the event counts as a floor, not a census. `.unrecognizedHostApp` only fires when the keyboard resolves a host bundle ID, so any period where resolution was broken under-reports every app at once - The script reads the tables from the working tree, so run it on the branch you are about to edit; a parse failure ("could not parse knownURLs / knownNoSchemeHosts") means the Swift declarations moved or were renamed - fix the regexes in `scripts/unrecognized_host_apps.py`, do not fall back to screenshots