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

@ptengine/hydrogen

v0.2.1

Published

Ptengine analytics integration for Shopify Hydrogen storefronts

Readme

@ptengine/hydrogen

Part of the Ptengine web packages monorepo. Which package do I need? @shopify/hydrogen (Hydrogen framework) → this package; @shopify/hydrogen-react + your own framework (Next.js etc.) → @ptengine/hydrogen-react. Each is self-contained — install exactly one.

Ptengine analytics for Shopify Hydrogen (headless) storefronts: loads the Ptengine SDK, forwards Hydrogen analytics events to Ptengine with the same event names/properties as the Ptengine Web Pixel, and writes __pt_* identity fields into cart attributes so server-side checkout_completed events are attributed to the visitor session.

Install

npm install @ptengine/hydrogen

Setup (4 steps)

1. Allow Ptengine hosts in your CSP (app/entry.server.jsx) — do this first

Hydrogen's default Content-Security-Policy blocks the Ptengine SDK script and its tracking requests. Without this step nothing loads — and it fails silently (no requests, no errors visible to users).

import {ptengineCspDirectives} from '@ptengine/hydrogen';

const {nonce, header, NonceProvider} = createContentSecurityPolicy({
  shop: {
    checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
    storeDomain: context.env.PUBLIC_STORE_DOMAIN,
  },
  // appends https://*.ptengine.com + https://*.ptengine.jp to
  // scriptSrc / connectSrc / styleSrc / frameSrc, keeping your own values
  ...ptengineCspDirectives({
    scriptSrc: ["'self'", 'https://cdn.shopify.com'],
    connectSrc: ["'self'"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    frameSrc: ["'self'"],
  }),
});

2. Mount <PtAnalytics> inside Analytics.Provider (app/root.jsx)

import {PtAnalytics} from '@ptengine/hydrogen';

<Analytics.Provider cart={data.cart} shop={data.shop} consent={data.consent}>
  <PtAnalytics ptengineId="YOUR_PTENGINE_ID" />
  {/* ...your layout */}
</Analytics.Provider>

Remove any hardcoded Ptengine <script> tag — PtAnalytics loads the SDK.

3. Handle the attribution write in your cart action (app/routes/cart.jsx)

This is the step most often missed — and missing it fails silently (behavior events keep working, but order attribution is lost entirely). Use the exported helper before your own switch:

import {handlePtCartAction} from '@ptengine/hydrogen';

export async function action({request, context}) {
  const {cart} = context;
  const formData = await request.formData();
  const {action, inputs} = CartForm.getFormInput(formData);

  const ptResult = await handlePtCartAction({action, inputs, cart});
  if (ptResult) {
    return json({cart: ptResult.cart ?? ptResult});
  }
  // ...your existing switch (action) {...}
}

Or hand-write the equivalent case:

case CartForm.ACTIONS.AttributesUpdateInput:
  result = await cart.updateAttributes(inputs.attributes);
  break;

3b. Include sku and productType in your cart line fragments

Cart-derived events (product_added_to_cart / product_removed_from_cart / cart_viewed) read merchandise.sku and merchandise.product.productType. Add both fields to the CartLine fragments in app/lib/fragments.js — the Hydrogen skeleton omits them.

4. Make sure standard Analytics views are present

<Analytics.ProductView> / <Analytics.CollectionView> / <Analytics.SearchView> / <Analytics.CartView> on the respective routes (the Hydrogen skeleton includes most of them; CartView on the cart route is the one most often missing).

For full property parity with theme stores, include sku, type (product type) and imageUrl in your ProductView data:

<Analytics.ProductView
  data={{
    products: [{
      id: product.id,
      title: product.title,
      price: selectedVariant?.price.amount || '0',
      vendor: product.vendor,
      variantId: selectedVariant?.id || '',
      variantTitle: selectedVariant?.title || '',
      quantity: 1,
      sku: selectedVariant?.sku || '',
      type: product.productType || '',
      imageUrl: selectedVariant?.image?.url || '',
    }],
  }}
/>

Events forwarded

product_viewed, collection_viewed, search_submitted, cart_viewed, product_added_to_cart, product_removed_from_cart, checkout_started.

checkout_completed is generated server-side from Shopify order webhooks by the Ptengine app — attribution works because of the __pt_* cart attributes this package maintains. Attributes are written whenever the cart exists and its __pt_* values differ from the current identity (value comparison, so a rotated sessionId triggers a rewrite), with bounded retries until the Ptengine SDK identity is ready. As a final guard, clicking a checkout link re-checks the values and, if stale, blocks navigation, rewrites via a keepalive request (1.5s cap), then continues to checkout — this guard is always on, independent of emitCheckoutStarted.

Props

| prop | default | description | | --- | --- | --- | | ptengineId | required | Ptengine profile ID (the id in your tag URL js.ptengine.com/{id}.js) | | loadSdk | true | inject https://js.ptengine.com/{id}.js | | scriptUrl | derived | override SDK URL (e.g. js.ptengine.jp) | | cartRoute | /cart | route handling CartForm actions | | emitCheckoutStarted | true | fire checkout_started when a checkout link is clicked; disable if the Ptengine Web Pixel version that emits it on the checkout page is rolled out (avoids double counting) | | waitForConsent | false | defer SDK loading until Hydrogen customer privacy (canTrack()) allows analytics; reacts to the visitorConsentCollected event with a 1s polling fallback | | debug | false | log events + expose window.__ptHydrogenEvents / window.__ptHydrogenCart |

Scope & boundaries

  • React Router 7 Hydrogen only (2025.1+). This package imports useFetcher from react-router. Remix-based Hydrogen (2024.x and earlier, where useFetcher lives in @remix-run/react) is not supported.
  • Checkout entry must be an <a href> link. checkout_started and the pre-checkout attribute guard listen for clicks on a[href*="/checkouts/"], a[href*="/cart/c/"]. Checkout triggered via button + window.location is not intercepted (attribution still works via the self-heal path; only the click-time guarantees are skipped).
  • Locale-prefixed cart routes need cartRoute. If your cart action lives at e.g. ($locale).cart.jsx, pass the actual path (<PtAnalytics cartRoute="/en-jp/cart" ... />) or the attribution write never reaches your action.
  • Non-.com regions need scriptUrl. Default SDK URL is js.ptengine.com/{id}.js; Japan-region profiles must pass scriptUrl="https://js.ptengine.jp/{id}.js". The CSP helper already allows both .com and .jp.
  • If your storefront already loads a Ptengine tag by other means, pass loadSdk={false} (or remove the old tag) to avoid loading the SDK twice.

Requirements

  • Install the Ptengine Shopify App (registers order webhooks + web pixel).
  • Enable SPA virtual pageviews on your Ptengine profile.
  • @shopify/hydrogen >= 2025.1, react >= 18, react-router >= 7.
  • Local dev: use http://127.0.0.1:<port> (the Ptengine tag does not start on single-label hostnames like localhost), and note the SDK only fully initializes on domains configured in your Ptengine profile — events are queued for up to 20s and dropped if the SDK never becomes ready.
  • If you install via a local file: path during development, add resolve: {preserveSymlinks: true} and ssr: {noExternal: ['@ptengine/hydrogen']} to your vite.config.js. Installs from the npm registry don't need this.