npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-gsc

Two 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.json

The 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.json

https://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 >> .env
GOOGLE_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_KEY wins. GSC_PROPERTY is unchanged: the property is this connector's alone.

7. Verify, and get the property string right

npx nytka-gsc sites

Listing 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 alone

Several properties

A project may span domains. Comma-separate them, and each becomes its own dataset:

GSC_PROPERTY=https://example.com/, sc-domain:other-example.com

One 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
              dimensions

monthly 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.
  • monthly starts 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-ga4 was already aligned, so the two series' first month had meant different things while being joined on the same Month column.
  • 25,000 rows per request; paging is automatic.
  • A date that is not a YYYY-MM-DD calendar 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 test

node --test against recorded API responses. No credential, no network, no client data.

  • Google omits rare queries for privacy, so summing query rows will not equal the date total. Expected, not a bug.
  • type: web only — no image, video or news search.