Skip to content

feat(integration): Google Search Console search terms#4117

Open
boutterudy wants to merge 50 commits into
umami-software:devfrom
boutterudy:feat-integration-google-search-console
Open

feat(integration): Google Search Console search terms#4117
boutterudy wants to merge 50 commits into
umami-software:devfrom
boutterudy:feat-integration-google-search-console

Conversation

@boutterudy
Copy link
Copy Markdown
Contributor

@boutterudy boutterudy commented Mar 29, 2026

👾 Feature

This PR includes one of the features most requested by users (see this discussion): integration of Google Search Console (GSC) data in Umami

The GSC data is filtered according to the filters applied on the page and is based on:

  • referrer (when the domain is a Google domain)
  • country (when the Google domain is not already region-specific)

Displayed data:

  • Visitors (the number of clicks from Google Search results)
  • Impressions
  • CTR
  • Position

🔀 Changes

Scope What changed
.env
  • GOOGLE_CLIENT_ID → Google API client ID
  • GOOGLE_CLIENT_SECRET → Google API client secret
Database
  • New website_google_auth table
  • Prisma model
  • migration
Libraries src/lib/google.ts → OAuth flow, token refresh, GSC API client
API 7 new routes under /api/auth/google and /api/websites/{id}/google-auth + /search-terms
Hooks
  • useWebsiteGoogleAuthQuery
  • useWebsiteSearchTermsQuery
  • useWebsiteGscPropertiesQuery
  • useWebsiteGscPropertyMutation
UI
  • WebsiteGoogleSearchConsole.tsx (settings)
  • WebsiteSearchTerms.tsx (panel)
  • WebsiteSearchTermsExpandedView.tsx (modal)
Constants
  • GOOGLE_DOMAINS
  • GOOGLE_DOMAIN_TO_COUNTRY
  • COUNTRY_ALPHA2_TO_ALPHA3

✨ Wonderful Results

Search terms for google.com referrer

Demo.-.Search.terms.global.mp4

Search terms for google.com referrer with country filter

Demo.-.Search.terms.filtered.by.country.mp4

🔧 Configuration

0. Not connected yet (website settings)

0  Not connected

1. Connect with Google

1  Log in

2. Ask for permissions

2  Permissions

3. Connected with Google

3  Connected

4. Select Google Search Console (GSC) property

4  Select property

5. Selected GSC property

5  Selected property

Setting up Google API for Umami

