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

falcon-event-tracker

v0.5.0

Published

Shared BigQuery event write/read for Avada Falcon apps — see _docs/product-analytics/ADR.md

Readme

falcon-event-tracker

Shared BigQuery event write/read for the 6 Avada Falcon apps. Design + trade-offs: _docs/product-analytics/ADR.md in the product-analytics repo.

Published on the public npm registry as falcon-event-tracker, not @avada/event-tracker as the ADR originally planned — publishing to the internal registry.avada.io under the @avada scope needs Tech Lead-granted publish permission (ADR §7 point 3) that wasn't available yet. Functionally identical; only the install name differs. Re-publish under @avada/event-tracker once that permission lands, and update every consuming app's package.json + imports.

Event types

Every feature is described by attributes (menu, card, group, scope, payload), not by inventing a new event type — adding a 23rd feature to an app means adding a constant, not a schema change. There are exactly 10, defined in src/schema.ts:

| Event | Fires when | | --------------------- | ---------------------------------------------------------------------------- | | app_opened | The app is opened (once per session/visit). | | menu_viewed | A menu landing page is viewed (no card). | | feature_opened | A specific feature/card screen is viewed. | | card_clicked | A feature card is clicked from a landing/grid page. | | feature_started | A merchant-initiated action begins (button click) — never for cron/auto-run. | | feature_completed | The action finished successfully — only fire when genuinely certain. | | feature_applied | A save/publish/toggle-style action the merchant took (not started→completed).| | feature_failed | The action failed — always include payload.error_reason. | | cross_sell_clicked | A card links out to another app's App Store listing, not an in-app feature. | | screen_left | The merchant navigates away from a screen (derived, not user-initiated). |

Install

pnpm add falcon-event-tracker

Env vars — just 1

GOOGLE_CLOUD_CREDENTIALS_JSON=<base64 of the service account JSON key>

Base64-encoded, not raw JSONbase64 -w0 key.json (macOS: base64 -i key.json | tr -d '\n'). Raw JSON breaks when piped through shell echo into a deployed app's .env file (unescaped quotes/newlines); base64 is always shell-safe. The package decodes it internally.

That's the only thing an installing app configures — the shared service account (same value across all 6 apps, ADR §7). Project/dataset/table are hardcoded in src/config.ts (plaza-staging-3 / product_analytics / events) since every app writes to the exact same table; there's nothing per-app to set. Missing the credential disables tracking silently — trackEvent() becomes a no-op, it never throws and never blocks the caller.

Usage — Koa apps (SEO Suite and the rest of Falcon), 1-line setup

// app.js — right after your session/auth middleware, before routes
import { setupEventTracker } from "falcon-event-tracker/koa";

setupEventTracker(app, { appId: "seo-suite", routeMap: SEO_ROUTE_MAP });

