@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/hydrogenSetup (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
useFetcherfromreact-router. Remix-based Hydrogen (2024.x and earlier, whereuseFetcherlives in@remix-run/react) is not supported. - Checkout entry must be an
<a href>link.checkout_startedand the pre-checkout attribute guard listen for clicks ona[href*="/checkouts/"], a[href*="/cart/c/"]. Checkout triggered viabutton+window.locationis 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 isjs.ptengine.com/{id}.js; Japan-region profiles must passscriptUrl="https://js.ptengine.jp/{id}.js". The CSP helper already allows both.comand.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 likelocalhost), 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, addresolve: {preserveSymlinks: true}andssr: {noExternal: ['@ptengine/hydrogen']}to yourvite.config.js. Installs from the npm registry don't need this.