To configure the Google API and enable Umami to retrieve data from Google Search Console, you need to:

  1. Create a project on Google Cloud (here)
  2. Register for OAuth (menu → APIs & Services → OAuth consent screen → Get started → Fill in the app information / audience / contact information / agree to terms → Create → Create OAuth client)
  3. Create OAuth Client (Create OAuth Client → Application type : Web application → Fill in the name → Add authorized JavaScript origins (for dev: http://localhost:3000) → Add authorized redirect URIs (for dev: http://localhost:3000/api/auth/google/callback) → Create → Download JSON (contains your GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET))
  4. Enable Google Search Console access (menu → APIs & Services → Library → Search for Google Search Console API (or click here) → Enable)
  5. Configure Data Access (menu → APIs & Services → OAuth consent screen → Data Access → Add or remove scopes → Tick /auth/webmasters.readonly (View Search Console data for your verified sites) → Update → Save)
  6. Add test users (for dev, menu → APIs & Services → OAuth consent screen → Audience → Tests users → Add users → Fill in the email → Save)
  7. Add your GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in .env (available in the previously downloaded JSON)

Normally, after all that, logging in via Google should work 💯

Note: For the application in a production environment, I assume you’ll need to go through Google’s application validation process.

Closes #645

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 29, 2026

@boutterudy is attempting to deploy a commit to the Umami Software Team on Vercel.

A member of the Team first needs to authorize it.

@boutterudy boutterudy marked this pull request as ready for review March 30, 2026 10:52
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 30, 2026

Greptile Summary

This PR adds a Google Search Console integration to Umami, allowing website owners to view GSC search term data (clicks, impressions, CTR, position) filtered by the active referrer/country context. It introduces a new website_google_auth table, an OAuth 2.0 flow backed by a JWT-signed state parameter, a token refresh deduplication mechanism, and several new API routes and UI components. Most issues raised in the previous review have been addressed.

Confidence Score: 4/5

Most prior review concerns are resolved; one previously flagged data-integrity issue (missing FK constraint in the handwritten migration) remains open and should be confirmed before merging.

Seven of the eight issues from the previous review round have been fixed. The two new findings in this pass are both P2. The score reflects the unresolved migration concern from the prior round rather than any new P0/P1 finding introduced here.

prisma/migrations/20_add_google_auth/migration.sql — the handwritten SQL is still missing the foreign-key constraint that enforces onDelete: Cascade declared in schema.prisma.

Important Files Changed

Filename Overview
src/lib/google.ts Core OAuth and GSC API logic; token refresh deduplication via in-memory Map is correctly implemented; error checks on all HTTP responses are in place.
prisma/migrations/20_add_google_auth/migration.sql Handwritten migration creates the table and unique index but is still missing the FK constraint to the website table; schema.prisma declares onDelete: Cascade which is only enforced by a database-level FK.
src/queries/prisma/googleAuth.ts Upsert no longer wipes propertyUrl on re-auth; token storage is encrypted; all operations look correct.
src/app/api/auth/google/callback/route.ts OAuth callback validates JWT-signed state, exchanges code for tokens, and redirects correctly; skipAuth is appropriate for the callback endpoint.
src/app/api/websites/[websiteId]/google-auth/route.ts GET uses canViewWebsite (returns email to all website viewers); DELETE uses canUpdateWebsite; P2025 error handling correct.
src/app/api/websites/[websiteId]/search-terms/route.ts Validates inputs, checks auth status, converts alpha-2 to alpha-3 country code (exhaustive Record type), and delegates to getSearchTerms.
src/app/(main)/websites/[websiteId]/settings/WebsiteGoogleSearchConsole.tsx saveError is now cleared before each retry; property selection and disconnect flows look correct.
src/app/(main)/websites/[websiteId]/WebsiteSearchTerms.tsx Correctly uses import type for GoogleDomain; process.env.basePath usage is consistent with the rest of the codebase; loading states and conditional rendering are handled properly.
src/components/hooks/queries/useWebsiteSearchTermsQuery.ts Uses import type for SearchTermRow (correct); query key includes all filter params; enabled guard prevents unnecessary fetches.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as WebsiteGoogleSearchConsole
    participant AuthRoute as /api/auth/google
    participant Google as Google OAuth
    participant Callback as /api/auth/google/callback
    participant DB as website_google_auth

    User->>UI: Click "Connect with Google"
    UI->>AuthRoute: GET /api/auth/google?websiteId=...
    AuthRoute->>AuthRoute: jwt.sign({websiteId}, APP_SECRET, 10m)
    AuthRoute-->>UI: { url: google_auth_url }
    UI->>Google: Redirect to OAuth consent
    Google-->>Callback: GET /callback?code=...&state=jwt
    Callback->>Callback: jwt.verify(state, APP_SECRET)
    Callback->>Google: Exchange code for tokens
    Google-->>Callback: access_token, refresh_token, expires_in
    Callback->>Google: GET /userinfo (Bearer access_token)
    Google-->>Callback: { email }
    Callback->>DB: upsertWebsiteGoogleAuth (encrypted tokens)
    Callback-->>User: Redirect to /websites/{id}/settings

    User->>UI: Select GSC property
    UI->>DB: PUT /google-auth/property { propertyUrl }
    DB-->>UI: { ok: true }

    User->>UI: View search terms panel
    UI->>AuthRoute: GET /websites/{id}/google-auth
    AuthRoute-->>UI: { connected, email, propertyUrl }
    UI->>AuthRoute: GET /websites/{id}/search-terms?startAt&endAt&...
    AuthRoute->>DB: getValidAccessToken (refresh if needed)
    AuthRoute->>Google: POST searchAnalytics/query
    Google-->>AuthRoute: { rows: [...] }
    AuthRoute-->>UI: { rows, connected: true }
Loading

Reviews (20): Last reviewed commit: "fix: return accurate value of `connected..." | Re-trigger Greptile

Comment thread src/queries/prisma/googleAuth.ts
Comment thread src/lib/google.ts
Comment thread src/lib/google.ts Outdated
Comment thread src/lib/google.ts
@Yashh56
Copy link
Copy Markdown
Contributor

Yashh56 commented Mar 31, 2026

Amazing Work. Hope it will merged soon.

@boutterudy
Copy link
Copy Markdown
Contributor Author

Amazing Work. Hope it will merged soon.

Thanks @Yashh56 🙏 I hope too 😊

Comment thread prisma/migrations/20_add_google_auth/migration.sql Outdated
Comment thread src/components/hooks/queries/useWebsiteSearchTermsQuery.ts Outdated
@boutterudy
Copy link
Copy Markdown
Contributor Author

@greptileai I think the issue about a missing FK constraint in migration is a false positive.

prisma/schema.prisma sets relationMode = "prisma" at line 9. If I'm not misunderstanding, this means Prisma handles all referential integrity at the application layer, not the database level, which means zero FK constraints is required in any of the migrations. If you check migrations any migrations, let's say 01, 04, 10, 13 and 15, none have FOREIGN KEY or REFERENCES SQL. Therefore, it seems to me like migration 20 is consistent with the entire project.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 1, 2026

You're absolutely right, and I apologize for the false positive.

Looking at the schema at prisma/schema.prisma:9, it indeed has relationMode = "prisma", which means Prisma handles all referential integrity at the application layer. I've verified that migrations 01, 04, and 13 have zero FOREIGN KEY or REFERENCES SQL statements - they only create tables, columns, and indexes.

Your migration 20 is entirely consistent with the project's approach. Thanks for the correction!

@boutterudy
Copy link
Copy Markdown
Contributor Author

@greptileai

@boutterudy
Copy link
Copy Markdown
Contributor Author

@greptileai did you find any issue? I don't see any in your comment, but still you mentioned "P2" issues 🤔

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 2, 2026

I did find two P2 issues, both now resolved:

  1. total field inaccuracy (src/lib/google.ts) - returned only current page size instead of actual total count
  2. Token refresh race condition (src/lib/google.ts) - concurrent requests could trigger multiple simultaneous refresh attempts, potentially invalidating tokens

Both were fixed in your 377a2ea commit.

@boutterudy
Copy link
Copy Markdown
Contributor Author

I did find two P2 issues, both now resolved:

  1. total field inaccuracy (src/lib/google.ts) - returned only current page size instead of actual total count
  2. Token refresh race condition (src/lib/google.ts) - concurrent requests could trigger multiple simultaneous refresh attempts, potentially invalidating tokens

Both were fixed in your 377a2ea commit.

@greptileai so does that mean the issues are still in the code right now? Or not?

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 2, 2026

No, the issues are not in the code anymore. They've already been fixed in commit 377a2ea.

When I said "both now resolved" and "Both were fixed in your 377a2ea commit", I meant you already addressed them. The current code is good to go on those fronts.

@boutterudy
Copy link
Copy Markdown
Contributor Author

@greptileai I think the issue about a missing FK constraint in migration is a false positive.

prisma/schema.prisma sets relationMode = "prisma" at line 9. If I'm not misunderstanding, this means Prisma handles all referential integrity at the application layer, not the database level, which means zero FK constraints is required in any of the migrations. If you check migrations any migrations, let's say 01, 04, 10, 13 and 15, none have FOREIGN KEY or REFERENCES SQL. Therefore, it seems to me like migration 20 is consistent with the entire project.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 2, 2026

You're absolutely right. I can confirm:

  1. relationMode = "prisma" is set at line 9 in prisma/schema.prisma, which means Prisma handles referential integrity at the application layer
  2. No existing migrations have FK constraints - I checked migrations 01, 04, and 13, and none contain FOREIGN KEY or REFERENCES SQL
  3. Migration 20 is consistent - it follows the same pattern as all other migrations in the project

This is indeed a false positive. The lack of FK constraints in migration 20 is intentional and matches the project's architecture. Thanks for catching this!

@kolosek
Copy link
Copy Markdown

kolosek commented Apr 7, 2026

@boutterudy This is awesome work. Do you have, by any chance, docker image built? So I run your version until umami merges this feature?

@boutterudy
Copy link
Copy Markdown
Contributor Author

@boutterudy This is awesome work. Do you have, by any chance, docker image built? So I run your version until umami merges this feature?

@kolosek Thanks! 🙏

Unfortunately, no, I don’t have one, but I reckon you could generate one by switching to the branch of this PR and then running the build 🤗

If you need info on how to fetch branches from other forks, this thread should help

@kolosek
Copy link
Copy Markdown

kolosek commented Apr 7, 2026

@boutterudy Ok, I did just that. Just so you know, in order for this to work on production I needed to make few changes kolosek@430645a (most important is the one that url is not 0.0.0.0:3000 but the url from domain that is used)

@boutterudy
Copy link
Copy Markdown
Contributor Author

@boutterudy Ok, I did just that. Just so you know, in order for this to work on production I needed to make few changes kolosek@430645a (most important is the one that url is not 0.0.0.0:3000 but the url from domain that is used)

@kolosek, thank you very much for your feedback! 🙏
Great, I see and understand for the domain URL, I’ll implement those changes. However, why is it necessary to add COPY --from=builder /app/prisma.config.ts ./prisma.config.ts to the Dockerfile? 👀

@kolosek
Copy link
Copy Markdown

kolosek commented Apr 21, 2026

@boutterudy something that AI proposed as it wasn't able to build docker image. As if it needed ENV variables in order to build the images, not exactly sure.

@boutterudy
Copy link
Copy Markdown
Contributor Author

@boutterudy something that AI proposed as it wasn't able to build docker image. As if it needed ENV variables in order to build the images, not exactly sure.

@kolosek That’s right, in fact, this issue about prisma.config.ts has also been fixed on the dev branch (see this commit) 👀

It was because the pattern was updated to match Prisma v7 pattern 💯

With that in mind, I’ve cherry-picked your commit to include it in the PR, so thank you very much for your help! 🙏

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.

4 participants