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

@mitchdesigns/tracking

v1.10.0

Published

Meta Pixel + CAPI + GTM tracking for Next.js, with a one-command project installer.

Readme

@mitchdesigns/tracking

Meta Pixel + Conversions API (CAPI) + Google Tag Manager tracking for Next.js.

Every event fires both the browser Pixel and the server-side CAPI with a shared eventID, so Meta deduplicates them and counts each action once.


Install

pnpm dlx @mitchdesigns/tracking init

The installer is idempotent. It adds the dep, appends env vars to .env.example, wires transpilePackages, creates the CAPI route, and drops in MetaEventTemplates.tsx. Two things stay manual: fill in .env, and mount <MetaPixel /> in the root layout.

Manual install:

pnpm add @mitchdesigns/tracking

Environment variables

NEXT_PUBLIC_META_PIXEL_ID=your_pixel_id
META_ACCESS_TOKEN=your_capi_access_token
NEXT_PUBLIC_META_PHONE_COUNTRY_CODE=20       # digits only; EG 20, UAE 971, KSA 966
NEXT_PUBLIC_META_DEFAULT_COUNTRY=EG          # ISO 3166-1 alpha-2 for the market
NEXT_PUBLIC_META_TEST_EVENT_CODE=TESTxxxxx   # remove before production
META_GRAPH_API_VERSION=v25.0                 # optional; defaults to v25.0
META_ALLOWED_EVENTS=Purchase,AddToCart       # optional; unset accepts any valid name
META_CAPI_RATE_LIMIT=60                      # optional; requests per IP per minute

META_ACCESS_TOKEN must never get the NEXT_PUBLIC_ prefix — it stays server-only.

NEXT_PUBLIC_META_PHONE_COUNTRY_CODE is what makes phone matching work at all. Storefront forms collect national numbers (01012345678); Meta hashes E.164 digits (201012345678). Without the country code there is nothing to replace the trunk prefix with, so numbers are sent as typed and never match. NEXT_PUBLIC_META_DEFAULT_COUNTRY is sent as the country matching parameter on every event, browser and server — the cheapest match quality available, since no visitor has to type anything for it.

next.config.mjs

const nextConfig = {
  transpilePackages: ["@mitchdesigns/tracking"],
};

CAPI route — src/app/api/meta/capi/route.ts

export const runtime = "edge";
export { POST } from "@mitchdesigns/tracking/server";

Root layout

Mount MetaPixelClient — not MetaPixel — and give it the tracking identity:

// src/components/meta/MetaPixelMount.tsx
"use client";

import { MetaPixelClient, useTrackingIdentity } from "@mitchdesigns/tracking";
import { useUser } from "@/features/auth/hooks/useUser"; // whatever your auth returns

export function MetaPixelMount() {
  return <MetaPixelClient user={useTrackingIdentity(useUser().user)} />;
}
// src/app/[locale]/layout.tsx
import { Suspense } from "react";
import { MetaPixelMount } from "@/components/meta/MetaPixelMount";

{process.env.NODE_ENV === "production" && process.env.NEXT_PUBLIC_META_PIXEL_ID && (
  <Suspense>
    <MetaPixelMount />
  </Suspense>
)}

<Suspense> is required — MetaPixel uses useSearchParams().


Advanced matching and Event Match Quality

Meta scores match quality per event instance, not per customer: an event with no user data counts against the score even when the same person identified themselves one page earlier. Attaching an identity only to signed-in customers therefore scores against the ~95% of storefront traffic that browses as a guest — the pixel this was built for reported Email on 3.92% of instances, Phone on 2.29%, First/Last name on 3.82% and External ID on 12.32%.

The fix is two halves, and both are needed:

1. Remember what the visitor already told you. Every form that legitimately collects identity — checkout step 1, the contact form, registration — calls rememberTrackingIdentity. It merges, so a later form never wipes an earlier field, and it lowercases the email to match what both the pixel and the Conversions API hash.

import { rememberTrackingIdentity } from "@mitchdesigns/tracking";

// in the submit handler, alongside the API call
rememberTrackingIdentity({ email, phone, firstName, lastName });

Clear it on logout — a shared device must not attribute the next visitor's browsing to the last one:

import { clearTrackingIdentity } from "@mitchdesigns/tracking";

clearTrackingIdentity(); // in useLogout, next to the token teardown

2. Feed that identity to every event. useTrackingIdentity(profile) merges the stored identity with the signed-in profile (the profile wins, and is written back), and returns the object useMetaTracking and MetaPixelClient both take:

const { user } = useUser();
const trackingUser = useTrackingIdentity(user);
const { trackAddToCart } = useMetaTracking(trackingUser);

Pass any profile shape you like — id, email, phone / mobileNumber, firstName, lastName, gender, city, state, zip, country, and either dateOfBirth or dobYear / dobMonth / dobDay. Missing fields are simply omitted; gender is normalised to Meta's m / f, dates to YYYYMMDD, and anything that will not fit Meta's shape is dropped rather than guessed — a wrong hash is a non-match, not a partial one.

