Skip to content

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

Draft
arberx wants to merge 2 commits into
mainfrom
arberx/gbp-integration
Draft

feat(gbp): Phase 1 — Google Business Profile auth + location discovery#508
arberx wants to merge 2 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
locations?: Array<{ name: string; title?: string; websiteUri?: string; categories?: { primaryCategory?: { displayName?: string } } }>
}) {
fetchSpy.mockImplementation((url: string) => {
if (url.includes('mybusinessaccountmanagement.googleapis.com')) {
if (url.includes('mybusinessaccountmanagement.googleapis.com')) {
return Promise.resolve({ ok: true, status: 200, text: async () => JSON.stringify({ accounts: opts.accounts ?? [] }) })
}
if (url.includes('mybusinessbusinessinformation.googleapis.com')) {
migrate(db)

const apiKeyPlain = `cnry_${crypto.randomBytes(16).toString('hex')}`
const hashed = crypto.createHash('sha256').update(apiKeyPlain).digest('hex')
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).
@arberx arberx force-pushed the arberx/gbp-integration branch from 2197a99 to 268bea2 Compare May 21, 2026 18:20
…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.
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