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

ga4-cli

v0.1.0

Published

GA4 Measurement Protocol simulator for synthetic user data

Readme

ga4-cli

License: ISC Node >= 18 TypeScript

TypeScript toolkit for generating realistic GA4 Measurement Protocol traffic and managing GA4 properties via the Admin API — from your terminal or as a library.

Feature Overview

| Feature | What it gives you | Main API | | --- | --- | --- | | Real-time simulation | Run concurrent synthetic users through journeys with optional jitter and callbacks | ScenarioRunner | | Historical backfill | Replay sessions across past windows (up to GA4's 72h MP limit) with virtual time | BackfillRunner, hoursAgo, minutesAgo | | Persona modeling | Mix behavior patterns using weighted journey selection (e.g. 70/30) | Persona, pickJourney | | Typed event builders | Compose GA4 event payloads safely with TypeScript types | pageView, viewItem, addToCart, purchase, ... | | Session/user helpers | Automatically attach client_id, session_id, engagement time, campaign context | User, Session | | Reliable transport | Timeout + retry handling (429/5xx retries, network-safe failure shape) | MeasurementProtocolClient | | Debug-first workflow | Validate payloads before production sends to avoid polluting analytics | endpoint: "debug" | | Admin API | List accounts, properties, data streams, MP secrets, custom dimensions, key events | GA4AdminClient | | Data API | Run reports, realtime queries, pivot reports, metadata lookups, audience exports | GA4DataClient |

How simulation works

Journeys and steps

A Journey is a sequence of steps that represents one user's visit. Each step sends a single GA4 event and optionally waits before the next one:

const journey: Journey = {
  name: "checkout-flow",
  steps: [
    step(pageView("/"),          1000),  // send page_view, wait 1 s
    step(addToCart(...),          500),  // send add_to_cart, wait 500 ms
    step(beginCheckout(...),     1500),  // send begin_checkout, wait 1.5 s
    step(purchase(...)),                 // send purchase, no wait
  ],
};

Real-time simulation (ScenarioRunner)

For each simulated user ScenarioRunner:

  1. Generates a synthetic identity — random client_id (UUID) and session_id (epoch seconds)
  2. Walks through the journey steps in order, honouring the delayMsAfter between steps as real async sleeps
  3. Sends each step as an MPPayload to GA4's Measurement Protocol endpoint
  4. Runs up to --concurrency users in parallel (default: 5)

Historical backfill (BackfillRunner)

BackfillRunner runs the same journeys but with virtual time — instead of sleeping between steps, it calculates timestamps that spread users evenly across the requested time window and stamps each event with timestamp_micros. This means a 24-hour backfill of 200 users finishes in seconds rather than hours.

GA4 accepts events up to 72 hours in the past via timestamp_micros. The runner validates this window before starting.

Personas (weighted randomness)

A Persona wraps multiple journeys with probability weights so different users follow different paths:

const persona: Persona = {
  name: "shopper",
  journeys: [
    { weight: 0.7, journey: browseOnly },    // 70% of users just browse
    { weight: 0.3, journey: browseAndBuy },  // 30% complete a purchase
  ],
};

Each user randomly picks a journey according to the weights before their session starts.

The Measurement Protocol payload

Every event is an HTTP POST to Google's endpoint:

{
  "client_id": "a1b2c3d4-...",
  "timestamp_micros": 1711234567000000,
  "events": [{
    "name": "purchase",
    "params": { "transaction_id": "txn-1", "currency": "USD", "value": 49.99 }
  }]
}

With --endpoint debug (the default) requests go to the validation endpoint — GA4 returns any payload errors but writes no data. Switch to --endpoint production only when you're ready to record real traffic.

Session bootstrap and attribution (--bootstrap)

By default, client_id and session_id are generated randomly in code. GA4 has no record of a real session starting, so traffic-source attribution (source / medium / campaign) will not appear correctly in reports.

Passing --bootstrap fixes this by spinning up a real headless browser for each user:

  1. A local HTTP server serves a minimal HTML page with the gtag.js snippet embedded
  2. Puppeteer launches headless Chromium and navigates to that page (with UTM params appended if a campaign is configured)
  3. The real gtag.js fires, GA4 assigns a client_id and session_id, and sets the _ga / _ga_XXXXXX cookies
  4. The cookies are read to extract those real IDs
  5. The browser context is closed, and the IDs are used for all subsequent synthetic events in that user's journey

This anchors each synthetic session to a legitimate GA4 session origin, so attribution works correctly. For batch runs a single shared browser is reused — each user gets an isolated incognito context to keep cookies separate, avoiding the cost of launching a new browser per user.

Note: GA4 may silently discard hits sent from headless browsers, even when the HTTP response is 2xx. There is no error from GA4 — events simply never appear in reports. This is likely caused by bot-detection heuristics on Google's side and is the most common reason for missing sessions when using --bootstrap.

--bootstrap requires Puppeteer: npm install puppeteer.

Authentication

Measurement Protocol (sim commands) authenticates with a plain API secret — no OAuth needed. Set GA4_API_SECRET in your .env.

Admin API and Data API (admin / data commands) use Google Application Default Credentials (ADC). Two auth methods are supported:

  • User account — interactive OAuth via gcloud. Best for local development.
  • Service account — JSON key file. Best for CI, automation, and production environments.

One login covers both the Admin API and the Data API — no separate setup is needed for each.

Getting Started

1. Install

Option A (recommended): install as a CLI package

npm install -g ga4-cli
ga4 --help

Or run without global install:

npx ga4-cli --help

Option B: install from source

git clone https://github.com/Liscor/ga4-cli.git
cd ga4-cli
npm install
npm run build
npm link        # makes `ga4` available globally

2. Configure Measurement Protocol credentials

The sim commands need a GA4 Measurement ID and API secret. The interactive init wizard sets these up:

ga4 sim init

This creates a .env file with GA4_MEASUREMENT_ID, GA4_API_SECRET, and GA4_ENDPOINT.

You can also pass them as flags (--measurement-id, --api-secret) or set the environment variables directly.

3. Set up credentials (for Admin API and Data API)

Choose the method that fits your environment.

Option A: User account (local development)

Requires the Google Cloud CLI.

  1. Go to Google Cloud Console > APIs & Services > Credentials
  2. If prompted, click "Configure consent screen":
    • Choose External user type
    • Fill in the app name (e.g. ga4-cli) and your email
    • Skip scopes and test users, save
  3. Back on the Credentials page, click "Create Credentials" > "OAuth client ID"
    • Application type: Desktop app
    • Name: anything (e.g. ga4-cli)
  4. Click Download JSON to save the client secret file
  5. Enable both APIs in your project:

Option B: Service account (CI / automation)

No browser or gcloud required.

  1. Go to Google Cloud Console > IAM & Admin > Service Accounts
  2. Click "Create Service Account", give it a name and click Done
  3. Open the service account, go to Keys > Add Key > Create new key > JSON — download the file
  4. In Google Analytics, go to Admin > Account/Property Access Management and add the service account email with at least Viewer role
  5. Enable both APIs in your GCP project:

4. Authenticate

User account:

ga4 auth login --client-id-file /path/to/client_secret.json

This opens a browser for Google OAuth with the correct Analytics API scopes. Credentials are stored locally by gcloud — you only need to do this once.

Service account:

ga4 auth login --service-account /path/to/sa.json

This validates the key file and confirms Analytics API access. Then export the path so subsequent commands pick it up:

export GOOGLE_APPLICATION_CREDENTIALS="/path/to/sa.json"

Add the export to your shell profile (~/.zshrc, ~/.bashrc) to persist it, or set it in your CI environment variables.

5. Verify everything works

ga4 sim doctor          # check Measurement Protocol setup
ga4 auth status         # check Admin API credentials
ga4 admin accounts      # list your GA4 accounts

Config file

Instead of passing every flag on the command line, you can put your simulation settings in a JSON file. By default ga4 sim run and ga4 sim send look for ga4.config.json in the current directory. Pass a different path with --config <path>.

CLI flags always win — the config file only fills in values you didn't explicitly pass.

Example: ga4.config.json

{
  "measurementId": "G-XXXXXXXXXX",
  "apiSecret": "your-api-secret-here",
  "endpoint": "debug",

  "users": 50,
  "concurrency": 5,

  "journeys": [
    {
      "template": "ecommerce-funnel",
      "weight": 40,
      "campaign": { "source": "google", "medium": "cpc", "campaign": "spring-sale" }
    },
    {
      "template": "content-browse",
      "weight": 35,
      "campaign": { "source": "newsletter", "medium": "email", "campaign": "weekly-digest" }
    },
    {
      "template": "search-and-discover",
      "weight": 25,
      "campaign": { "source": "organic", "medium": "search" }
    }
  ],

  "backfill": { "hours": 24 },

  "userProperties": { "plan": "free", "region": "us-east" }
}

A starter file with all available options is included in the repo as ga4.config.example.json.

Config reference

| Key | Type | Default | Description | |-----|------|---------|-------------| | measurementId | string | env GA4_MEASUREMENT_ID | GA4 Measurement ID (G-XXXXXX) | | apiSecret | string | env GA4_API_SECRET | Measurement Protocol API secret | | endpoint | "debug" | "production" | "debug" | Validation only vs real writes | | users | integer | 10 | Number of simulated users | | concurrency | integer | 5 | Max users running in parallel | | dryRun | boolean | false | Skip all HTTP requests | | bootstrap | boolean | false | Use Puppeteer for real attribution | | minIntervalMs | number | — | Minimum ms between requests | | template | string | — | Shorthand for a single journey (ignored when journeys is set) | | journeys | array | — | Weighted journey mix (see below) | | backfill | object | — | Run as a historical backfill | | userProperties | object | — | User-scoped string properties on every event |

journeys[]

| Key | Type | Description | |-----|------|-------------| | template | string | Built-in template name (e.g. "ecommerce-funnel") | | weight | number | Relative traffic share (weights are normalised automatically) | | campaign | object | UTM params for users on this journey (source, medium, campaign, term, content) |

When journeys contains more than one entry, or any entry has a weight, a Persona is created automatically and users are randomly assigned based on the weights. A single entry with no weight runs that journey for all users.

backfill

| Key | Type | Description | |-----|------|-------------| | hours | number | Shorthand: backfill the last N hours | | start | string | ISO 8601 start of window (mutually exclusive with hours) | | end | string | ISO 8601 end of window (defaults to now) |

Using the config programmatically

import { loadSimConfig, buildTargetFromConfig } from "ga4-cli";

const config = loadSimConfig("ga4.config.json");

const target = buildTargetFromConfig(config, (name) => {
  // map template names to your own Journey objects
  if (name === "my-journey") return myJourney;
  throw new Error(`Unknown template: ${name}`);
});

await new ScenarioRunner(client, target).run({ users: config.users ?? 10 });

CLI Reference

Full CLI command reference lives here: docs/cli-reference.md.

ga4 <command> [subcommand] [options]

Commands:
  auth    Authenticate with Google for Admin API access
  sim     Simulate GA4 traffic via the Measurement Protocol
  admin   Manage GA4 properties via the Admin API
  data    Query GA4 reports via the Data API

See docs/cli-reference.md for all subcommands, options, and examples.

Library Usage

Minimal example

import {
  MeasurementProtocolClient,
  ScenarioRunner,
  step,
  pause,
  pageView,
  type Journey,
} from "ga4-cli";

const client = new MeasurementProtocolClient({
  measurementId: process.env.GA4_MEASUREMENT_ID!,
  apiSecret: process.env.GA4_API_SECRET!,
  endpoint: "debug", // use production only when ready
});

const journey: Journey = {
  name: "landing-flow",
  steps: [
    step(pageView("https://example.com/", { title: "Home" })),
    pause(1000),
    step(pageView("https://example.com/pricing", { title: "Pricing" })),
  ],
};

const runner = new ScenarioRunner(client, journey);
await runner.run({ users: 25, concurrency: 5 });

Personas (weighted behavior)

import {
  ScenarioRunner,
  pageView,
  viewItem,
  purchase,
  step,
  pause,
  type Journey,
  type Persona,
} from "ga4-cli";

const browseOnly: Journey = {
  name: "browse-only",
  steps: [
    step(pageView("https://example.com/")),
    pause(800),
    step(viewItem("USD", 59.99, [{ item_id: "sku-1", item_name: "Hoodie" }])),
  ],
};

const browseAndBuy: Journey = {
  name: "browse-and-buy",
  steps: [
    ...browseOnly.steps,
    pause(1200),
    step(
      purchase("txn-1", "USD", 59.99, [{ item_id: "sku-1", item_name: "Hoodie" }])
    ),
  ],
};

const persona: Persona = {
  name: "organic-shopper",
  journeys: [
    { journey: browseOnly, weight: 7 },
    { journey: browseAndBuy, weight: 3 },
  ],
};

await new ScenarioRunner(client, persona).run({ users: 100, concurrency: 10 });

Backfill

Backfill runs with virtual time — pauses advance timestamps without slowing execution.

import {
  BackfillRunner,
  hoursAgo,
  step,
  pause,
  pageView,
  type Journey,
} from "ga4-cli";

const journey: Journey = {
  name: "recent-journey",
  steps: [
    step(pageView("https://example.com/")),
    pause(3000),
    step(pageView("https://example.com/product")),
  ],
};

const runner = new BackfillRunner(client, journey);
await runner.run({
  start: hoursAgo(24),
  users: 200,
  concurrency: 20,
});

Backfill constraints:

  • start must be before end
  • end must be in the past
  • start cannot be older than 72 hours (GA4 MP drop window)
  • Journey duration must fit into the selected time window

Admin API (library)

import { GA4AdminClient } from "ga4-cli";

const admin = new GA4AdminClient();

const accounts = await admin.listAccountSummaries();
const streams = await admin.listDataStreams("123456789");
const secrets = await admin.listMeasurementProtocolSecrets("123456789", "1234567");
const dims = await admin.listCustomDimensions("123456789");
const keyEvents = await admin.listKeyEvents("123456789");

Data API (library)

import { GA4DataClient } from "ga4-cli";

const data = new GA4DataClient();

const report = await data.runReport({
  property: "123456789",
  metrics: [{ name: "activeUsers" }, { name: "sessions" }],
  dimensions: [{ name: "city" }],
  dateRanges: [{ startDate: "28daysAgo", endDate: "today" }],
});

const realtime = await data.runRealtimeReport({
  property: "123456789",
  metrics: [{ name: "activeUsers" }],
});

Client reliability settings

const client = new MeasurementProtocolClient({
  measurementId: "G-XXXX",
  apiSecret: "secret",
  endpoint: "debug",
  requestTimeoutMs: 10_000, // default
  maxRetries: 2,            // default
  retryBaseDelayMs: 250,    // default, exponential backoff
});

Retry behavior:

  • Retries on 429 and 5xx
  • Does not retry most 4xx
  • Network-level failures return status: 0

Requirements

  • Node.js 18+ (uses global fetch and AbortController)
  • GA4 Measurement ID + API secret (for sim commands)
  • User account: Google Cloud CLI + OAuth client (for admin / data commands)
  • Service account: JSON key file + GOOGLE_APPLICATION_CREDENTIALS env var (for admin / data commands)

Scripts

  • npm run build — Compile to dist/
  • npm run typecheck — TypeScript checks (no emit)
  • npm run lint — ESLint
  • npm run lint:fix — ESLint with auto-fix
  • npm test — Jest test suite
  • npm run test:coverage — Jest with coverage report
  • npm run test:watch — Jest in watch mode

Project structure

src/
  core/
    admin.ts              # GA4 Admin API client
    data.ts               # GA4 Data API client (reports, realtime, audiences)
    client.ts             # GA4 Measurement Protocol transport
    scenario.ts           # real-time runner + journey steps
    backfill.ts           # historical runner + window validation
    session-bootstrap.ts  # Puppeteer-based session bootstrapping
    user.ts               # user/session payload construction
    events.ts             # typed GA4 event builders
    persona.ts            # weighted journey selection
    types.ts              # shared types
  templates/       # built-in journey templates + personas
  cli.ts           # CLI entry point
  config.ts        # env var loading
  index.ts         # package exports
  __tests__/       # Jest tests
bin/
  ga4.js           # CLI binary
deploy/            # Terraform + Docker for GCP deployment
functions/         # Google Cloud Functions entry point

Safety tips

  • Start in debug mode and inspect validation responses first.
  • Move to production only when payloads are valid.
  • Use lower users + concurrency for first production dry runs.

Read-only mode (GA4_READ_ONLY)

Set the GA4_READ_ONLY environment variable to prevent all write, delete, and mutating operations. Read-only commands (admin accounts, admin properties, data report, sim doctor, sim list-templates, etc.) continue to work normally.

# any truthy value enables read-only mode
export GA4_READ_ONLY=true

ga4 admin accounts              # ✓ works
ga4 data report --property 123  # ✓ works
ga4 admin delete-property ...   # ✗ blocked
ga4 sim run ...                 # ✗ blocked

Blocked commands exit with:

Error: write operations are disabled (GA4_READ_ONLY is set).

Use with AI agents

When an AI agent (e.g. Claude Code) has access to the CLI, set this variable in the agent's environment so it can query analytics data but cannot modify or delete anything.

Claude Code — add to .claude/settings.json:

{
  "env": {
    "GA4_READ_ONLY": "true"
  }
}

Docker — set in the container environment:

ENV GA4_READ_ONLY=true

Troubleshooting

  • Missing required env var: ensure .env has GA4_MEASUREMENT_ID and GA4_API_SECRET, or run ga4 sim init.
  • No data in GA4: confirm --endpoint production and check property config.
  • Validation errors in debug mode: inspect returned validation messages and adjust event params.
  • PERMISSION_DENIED on admin commands: for user accounts, run ga4 auth login --client-id-file <path> to re-authenticate; for service accounts, ensure the service account email has been added to your GA4 property with at least Viewer access.
  • This app is blocked: you need to create your own OAuth Desktop client — see Set up credentials.
  • Service account not working: verify GOOGLE_APPLICATION_CREDENTIALS is set and points to a valid file, then run ga4 auth status.

Contributing

Contributions are welcome.

Security

Please report vulnerabilities privately according to SECURITY.md.

License

ISC