The hook is for the pixel, not for the events. getEnhancedUserData reads the same store synchronously at the moment an event is sent, so an event fired in a component's first effect — ViewContent on a product page is exactly that — still carries the identity. useTrackingIdentity hydrates from localStorage in an effect, one commit later; a component whose guard ref blocks the re-fire would otherwise have sent that event permanently anonymous. That single race is what held Email coverage on ViewContent to 1.43% of instances.

Two rules worth keeping

  • Do not pass user to <MetaPixel />. A user that resolves after mount changes the identity of trackPageView, and MetaPixel re-fires PageView when that changes — double-counting the same view. MetaPixelClient uses fbq('init', …) instead, which Meta merges into the pixel and applies to every subsequent browser event without re-firing anything.

    This rule used to also say the prop kept email off the Conversions API PageView, to avoid Meta's "duplicate client Emails" diagnostic. It no longer does either job: the identity store is read when an event is sent, so every server event now carries em/ph for a visitor who has identified themselves, PageView included. That is deliberate — Meta's own guidance is to send email on all events, and an anonymous PageView is a scored event with no match — but expect that diagnostic to reappear on a storefront where a handful of customers browse many pages each. It reports repetition, not error.

  • Keep the identity object referentially stable. useMetaTracking keys its callbacks off it and MetaPixel re-fires PageView when they change. useTrackingIdentity memoises on primitives; if you build the object yourself, do the same.

City, State and Zip are stored and sent when a project supplies them. Most checkouts hold city/area ids rather than names, so raising those coverage numbers means resolving the names at the call site first; country needs nothing beyond the env var.

Normalisation

Every matching parameter is normalised the way Meta normalises it before hashing, on write to the store and again on the server leg (idempotent, so a caller posting raw values still lands on the right hash):

| Parameter | Shape sent | |---|---| | em | lowercased, trimmed | | ph | digits only, country code included, no trunk prefix | | fn / ln | lowercased, punctuation and whitespace removed | | ct / st | lowercased, letters only | | zp | lowercased, alphanumeric only | | country | ISO 3166-1 alpha-2, lowercased | | ge | m / f | | db | YYYYMMDD |

The browser and server legs are handed the same string for the same person, which is the only way the two hashes agree.


Usage — built-in events

Call the hook wherever a user action happens:

"use client";
import { useMetaTracking } from "@mitchdesigns/tracking";

export function AddToCartButton({ product, user }) {
  const { trackAddToCart } = useMetaTracking(user);

  return (
    <button
      onClick={() =>
        trackAddToCart({
          content_ids: [product.sku],
          contents: [{ id: product.sku, quantity: 1, item_price: product.price }],
          content_name: product.name,
          value: product.price,
          currency: "EGP",
        })
      }
    >
      Add to cart
    </button>
  );
}

Available trackers on useMetaTracking(user?)

| Method | Meta event | Typical trigger | |---|---|---| | trackPageView() | PageView | Automatic via <MetaPixel /> | | trackViewContent(data) | ViewContent | Product detail page | | trackViewCategory(data) | ViewCategory (custom) | Category / shop page | | trackAddToCart(data) | AddToCart | Add-to-cart handler | | trackInitiateCheckout(data) | InitiateCheckout | Checkout page mount | | trackAddPaymentInfo(data) | AddPaymentInfo | Payment step | | trackPurchase(data) | Purchase | Thank-you page | | trackContact() | Contact | Contact form submit | | trackCompleteRegistration() | CompleteRegistration | After registration |

The MetaEventTemplates.tsx file (created by the installer) wraps the page-mount events (ProductViewTracking, InitiateCheckoutTracking, PurchaseTracking, CategoryViewTracking, AddPaymentInfoTracking) as drop-in components. Pass cartItems / products / user as props.

Passing user to the hook enriches CAPI match quality (email, phone, name, address are hashed automatically before leaving the server).

Purchase value excludes shipping

value on a purchase is the order total without shipping — the goods the customer paid for, after discounts. Delivery fees are not revenue, so they go out in their own field (shipping on the GA4 push) and never inside value. Use the exported purchaseValue() helper when the only total you hold is the shipping-inclusive one:

import { purchaseValue } from "@mitchdesigns/tracking";

trackPurchase({
  orderId: orderNumber,
  value: purchaseValue(order.total_amount, order.shipping_fees), // ex-shipping
  content_ids,
  contents,
  numItems,
});

The GA4 tracker takes the extras the spec asks for alongside it:

gtmTrackPurchase({
  orderId: orderNumber,
  value: purchaseValue(order.total_amount, order.shipping_fees),
  shipping: order.shipping_fees,
  tax: order.tax,
  coupon: order.coupon?.code,
  contents,
  user,
});

