@nytka/plugin-gsc
v0.3.3
Published
Google Search Console connector for nytka projects. Collects performance data into datasets/ and registers it with provenance.
Downloads
53
Readme
@nytka/plugin-gsc
Google Search Console connector for nytka projects. Collects performance data into the
project's datasets/ and registers it in datasets/index.json with provenance.
Read-only — it holds webmasters.readonly and can write nothing back to Google.
Install
npm install @nytka/plugin-gscTwo dependencies. @googleapis/searchconsole, 245 KB — until 0.2.0 it was googleapis, the
same generated client for the two endpoints this package calls plus every other Google API, at
207 MB unpacked. And @nytka/core, which has no dependencies of its own and holds the project
plumbing this package used to carry a private copy of.
Setup
About ten minutes, once per project. Auth is a service account — no consent screen, no refresh tokens, works headless. The cost is step 5: the account must be granted access to each property by hand, because Cloud IAM does not do it for you.
1. Enable the API
https://console.cloud.google.com/apis/library/searchconsole.googleapis.com → Enable.
Per Cloud project. If you later use a different project, enable it there too.
2. Create the service account
IAM & Admin → Service accounts → Create service account (https://console.cloud.google.com/iam-admin/serviceaccounts)
Name it something like <project>-gsc. Skip both optional steps — Permissions and
Principals with access. No IAM role is needed.
Do not go via APIs & Services → Credentials. That route pushes you into the OAuth consent screen, asking for Internal/External audience and test users. Service accounts never use it: OAuth is for acting on a human's behalf, and a service account is its own identity. If you are being asked about an audience, you are in the wrong place.
3. Download the key
Open the account → Keys → Add key → Create new key → JSON.
4. Move it into the project and rename it
mv ~/Downloads/<project>-<hash>.json <project>/private/gsc-service-account.jsonThe rename matters. Google names the download after the Cloud project and a key fingerprint —
acme-503822-bd4313339dc2.json. Rotate the key and the next file has a different hash, so
anything referencing the old name breaks. A fixed name means rotation is dropping a file in
place. Nothing is lost: project_id, private_key_id and client_email are all inside the
JSON.
private/ is gitignored by SPEC §3, so credentials sit in the same predictable place in every
nytka project.
5. Grant it access to the property
grep client_email <project>/private/gsc-service-account.jsonhttps://search.google.com/search-console → select the property → Settings → Users and permissions → Add user → paste the email, permission Restricted.
This is the step people skip, and it produces a 403 that reads like an auth failure.
6. Configure
One .env at the project root. Every key for the project lives there and nowhere else.
The names below ship with the package too, so recovering them later never means re-reading this guide:
cat node_modules/@nytka/plugin-gsc/.env.example >> .envGOOGLE_SERVICE_ACCOUNT_KEY=private/gsc-service-account.json
GSC_PROPERTY=https://example.com/GOOGLE_SERVICE_ACCOUNT_KEY is a path, not a key. Relative paths resolve from the project
root, so they work from any subdirectory; absolute paths also work.
Renamed in 0.2.0. It was
GSC_SERVICE_ACCOUNT_KEY. The GA4 connector authenticates with the same service-account file, so a per-connector name meant one key under two variables and two ways to get it wrong. The old name is still read — it prints a one-line deprecation notice to stderr and keeps working — and a later release drops it. If both are set,GOOGLE_SERVICE_ACCOUNT_KEYwins.GSC_PROPERTYis unchanged: the property is this connector's alone.
7. Verify, and get the property string right
npx nytka-gsc sitesListing your property means every step above worked.
Copy GSC_PROPERTY from this output — do not compose it by hand. Search Console has two
property types and they are different objects to the API:
| Type | Identifier |
|---|---|
| Domain | sc-domain:example.com |
| URL prefix | https://example.com/ |
Guessing sc-domain: when the property is a URL prefix returns a 403 or an empty result that
looks like "no data", not like a misconfiguration. sites prints the exact string.
For an internationalised domain, use the punycode form Google reports
(https://xn--demo1a2b.xn--tld3c4d/), not the Unicode spelling.
Use
npx nytka-gsc sites # auth smoke test — lists readable properties
npx nytka-gsc monthly # monthly clicks/impressions, full API window
npx nytka-gsc query --dimensions query --limit 1000
npx nytka-gsc monthly --no-register # write the payload, leave the registry aloneSeveral properties
A project may span domains. Comma-separate them, and each becomes its own dataset:
GSC_PROPERTY=https://example.com/, sc-domain:other-example.comOne service account can read all of them — add its client_email to each property. The
property is part of every dataset id, so two domains over the same dates never collide.
Programmatic:
import { run, collect, listSites } from '@nytka/plugin-gsc'
const { id, rowCount, rawPath } = await run({ kind: 'monthly' })run() returns counts and paths, never rows.
Troubleshooting
| What you see | What it means |
|---|---|
| GOOGLE_SERVICE_ACCOUNT_KEY is not set … | Neither the new name nor the old one is in .env — see step 6 |
| warning: GSC_SERVICE_ACCOUNT_KEY is deprecated | Working as configured, on the old name. Rename the variable — see step 6 |
| service-account key not found at … | The path in .env does not match the file. Usually the download was moved but not renamed — see step 4 |
| no project.yaml found walking up from cwd | Not inside a nytka project. cd to the project root |
| sites lists nothing | Step 5 was skipped, or the email was added to a different property |
| 403 / permission on monthly or query | Same cause — the property exists but this account is not a user on it |
| API … has not been used in project … before or it is disabled | Step 1 not done, or the key belongs to a different Cloud project |
| Runs fine, returns 0 rows | GSC_PROPERTY is probably the wrong type. Compare it against sites output exactly — see step 7 |
| Empty rows for the last few days | Expected. Data finalises on a 2–3 day lag; ranges already end 3 days back |
| Being asked for Internal/External audience | You are in the OAuth consent screen. Service accounts do not use it — see step 2 |
What it writes
| Path | Committed? |
|---|---|
| datasets/payloads/<id>.json | no — the directory ships its own .gitignore |
| datasets/index.json | yes — one entry, added or replaced |
The dataset id
monthly gsc-monthly-example-com-2025-04-to-2026-07
└──────┬───────┘ reporting period, month granularity
query gsc-date-query-example-com-2025-04-01-to-2026-07-25
└────┬────┘ └────────┬─────────┘ the exact window fetched
dimensionsmonthly names the reporting period; dateRange keeps the exact window fetched. Those are
different things on purpose. A collection runs up to 3 days back, so a window-keyed id moved
every day — and the registry matches on id, so running monthly daily for a month left thirty
near-identical entries describing one series. At month granularity the same month collected
twice replaces in place, and a new month is genuinely a new entry.
query keys the exact window instead, and that difference is deliberate. The id follows
the command's lifecycle, not the code path: monthly recurs on a schedule and must be
idempotent, while query is ad-hoc — two sub-periods of one month with the same dimensions are
two different questions, and a shared id would silently replace the first result and its
payload. Extra entries are a mess you can see and delete; a destroyed answer is not.
The monthly form changed in 0.3.0, which is why that release is a minor bump and not a
patch. Entries written by 0.2.0 carry the old gsc-monthly-…-2025-04-02-to-2026-07-26 form and
will not be matched by a 0.3.0 run; it appends a new entry beside them. Delete the old ones
once the new collection looks right. query ids are unchanged from 0.2.0.
@nytka/plugin-ga4 uses the identical convention, so the two connectors' entries sort and read
together.
Every date it writes — collectedAt, both ends of dateRange, the months inside the id — is a
local calendar date, like every other date in a nytka project. Before 0.2.0 they were UTC,
so an evening run west of Greenwich stamped tomorrow.
The registry writer preserves the file's existing formatting: adding one dataset produces a one-entry diff, not a reformat of every entry already there.
Rules it follows
- Payloads never enter agent context. Query them with a script; write conclusions to
research/. A dataset is evidence, a research item is knowledge. The CLI prints row counts and paths for this reason. - The project is found by walking up for
project.yaml, so it works at any install depth. - No YAML parsing, no config file. Secrets from
.env, everything else from flags.
Limits worth knowing
- The API retains 16 months. A longer baseline from a UI export stays the record before that date — this extends the series forward, it does not regenerate it.
- Data finalises on a 2–3 day lag. Ranges end 3 days back by default; including today gives a partial day that reads as a traffic collapse.
monthlystarts on the 1st of the month 16 months back. Before 0.3.0 it counted back 480 days, which landed mid-month on most days of the year and reported a part-month at the far end — the same partial-period dip the lag above exists to avoid.@nytka/plugin-ga4was already aligned, so the two series' first month had meant different things while being joined on the sameMonthcolumn.- 25,000 rows per request; paging is automatic.
- A date that is not a
YYYY-MM-DDcalendar date fails the collection rather than being bucketed under a guess. A wrong number is worse than a failure, because the failure is visible.
Tests
npm testnode --test against recorded API responses. No credential, no network, no client data.
- Google omits rare queries for privacy, so summing
queryrows will not equal thedatetotal. Expected, not a bug. type: webonly — no image, video or news search.
