Get set up in three steps
About ten minutes, most of it waiting for a build.
Install and connect
One command. It signs you in, creates a sourceA source is one app in one environment — your web app in
production, the same app in staging. Each keeps its own data, so dev and prod never mix. Run
init again elsewhere to make another., and writes your keys.
npm i @datascribe/tagger-sdk
npx datascribe init
What just happened
init opens your browser to sign in — there is no key to paste — then writes
.env.local with that source's public key and the events endpoint.
DATASCRIBE_PUBLIC_KEY=pk_live_51QhR2x…
DATASCRIBE_ENDPOINT=https://api.datascribe-z9r.com/v1/events
Run it again in another app, or with a different environment name, and you get a second source; each keeps its own data. The plugin injects the runtime client for you, so there is no entrypoint to edit.
It also prints a sk_live_… secret for this source, once, for you to set as a CI
secret. Your account keeps a separate secret of its own for the MCP.
The two kinds of key
They do different jobs and live in different places. Which secret you hold matters — the manifest upload files your tag catalog under whichever source its key belongs to.
| Key | Lives in | Used for |
|---|---|---|
pk_live_… Public |
Your build env & browser bundle | Emitting render & click events. World-readable by design — it identifies a project, it does not authenticate one. |
sk_live_… Secret |
CI env & your MCP config | Uploading the tag manifest and reading analytics. Treat it like a password. |
pk_live_ key is the one that ships in your bundle.
Add the plugin and build
Two lines in your bundler config. Your source files are never touchedNothing is committed. Tagging happens in the compiled output during the build, so your repository is unchanged and there is no workflow file to maintain. Tag ids are derived from where an element lives in your source, so they re-bind to the same elements after a refactor — a button's history follows it when it moves or gets renamed..
import { datascribe } from '@datascribe/tagger-sdk/vite'
plugins: [datascribe()]
Ship it. Events start arriving on the next page view.
Uploading the catalog from CI
The plugin posts your tag catalog during the build you already run, when both
CI and a secret key are set in the environment. Add two settings to your repository
and set them on your build step:
- A secret
DATASCRIBE_SECRET_KEY— thesk_live_…for this source. Your account's MCP secret is a different key, and once you have more than one source an upload presenting it is refused with a403: it would have to guess which source the tags belong to, and guessing wrong stays invisible until a lookup comes back empty. - A variable
DATASCRIBE_API— the URL of this service, e.g.https://api.datascribe-z9r.com.
# CI=true is set for you by Actions; these two are yours to add
- run: npm run build
env:
DATASCRIBE_SECRET_KEY: ${{ secrets.DATASCRIBE_SECRET_KEY }}
DATASCRIBE_API: ${{ vars.DATASCRIBE_API }}
The CI check is what stops every local npm run build from overwriting
your catalog with a working-tree snapshot. When you do want one anyway — a first catalog before
any CI run exists, or a pipeline that does not set CI — ask for it explicitly:
// ask the plugin for it explicitly, whatever CI says
plugins: [datascribe({ upload: true })],
# or run the upload after the build, from the manifest it cached
DATASCRIBE_SECRET_KEY=sk_live_… DATASCRIBE_API=https://api.datascribe-z9r.com \
npx datascribe upload
Locking down which origins may post
A new source is unpinned: it accepts events from any origin and answers CORS
to your local dev servers, so npm run dev works with nothing to configure. When you
ship, pin the origins the app actually posts from — init offers this, and you can
change it any time:
curl -X PUT https://api.datascribe-z9r.com/v1/origins \
-H 'authorization: Bearer sk_live_…' \
-H 'content-type: application/json' \
-d '{"origins":["https://shop.example.com","http://localhost:5173"]}'
Pinning replaces the open default rather than adding to it, so the list has to name every
origin the source posts from, dev ports included. Once pinned, an event from anywhere else is
refused with a 403 that says so.
Telling list items apart
A single tagged element — an “Add to basket” button rendered from a .map() — has
one id across every card, so on its own it answers how many clicks, not which
item. The SDK piggybacks on the React key you already write: each list item's
key rides along on its events as an instance_key, and
element_items reads clicks back broken down by it. This is
on by default.
key holds. That value leaves the page
verbatim, so bind opaque record ids (a product id, a SKU) — not key={user.email} or a
name. Turn it off for a build with datascribe({ instanceKeys: false }), or override a
single element with an explicit data-ds-key.
Ask your questions
Paste one URL into Claude, Cursor, or any MCP clientHosted for you. Choose Add server, paste the URL, done — no JSON and no key. Your client opens a browser to sign in once, then reconnects on its own..
https://api.datascribe-z9r.com/mcp
The tools it brings
list_sources
Your apps and environments. Data is never merged across them.
list_elements
Every tagged element, with its source file and line.
element_stats
Renders and clicks for one element, over a window you choose.
element_metadata
What an element is and where it lives — even before you name it.
top_elements
Rank elements by how often they were rendered or clicked.
element_items
Split one list-rendered element's traffic by item.
session_funnel
Saw this, then clicked that — counted in sessions, not clicks.
top_routes
Rank the pages people viewed, including client-side navigations.
route_elements
What got clicked on one route.
days but take a minutes override too, so “how many clicks in the last 15
minutes?” is answerable. When both are given, minutes wins.
Connecting a client that cannot do OAuth
The first time you connect, your client opens a browser to sign you in, then reconnects on its
own — standard OAuth, so the sk_live_… key never has to leave your account page. For
a client that cannot do that, point it at the same /mcp URL and send your
secret key as a bearer token. Both paths reach identical tools and data.
{
"mcpServers": {
"datascribe": {
"url": "https://api.datascribe-z9r.com/mcp",
"headers": {
"Authorization": "Bearer sk_live_…"
}
}
}
}