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

@aranova/tracking-react

v0.20.1

Published

React tracking and consent utilities for Aranova client sites

Readme

@aranova/tracking-react

React tracking and consent utilities for Aranova client sites.

Install

npm install @aranova/tracking-react

Setup

Create one shared tracking module and import the scoped TrackingProvider / useTracking from that module throughout your app.

// src/lib/tracking.ts
import { createTracking } from "@aranova/tracking-react";
import { ARANOVA_TRACKING_CONFIG } from "./aranova-services";

export const { TrackingProvider, useTracking } = createTracking({
  apiKey: import.meta.env.VITE_ARANOVA_TRACKING_API_KEY,
  endpoint: import.meta.env.VITE_ARANOVA_TRACKING_ENDPOINT,
  trackingConfig: ARANOVA_TRACKING_CONFIG,
  triggers: {
    automatic: {
      page_view: {},
      time_on_site: { thresholdSeconds: 60 },
      specific_page_visit: {
        pages: [
          { name: "contact_page", pathPattern: /^\/(contact|book|get-started)/ },
          { name: "google_ads_service_page", pathPattern: /^\/services\/google-ads\/?$/ },
        ],
      },
    },
    manual: {
      form_submit: {},
      phone_click: {},
      cta_click: {},
    },
  },
  debug: import.meta.env.MODE !== "production",
});

Mount the provider at the root of your React tree.

// src/main.tsx
import { createRoot } from "react-dom/client";
import { App } from "./App";
import { TrackingProvider } from "./lib/tracking";

createRoot(document.getElementById("root")!).render(
  <TrackingProvider>
    <App />
  </TrackingProvider>,
);

Legacy static gtag IDs

gtagId, gtagIds, GoogleAdsTracking, and generated ARANOVA_GTAG_IDS remain compatibility APIs. Do not use them for new installs: static IDs cannot consume dashboard tag changes or tombstones without a redeploy. If maintaining a legacy install, gtagIds loads every entry and takes precedence over gtagId.

<TrackingProvider gtagIds={{ production: "AW-111111111", test: "AW-222222222" }}>
  <App />
</TrackingProvider>

Manual Events

Manual events must be registered under triggers.manual before trackEvent() accepts them.

import { useTracking } from "./lib/tracking";

export function LeadForm() {
  const tracking = useTracking();

  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    const form = event.currentTarget;
    const data = new FormData(form);

    tracking.trackEvent("form_submit", {
      form: {
        id: form.id,
        action: form.getAttribute("action"),
        fields: [
          {
            name: "service_interest",
            type: "select",
            label: "Service interest",
            value: String(data.get("service_interest") ?? ""),
          },
          {
            name: "is_existing_patient",
            type: "checkbox",
            label: "Existing patient",
            value: data.get("is_existing_patient") === "on",
          },
        ],
      },
      page: { path: window.location.pathname },
    });
  }

  return (
    <form id="lead-form" action="/api/lead" onSubmit={handleSubmit}>
      {/* fields */}
    </form>
  );
}

fields[].value can be any JSON value: string, number, boolean, null, array, or object. Values must be JSON-serializable because events are stored as first-party JSONB. Intentionally submitted lead fields may include raw names, emails, phone numbers, addresses, selections, free-text messages, and submitted file data for first-party analytics and lead operations. Build the array explicitly from the submitted form; the SDK never scrapes arbitrary DOM fields. File/Blob objects must be converted to a JSON representation, and the complete event metadata must fit the 4 KB limit; upload larger files separately and send their storage reference. Never send passwords, authentication tokens, payment-card/bank credentials, or private keys. Apply the client's privacy notice, consent, retention, and regulated-data requirements. Google offline matching uses normalized, server-side SHA-256-hashed identifiers — not raw free-text/file metadata.

Phone clicks (tel: taps) — manual or auto-capture

