Blotout Qinto Analytics
First-party analytics with Blotout Qinto
@blotout/platform-medusa
By Blotout. Connects your Medusa store to Qinto, Blotout's first-party analytics and identity platform, and sends your storefront and order events there — without an ad blocker in the way.
The package is Copy to clipboard@blotout/platform-medusa; the service it talks to is Qinto, which is what you will see named throughout the admin and in your Qinto dashboard.
Install it, paste your Qinto URL in the Medusa admin, mount one component in your storefront.
One line in Copy to clipboardmedusa-config.ts, and everything a merchant chooses — destination, on/off — is set in the admin and stored in your database, so changing it needs no redeploy.
What you get
Event When Copy to clipboardPageView every navigation Copy to clipboardViewContent a product page Copy to clipboardAddToCart / Copy to clipboardRemoveFromCart cart changes Copy to clipboardInitiateCheckout every arrival at checkout, including each step change Copy to clipboardAddShippingInfo / Copy to clipboardAddPaymentInfo checkout steps Copy to clipboardPurchase order confirmation page Copy to clipboardCompleteRegistration account created
Identity (email, name, city, country, postcode, address) attaches to the shopper's profile at checkout and on the order.
Every event is sent from the browser, including Copy to clipboardPurchase, which is keyed on the order id so a refreshed confirmation page still counts once. Nothing is sent from your server, and your server holds no Qinto credential — there is no column for one.
1. Install
1npm install @blotout/platform-medusa
Add it to Copy to clipboardmedusa-config.ts:
1234module.exports = defineConfig({// …plugins: ["@blotout/platform-medusa"],})
That is the whole configuration. Everything else — the destination, on/off — is set in Admin → Blotout and stored in your database.
There is no OAuth, no client secret, and no credential of any kind. The one thing this plugin needs is your site's Qinto URL, which is a public delivery host — it is already visible in the page source of any site running Qinto. You copy it from the Qinto portal and paste it in the admin. The plugin's schema has two meaningful columns, Copy to clipboardqinto_urland Copy to clipboardenabled, and no column for a token because nothing here ever holds one.
Run the migration and start:
12npx medusa db:migratenpm run dev
In production, rebuild so the admin page is bundled — Copy to clipboardnpm run dev picks it up automatically, a built admin does not:
1npx medusa build && npx medusa start
No environment variables are required. Nothing calls back into the store, so a backend the internet cannot reach (a local dev store, a private network) configures like any other.
2. Connect, in the admin
The plugin appears as Blotout in the sidebar. Open it:
- Paste your Qinto URL and press Save. Find it in the Qinto portal under Developer tools, on your site's page.
- Leave Tracking on. The switch stops sending without losing the destination, so you can pause and resume without re-entering anything.
- The badge reads Sending events when it is ready.
Everything is stored in your database, so changing destination or turning tracking off needs no redeploy. A storefront picks the change up on its next page load; tabs already open keep the previous setting until then, and the config response is cached for 60 seconds.
3. Storefront
The backend half is done. Your storefront still has to say when a shopper viewed a product or added to cart, because only it knows — but it works that out for itself.
Install the same package in your storefront and import from it — nothing is copied, so plugin updates reach the capture logic through Copy to clipboardnpm update like any other dependency. From your storefront's root:
1npm install @blotout/platform-medusa
Let Next compile the package's TypeScript, in Copy to clipboardnext.config.js:
1234const nextConfig = {transpilePackages: ["@blotout/platform-medusa"],// ...the rest of your config}
Mount one component in Copy to clipboardsrc/app/layout.tsx:
123456import QintoAutoCapture from "@blotout/platform-medusa/storefront/autocapture"<body><QintoAutoCapture /><main className="relative">{props.children}</main></body>
And add one three-line file, Copy to clipboardsrc/app/api/qinto/context/route.ts:
1export { GET } from "@blotout/platform-medusa/storefront/context-route"
That one file has to exist for real. Next finds route handlers by scanning your filesystem, so a route sitting in Copy to clipboardnode_modules is invisible to the router — but what it does still lives in the package and updates with it.
That is two edits to files you already had, plus one three-line file, and the last of it you will ever write for this plugin. New events ship in a package update; nothing in your product, cart or checkout code is touched, so restyling them cannot break tracking.
It reads the Qinto URL from Copy to clipboardGET /store/qinto/config, so there is no environment variable to set — if you would rather pin it, set Copy to clipboardNEXT_PUBLIC_QINTO_URL and it wins.
How it captures without touching your pages
Three signals, none of which needs a call site:
- Route changes give Copy to clipboard
PageViewdirectly, and say when a shopper is on a product page, the checkout, or an order confirmation. - Copy to clipboard
/api/qinto/context— the route handler you copied — runs on your storefront's own server, where the Copy to clipboardhttpOnlyCopy to clipboard_medusa_cart_idcookie is readable, and hands the browser the cart, product or order. The browser cannot read that cookie, which is why this file exists. - A Copy to clipboard
PerformanceObservernotices when a Server Action completes. Every cart mutation in a Medusa storefront is one, so the component re-reads the cart and compares it to the previous state. The difference is the event: a line going 1 → 3 is Copy to clipboardAddToCartof 2. - A Copy to clipboard
submitlistener on Copy to clipboarddocumentpicks up identity from any form a shopper sends — register, login, and each of the profile forms — without those components being touched. A signed-in shopper is also identified from the session on page load, which covers a returning visit where no form is ever submitted. Only verified identities are aliased (linked to a person profile): the signed-in customer's id and email, and the email on a placed order. A form submission sends traits only, because the submit may still fail and an alias is permanent.
The visitor cookie
The context route also issues Copy to clipboardtruid — a first-party visitor id, the same mechanism the WooCommerce, Magento and Salesforce integrations use, under the same name.
Value UUID v4, minted once and never replaced Lifetime 400 days — the real browser maximum, not the century a cookie can ask for Flags Copy to clipboardHttpOnly, Copy to clipboardSameSite=Lax, Copy to clipboardSecure on https Scope widened to the domain shared with your Qinto host when there is one, host-only otherwise
It exists because Qinto's own anonymous id is set by the collector, which makes it third-party until your DNS setup puts the collector on your domain. Copy to clipboardtruid is set by your storefront's own server, so it is first-party from the first request and stable across page loads either way.
Copy to clipboardSameSite=Lax rather than Copy to clipboardStrict on purpose: the starter's cart and auth cookies are Copy to clipboardLax so they survive the cross-site return from a redirect-based payment method, and a visitor id that did not would be lost at exactly the moment a conversion is attributed.
The scope is only widened when your storefront actually sits under that domain. A browser discards a cookie scoped to a domain the response did not come from, so a store at Copy to clipboardshop.comreporting to Copy to clipboardxyz.dev.qinto.techgets a host-only cookie rather than none at all.
An explicit allowlist decides what is read — the nine identity traits, each with the field names that feed it, including the Copy to clipboardshipping_address.*names the checkout uses. The form is never enumerated and no other input is inspected, so a password or a card number in the same submission cannot be picked up by accident. Widening what is collected means editing Copy to clipboardlib/qinto/form-traits.ts, which is deliberate.
Deriving beats declaring in two ways beyond the edit count: Copy to clipboardvalue and Copy to clipboardcurrency come from the authoritative cart rather than a recomputed variant price, and an event only fires for a mutation that actually succeeded.
Patching Copy to clipboardwindow.fetchlooks like the obvious way to catch those Server Actions. It does not work — Next captures its own Copy to clipboardfetchreference while the framework boots, long before a component effect can replace the global, so the patch silently never fires. Copy to clipboardPerformanceObserverhas no such ordering problem.
Route matching defaults to the Medusa Next.js starter's shapes, so Copy to clipboard/[countryCode]/products/[handle], Copy to clipboard/[countryCode]/checkout and Copy to clipboard/[countryCode]/order/[id]/confirmed. A storefront that moved them passes Copy to clipboardroutes to the component, as a pattern per event or a function where a pattern will not reach. The rules live in Copy to clipboardlib/qinto/routes.ts, pure and unit-tested, and MERCHANT-SETUP.md has the reference. Only Copy to clipboardViewContent, Copy to clipboardInitiateCheckout and Copy to clipboardPurchase read the path at all, the other six come from cart and customer state and never need configuring.
Identity (email, name, city, country, postcode, address) attaches to the shopper's profile at checkout and on the order.
Every event is sent from the browser, including Copy to clipboardPurchase, which is keyed on the order id so a refreshed confirmation page still counts once. Nothing is sent from your server, and your server holds no Qinto credential — there is no column for one.
Notes worth reading before you debug
- Cart mutations are Server Actions. Medusa's Copy to clipboard
lib/data/cart.tsis Copy to clipboard"use server", so nothing inside it can reach the browser runtime — which is why capture observes actions completing rather than hooking their call sites. - Copy to clipboard
q.jsderives a Purchase's event id from Copy to clipboardproperties.orderId. Do not override it, or a refreshed confirmation page counts the order twice. - Field names are exact. A content item's price is Copy to clipboard
value, per unit, not Copy to clipboardpriceor Copy to clipboarditem_price; traits are Copy to clipboardfirst_name, Copy to clipboardlast_nameand Copy to clipboardpostal_code, not Copy to clipboardfirstNameor Copy to clipboardzip(Copy to clipboardaddressis sent as a custom trait); Copy to clipboardvariantIdnot Copy to clipboardvariant_id; Copy to clipboardtaxCostand Copy to clipboardshippingCost. A wrong name is never rejected: an event field is silently dropped, and a trait silently becomes a custom one.
Configuration reference
Everything below is optional. Plugin options win over environment variables, and anything set in the admin wins over both.
Option Env fallback Meaning
| Copy to clipboardqintoUrl | Copy to clipboardQINTO_URL | fallback before a site is chosen in the admin |
Nothing is encrypted at rest because nothing stored is secret: the destination is a public host and the switch is a boolean.
One thing to know about Copy to clipboardqintoUrl. It is a fallback, not a default: a value set here keeps applying even after you disconnect in the admin. The admin page says so when the destination is coming from configuration. To stop sending entirely, use the tracking switch, or remove the option and restart.
Troubleshooting
The admin page shows stale behaviour after upgrading the plugin. Medusa's dev server pre-bundles plugin admin extensions with Vite, and the cache key does not change when only the plugin's contents change — so the browser keeps running the previous build and can call routes that no longer exist. Clear both caches and restart:
12rm -rf node_modules/.vite .medusanpm run dev
Then hard-reload the browser. This only affects development; a production Copy to clipboardmedusa build produces a fresh bundle each time.
Events are not arriving. Three things, in order: the destination in Admin → Blotout matches exactly what the Qinto portal shows; the badge reads Sending events; and your storefront's root layout mounts the component. Copy to clipboardGET /store/qinto/config on your backend answers the first two — it returns the URL the storefront will use, or Copy to clipboard{"enabled": false}.
Every page load looks like a new visitor. Qinto's own anonymous id is set by the collector, so it only persists when the collector is served from your domain; on a Copy to clipboard*.qinto.tech host the browser partitions it away and mints a fresh one per page load. The plugin's Copy to clipboardtruid cookie (below) is not affected — it is set by your own server and is stable from the first request. Complete the DNS/CNAME setup in the Qinto portal so the collector is a subdomain of your store, and the two line up.
API routes
Route Purpose Copy to clipboardGET /store/qinto/config the storefront reads its Qinto URL here Copy to clipboardGET /admin/qinto connection status Copy to clipboardPOST /admin/qinto/settings set the Qinto URL, or turn tracking on and off
Three routes, and only one of them is public. Copy to clipboard/store/qinto/config returns a URL that is already public and nothing else — no token, because there is none to leak.
License
MIT
