datascribe / docs Account
Docs / Getting started

Get set up in three steps

About ten minutes, most of it waiting for a build.

1

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.

bash
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.

.env.local
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.

KeyLives inUsed 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.
Keep the secret key out of the browser. It only belongs in CI and your MCP config. The public pk_live_ key is the one that ships in your bundle.
2

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..

js · vite.config.js
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 — the sk_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 a 403: 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.
your existing build/deploy workflow
# 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:

js · vite.config.js — or a separate step
// 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:

bash · pin this source to its origins
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.

It sends whatever your 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.
3

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..

the only thing you paste
https://api.datascribe-z9r.com/mcp
then just askOf everyone who saw the trial button, how many also saw pricing — and which did they click?
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.

Windows can be as fine as minutes. The timed reads default to a window in 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.

json · claude_desktop_config.json
{
  "mcpServers": {
    "datascribe": {
      "url": "https://api.datascribe-z9r.com/mcp",
      "headers": {
        "Authorization": "Bearer sk_live_…"
      }
    }
  }
}
Prefer stdio? A client that cannot send a bearer token to a remote server can run the same tools locally over stdio. Same tools, same data.