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

@dxv-systems/metis-client

v0.8.0

Published

DXV Metis conversion tracking SDK — click-ID capture and signed conversion event emission

Readme

@dxv-systems/metis-client

DXV Metis conversion tracking SDK. Captures click IDs (gclid, fbclid, gbraid, wbraid, utm_*) on landing and emits signed server-side conversion events to the Metis Portal's relay endpoint.

Published as @dxv-systems/conversions up to 0.3.0. That name is deprecated; the API is identical, only the package name changed.

Install

npm install @dxv-systems/metis-client

Onboarding

The DXV Metis operator creates a customer in the Portal and provisions you with three values:

  • customerId — integer, identifies your customer record in the Portal
  • relaySecret — HMAC signing key (treat as a credential)
  • relayUrl — Portal endpoint base, typically https://metis.dxvagentic.com (existing integrations may still use https://marketing.dxvagentic.com during the rebrand migration; both work)

Store these as environment variables in your webshop's deployment.

Client side: capture click IDs on landing

In your landing-page handler (Next.js middleware, Express handler, anywhere):

import { CLICK_IDS_COOKIE, captureClickIds, parseClickIdsCookie, serializeClickIdsCookie } from "@dxv-systems/metis-client/client";

// On every request to a public page:
const url = new URL(request.url);
const captured = captureClickIds(url.searchParams);

if (captured) {
  // First-touch wins: don't overwrite an existing valid cookie.
  const existing = parseClickIdsCookie(request.cookies.get(CLICK_IDS_COOKIE)?.value);
  if (!existing) {
    response.cookies.set(CLICK_IDS_COOKIE, serializeClickIdsCookie(captured), {
      maxAge: 30 * 24 * 60 * 60, // 30 days
      httpOnly: false,           // intentionally readable from client (analytics)
      sameSite: "lax",
      secure: true,
    });
  }
}

CLICK_IDS_COOKIE ("dxv_click_ids") is exported from both /client and /server, so apps no longer repeat the cookie name. Cookies written before 0.7.0 (without gbraid/wbraid) still parse.

Cookie encoding

parseClickIdsCookie takes the value in whatever form you happen to hold it: the raw percent-encoded string from a Cookie header, or the already-decoded string a framework cookie API returns. You do not have to know which you have.

That tolerance is load-bearing rather than defensive. next/headers cookies() and NextRequest.cookies.get() decode the header themselves, while NextResponse.cookies.set() encodes what you give it — so a cookie written through middleware, on top of serializeClickIdsCookie's own encoding, is stored double-encoded. Before this was handled, a value containing a literal % (a utm_campaign of 50%-korting, say) failed to parse and the function returned null with no error, losing attribution silently.

Server side: emit purchase event

In your order-completed handler (Stripe webhook, etc.):

import { emitPurchase } from "@dxv-systems/metis-client/server";
import { parseClickIdsCookie } from "@dxv-systems/metis-client/client";

await emitPurchase({
  customerId: Number(process.env.DXV_CUSTOMER_ID),
  relaySecret: process.env.DXV_CONVERSIONS_RELAY_SECRET!,
  relayUrl: process.env.DXV_CONVERSIONS_RELAY_URL!,
  eventId: order.id,   // optional; see "Deduping with a browser pixel" below
  order: {
    id: order.id,
    totalCents: order.totalCents,
    currency: "EUR",
  },
  clickIds: parseClickIdsCookie(cookies.get(CLICK_IDS_COOKIE)),
  customer: {
    email: order.customer.email,    // plaintext; package hashes per Meta CAPI spec
    phone: order.customer.phone,
  },
  consent: {
    adUserData: consentState.adUserData,
    adPersonalization: consentState.adPersonalization,
  },
});

emitPurchase retries 3 times on 5xx with exponential backoff. It throws typed errors:

  • InvalidConfig — missing/empty relaySecret, relayUrl, or customerId. Thrown synchronously before any network call.
  • RelayBadRequest — Portal returned 4xx (likely a stale relaySecret or wrong customerId). Not retried.
  • RelayUnavailable — Portal unreachable after 3 attempts.

Server side: emit lead event

For form-fill / signup conversions with no order attached (a quote request, a contact form, a demo booking):

import { emitLead } from "@dxv-systems/metis-client/server";
import { parseClickIdsCookie } from "@dxv-systems/metis-client/client";

await emitLead({
  customerId: Number(process.env.DXV_CUSTOMER_ID),
  relaySecret: process.env.DXV_CONVERSIONS_RELAY_SECRET!,
  relayUrl: process.env.DXV_CONVERSIONS_RELAY_URL!,
  eventId: submission.id,   // optional; see "Deduping with a browser pixel" below
  lead: {
    id: submission.id,
    valueCents: 0,   // optional; omit if the lead has no estimated value
    currency: "EUR", // optional; only meaningful alongside valueCents
  },
  clickIds: parseClickIdsCookie(cookies.get(CLICK_IDS_COOKIE)),
  customer: {
    email: submission.email,
    phone: submission.phone,
  },
});

emitLead shares config validation, PII hashing, signing and retry behavior with emitPurchase — same typed errors, same 5xx retry / 4xx-fatal rules. Only the input shape differs: lead (no order), and lead.id is the only required field.

