Add Privatus Analytics to Shopify Hydrogen
Count real visitors on a Shopify Hydrogen storefront without cookies, track Add to Cart in the browser and record purchase revenue from Shopify checkout.
Many analytics tools set cookies. In many countries that means you must ask shoppers for permission first, and many say no. Those shoppers never show up in your numbers. Bots add fake visits on top.
Privatus Analytics counts visits without cookies and leaves bots out. This guide adds it to a Hydrogen storefront (including Weaverse themes such as Pilot), tracks Add to Cart and records purchase revenue from Shopify checkout. For an Online Store theme, see Shopify instead.
File names and route paths are examples, not a required project structure.
How it fits together#
Hydrogen storefront (browser)
│ <PrivatusAnalytics /> loads pa.js
│ page views on every route change
│ Add to Cart from Hydrogen analytics events
▼
privatusstats.com
▲
│ Purchase with revenue
Shopify checkout (custom pixel)
All events are sent from the shopper's browser. Nothing runs on the Hydrogen server, and no token or secret is needed.
Before you start#
- A Privatus Analytics site for your storefront domain. The site ID looks like
pa_AbC123. - A Hydrogen storefront.
- Access to the Shopify admin, for the checkout pixel.
Add the site ID to your local and deployed environment variables:
PUBLIC_PRIVATUS_SITE_ID=pa_YOURSITEID
The site ID is public. It appears in your page source, so it does not need to be kept secret.
Install the package#
npm install @privatus-analytics/react
<PrivatusAnalytics /> adds the tracking script once, from an effect, so it
only runs in the browser and is safe for server rendering. It renders nothing.
Load the script in the root layout#
Return the site ID from the root loader:
// app/root.tsx
export async function loader({ context }: LoaderFunctionArgs) {
// ...your existing loader data
return {
// ...
privatusSiteId: context.env.PUBLIC_PRIVATUS_SITE_ID,
};
}
Then render the component in the layout. Pass the Hydrogen nonce so the script passes your Content Security Policy:
// app/root.tsx
import { useNonce } from "@shopify/hydrogen";
import { PrivatusAnalytics } from "@privatus-analytics/react";
import { useRouteLoaderData } from "react-router";
export function Layout({ children }: { children?: React.ReactNode }) {
const nonce = useNonce();
const data = useRouteLoaderData<typeof loader>("root");
return (
<html lang="en">
<head>{/* ...Meta, Links */}</head>
<body>
{children}
{data?.privatusSiteId ? (
<PrivatusAnalytics
site={data.privatusSiteId}
nonce={nonce}
modules={["engage", "auto", "vitals", "clicks", "ask"]}
/>
) : null}
{/* ...ScrollRestoration, Scripts */}
</body>
</html>
);
}
Hydrogen changes routes with pushState. The script follows those changes by
itself, so every page view is counted without extra code. See
single-page apps.
The modules list turns on extra features: engage (time on page and scroll
depth), auto (outbound links, file downloads and form sends), vitals
(Core Web Vitals), clicks (click maps) and ask (short on-site surveys).
Leave out any you do not want. See script attributes.
Prefer a plain script tag? Put this in the head instead of the component, with
nonce={nonce} when your policy uses nonces:
<script defer src="https://privatusstats.com/js/pa.js" data-site="pa_YOURSITEID" data-modules="engage,auto,vitals,clicks,ask"></script>
Allow the script in your Content Security Policy#
Hydrogen builds a Content Security Policy that lists which hosts may load scripts and receive data. Add the Privatus Analytics host to both lists.
In Weaverse themes such as Pilot, these lists live in app/weaverse/csp.ts:
// app/weaverse/csp.ts
const updatedCsp = {
// ...defaultSrc, styleSrc
connectSrc: [
// ...existing hosts
"https://privatusstats.com",
],
scriptSrc: ["https://cdn.shopify.com", "https://privatusstats.com"],
};
In other Hydrogen projects, pass the same hosts to
createContentSecurityPolicy in app/entry.server.tsx:
// app/entry.server.tsx
const { nonce, header, NonceProvider } = createContentSecurityPolicy({
shop: {
checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
storeDomain: context.env.PUBLIC_STORE_DOMAIN,
},
scriptSrc: ["'self'", "https://cdn.shopify.com", "https://privatusstats.com"],
connectSrc: ["'self'", "https://privatusstats.com"],
});
Keep any hosts your theme already lists. If the browser console shows a
blocked request for privatusstats.com, the host is missing from one of these
lists.
Track Add to Cart#
Hydrogen publishes cart events through its analytics bus. Subscribe to
product_added_to_cart and send a Privatus Analytics event:
// app/components/privatus-cart-events.tsx
import { useEffect } from "react";
import { AnalyticsEvent, useAnalytics } from "@shopify/hydrogen";
import { usePrivatus } from "@privatus-analytics/react";
export function PrivatusCartEvents() {
const { subscribe } = useAnalytics();
const privatus = usePrivatus();
useEffect(() => {
subscribe(AnalyticsEvent.PRODUCT_ADD_TO_CART, ({ currentLine, prevLine }) => {
const merchandise = currentLine?.merchandise;
if (!merchandise) return;
const added = (currentLine?.quantity ?? 0) - (prevLine?.quantity ?? 0);
privatus.track("Add to Cart", {
product: merchandise.product?.title,
vendor: merchandise.product?.vendor,
quantity: added > 0 ? added : 1,
price: Number(merchandise.price?.amount),
currency: merchandise.price?.currencyCode,
});
});
}, [subscribe, privatus]);
return null;
}
Render <PrivatusCartEvents /> inside your <Analytics.Provider>, next to
<PrivatusAnalytics />. Calls made before the script loads wait in a queue, so
no event is lost.
The fields come from your cart query, so use the ones your theme fetches. Send only product details like these. Do not add names, emails or other customer data to event properties.
Note: This uses the useAnalytics hook from Hydrogen 2026.4 and earlier.
Hydrogen 2026.10 replaces it with analytics destinations. On that version,
register the same mapping with addDestination instead.
Record purchase revenue#
Shopify checkout and the thank-you page are hosted by Shopify, not by your Hydrogen app, so your storefront code never sees the finished order. A Shopify custom pixel does, and it runs in the shopper's browser.
- In the Shopify admin, open Settings, then Customer events, then Add custom pixel. Name it "Privatus Analytics".
- Paste the code from custom-pixel.js.
- At the top of the code, set
SITE_IDto yourpa_...site ID. - Set
STOREFRONT_PAGEVIEWStofalse. Your Hydrogen storefront already counts its own page views, so the pixel then counts only checkout pages and nothing is counted twice. - Click Save, then Connect.
The pixel sends a Purchase event with the order total and currency when
checkout finishes. It sends no customer data, and order links are cut down to
fixed paths so no order IDs leave the browser.
In Privatus Analytics, create a goal for the Purchase
event to see revenue by source, page and campaign.
Track your own events#
Use the hook in any component:
import { usePrivatus } from "@privatus-analytics/react";
export function NewsletterButton() {
const privatus = usePrivatus();
return (
<button onClick={() => privatus.track("Newsletter signup")}>
Join the newsletter
</button>
);
}
To record money for any event, add revenue and currency:
privatus.track("Gift card bought", { revenue: 25, currency: "USD" });
With Weaverse#
Load the script in the root layout, not inside a Weaverse section. Sections can be added, moved or removed in Studio, and the script must load on every page exactly once.
To track a click inside a section, call usePrivatus() in that section's
component. Keep event names in code: letting merchants type event names in
Studio leads to many spellings of the same event.
Test it#
- Run
npm run devand open the storefront. - Pass
debugto the component while testing, to log events in the console instead of sending them. - Move between pages and check that a page view is logged for each route.
- Add a product to the cart and check for one
Add to Cartevent with the right quantity. - Remove
debugand deploy. Open Live in the dashboard and check that your visit appears. - Place a test order and check for a
Purchaseevent with the order total. - Check the browser console for Content Security Policy errors.
Visits from localhost are not counted by default. Pass allowLocal to the
component to test against your dashboard from your own machine.