Opt into auto-capture and every tel: link is tracked with no per-link code. Add autoCapture to the phone_click registration — a single delegated click listener does the rest:

manual: {
  phone_click: { autoCapture: {} }, // default selector: a[href^="tel:"]
  // or narrow it: { autoCapture: { selector: "a.call" } }
},
<a href="tel:+14165550199" data-aranova-section="header">
  (416) 555-0199
</a>

The number is read from the href (normalized to E.164); section comes from an optional data-aranova-section. Unlike cta_click, a phone_click also fires the Google Ads conversion for a linked phone-call goal (a CLICK_TO_CALL action) — auto-fired the moment the tap is captured, so you never call trackConversion (firing needs the sales client wired with the same config; see below).

Prefer to instrument links yourself? Fire it from a click handler — this fires the conversion the same way:

tracking.trackEvent("phone_click", {
  phone_number: "+14165550199",
  page: { path: window.location.pathname },
  section: "header",
});

CTA clicks — manual or auto-capture

Fire cta_click yourself for full control over the name:

tracking.trackEvent("cta_click", {
  cta_name: "Book appointment",
  section: "hero",
  destination_url: "/booking",
  page: { path: window.location.pathname },
});

Or opt into auto-capture and skip per-button code. Add autoCapture to the cta_click registration — this attaches a single delegated click listener:

manual: {
  cta_click: { autoCapture: {} }, // default selector: [data-aranova-cta]
  // or target existing classes: { autoCapture: { selector: "a.cta, .btn-primary" } }
},

Then tag your CTAs in markup — no imports, no handlers:

<a href="/booking" data-aranova-cta="Book now" data-aranova-section="hero">Book now</a>
<button data-aranova-cta="Get a quote">Get a quote</button>

cta_name resolves to the data-aranova-cta value, else the element's trimmed text (capped 120 chars), else tag#id. section comes from an optional data-aranova-section; href/destination_url and a short element descriptor are captured automatically. Point the selector at real CTAs (an explicit attribute is the safe default) — every matching click is counted, including ones that later stopPropagation.

Automatic dwell + exit (page_exit)

page_exit is captured automatically — no registration. On SPA navigation, tab hide, and page unload it records the active (visible) time spent on the page plus the max scroll depth reached, delivered via a keepalive beacon so the final page still counts. This powers per-page dwell and drop-off in the dashboard Analytics tab; there is nothing to configure.

Phone Fields

Bundled libphonenumber-js: parse/format utils + a React input. Display is configurable; the value sent to the backend is always E.164. Configure once via createTracking({ phone: { defaultCountry: 'CA', display: 'national' } }).

import { usePhoneField, PhoneField, toE164 } from "@aranova/tracking-react";

const phone = usePhoneField(); // phone.value (display), phone.e164 (wire), .isValid, .error
<input {...phone.inputProps} />; // or the batteries-included <PhoneField name="phone" />

toE164("416-555-0199"); // '+14165550199' (null if invalid)

Pure, isomorphic utils are also at @aranova/tracking-react/phone (no React). Full guide: phone.md.

Recording Sales

Record a sale / conversion (a first-class, mutable resource — not a fire-and-forget event). Money is integer minor units (cents); currency is a required ISO-4217 enum.

One isomorphic createSalesClient (on the root entry) serves both sides — what a key may do is enforced by the backend, not by hiding methods. Browser write with your public key:

import { createSalesClient, toMinor } from "@aranova/tracking-react";

const sales = createSalesClient({
  apiKey: import.meta.env.VITE_ARANOVA_TRACKING_API_KEY,
  endpoint,
});
await sales.record({ currency: "CAD", amount_total_cents: toMinor(250, "CAD"), service: "tires" });

Reads / full CRUD require a secret key and must run server-side — never ship a secret key in the browser bundle. In a Vite + Vercel app, hold it in a serverless function. Import from the /sales subpath — it's React-free, so the server bundle never pulls in the provider/components:

