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

@eventra_dev/eventra-cli

v2.0.9

Published

Static analytics event extraction CLI powered by the TypeScript compiler API

Readme

Eventra CLI

Eventra CLI statically discovers analytics events from @eventra_dev/eventra-sdk usage in your codebase — including function wrappers and cross-file propagation chains.


Overview

The CLI scans TypeScript and JavaScript with the TypeScript compiler API and extracts only calls to Eventra.prototype.track() on instances of Eventra imported from @eventra_dev/eventra-sdk.

It does not detect generic track(), Segment, Google Analytics, or other libraries out of the box. Framework files (e.g. .vue, .svelte, .astro, Angular's .html templates) require an optional plugin.


Installation

npm install -D @eventra_dev/eventra-cli
# or
pnpm add -D @eventra_dev/eventra-cli

Your project should use the runtime SDK:

pnpm add @eventra_dev/eventra-sdk

Quick Start

eventra init
eventra sync
eventra check
eventra send

What gets detected

Direct SDK calls

import { Eventra } from "@eventra_dev/eventra-sdk";

const tracker = new Eventra({ apiKey: "YOUR_PROJECT_API_KEY" });

tracker.track("checkout.completed");
tracker.track("app.loaded", { userId: "user_123" });
tracker?.track("optional.chain");

Function wrappers (propagation)

Wrappers that call Eventra.track() inside are registered automatically:

function trackFeature(name: string) {
  sdk.track(name);
}

trackFeature("purchase");

The event name can also come from a property on the wrapper's parameter — plain access, optional chaining, element access, or destructuring (including aliased and nested):

function trackWrapper(payload: { event: string }) {
  sdk.track(payload.event);        // also: payload?.event, payload["event"]
}
trackWrapper({ event: "checkout" });

function trackDestructured({ event: name }: { event: string }) {
  sdk.track(name);
}
trackDestructured({ event: "checkout" });

Variables, templates, conditionals

const EVENT = "signup";
tracker.track(EVENT);

tracker.track(`feature_${type}`);

tracker.track(flag ? "path.a" : "path.b");

Cross-file

// tracker.ts
export function trackFeature(name: string) {
  client.track(name);
}

// app.ts
import { trackFeature } from "./tracker";
trackFeature("purchase");

Event name rules (aligned with SDK)

| Rule | Limit | |------|--------| | Max length | 64 characters (same as SDK) | | Allowed characters | a-zA-Z0-9:_./- | | Position | First argument of .track(name, options?) |

The second argument (userId, properties) is not used as the event name.

A literal name that violates either rule (too long, or a character outside the allowed set) is not truncated and not flagged as a dynamic occurrence - it's silently excluded from the scan result, with no warning and no diagnostic anywhere. It simply never appears in eventra.json after sync. If an event you expect to see is missing, check this before assuming a detection bug.


What is ignored

// Not from @eventra_dev/eventra-sdk
track("legacy");
analytics.track("ga");
segment.track("x");

// Wrong API shape for SDK (object as first argument)
tracker.track({ event: "click" });

Commands

eventra sync

Full project scan → updates eventra.json:

  • events — discovered event names
  • functionWrappers — detected wrapper functions

eventra check

Compares config with the current scan (events + wrappers). Exit code 1 on drift.

eventra check --fix

Writes scan results into eventra.json.

eventra watch

Incremental scan with the same rules as sync.

eventra send

Uploads events from config to the Eventra API. Requires an API key and (optionally) an endpoint. Events are POSTed to POST /api/v1/cli/events and marked as non-billable on the backend.

API key resolution (checked in this order, never written to eventra.json):

  1. EVENTRA_API_KEY environment variable — recommended for CI
  2. eventra.local.json (created by eventra init/eventra send, automatically gitignored)
  3. legacy inline apiKey in eventra.json (deprecated — avoid committing real keys here)

eventra.json is meant to be committed so CI can diff it via eventra check; keeping the key out of it avoids leaking it into source control. In a non-interactive shell (no TTY), send fails fast with a message pointing at EVENTRA_API_KEY instead of hanging on a prompt.

Endpoint trust. The default production endpoint is always used with no extra step. A custom endpoint is only trusted without approval when it comes from the EVENTRA_ENDPOINT environment variable (setting an env var is already a local/CI action). A custom endpoint committed inline in eventra.json is trust-on-first-use: send refuses to run until it's approved once with:

eventra send --trust-endpoint

This records the endpoint in eventra.local.json (gitignored). If eventra.json's endpoint later changes to a different value — e.g. a PR silently pointing it at another host — send blocks again until re-approved, so a committed config change alone can't redirect where your API key and events get sent.

Once eventra.json already commits an endpoint, that committed value is what events actually get sent to - EVENTRA_ENDPOINT only controls whether the approval prompt is skipped; its own value is not read or compared against anything in that case. EVENTRA_ENDPOINT becomes the real destination only when eventra.json has no endpoint set at all. In other words, setting the variable to point somewhere else does not redirect traffic away from an already-committed endpoint - it only means you won't be asked to approve it.

Network resilience:

  • Up to 4 attempts with exponential backoff + jitter (capped at 8 s)
  • Retries on 429 and 5xx responses, and on transport errors (timeout / DNS / connection reset)
  • Permanent failure (4xx other than 429) surfaces immediately without retry
  • 10 s timeout per attempt via AbortController

Plugins

The CLI core is framework-agnostic. Extensions are separate npm packages with their own types — the CLI loads them from eventra.json and adapts their output internally.

Vue (.vue SFC)

Install the official Vue plugin and enable it in config:

pnpm add -D @eventra_dev/cli-plugin-vue
{
  "plugins": ["@eventra_dev/cli-plugin-vue"],
  "sync": {
    "include": ["**/*.{ts,tsx,js,jsx}"],
    "exclude": ["node_modules", "dist", ".next", ".git"]
  }
}

The plugin:

  • parses each .vue file with the real Vue compiler (@vue/compiler-sfc) into a single virtual TypeScript module combining <script>/<script setup>
  • extracts track() calls from the script exactly like a regular .ts file (direct calls, wrappers, propagation)
  • detects event="feature_name" attributes in <template> — both literal and dynamic (:event="expr"); dynamic bindings resolve through the same module scope as the script, or surface as a dynamic occurrence if unresolvable
  • Nuxt .vue files (pages/layouts/components) are handled the same way; auto-imported composables resolve once the project has run nuxt prepare/nuxt dev

sync.include does not need **/*.vue manually — the plugin registers **/*.vue via includeGlobs. See @eventra_dev/cli-plugin-vue for details.

Svelte (.svelte)

pnpm add -D @eventra_dev/cli-plugin-svelte
{
  "plugins": ["@eventra_dev/cli-plugin-svelte"]
}

The plugin:

  • parses each .svelte file with the real Svelte compiler (svelte/compiler) into a single virtual TypeScript module combining context="module" and instance <script> blocks
  • extracts track() calls from the script exactly like a regular .ts file (direct calls, wrappers, propagation); Svelte 5 runes ($props, $state, …) pass through untouched
  • detects event="feature_name" attributes on any tag — plain elements, components, {#if}/{#each}/{#await}/{#key} blocks, slots — both literal and dynamic (event={expr}); dynamic bindings resolve through the same module scope as the script, or surface as a dynamic occurrence if unresolvable

sync.include does not need **/*.svelte manually. See @eventra_dev/cli-plugin-svelte for details.

Astro (.astro)

pnpm add -D @eventra_dev/cli-plugin-astro
{
  "plugins": ["@eventra_dev/cli-plugin-astro"]
}

The plugin:

  • parses each .astro file with the real Astro compiler (@astrojs/compiler) into a single virtual TypeScript module from the frontmatter
  • extracts track() calls from the frontmatter exactly like a regular .ts file (direct calls, wrappers, propagation); Astro.props and other globals pass through untouched
  • detects event="feature_name" attributes on any tag, including inside JSX expressions ({cond && <Button event="..." />}, .map()) — both literal and dynamic (event={expr}, including the {event} shorthand); dynamic bindings resolve through the same module scope as the frontmatter, or surface as a dynamic occurrence if unresolvable

sync.include does not need **/*.astro manually. See @eventra_dev/cli-plugin-astro for details.

Angular (.html, via templateUrl)

pnpm add -D @eventra_dev/cli-plugin-angular
{
  "plugins": ["@eventra_dev/cli-plugin-angular"],
  "sync": {
    "include": ["**/*.{ts,tsx,js,jsx}"],
    "exclude": ["node_modules", "dist", ".angular", ".git"]
  }
}

The plugin:

  • parses each component template with the real Angular compiler (@angular/compiler) into a virtual TypeScript module, paired with its component class (foo.component.html ↔ foo.component.ts, Angular CLI's default naming)
  • extracts track() calls from the component class exactly like a regular .ts file (direct calls, wrappers, propagation) - no plugin needed for that part
  • detects event="feature_name" attributes on any tag, including *ngIf/*ngFor and the @if/@for/@switch block syntax - both literal and dynamic; a dynamic binding resolves against the component instance (this.field, including getters), or surfaces as a dynamic occurrence if unresolvable
  • use [attr.event]="expr", not [event]="expr", for a dynamic binding on a plain HTML element - [event] is a property binding that Angular's real compiler rejects (NG8002) unless the element is a component that actually declares @Input() event; both forms are detected identically by the plugin, only [attr.event] is guaranteed to compile

sync.include does not need **/*.html manually. Only templateUrl-based components are covered - an inline template: "..." isn't yet. Requires Node ^20.19.0 || ^22.12.0 || >=24.0.0 (higher than the other plugins' >=18 floor - @angular/compiler's own requirement). See @eventra_dev/cli-plugin-angular for details.

Before publishing / local development

Plugins are resolved from your project's node_modules (same as any dependency):

{
  "devDependencies": {
    "@eventra_dev/cli-plugin-vue": "file:../cli-plugin-vue"
  }
}

The plugin package must be built (dist/) before use. Unpublished plugins work the same way — only the install source differs.

Plugin contract (for authors)

External plugins export an object (or factory) with:

| Field | Purpose | |-------|---------| | id | Unique preprocessor name | | includeGlobs | Extra glob patterns merged into the scan | | match(path) | Whether this plugin handles a file | | transform({ path, source }) | Returns { modules: [{ path, content }] } | | staticSinks? | Declarative callee-based sink rules (CLI converts to internal detectors) |

No dependency on @eventra_dev/eventra-cli is required — the host CLI loads the plugin, converts staticSinks into its own internal sink detectors, and calls transform() on matching files; the plugin never imports anything from the CLI itself.

Only official @eventra_dev/cli-plugin-* packages can be loaded. eventra.json is meant to be committed to git, so its plugins array is effectively PR-editable; since the CLI import()s whatever is listed there, an arbitrary specifier would be arbitrary code execution on every machine that runs sync/check/watch. Because @eventra_dev is an npm scope only the Eventra maintainers can publish to, restricting to @eventra_dev/cli-plugin-* means nothing outside that scope can ever be loaded this way. Any other entry in plugins is skipped with a console warning instead of being imported — third-party/community plugins aren't supported today.


Configuration

{
  "apiKey": "",
  "endpoint": "",
  "events": [],
  "functionWrappers": [],
  "plugins": [],
  "sync": {
    "include": ["**/*.{ts,tsx,js,jsx}"],
    "exclude": ["node_modules", "dist", ".next", ".git"]
  }
}

| Field | Description | |-------|-------------| | apiKey | Legacy inline API key — leave empty. Use EVENTRA_API_KEY or eventra.local.json instead (see eventra send) so a real key never ends up in this committed file | | endpoint | Custom eventra send target (e.g. self-hosted). Non-default values from this file need one-time local approval — see eventra send | | plugins | @eventra_dev/cli-plugin-* specifiers to import() at startup. Anything outside that scope is skipped (see Plugin contract) | | sync.include | Base glob patterns; plugin includeGlobs are merged automatically | | sync.exclude | Paths skipped during scan |

eventra init/eventra send never write a real key into this file — they write to eventra.local.json instead, which is automatically added to .gitignore.


How it works

  1. Load built-in plugins (eventra-sdk sink detector) and any packages listed in plugins.
  2. Glob project files (sync.include + plugin includeGlobs).
  3. Run file preprocessors (e.g. .vue → virtual .ts modules).
  4. Load sources into an incremental TypeScript program (with SDK type shim).
  5. Phase 1 — find Eventra instances from @eventra_dev/eventra-sdk and register function wrappers that call .track().
  6. Phase 2 — resolve static event names and wrapper propagation chains (sink detector chain includes plugin sinks).
  7. Write results to eventra.json.

watch tracks disk source files (including .vue), re-runs preprocessors on change, and incrementally updates the engine.

No runtime execution. No monkey-patching.


Requirements

  • Node.js 18+
  • TypeScript/JavaScript source using @eventra_dev/eventra-sdk

Test Coverage

Two test layers, 445 unit tests + 12 e2e fixtures + 3 check exit-code scenarios + 1 watch scenario.

100% statement/branch/function/line coverage (v8 provider, pnpm test:coverage), enforced via a coverage.thresholds block in vitest.config.ts so a regression fails the build instead of silently slipping under 100%. This measures the vitest unit layer only; the e2e/check/watch fixtures run against the compiled dist/ binary via tsx tests/run.ts and aren't coverage-instrumented. A handful of genuinely unreachable defensive branches (TS-internal invariants — e.g. an aliased import symbol is always fully resolved before its own declarations are ever inspected — that don't hold a second outcome in practice) are marked with /* v8 ignore */, with an inline comment explaining why, rather than tested.

Unit tests (vitest) — 39 suites covering core modules:

| Module | Covers | |---|---| | ImportGraph | Forward/reverse edges, cycles, stale-edge cleanup, file removal | | Scheduler | Batch coalescing, last-write-wins per file, sequential bursts, error propagation | | DocumentRegistry | Path normalization, version bumping, no-op on identical content, ensure() from disk | | CompilerContext | Stage/update/remove files, resolveModule with tsconfig.json paths, source-file enumeration | | EventraEngine | Direct calls, SDK isolation, cross-file wrappers, file updates, file removal, wrapper filtering | | PluginRegistry | Built-in SDK sink, preprocessors, virtual-path mapping, include-pattern dedup | | external plugin adapter | Transform output mapping, static sink registration, invalid result rejection | | external plugin dynamic sink | A non-literal argument at a plugin-declared static sink surfaces as a dynamic occurrence instead of being silently dropped | | vue-shaped/svelte-shaped/astro-shaped external plugin | Adapter path for script + template virtual modules and staticSinks, one suite per framework | | processFile | Script-kind detection, import/export specifier extraction | | extractTemplateExpressions | Vue/Svelte/Astro attribute patterns | | config | normalizeConfig (sort events, dedupe wrappers, defaults, preserve apiKey/endpoint/sync/plugins); resolveApiKey env-var > eventra.local.json > legacy-inline priority, saveLocalApiKey's gitignore-append | | scanResults (buildConfigFromScan) | Replace events + wrappers, preserve everything else | | hash | Stability and uniqueness | | cross-file SDK detection | Direct .track() and wrapper calls resolve across files even when the call-site file never imports the SDK package itself | | dynamic event reporter | Dynamic vs. static call classification, end-to-end registerDynamicEventReporter invocation | | load plugins | Trusted-specifier allowlist (^@eventra_dev/cli-plugin-[a-z0-9-]+$), accepted/rejected plugin shapes | | Nuxt auto-import compatibility | Ambient declare const x: typeof import(...) shape resolution — a hand-written fixture, not a real nuxt build | | property propagation | Direct/optional/nested property access, destructured/aliased/nested-destructured params, element access, enum member access (same-file and cross-file), wrapper detection surviving a cast/non-null assertion, and direct object-literal payloads to track() staying ignored (including when cast) | | send() | Endpoint trust-on-first-use (TOFU): default-trusted, blocked-until-approved, approve-then-persists, re-block-on-change, env-var-bypasses-approval | | wrapper detector plugin | A custom WrapperDetector (e.g. a template-literal wrapper shape the built-in analyzer can't cover) alongside the built-in fallback |

End-to-end fixtures — 12 isolated TS projects scanned via eventra sync:

| Fixture | Covers | |---|---| | sdk/direct | Plain tracker.track() calls | | frontend/react, frontend/next, frontend/vue | Framework-specific code shapes | | backend/node, backend/express, backend/nest | Backend wrappers and middleware chains | | wrappers/function | Local wrapper functions, methods, objects, ternaries, templates | | wrappers/barrel | export * from "./tracker" re-exports | | wrappers/default-export | export default function trackFeature propagation | | wrappers/path-aliases | tsconfig.json paths mapping (@app/*) | | watch-incremental | Engine state across sequential file updates (matches sync output) |

eventra check exit-code scenarios:

  • Drift → exit 1
  • --fix writes scan results into eventra.json → exit 0
  • Parity (no drift) → exit 0

eventra watch scenario: a real watch process detects a brand-new file (created after startup) and its .track() call lands in eventra.json.

Run locally:

pnpm --filter @eventra_dev/eventra-cli test       # unit + e2e + exit codes
pnpm --filter @eventra_dev/eventra-cli test:unit  # vitest only
pnpm --filter @eventra_dev/eventra-cli test:e2e   # fixtures + check exit codes

Node version note: the CLI itself supports Node 18+ (see engines), but vitest@4 depends on node:util's styleText, which requires Node ≥ 20.12 — running the test suite on an older Node 20.x patch (or Node 18) fails to start. CI runs the matrix on Node 20 and 22; locally, use the version pinned in the repo's .nvmrc.


License

MIT