This mounts two things:

  1. POST /api/track-event — ready-made route for frontend-observable events (feature_started, menu_viewed, card_clicked, screen_left...). Your frontend POSTs here (same origin, uses your app's existing session — never talks to BigQuery or this package directly). Requires a body parser already mounted upstream (koa-bodyparser or equivalent) — this route doesn't parse the body itself. shopId is read from ctx.state.user.shopID by default (the @avada/core session convention); pass getShopId to override.

  2. Auto-tracking middleware — fires feature_completed/feature_failed for every request matching a route in SEO_ROUTE_MAP (same shape as SEO Suite's existing config/activityTracking.js, ADR §3):

    const SEO_ROUTE_MAP = {
      "POST /rule": { menu: "search-optimization", card: "meta-tags" },
      "POST /optimize/image": { menu: "performance", card: "image-compression" },
      // ...
    };

    Need extra payload (credits_used, duration_ms, item_count...) on an auto-tracked event? Set it in the handler, no extra import needed:

    ctx.state.eventPayload = { credits_used: 2, item_count: 500 };

Omit routeMap to mount only the ingest route (e.g. if you'd rather call trackEvent() by hand everywhere). appId defaults to process.env.APP_ID if omitted.

Usage — frontend (browser)

trackEvent/trackJobOutcome above are server-only (they insert into BigQuery directly — never ship a service-account credential to the browser). The frontend instead POSTs to your app's own /track-event route (mounted by setupEventTracker, see above). falcon-event-tracker/browser is a tiny, zero-dependency helper for that POST — write it once per app, not by hand:

import { createTrackEvent } from "falcon-event-tracker/browser";
import { fetchAuthenticatedApi } from "./yourAppFetchWrapper"; // (path, {method, body}) => Promise

export const trackEvent = createTrackEvent({ fetcher: fetchAuthenticatedApi });

trackEvent("feature_started", { menu: "ai-content", card: "meta-title", scope: "single" });

Most call sites fire several events (started/completed/failed) for the same {menu, card, scope}createFeatureTracker locks that base in once so you don't repeat it:

import { createFeatureTracker } from "falcon-event-tracker/browser";

const trackFix = createFeatureTracker(trackEvent, {
  menu: "seo-audit",
  card: "onpage",
  scope: "single",
  element: "ai_fix_issue",
});

trackFix("feature_started", { credits_used: 1, credit_balance: 4 });
trackFix("feature_completed", { credits_used: 1 });

Only reach for createFeatureTracker when 2 or more trackEvent calls in the same function genuinely share the same base — it's a dedup helper, not a mandatory wrapper. A single call, or calls whose menu/card differ per iteration (e.g. a loop over rule types), stay as plain trackEvent(...) calls; forcing the abstraction there adds indirection for nothing.

React appsuseScreenTracker fires app_opened once, then menu_viewed/feature_opened/ screen_left on every route change, from a resolver you own (pathname → {menu, card, group}). Router-agnostic — pass whatever pathname your router gives you. The resolver is just a lookup table keyed by URL segment, one entry per menu × card:

function resolveScreen(pathname) {
  const [menu, card] = pathname.split("/").filter(Boolean);
  if (!menu) return null;
  return { menu, card }; // card omitted → fires menu_viewed instead of feature_opened
}
import { useScreenTracker } from "falcon-event-tracker/react";
import { useLocation } from "react-router-dom";

function ScreenTracker() {
  const location = useLocation();
  useScreenTracker({ pathname: location.pathname, resolveScreen, trackEvent });
  return null;
}

Mount <ScreenTracker /> once at the root of your routes — it covers app_opened, menu_viewed, feature_opened, and screen_left for every screen with zero per-page wiring.

Usage — background jobs (Pub/Sub, cron, queue workers)

A job that keeps running after the HTTP request that triggered it has ended (bulk generation, sitemap build, any fan-out worker) is the only thing that knows how it actually turned out — the frontend that started it is long gone. trackJobOutcome turns a plain success boolean into the right event, so every worker across every Falcon app reports outcomes the same way:

import { trackJobOutcome } from "falcon-event-tracker";

// at the end of a Pub/Sub subscriber, once the job is fully done
await trackJobOutcome({
  appId: "seo-suite",
  success: true, // → feature_completed; false → feature_failed
  shopId,
  plan: shop.plan,
  menu: "ai-content",
  card: "meta-title",
  scope: "bulk",
  payload: { item_count: resources.length },
});

Usage — any runtime (non-Koa, or manual calls)

Backend, after knowing the outcome (feature_completed/feature_failed — fire at the END of the handler, once, not awaited on the response path):

import { trackEvent } from "falcon-event-tracker";

trackEvent({
  appId: "seo-suite",
  eventType: "feature_completed",
  shopId: ctx.state.user.shopID, // from session — never from request body
  menu: "ai-content",
  card: "meta-title",
  scope: "single",
  payload: { credits_used: 2, duration_ms: 840 },
}).catch(() => {}); // already never throws, .catch is defence in depth only

Shopify shop/redact webhook:

import { deleteShopEvents } from "falcon-event-tracker";
await deleteShopEvents("seo-suite", shopId);

CS shop lookup:

import { queryShopEvents } from "falcon-event-tracker";
const rows = await queryShopEvents({ appId: "seo-suite", shopId, limit: 50 });

Tracking principles (read before instrumenting a new feature)

  • No button, no feature_started. A cron job, auto-scan, or system-triggered action never gets a started event — there was no merchant click to start. Report its outcome with trackJobOutcome instead (see above).
  • Only fire feature_completed when genuinely certain. If the frontend can't reliably know the outcome (network drop, tab closed mid-request), don't guess — let the backend confirm it instead (a route-map middleware watching the real HTTP response, or a background job emitting its own outcome).
  • Bulk operations are ONE event with payload.item_count, never N events per item. A "fix 12 issues" button fires a single feature_started/feature_completed pair with item_count: 12, not 12 pairs.
  • Cross-app promotional cards use cross_sell_clicked, not a feature event. A card that links out to another app's App Store listing isn't "a feature nobody uses" — it's advertising, and mixing it into feature-usage numbers skews them.
  • Conditionally-hidden features need visibility tracking, not just usage. If a card only shows for some shops (a flag, a plan tier, an A/B bucket), fire feature_opened with payload: {visibility_check: true, visible: boolean} on mount even when the merchant never clicks anything — otherwise "0 uses" is indistinguishable from "nobody could see it."
  • Never log what the merchant typed. Titles, descriptions, URLs, anchor text, prompt content — log that they saved something (a count, a boolean, an enum), never what they saved.

Gotchas

  • shopId must come from your app's own authenticated session — this package trusts whatever you pass it, it has no way to verify a shop's identity itself.
  • Don't JSON.stringify() payload yourself — pass a plain object.
  • event_type must be one of the 10 in EVENT_TYPES (schema.ts). Adding a new one needs PO sign-off (spec §7) — it's not just a code change.
  • Streaming inserts can take 10-30s to become query-visible. Don't queryShopEvents immediately after trackEvent() in a test and expect to see the row.
  • No batching — each trackEvent() call is one insert. Fine at the scale this spec targets (ADR §2); revisit only if a single app's write volume alone approaches millions/day.