// api/sales.ts  (Vercel serverless function — runs on the server)
import { createSalesClient } from "@aranova/tracking-react/sales";

const sales = createSalesClient({
  apiKey: process.env.ARANOVA_TRACKING_SECRET_KEY!,
  endpoint: process.env.ARANOVA_TRACKING_ENDPOINT!,
});
export default async function handler(_req, res) {
  res.json(await sales.list({ limit: 50 }));
}

Secret-key reads power dashboards (all currency-grouped — never summed across currencies):

await sales.summary({ range: "mtd", timezone: "America/Toronto", compare_to: "previous_period" });
await sales.list({ sort: "amount_total_cents", want_total: true }); // → { items, total_count, … }
await sales.customers.list({ segment: "returning", sort: "total_spent" }); // phone-keyed roster
await sales.customers.summary({ range: "mtd" });
const cfg = await sales.business.config(); // tz / currencies / services (pk or sk)

summary() gains tz-aware calendar/custom ranges, granularity, and period-over-period compare_to; legacy 24h/7d/30d are unchanged. A customer is their phone (E.164)customers.* is a live roster, no separate table. A public-key client calling any read gets a 403.

The root entry (@aranova/tracking-react) still re-exports createSalesClient and the money/date helpers for back-compat, so existing imports keep working — but prefer /sales in server code so the React surface never reaches your server bundle.

Generate the typed AranovaService union from your dashboard services with the CLI (install it as a devDependency):

npm install --save-dev @aranova/tracking-cli
npx @aranova/tracking-cli gen          # reads ARANOVA_TRACKING_SECRET_KEY from .env

See the full guide: sales-tracking.md and the CLI reference: cli.md.

On-site conversion firing

Run tracking-cli gen once to emit ARANOVA_TRACKING_CONFIG, then use that reference everywhere Google tracking is initialized. R2 is authoritative: the SDK confirms the current object before emitting Google tag, page-view, or conversion commands. Dashboard tag changes, goal remaps, and disabled-state tombstones propagate under public, max-age=60, must-revalidate with no CLI run or site redeploy.

import {
  createSalesClient,
  createTracking,
  getTrackingConfigRuntime,
  toMinor,
} from "@aranova/tracking-react";
import {
  ARANOVA_TRACKING_CONFIG,
  type AranovaConversion,
  type AranovaService,
} from "./aranova-services";

const googleTracking = getTrackingConfigRuntime(ARANOVA_TRACKING_CONFIG);

export const { TrackingProvider, useTracking } = createTracking({
  apiKey,
  endpoint,
  triggers,
  trackingConfig: ARANOVA_TRACKING_CONFIG,
});

// Bind the generated unions so `service` and `trackConversion` keys are type-checked.
const sales = createSalesClient<AranovaService, AranovaConversion>({
  apiKey,
  endpoint,
  firing: googleTracking,
});

await sales.recordSale({
  currency: "CAD",
  amount_total_cents: toMinor(250, "CAD"),
  service: "tires",
}); // records + fires
  • Automatic event-goals fire when their registered detector crosses the published threshold.
  • Sales + manual goals use the shared runtime above. Firing is consent-gated, transaction-deduped, and browser-only.

Manual conversions (multiple forms): register the trigger once, then call trackConversion(key) in each form's submit handler. Keys come from the dashboard Goals tab (event-goals with a form_submit/phone_click/cta_click trigger mapped to a WEBPAGE action) and are emitted as the AranovaConversion union by gen — so they autocomplete and reject typos:

// createTracking({ …, triggers: { manual: { form_submit: {} } } })  // register once

// in the contact form's onSubmit:
sales.trackConversion("contact_form"); // ✅ typed by AranovaConversion
// in the demo form's onSubmit:
sales.trackConversion("demo_request");
sales.trackConversion("typo"); // ❌ compile error — not a known goal key