The Portal stores lead events with event_type = 'lead'. As of this release nothing forwards leads to Meta CAPI / Google offline conversions yet — that's a later stage of the conversions rollout; purchases already forward.

Consent

Both emitPurchase and emitLead accept an optional consent field:

consent: {
  adUserData: boolean;        // Google Consent Mode v2 "ad_user_data"
  adPersonalization: boolean; // Google Consent Mode v2 "ad_personalization"
  decided?: boolean;          // has the visitor answered the banner? absent = yes
}

This is not optional in practice. The Portal stores every event regardless, but its Meta CAPI / Google offline-conversions forwarders fail closed: an event that arrives without a consent object is never forwarded, only stored. If you want this conversion to actually reach Meta or Google, you must pass consent — read it from whatever consent-management platform your storefront already runs (a CMP, a cookie banner, your own preference store) and mirror its ad-consent state here. adUserData and adPersonalization are required when the object is present; there is no partial form.

Visitors who have not answered the banner

decided (added in 0.7.0) lets Metis tell a visitor who refused from one who never answered. Absent means decided, so existing senders keep their meaning. When the visitor has not answered the banner yet, send:

consent: { adUserData: false, adPersonalization: false, decided: false }

This still fails closed like any other non-consent: nothing is forwarded to Meta or Google. Omit consent altogether only if your integration has no consent information at all; Metis reports that as a sender gap, which is different from "undecided". decided must be a boolean when present.

Paid-touch flag

detectPaidTouch(input) (from /client or /server, added in 0.8.0) returns true when a landing URL says the visit came from an ad: any of gclid, fbclid, gbraid or wbraid is present and non-empty, or utm_medium (trimmed, case-insensitive) is one of cpc, ppc, cpm, cpv, paid, paidsocial, paid_social, paid-social, paidsearch, paid_search, paid-search, display. It accepts a URLSearchParams, a query string, a full URL string or a URL.

import { detectPaidTouch } from "@dxv-systems/metis-client/client";

const paidTouch = detectPaidTouch(window.location.search); // keep in a variable
await emitLead({ /* ... */, paidTouch });

Both emitPurchase and emitLead accept an optional paidTouch: boolean, sent as a top-level field. Metis's consent monitor counts it to split refused and undecided visitors into paid and organic; it is never forwarded to Meta or Google. Leave it out when you do not know (absent means unknown). It must be a boolean when present.

Hold the result in memory only. Never write it to a cookie, localStorage or any other storage: that would be device storage without consent. A full reload or a return visit should give "unknown" (omit the flag), not a guess.

Deduping with a browser pixel

If your site also reports this conversion from the browser (a Meta Pixel Purchase/Lead event, a gtag conversion event), pass the same eventId to both the pixel call and emitPurchase/emitLead. Meta and Google dedupe a browser event against a server event by matching event name plus a shared event ID — without it, a conversion reported from both sides gets counted twice once Metis starts forwarding server events onward.

eventId is optional — omit it if you have no browser-side counterpart for this conversion. When supplied it must be a non-empty string (whitespace-only is rejected) of at most 256 characters; use whatever ID you already share between your pixel and your server call (a nanoid, an order reference, a UUID).

Deriving a reproducible event id

If your browser pixel and your server-side emit don't share process state — the common case, since the pixel fires on the confirmation page and the server emit fires later from a webhook — you need a way to derive the same id on both sides from something they both already know, like the order number:

import { purchaseEventUuid } from "@dxv-systems/metis-client/client"; // or /server

const eventId = purchaseEventUuid(order.number);
// Pass this to both the pixel call (fbq('track', 'Purchase', {}, { eventID: eventId })
// or posthog.capture(..., { uuid: eventId })) and to emitPurchase's `eventId` field.

purchaseEventUuid and the more general deterministicEventUuid are pure functions available from both /client and /server — the browser needs one to compute the id for its pixel call, and the server needs the identical function to recompute the same id later for the relay emit, without the two ever coordinating. Determinism is the whole point: a random per-event id can't be reproduced after the fact, and Meta/PostHog dedupe a server event against its browser counterpart by matching that id — get it wrong and every forwarded conversion double-counts, silently.

The output is also shaped as a valid RFC-4122 uuid rather than a readable string like purchase:ORD-0001, because several platforms in this chain (PostHog included) accept only a valid uuid as an event id and silently swap in a random one otherwise — again with no error, just a dedup that quietly stops working.

For lead conversions use leadEventUuid(lead.id) the same way (equal to deterministicEventUuid("lead:" + lead.id)), so a browser Lead pixel and emitLead share one event id.

Both sides must use it, or dedup does not work. If your storefront's pixel derives its id one way and the server emit derives it another (or one side uses a random id), Meta/PostHog cannot tell the two events are the same conversion, and it gets counted twice once Metis forwards the server-side event onward.

Conventions

  • Money in cents (integer). The Portal validates totalCents > 0.
  • Currency as ISO 4217 (e.g. EUR, USD).
  • PII (email, phone) is hashed in-memory before the network call and never persisted by the Portal.
  • Click-ID cookie is first-touch wins for 30 days — do not overwrite if a valid cookie already exists.

License

MIT