Adding a custom event

The built-in trackers cover the standard e-commerce funnel. For anything else — Lead, Subscribe, StartTrial, NewsletterSignup, or a fully custom name — compose the two primitives the package exports:

  1. Fire the browser pixel with window.fbq(...).
  2. Fire the server via sendCAPIEvent(...).
  3. Share a single eventID between them so Meta deduplicates.

Option A — inline, one-off

"use client";
import { useMetaTracking } from "@mitchdesigns/tracking";
import { generateEventId, sendCAPIEvent, getEnhancedUserData } from "@mitchdesigns/tracking";

export function NewsletterForm({ user }) {
  const enhancedUserData = getEnhancedUserData(user);

  async function onSubmit(email: string) {
    const eventId = generateEventId("lead");

    // 1. Browser Pixel
    if (typeof window !== "undefined" && window.fbq) {
      window.fbq("track", "Lead", { content_name: "Newsletter" }, { eventID: eventId });
    }

    // 2. Server CAPI (shares eventId → deduped)
    sendCAPIEvent({
      eventName: "Lead",
      eventId,
      customData: { content_name: "Newsletter" },
      userData: { ...enhancedUserData, em: email },
    });
  }

  // ...
}
  • Use "track" for Meta's standard events (Lead, Subscribe, Search, StartTrial, SubmitApplication, ...).
  • Use "trackCustom" for a name Meta doesn't know ("NewsletterSignup", "VideoWatched75", ...).
  • The generateEventId(prefix) prefix is free-form — it's just for readability in Events Manager.

Option B — reusable wrapper (recommended)

Add the tracker next to the built-in ones so callers don't have to remember the pixel/CAPI dance. Create src/hooks/useCustomTracking.ts in your app:

"use client";
import { useCallback } from "react";
import {
  useMetaTracking,
  generateEventId,
  sendCAPIEvent,
  getEnhancedUserData,
} from "@mitchdesigns/tracking";

export function useCustomTracking(user?: any) {
  const built = useMetaTracking(user);
  const enhancedUserData = getEnhancedUserData(user);

  const trackLead = useCallback(
    (data: { content_name: string; value?: number; currency?: string }) => {
      const eventId = generateEventId("lead");
      if (typeof window !== "undefined" && window.fbq) {
        window.fbq("track", "Lead", data, { eventID: eventId });
      }
      sendCAPIEvent({
        eventName: "Lead",
        eventId,
        customData: data,
        userData: enhancedUserData,
      });
    },
    [enhancedUserData],
  );

  return { ...built, trackLead };
}

Then swap useMetaTracking for useCustomTracking at call sites — all the built-in trackers plus your new trackLead are available on the same object.

Server-only custom events

For events triggered outside the browser (webhooks, cron jobs, backend order completion), call sendCAPIEvent from a server route or action. The pixel side is skipped — Meta will still count the event, just without a browser match.

// e.g. inside a webhook handler
import { sendCAPIEvent } from "@mitchdesigns/tracking";

await sendCAPIEvent({
  eventName: "Subscribe",
  eventId: `sub_${subscriptionId}`,
  customData: { value: 29, currency: "USD", predicted_ltv: 348 },
  userData: { em: hashedEmail },   // must be pre-hashed if you're calling directly
});

Checklist for a new event

  • [ ] Standard-event name if Meta has one; otherwise trackCustom.
  • [ ] Same eventID in the fbq call and the sendCAPIEvent call.
  • [ ] userData passed (via getEnhancedUserData(user)) for match quality.
  • [ ] Verified live in Events Manager → Test Events with NEXT_PUBLIC_META_TEST_EVENT_CODE set.

Testing locally

CAPI works on localhost; the pixel is production-only (Meta doesn't accept localhost as a domain).

  1. Set NEXT_PUBLIC_META_TEST_EVENT_CODE=TESTxxxxx in .env.
  2. Open Events Manager → Test Events.
  3. Trigger the action — events land in real time with match-quality scores.
  4. Remove the test code before deploying.

GTM

GTM / GA4 helpers live in a separate entry point, @mitchdesigns/tracking/gtm:

import {
  GTMScript,
  useGTMTracking,
  pushToDataLayer,
  pushEcommerceEvent,
  contentsToGA4Items,
  buildGTMUserData,
} from "@mitchdesigns/tracking/gtm";

Why a subpath? As of 1.4.0 these helpers are no longer re-exported from the main barrel (@mitchdesigns/tracking). Meta-Pixel-only apps therefore never pull the GTM dataLayer code into their type program. This keeps the package compatible with @next/third-parties (GoogleTagManager), which also declares Window.dataLayer — previously importing both in the same app produced a TS2717 "Subsequent property declarations must have the same type" build error. The GTM helpers no longer augment the global Window at all, so they can coexist with @next/third-parties.

See SETUP.md for the full guide.


License

UNLICENSED — internal use for MitchDesigns projects.