feat(gbp): Phase 1 — Google Business Profile auth + location discovery#508
Merged
Conversation
bb4d87e to
2197a99
Compare
ddf0f82 to
636ca8c
Compare
Adds the foundation for the local-AEO surface: a new `integration-google-business-profile` package, OAuth scope branching for the existing Google connector (`gsc | ga4 | gbp`), and a `gbp_locations` table with explicit selection state. Surfaces four new endpoints (`POST /gbp/locations/discover`, `GET /gbp/locations`, `PUT /gbp/locations/:locationName/selection`, `DELETE /gbp/connection`), matching ApiClient methods, `canonry gbp …` CLI commands, and a new `gbp` MCP toolkit with four tools and OpenAPI classifications. Ships the setup playbook as a canonry skill reference (`skills/canonry-setup/references/google-business-profile.md`) including Google's access-form prerequisites quoted from the official docs. Phase 2 (sync run + reviews/keywords/metrics/Q&A/lodging/place-actions tables) is unblocked and follows the data-model decisions baked in here (range-replace, value-or-threshold union, snapshot-on-change for lodging attributes).
…guards Adds `packages/contracts/src/retry.ts` with `backoffDelayMs`, `withRetry`, and `isRetryableHttpError` — Google's documented jittered exponential backoff (`random() * baseDelayMs * 2^attempt`) plus a generic retry wrapper that takes any `isRetryable` predicate and supports `Retry-After` overrides via `computeDelayMs`. Replaces five near-identical `withRetry` copies in the provider packages (claude, gemini, openai, perplexity, local) with thin shims, migrates GA4's `withGa4Retry` and GBP's `gbpFetchGet` to wrap the shared helper, and threads GBP-specific guards: a new `GbpApiError.quotaLimitValue` field distinguishes the 0-QPM access-form gate (don't retry — Google hasn't approved you yet) from a transient 300-QPM ceiling (retry with backoff). The route mapper now emits distinct quotaExceeded messages for the two cases. Net: ~250 lines of duplicated retry math + control flow collapse into one 90-line helper. Adds 17 retry tests + 7 GBP-specific guard tests; total suite jumps from 3271 to 3288 green.
Updates the Google Business Profile skill reference and SKILL.md with everything confirmed against three live businesses (computer-support shop, roofing contractor, Venice Beach hotel): - Q&A removed everywhere — Google shut down the My Business Q&A API (HTTP 501 API_UNSUPPORTED). Scrubbed from the toolkit description, integration AGENTS.md, and the skill doc; each carries a "retired" note so it isn't re-added. - Reviews (v4) documented as separately gated — the Basic API Access approval grants the v1 family but NOT the legacy mybusiness.googleapis.com; it's producer-restricted (gcloud enable → PERMISSION_DENIED 110002) and can't be self-enabled even by the approved account. Corrected the prior "both work post-approval" claim. - Added a "Real-world data shapes & signal patterns" section: string-encoded values, zero-day omission in daily metrics, split date objects, direction-requests being the most reliable conversion signal, keyword thresholding scaling with volume (100% for small businesses), and empty lodging/place-action profiles being the AEO finding itself. - Added the ADC-vs-gcloud-CLI account gotcha, the lodging 400 FAILED_PRECONDITION (not 404) correction, and a Local AEO section + reference-table entry in SKILL.md.
Adds the gbp-sync run kind and a performance sync over the GBP surfaces that are actually reachable (validated against three live businesses). Scoped deliberately: - Daily performance metrics (all 11 DailyMetric) + monthly search-keyword impressions, persisted to gbp_daily_metrics and gbp_keyword_impressions (migration v68). Keyword rows carry the value-or-threshold privacy union; the worker parses string-encoded values, treats omitted zero-days as 0, and range-replaces each location's rows per sync. - New performance-client in the integration package (TDD against the real response shapes), executeGbpSync worker (bounded concurrency 4, per-location error capture → completed/partial/failed), full run-lifecycle wiring (contracts run kind, both web exhaustive switches, run-invalidations, ApiRoutesOptions callback, server.ts). - POST /gbp/sync, GET /gbp/metrics, GET /gbp/keywords (the keywords endpoint computes thresholdedPct and sorts exact values first), typed DTOs + OpenAPI + regenerated SDK, ApiClient methods, canonry gbp sync/metrics/keywords CLI, and three gbp-tier MCP tools. Reviews are intentionally excluded — the v4 reviews API is separately access-gated by Google and can't be self-enabled (see Phase-1 findings). Q&A is dropped — Google retired the API. Adds 8 performance-client unit tests + 5 route integration tests; full suite 3363 green.
…recard Stacks the remaining reachable GBP signals on top of Phase 2's performance sync, with the derived math kept in a pure, exhaustively-tested module. - Place action links (booking / reservation / order CTAs): new place-actions-client (paginated, Business Information v1 host), gbp_place_actions table (migration v69, range-replaced per location each sync), GET /gbp/place-actions, canonry gbp place-actions, canonry_gbp_place_actions MCP tool. Captures placeActionType, providerType (MERCHANT vs AGGREGATOR), isPreferred, uri. - Lodging attribute snapshots: new lodging-client (getLodging maps HTTP 400 FAILED_PRECONDITION → null so non-lodging locations skip cleanly; countPopulatedGroups + stable-hash for snapshot-on-change), gbp_lodging_snapshots table (migration v69, a new row only when the content hash changes), GET /gbp/lodging (collapses to the latest snapshot per location), canonry gbp lodging, canonry_gbp_lodging MCP tool. - Composite summary: new pure gbp-summary module (computeMetricTotals, computeWindowDelta, computeKeywordCoverage, summarizePlaceActions, summarizeLodging, buildGbpSummary). Window deltas are recent-7d vs prior-7d anchored to the max stored metric date (GBP data lags ~2-3d), both windows backfilled with the metric union as explicit 0s, deltaPct null when the prior window is 0. GET /gbp/summary, canonry gbp summary, canonry_gbp_summary MCP tool. - Worker extends executeGbpSync to fetch place-actions + lodging alongside metrics + keywords per location (bounded concurrency 4); typed DTOs + OpenAPI + regenerated SDK; ApiClient methods; db-dto-coverage + MCP drift tests updated (95 tools / 61 read / 10 gbp). Calculation testing: 22 gbp-summary unit tests assert exact math and edge cases (zero totals, divide-by-zero prior windows, rounding boundaries, empty inputs) per the Calculation Testing rule; place-actions + lodging clients are TDD'd against the real response shapes. Docs: api-routes / canonry / integration AGENTS.md, the GBP skill reference, and a new Local AEO section in the CLI reference. Full suite 3391 green.
Two review findings from the Phase 2b pass:
- DELETE /gbp/connection only removed gbp_locations + the OAuth connection, leaving gbp_daily_metrics / gbp_keyword_impressions / gbp_place_actions / gbp_lodging_snapshots orphaned (those tables cascade only on PROJECT deletion). Result: metrics/keywords/place-actions/lodging/summary reads kept returning stale data after a disconnect, and reconnecting a different Google account mixed the old account's rows into the project-scoped aggregates. The disconnect transaction now clears the full GBP footprint. Test extended to seed a row in each of the four data tables and assert all are gone after disconnect.
- CodeQL flagged url.includes('...googleapis.com') substring matching in the gbp.test.ts fetch mock. Not a real vulnerability (test-only routing over URLs our own clients build from trusted constants), but switched to exact hostname matching via new URL(url).hostname — clears the alert and makes the mock stricter: a production bug that built a URL against the wrong host with the API domain in its path would now be caught instead of silently routed.
Also corrected the stale LOCATION_CONCURRENCY comment in gbp-sync.ts (each location now issues ~4 calls across four API hosts, not ~2). Full suite 3391 green.
Adds account selection (the limitation surfaced in review: discover always auto-picked the first account a user could see, with no override) and fixes the summary's location-scope bug.
Account selection — a single OAuth user often manages several GBP accounts, so each project now picks one explicitly:
- GET /projects/:name/gbp/accounts (CLI `canonry gbp accounts`, MCP `canonry_gbp_accounts`) lists the accounts the connection can access.
- discover accepts `accountName` ("accounts/{n}"; CLI `--account`, MCP arg). Resolution order: explicit account > the account the project already tracks (derived from its locations) > first visible account on the first discover. An explicit account is validated against the user's account list (unknown → 400).
- Re-pointing a project at a DIFFERENT account is destructive (drops the old account's locations + synced data), so it's rejected unless `switchAccount: true` (CLI `--switch-account`); the switch reuses the shared clearGbpProjectData helper introduced for disconnect.
Summary scope fix (review finding M2): GET /gbp/summary now aggregates over the project's SELECTED locations only (an explicit ?locationName still narrows to one). Previously a deselected location's stale synced rows kept polluting the totals while locationCount counted only selected locations — the number and the data disagreed. Now deselected/stale rows are excluded and locationCount matches the data the numbers came from.
Wiring: contracts (gbpAccount DTOs + accountName/switchAccount on the discover request), OpenAPI + regenerated SDK, ApiClient (listGbpAccounts, discover passthrough), CLI (`gbp accounts`, discover `--account`/`--switch-account`), MCP (`canonry_gbp_accounts`, discover schema; counts 96/62/11). Tests: 5 new route tests (list accounts, explicit-account discover, unknown-account 400, switch-guard + clear-on-switch, summary excludes deselected). Docs: AGENTS.md (canonry + api-routes), GBP skill reference, CLI reference. Full suite 3396 green.
93ee4d3 to
fe7b2f7
Compare
Captures the next-owner plan for the Google Business Profile integration now that Phases 1–2b have shipped (PR #508, v4.58.0): - What exists today (data plane on API/CLI/MCP, account selection, summary) and the deliberate scope-outs (reviews gated, Q&A retired), with a key-files map. - Phase 3 (web UI): build GbpSection.tsx following the GscSection pattern; the data + generated SDK functions already exist, so it's pure consumption. - Phase 4 (operationalize): make gbp-sync a schedulable kind (mirror traffic-sync), add gbp.auth.* doctor checks (mirror ga-auth), and add GBP insights after a gbp-sync run (new pure analyzers in packages/intelligence + a run-coordinator branch; notifier + Aero wake-up already fire for non-probe runs). - Verified extension points with file references; noted open decisions (UI placement, location-scoped insight dismissal keys, metric range-replace vs accumulate). Indexed under docs/README.md "Active Plans".
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lays the foundation for canonry's local-AEO surface: a new
integration-google-business-profilepackage, OAuth scope branching for the existing Google connector (gsc | ga4 | gbp), and agbp_locationstable with explicit per-location selection state. Adds four endpoints (POST /gbp/locations/discover,GET /gbp/locations,PUT /gbp/locations/:locationName/selection,DELETE /gbp/connection) with matching typed ApiClient methods,canonry gbp …CLI commands, and a newgbpMCP toolkit (4 tools) wired through OpenAPI classifications. The setup playbook — including Google's access-form prerequisites quoted from the official docs — ships as a canonry skill reference atskills/canonry-setup/references/google-business-profile.md. Phase 2 (sync run + reviews/keywords/metrics/Q&A/lodging/place-actions tables) is unblocked.Test plan