Each call fires only the WEBPAGE conversion for that goal (no /sales write); the SDK no-ops if the goal isn't mapped. Remapping an existing key needs no codegen. A new manual goal key still requires tracking-cli gen to refresh AranovaConversion, plus site handler wiring. ARANOVA_CONFIG_URL and conversionConfig: { cdnUrl } remain legacy compatibility APIs; do not use them for new installs. See conversion-config-schema.md.

Before any manual goal exists, gen emits an empty ARANOVA_CONVERSIONS, so AranovaConversion resolves to never — binding it (createSalesClient<AranovaService, AranovaConversion>) then makes every trackConversion call a compile error. Until you've mapped at least one manual goal and re-run gen, bind only the service generic (createSalesClient<AranovaService>(…)); the conversion key defaults back to string.

Consent (v2 — opt-out model)

Tracking is on by default: gtag, the Meta Pixel, and on-site conversion firing all run from first paint unless the visitor has stored an explicit, unexpired decline. A decline is honored for 90 days (then the visitor reverts to default-granted); an explicit grant never expires. There is no pending state and the packages ship no consent UI — each site provides its own footer "cookie preferences" control and discloses tracking in its privacy policy (that notice is what makes the opt-out model defensible).

Footer control — useCookiePreferences()

import { useCookiePreferences } from "@aranova/tracking-react";

function CookiePreferences() {
  const { isDenied, isDefault, optOut, optIn } = useCookiePreferences();

  return isDenied ? (
    <button onClick={optIn}>Enable ad measurement</button>
  ) : (
    <button onClick={optOut}>Opt out of ad measurement</button>
  );
}

The hook returns the effective state ("granted" | "denied"), its source ("default" = no explicit choice, "explicit"), boolean helpers (isDefault / isGranted / isDenied), the choice's updatedAt / expiresAt, and the optOut() / optIn() / reset() actions. It stays in sync with other components in the same tab (via onConsentChange) and other tabs (via storage events). optOut accepts a custom TTL via the hook's { declineTtlDays } option.

For non-component contexts the same primitives are exported as plain functions: optOut(), optIn(), resetConsent(), getConsentChoice(), and onConsentChange().

Deprecated opt-in flow

<ConsentBanner />, useConsent(), and useConsentState() are @deprecated and scheduled for removal. The banner is permanently inert — it rendered only while consent was pending, which no longer occurs — so leaving it mounted is harmless but pointless. useConsent() still works as a shim (isPending is always false; accept/decline map to optIn/optOut).

Migrating from the banner flow: delete <ConsentBanner />, add a footer control built on useCookiePreferences, and mention the tracking + opt-out in your privacy policy. Existing visitors' stored grants stay granted; stored declines stay denied for 90 days from their first visit after the upgrade.

Exports

  • createTracking()
  • TrackingProvider and scoped useTracking
  • AdPlatformTracking and getTrackingConfigRuntime; legacy GoogleAdsTracking
  • Consent (v2): useCookiePreferences() + UseCookiePreferencesResult; standalone optIn(), optOut(), resetConsent(), getConsentChoice(), onConsentChange(), getConsentState(), setConsentState()
  • Deprecated consent shims: ConsentBanner + ConsentBannerProps, useConsent() + UseConsentResult, useConsentState()
  • Attribution hooks: useTrackingParams(), useGclid()
  • createSalesClient() (isomorphic — public key writes; secret key reads/CRUD, summary, customers.*, business.config) + money/date helpers (toMinor/fromMinor/formatMoney/formatDateInTz). Also available React-free at @aranova/tracking-react/sales (with all sale/customer/config types) — the recommended import for server/serverless code.
  • Phone: parsePhone/toE164/formatPhone/formatPhoneAsTyped/phoneField, usePhoneField, PhoneField (utils also at /phone)
  • Codegen: @aranova/tracking-cligen typed service unions (devDependency)
  • Event metadata/config types such as FormSubmitMetadata, PhoneClickMetadata, and JsonValue