Skip to content

feat(gbp): Phase 1 — Google Business Profile auth + location discovery#508

Merged
arberx merged 8 commits into
mainfrom
arberx/gbp-integration
May 29, 2026
Merged

feat(gbp): Phase 1 — Google Business Profile auth + location discovery#508
arberx merged 8 commits into
mainfrom
arberx/gbp-integration

Conversation

@arberx
Copy link
Copy Markdown
Member

@arberx arberx commented May 15, 2026

Summary

Lays the foundation for canonry's 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 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 new gbp MCP 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 at skills/canonry-setup/references/google-business-profile.md. Phase 2 (sync run + reviews/keywords/metrics/Q&A/lodging/place-actions tables) is unblocked.

Test plan

  • `pnpm typecheck` passes across all 21 packages
  • `pnpm lint` clean
  • `pnpm test` green — 1546 tests including 22 new GBP tests (6 HTTP unit + 7 CLI + 9 route integration)
  • Manual: once GBP access form is approved, run `canonry gbp connect ` → `canonry gbp locations discover ` → `canonry gbp locations ` and confirm locations persist with selection state

@arberx arberx force-pushed the arberx/gbp-integration branch 5 times, most recently from bb4d87e to 2197a99 Compare May 20, 2026 21:00
Comment thread packages/api-routes/test/gbp.test.ts Fixed
Comment thread packages/api-routes/test/gbp.test.ts Fixed
Comment thread packages/canonry/test/gbp-commands.test.ts Dismissed
@arberx arberx force-pushed the arberx/gbp-integration branch 2 times, most recently from ddf0f82 to 636ca8c Compare May 27, 2026 04:05
arberx added 7 commits May 29, 2026 11:58
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.
@arberx arberx marked this pull request as ready for review May 29, 2026 16:01
@arberx arberx force-pushed the arberx/gbp-integration branch from 93ee4d3 to fe7b2f7 Compare May 29, 2026 16:01
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".
@arberx arberx merged commit 9cbaccb into main May 29, 2026
12 checks passed
@arberx arberx deleted the arberx/gbp-integration branch May 29, 2026 16:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants