Плагин Blotout для Medusa отправляет события витрины и заказов в Qinto для first-party аналитики и идентификации, с процессом подключения в Admin.
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 ; 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 , and everything a merchant chooses — destination, on/off — is set in the admin and stored in your database, so changing it needs no redeploy.
| Event | When |
|---|---|
| every navigation | |
| a product page | |
| / | cart changes |
| every arrival at checkout, including each step change | |
| / | checkout steps |
| order confirmation page | |
| 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 , 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.
Add it to :
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, and , and no column for a token because nothing here ever holds one.
Why the line is not optional. Any plugin that ships admin UI imports . Medusa's admin bundler pre-bundles React but does not deduplicate it, so with pnpm — or any install that does not hoist — that import can resolve to a second copy of React inside the plugin. Hooks then get a null dispatcher and the page dies with , which looks like a bug in the plugin and is not. One line of fixes it. If you already set , merge the into what you have rather than replacing it.
Run the migration and start:
In production, rebuild so the admin page is bundled — picks it up automatically, a built admin does not:
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.
The plugin appears as Blotout in the sidebar. Open it:
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.
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 like any other dependency. From your storefront's root:
Let Next compile the package's TypeScript, in :
Mount one component in :
And add one three-line file, :
That one file has to exist for real. Next finds route handlers by scanning your filesystem, so a route sitting in 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 , so there is no environment variable to set — if you would rather pin it, set and it wins.
Three signals, none of which needs a call site:
The context route also issues — 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 | , , 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. is set by your storefront's own server, so it is first-party from the first request and stable across page loads either way.
rather than on purpose: the starter's cart and auth cookies are 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 reporting to 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 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 , which is deliberate.
Deriving beats declaring in two ways beyond the edit count: and come from the authoritative cart rather than a recomputed variant price, and an event only fires for a mutation that actually succeeded.
Patching looks like the obvious way to catch those Server Actions. It does not work — Next captures its own reference while the framework boots, long before a component effect can replace the global, so the patch silently never fires. has no such ordering problem.
Route matching defaults to the Medusa Next.js starter's shapes, so , and . A storefront that moved them passes to the component, as a pattern per event or a function where a pattern will not reach. The rules live in , pure and unit-tested, and MERCHANT-SETUP.md has the reference. Only , and 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 , 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.
Everything below is optional. Plugin options win over environment variables, and anything set in the admin wins over both.
| Option | Env fallback | Meaning |
|---|
| | | 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 . 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.
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:
Then hard-reload the browser. This only affects development; a production 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. on your backend answers the first two — it returns the URL the storefront will use, or .
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 host the browser partitions it away and mints a fresh one per page load. The plugin's 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.
| Route | Purpose |
|---|---|
| the storefront reads its Qinto URL here | |
| connection status | |
| set the Qinto URL, or turn tracking on and off |
Three routes, and only one of them is public. returns a URL that is already public and nothing else — no token, because there is none to leak.
MIT
npm install @blotout/platform-medusa1module.exports = defineConfig({2 // …3 admin: {4 // Required. Without it the Blotout admin page will not render — see the note below.5 vite: () => ({ resolve: { dedupe: ["react", "react-dom"] } }),6 },7 plugins: ["@blotout/platform-medusa"],8})1npx medusa db:migrate2npm run devnpx medusa build && npx medusa startnpm install @blotout/platform-medusa1const nextConfig = {2 transpilePackages: ["@blotout/platform-medusa"],3 // ...the rest of your config4}1import QintoAutoCapture from "@blotout/platform-medusa/storefront/autocapture"2
3<body>4 <QintoAutoCapture />5 <main className="relative">{props.children}</main>6</body>export { GET } from "@blotout/platform-medusa/storefront/context-route"1rm -rf node_modules/.vite .medusa2npm run dev