Overview
Icon for Blotout Qinto Analytics

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

npm install @blotout/platform-medusa

Add it to Copy to clipboardmedusa-config.ts:

module.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_url and Copy to clipboardenabled, and no column for a token because nothing here ever holds one.

Run the migration and start:

npx medusa db:migrate
npm 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:

npx 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:

  1. Paste your Qinto URL and press Save. Find it in the Qinto portal under Developer tools, on your site's page.
  2. Leave Tracking on. The switch stops sending without losing the destination, so you can pause and resume without re-entering anything.
  3. 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:

npm install @blotout/platform-medusa

Let Next compile the package's TypeScript, in Copy to clipboardnext.config.js:

const nextConfig = {
transpilePackages: ["@blotout/platform-medusa"],
// ...the rest of your config
}

Mount one component in Copy to clipboardsrc/app/layout.tsx:

import 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:

export { 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 clipboardPageView directly, 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 clipboardhttpOnly Copy to clipboard_medusa_cart_id cookie 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 clipboardPerformanceObserver notices 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 clipboardAddToCart of 2.
  • A Copy to clipboardsubmit listener on Copy to clipboarddocument picks 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 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.com reporting to Copy to clipboardxyz.dev.qinto.tech gets 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.fetch looks like the obvious way to catch those Server Actions. It does not work — Next captures its own Copy to clipboardfetch reference while the framework boots, long before a component effect can replace the global, so the patch silently never fires. Copy to clipboardPerformanceObserver has 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 clipboardlib/data/cart.ts is 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 clipboardq.js derives 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 clipboardvalue, per unit, not Copy to clipboardprice or Copy to clipboarditem_price; traits are Copy to clipboardfirst_name, Copy to clipboardlast_name and Copy to clipboardpostal_code, not Copy to clipboardfirstName or Copy to clipboardzip (Copy to clipboardaddress is sent as a custom trait); Copy to clipboardvariantId not Copy to clipboardvariant_id; Copy to clipboardtaxCost and 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:

rm -rf node_modules/.vite .medusa
npm 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

You may also like

Browse all integrations

Build your own

Develop your own custom integration

Build your own integration with our API to speed up your processes. Make your integration available via npm for it to be shared in our Library with the broader Medusa community.

gift card interface

Ready to build your custom commerce setup?