Quickstart

~2 minSDK @adtivity/adtivity-sdkv1.5.1

Adtivity turns your product, payment and on-chain data into a signal you can read, share and act on.

Connect Adtivity in 2 minutes4 steps · no config files

The shortest path from nothing to live data. Everything else in these docs is optional on top of this.

Create an app and copy your API key

In your dashboard, click New Company. The key is shown once, when the app is created — copy it then. Put it in your environment file:

.env.local
NEXT_PUBLIC_ADTIVITY_API_KEY=your_key_here

Vite uses VITE_ADTIVITY_API_KEY; plain Node uses ADTIVITY_API_KEY. The key is publishable — it is meant to ship in your client bundle.

Install the SDK

bash
npm install @adtivity/adtivity-sdk

Initialise it once, where your app boots

Three calls. init() connects, and the two trackers start collecting page views and clicks with no further wiring.

tsx
"use client"
import { useEffect } from "react"
import { init, initPageTracking, initClickTracking } from "@adtivity/adtivity-sdk"

export function Analytics() {
  useEffect(() => {
    init({ apiKey: process.env.NEXT_PUBLIC_ADTIVITY_API_KEY! })
    initPageTracking()
    initClickTracking({ trackAllButtons: true })
  }, [])
  return null
}

Render <Analytics /> in your root layout. Not on Next.js? Call the same three functions once at startup — see Initialization for React, Vue and React Native.

Load your site once, then check the dashboard

Open any page with the snippet in place. Your first event usually appears within seconds, and the setup checklist in the dashboard ticks Install the SDK on its own when it arrives — you do not have to tell it you are done.

Nothing after a minute?It is almost always the API key. Check it matches the app you have selected, and that it is set in the environment your site actually builds with — NEXT_PUBLIC_* variables are baked in at build time, so changing one needs a redeploy, not a restart.
Or let your AI do all fourRun claude mcp add adtivity -- npx adtivity-mcp and say “install Adtivity”. It detects your framework, installs the SDK, writes the init code and verifies the integration. MCP setup →

What to do next

Once data is flowing, the highest-value next step is identify(). Without it every error, recording and churn prediction shows a device ID instead of a person you can email. After that, pick whichever of session replay, error tracking or At-Risk Users you want first — they all run on the snippet you just added.

Two ways in

There are two routes into Adtivity — pick the one that matches how you build.

Path 1 — available now

NPM SDK

Drop @adtivity/adtivity-sdk into your web app. Auto-tracks page views, clicks, geolocation and identity with a single init().

Install the SDK →
Path 2 — MCP-native Beta

MCP Server

Point Claude, Cursor or any MCP-compatible agent at Adtivity. Query your metrics in plain English — no SDK, no SQL.

Connect MCP →
You can run both at the same timeThe SDK collects events from your product. The MCP server reads them back to your AI. Same workspace, same API key.

MCP Server Beta

adtivity-mcp is an AI-native setup assistant. Add it once to Claude Code or Cursor, tell your agent “Install Adtivity into this project”, and it detects your framework, installs the SDK, patches your entry file, and verifies the integration — no manual steps, no config wrestling.

Claude Code

Run once in your terminal. Claude Code picks it up on the next session.

Terminal
claude mcp add adtivity -- npx adtivity-mcp

To use it across all your projects, add it globally:

claude mcp add --scope user adtivity -- npx adtivity-mcp

Cursor / Claude Desktop / Windsurf

Add to ~/.cursor/mcp.json (or the project-level .cursor/mcp.json):

mcp.json
{
  "mcpServers": {
    "adtivity": {
      "command": "npx",
      "args": ["adtivity-mcp"]
    }
  }
}

Five tools, one prompt

When you say “Install Adtivity”, the agent runs these tools in order:

ToolWhat it does
adtivity_detect_frameworkReads package.json — identifies Next.js App/Pages Router, React, Vue, Express, or React Native and finds the right entry file.
adtivity_install_sdkRuns npm/yarn/pnpm/bun install @adtivity/adtivity-sdk using your detected package manager.
adtivity_write_init_codeCreates AdtivityProvider.tsx (Next.js App Router) or patches your entry file with init(), initPageTracking(), and initClickTracking(). Pass features to include session replay and error tracking from the start.
adtivity_add_monitoringSwitches on session replay, error tracking, or the support widget on a project that is already set up. Merges with what is there rather than overwriting it.
adtivity_add_identifyReturns the identify() / resetIdentity() code for your sign-in flow. This is the step that turns device IDs into people you can email, and the one that makes At-Risk Users actionable.
adtivity_wrap_eventReturns a ready-to-paste trackEvent() snippet for any named action — signup, upgrade, checkout, and more.
adtivity_verify_integrationChecks SDK installed, init code present, API key configured, whether identify() is wired up, and which monitoring features are on. Returns a pass / warn / fail checklist.

Quick test

Confirm the server starts before adding it to your AI client:

Terminal
npx adtivity-mcp

The process will hang — that means it's listening on stdio and is ready. Press Ctrl+C to exit.

Once connected to Claude Code or Cursor, open any project and try the prompt below. The demo shows you exactly what the agent will do:

Claude Code — adtivity MCP

Click “Run demo” to see what happens when you ask your AI to install Adtivity.

Prompts that work well

After setup, you can also use the MCP tools individually:

PromptWhat happens
“Install Adtivity into this project”Runs all five tools in sequence — detect, install, init, verify.
“Add Adtivity event tracking to my checkout button”Calls adtivity_wrap_event and pastes the snippet into your component.
“Is Adtivity set up correctly in this project?”Runs adtivity_verify_integration and reports the checklist.
“What framework is this project using?”Calls adtivity_detect_framework and summarises the result.
The track names you choose become the vocabulary your AI usesNaming a button signup-cta-hero vs btn-1 changes whether your agent can answer “how is the hero CTA doing?”.

Claude Prompt

No MCP server? No problem. Copy the prompt below and paste it directly into Claude (or any AI assistant). It contains everything the AI needs to install and wire up Adtivity in your project — SDK install, provider component, layout wiring, UTM & click tracking, and identity.

Works best in Claude Code or CursorPaste this into an AI coding assistant that has access to your file system. It will read your package.json, install the SDK, create the right files, and patch your layout automatically.
Paste into Claude / Cursor / any AI assistant
You are implementing Adtivity analytics into this project. Adtivity is a lightweight product analytics SDK for web apps. Follow these steps exactly:

## 1. Install the SDK
Run the appropriate command for this project's package manager:
  npm install @adtivity/adtivity-sdk
  # or: yarn add @adtivity/adtivity-sdk
  # or: pnpm add @adtivity/adtivity-sdk

## 2. Create the provider (Next.js App Router)
Create src/components/AdtivityProvider.tsx:

"use client"
import { useEffect } from "react"
import {
  init,
  initClickTracking,
  initPageTracking,
  initLocationTracking,
} from "@adtivity/adtivity-sdk"

export function AdtivityProvider({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    init({
      apiKey: process.env.NEXT_PUBLIC_ADTIVITY_API_KEY ?? "",
      collectData: true,
    })
    initPageTracking()                          // auto-tracks page views + UTM source/medium/campaign
    initClickTracking({ trackAllButtons: true }) // auto-tracks ALL button/link clicks globally
    initLocationTracking()                      // enriches events with country/city from IP
  }, [])
  return <>{children}</>
}

For React (not Next.js), call the same functions inside a top-level useEffect in your App component instead.

## 3. Wrap the root layout (Next.js)
In src/app/layout.tsx, import AdtivityProvider and wrap children:

import { AdtivityProvider } from "@/components/AdtivityProvider"

// inside the JSX:
<AdtivityProvider>
  {children}
</AdtivityProvider>

## 4. Add the API key to .env.local
NEXT_PUBLIC_ADTIVITY_API_KEY=your_api_key_here

The user can get their API key at adtivity.xyz/dashboard → Settings → API Keys.

## 5. Identify users after sign-in
Wherever auth resolves (e.g. after login), add:

import { identify } from "@adtivity/adtivity-sdk"

identify(user.id, {
  email: user.email,
  plan: user.plan,          // optional
  createdAt: user.createdAt // optional
})

## 6. Track custom events (optional)
import { trackEvent } from "@adtivity/adtivity-sdk"

trackEvent("Checkout Completed", { amount: 99, plan: "pro" })
trackEvent("Feature Used", { feature: "export", format: "csv" })

## What you get automatically (no extra code)
- Page Viewed event on every route change with UTM params (utm_source, utm_medium, utm_campaign, utm_term, utm_content)
- UTM params persisted in sessionStorage — deep pages without UTMs still report the original source
- Every button/link click tracked with the element's text or aria-label as the event name
- Country, city, and region on every event via IP geolocation
- Anonymous ID (localStorage) and session ID (sessionStorage) stitched automatically

## Optional: suppress specific elements from click tracking
Add data-no-track to any element you don't want tracked:
<button data-no-track>Internal debug button</button>

## Optional: override the auto-derived click event name
<button data-adtivity-button-track="upgrade-plan-cta">Upgrade</button>
<a href="/pricing" data-adtivity-link-track="header-pricing-link">Pricing</a>

## Verify it works
Set debug: true in the init() call during development — you'll see every event logged to the console.
init({ apiKey: "...", debug: true })

Installation

Install the Adtivity SDK from npm. The package name is @adtivity/adtivity-sdk — note the double adtivity.

npm
npm install @adtivity/adtivity-sdk

React Native — also install the async storage peer dependency:

npm install @react-native-async-storage/async-storage

The SDK ships as a single isomorphic package for both browser and React Native environments.

Authentication

The Adtivity SDK authenticates with a single API key. There's no separate sign-in step or token exchange — the key you pass to init() identifies your project for every event the SDK sends.

Find your key in Settings → API Keys in the Adtivity dashboard. Expose it to your client through a public env var, e.g. NEXT_PUBLIC_ADTIVITY_API_KEY.

The SDK key is public by designIt ships in your client bundle. Anything that needs server-side access to your full dataset (the MCP server, future REST clients) uses a separate server key.

Initialization

Initialise the SDK once at app startup. In Next.js this goes in your root layout or _app.tsx; in React, in your top-level component.

Basic setup

Import the SDK
import {
  init,
  initClickTracking,
  initPageTracking,
  initLocationTracking,
} from "@adtivity/adtivity-sdk";
Initialise inside useEffect
useEffect(() => {
  if (typeof window === "undefined") return;
  if (window.__ADTIVITY_BOOTSTRAPPED__) return;
  window.__ADTIVITY_BOOTSTRAPPED__ = true;

  const API_KEY = process.env.NEXT_PUBLIC_ADTIVITY_API_KEY;
  if (!API_KEY) {
    console.error("Adtivity SDK: API key is not configured");
    return;
  }

  init({
    apiKey: API_KEY,
    debug:  false,
    // Optional tuning:
    // batchSize: 10,
    // flushInterval: 5000,
  });

  initPageTracking();
  initClickTracking({ trackAllButtons: true });  // or omit options for data-attr opt-in mode
  initLocationTracking();
}, []);

Configuration options

OptionTypeDefaultDescription
apiKeystringRequired. Your project key from Settings → API Keys.
apiBaseUrlstringAdtivity defaultOverride the backend endpoint.
debugbooleanfalseLogs all SDK activity to the console. Use during development.
batchSizenumber10Events to collect before an automatic flush.
flushIntervalnumber5000Milliseconds between automatic flushes.
maxRetriesnumber3How many times to retry a failed batch.
retryDelayMsnumber1000Base delay for exponential backoff retries (doubles each attempt).
collectDatabooleantrueMaster switch — set false to start with all tracking off.

Click Tracking

initClickTracking() attaches a single delegated listener to document.body — no onClick handlers, no manual track() calls needed. There are two modes:

ModeHow to enableWhat gets tracked
GlobalinitClickTracking({ trackAllButtons: true })Every button, a, input[submit], and [role=button] click. Event name is derived automatically from aria-labelinnerText → element type.
Opt-in (default)initClickTracking()Only elements with data-adtivity-* attributes. Use this when you want precise control over what gets named and tracked.
Global mode + data attributes work togetherWith trackAllButtons: true, any element that also has a data-adtivity-button-track attribute uses the attribute value as the event name instead of auto-deriving it. Add data-no-track to any element you want to silence.

Opt-in: data attribute tracking

Use data-adtivity-track on any clickable element.

html
<div data-adtivity-track="feature-card-click">
  Click me
</div>

Buttons

For <button> elements, use the more specific data-adtivity-button-track.

jsx
<Button data-adtivity-button-track="web3-simulate-token-transfer">
  <Rocket className="mr-2 h-5 w-5" />
  Simulate Token Transfer
</Button>

Links

For navigation, use data-adtivity-link-track.

jsx
<Link href="/cars" data-adtivity-link-track="header-nav-cars">
  <Car className="mr-2 h-4 w-4" />
  Cars
</Link>

Custom properties

Attach arbitrary JSON with data-adtivity-props.

html
<button
  data-adtivity-button-track="add-to-cart"
  data-adtivity-props='{"productId": "123", "price": 49.99}'>
  Add to Cart
</button>
Use descriptive track namessignup-button-click is far more useful in your dashboard (and to your AI agent) than btn1.

Page Tracking

Once initPageTracking() is running, Adtivity fires a “Page Viewed” event automatically. Duplicate URLs are deduplicated — if the URL has not changed, no event fires. Not available in React Native.

TriggerWhen it fires
Initial loadWhen the user first lands on your site.
history.pushStateClient-side navigation in React Router, Next.js App Router, etc.
history.replaceStateURL replacement without adding a history entry.
popstateBrowser back / forward navigation.

Properties captured on every page view:

PropertyDescription
pageTitledocument.title
pathwindow.location.pathname
querywindow.location.search
fullUrlwindow.location.href

UTM Tracking

UTM parameters are captured automatically — no extra setup beyond calling initPageTracking(). When a visitor lands on any URL with UTM params, they are parsed, stored in sessionStorage, and merged into every “Page Viewed” event for the rest of that session.

Session persistenceUTM params are stored on the first page of a session. Deep-linked pages that don't have UTMs in their URL still report the original source — Adtivity carries it forward automatically.

Parameters captured

ParameterProperty nameExample
utm_sourceutm_sourcegoogle, twitter, newsletter
utm_mediumutm_mediumcpc, email, organic
utm_campaignutm_campaignsummer_sale, launch_2026
utm_termutm_termanalytics+tool
utm_contentutm_contenthero-cta, sidebar-link

What a tagged Page Viewed event looks like

Event payload
{
  "eventName": "Page Viewed",
  "properties": {
    "pageTitle":    "Adtivity — The metrics layer for founders",
    "path":         "/pricing",
    "fullUrl":      "https://adtivity.xyz/pricing?utm_source=google&utm_medium=cpc",
    "utm_source":   "google",
    "utm_medium":   "cpc",
    "utm_campaign": "summer_sale",
    "utm_term":     "product analytics",
    "utm_content":  "hero-cta"
  }
}

No extra code needed

UTM capturing is built into initPageTracking(). There is nothing else to call. If you need to read the current UTM values yourself (e.g. to pre-fill a form), the dashboard UTM Analytics report under KPIs shows breakdown by source, medium, and campaign.

Location Tracking

Calling initLocationTracking() enriches every event with geolocation derived from the visitor's IP address — country, region, city. No browser permission prompt; it's resolved server-side.

FieldDescription
countryISO country code, e.g. “US”.
regionState or province.
cityCity-level location.
ip_addressVisitor's public IP address.

These fields are promoted to top-level in the API payload. Works in both browser and React Native.

Identity

The SDK gives you three layers of identity: anonymous ID (free), session ID (free), and user ID (one identify() call). Together they let Adtivity stitch a complete journey from first visit through sign-in and beyond.

Anonymous ID

An auto-generated UUID stored in localStorage, created the first time a visitor hits any page where the SDK is initialised. It persists across sessions, so a returning visitor on the same browser keeps the same anonymous ID.

Automatic
// Automatically handled by the SDK.
// No code needed — created on first visit.

Session ID

A separate identifier scoped to a single browser tab. Open your site in a new tab and you get a new session ID; close the tab and that session ends.

Automatic
// Automatically handled by the SDK.
// New session per tab/window.

User ID (custom)

When a user signs in, call identify(userId, properties?) with your own user ID. It is sent immediately — not batched. After this call, user_id is attached to all subsequent events for the session.

Pass an email — this is what makes users reachableWithout it, a churning user or a broken session is a device ID you can count but cannot contact. With it, the person appears next to their errors, their session replays, any support ticket they file, and every row of At-Risk Users — so you can actually follow up.

email and name are treated specially: they are stored against the person, not just recorded as event properties. Everything else you pass is kept as traits — plan, role, signup date, whatever you want to see next to a failing session.

After sign-in
import { identify } from "@adtivity/adtivity-sdk";

identify("user_12345", {
  email:      "user@example.com",
  plan:       "pro",
  signupDate: "2025-01-01",
});

Activity captured before sign-in

Identification almost always happens mid-session: someone browses anonymously, hits the error that made them give up, and only then signs in. Adtivity attributes that earlier activity retroactively by matching on the anonymous ID, so the error that actually mattered is still tied to a real person.

You do not have to do anything for this — call identify() whenever you learn who someone is, even late in the session.

Clearing identity on logout

Call resetIdentity() when a user signs out. Identity is persisted to localStorage so it survives a page refresh — which means without this, the next person to use a shared browser inherits the last one’s identity.

javascript
import { identify, resetIdentity } from "@adtivity/adtivity-sdk"

// on sign-in
identify("user_5150", { email: "dana@buyer.io", name: "Dana Okafor", plan: "pro" })

// on sign-out
resetIdentity()

Where the person shows up

SurfaceWhat you see
Error TrackingEvery person who hit an issue, so you can email the ones affected instead of knowing only that “12 users” were.
Session ReplayRecordings listed by person rather than by anonymous ID.
Support InboxThe reporter’s email is filled in from their identity, so a logged-in user who leaves the field blank is still reachable.
At-Risk UsersThe people predicted to churn, by name and email — the difference between knowing someone is leaving and being able to do anything about it. See At-Risk Users.
Identity is scoped to your companyTwo Adtivity customers can each have a user with the same email address without colliding. A person identified in your app is only ever visible in your dashboard.

Web3 / wallet identity

Use setWallet(address, chainId?) when a user connects a wallet. Both fields are automatically included on every subsequent event — trackEvent, identify, and trackTx alike. Pass null to clear on disconnect.

On wallet connect
import { setWallet, identify } from "@adtivity/adtivity-sdk";

setWallet("0x742d35Cc...", "1");  // address, chainId
identify("0x742d35Cc...");        // use wallet as user ID

// On disconnect
setWallet(null);

Common chainId values: “1” Ethereum · “137” Polygon · “56” BNB Chain · “8453” Base · “42161” Arbitrum.

Tracking blockchain transactions

Use trackTx(name, properties?) for on-chain transactions.transaction_hash and contract_address are promoted to top-level fields in the API payload (not nested inside properties).

On transaction confirmed
import { trackTx } from "@adtivity/adtivity-sdk";

trackTx("NFT Minted", {
  transaction_hash:  "0xdef...789",
  contract_address:  "0x123...456",
  token_id:          "42",
  price_eth:         "0.08",
});

Auto-Captured Data

Every event Adtivity sends — page view, click, identify — is automatically enriched with the following context. You don't need to attach any of it manually.

GroupFields
Time & URLISO 8601 timestamp · current URL with query · referrer
DeviceUser agent · browser · device type · OS
LocationCountry · region · city · anonymised IP (needs initLocationTracking)
IdentityAnonymous ID · session ID · user ID & wallet (if identify'd)
ElementElement ID · text · tag · href · CSS classes · form values (for click events)

Session Replay

Session replay records the DOM and reconstructs what a user actually saw — mouse movement, clicks, scrolls, form input, navigation, and errors — so you can watch a session back instead of guessing from event names. It ships as a separate import, so you only pay for it if you use it.

bash
npm install @adtivity/adtivity-sdk
javascript
import { SessionReplay } from "@adtivity/adtivity-sdk/session-replay"

const replay = new SessionReplay({
  apiKey: "YOUR_API_KEY",
})
replay.start()
You do not need to configure an endpointReplay sends to the same API base URL init() already resolved. Set endpoint only if you want replay to go somewhere different from your events.

Masking sensitive content

Password and credit-card inputs are masked automatically. Add your own selectors for anything else — masked elements keep their shape and position in the replay, but their text is replaced.

javascript
new SessionReplay({
  apiKey: "YOUR_API_KEY",
  maskSelectors: ["input[type=email]", "[data-sensitive]"],
  blockSelectors: [".customer-details"],  // replaced entirely with a placeholder
  sampleRate: 0.25,                    // record 25% of sessions
})

You can also mark elements in your markup without touching SDK config — data-adtv-mask masks the text, data-adtv-block replaces the element entirely.

OptionDefaultWhat it does
sampleRate1Fraction of sessions to record. 0.1 records one in ten.
maskSelectors[]Extra selectors whose text is masked.
blockSelectors[]Selectors replaced with a grey placeholder.
captureNetworkfalseRecord fetch/XHR method, URL, status, duration.
captureErrorstrueMark JS errors on the replay timeline.
idleTimeout5 minEnd the session after this much inactivity.
maxSessionDuration30 minHard cap on a single recording.

Pausing and resuming

Stop recording around a screen you would rather not capture, then pick up again.

javascript
replay.pause()   // stop capturing, keep the session open
replay.resume()  // takes a fresh snapshot and continues
replay.stop()    // end the session and flush
Do not record screens showing other people’s dataIf your app has an admin area displaying your own customers’ information, exclude it. Masking is for fields; whole screens are better handled by simply not starting the recorder there.

Error Tracking

One call captures uncaught exceptions and unhandled promise rejections, batches them, and groups them into issues in your dashboard.

javascript
import { initErrorTracking } from "@adtivity/adtivity-sdk/errors"

initErrorTracking({ apiKey: "YOUR_API_KEY" })
Unhandled promise rejections are includedFailed await calls and rejected fetches never reach window.onerror. On most modern apps they are the majority of real errors, so Adtivity listens for them separately.

How errors are grouped

Raw errors are a firehose — one broken component can throw thousands of times. Adtivity collapses them into issues using two rules:

  • Variable data is stripped from the message. User 4821 not found and User 9134 not found are the same issue. IDs, UUIDs, URLs, emails and quoted strings are normalized before grouping.
  • The stack’s first line of your own code wins. Cannot read properties of undefined gets thrown from dozens of unrelated places — those are different bugs, and are kept apart. Frames inside node_modules are skipped when deciding which code is yours.

Deploy-hashed bundle filenames (main.a1b2c3.js) group stably across releases, so a redeploy does not reset your issue history.

Reporting errors yourself

For errors you already catch, report them explicitly.

javascript
import { captureError } from "@adtivity/adtivity-sdk/errors"

try {
  await checkout()
} catch (err) {
  captureError(err, { step: "payment" })
}

Options

OptionDefaultWhat it does
widgettrueShow the report widget when an error occurs.
releaseYour app version, so issues can be attributed to a deploy.
maxErrorsPerPage25Hard cap per page load. Protects a struggling page from a flood of requests.
beforeSendReturn false to drop an error before it is sent.
flushInterval4000How often queued errors are sent, in ms.

Filter out noise you cannot fix — a third-party script, a browser extension:

javascript
initErrorTracking({
  apiKey: "YOUR_API_KEY",
  release: "2.4.1",
  beforeSend: (error) => !error.message.includes("chrome-extension"),
})
Identical errors are collapsed before they are sentA render loop throwing the same error 5,000 times sends one entry, not 5,000. You do not need to throttle anything yourself.

Support Widget

When an error occurs, a small panel appears in the bottom-right corner offering the user a way to say what happened. Their report lands in your Support Inbox, linked to both the error and the session replay of what they were doing.

It is on by default with initErrorTracking(). Turn it off with widget: false if you only want silent error collection.

The widget never shows technical detailNo error message, no stack trace, no filename. Your end users cannot act on a TypeError, and showing your internals to anyone who can trigger an error is a leak. All of that goes to your dashboard instead.

Everything visible is yours to rewrite:

javascript
initErrorTracking({
  apiKey: "YOUR_API_KEY",
  widgetTitle: "That didn't work",
  widgetMessage: "Tell us what happened and we'll take a look.",
  widgetSubmitLabel: "Send report",
  widgetSuccessMessage: "Thanks — we're on it.",
  widgetAccent: "#e05104",
  widgetAskEmail: true,
})
BehaviourWhy
Shows once per page loadA page that throws repeatedly must not nag the person using it.
Renders in a shadow rootYour CSS cannot affect it and it cannot affect yours — no inherited resets or z-index fights.
Confirms even if the upload failsA failed report is our problem, not the user’s. Showing an error inside an error reporter is a bleak experience.
Excluded from session replayThe widget is our UI, not your page — it never appears in your recordings.

Everything together

A complete setup, in one file:

tsx
"use client"
import { useEffect } from "react"
import { init, initClickTracking, initPageTracking } from "@adtivity/adtivity-sdk"
import { SessionReplay } from "@adtivity/adtivity-sdk/session-replay"
import { initErrorTracking } from "@adtivity/adtivity-sdk/errors"

const API_KEY = process.env.NEXT_PUBLIC_ADTIVITY_API_KEY!

export function Analytics() {
  useEffect(() => {
    init({ apiKey: API_KEY })
    initPageTracking()
    initClickTracking({ trackAllButtons: true })

    initErrorTracking({ apiKey: API_KEY })

    const replay = new SessionReplay({ apiKey: API_KEY, sampleRate: 0.5 })
    replay.start()
    return () => replay.stop()
  }, [])

  return null
}

That covers everything anonymous. The last piece is telling Adtivity who each visitor is — call identify() wherever your app learns that, and resetIdentity() on sign-out:

tsx
import { identify, resetIdentity } from "@adtivity/adtivity-sdk"

// wherever your sign-in succeeds
identify(user.id, {
  email: user.email,   // without this, every screen says "Anonymous"
  name:  user.name,
  plan:  user.plan,
})

// wherever your sign-out happens
resetIdentity()
Skipping identify() is the one thing that limits every feature aboveErrors, replays, tickets and churn predictions all still work without it — but each one shows a device ID instead of a person, so you can see exactly what went wrong and have no way to contact whoever it happened to.

At-Risk Users

Adtivity scores every one of your end users on how likely they are to stop coming back, and shows you the page they were last on before they went quiet. It answers two questions: who is falling off, and where they fall off.

No extra setup — this runs on page views you already sendIf you have called initPageTracking(), scoring is already happening. There is no new import and nothing to switch on. Open At-Risk Users in your dashboard.

How the score works

Each person is measured against their own visit rhythm rather than a fixed cut-off. Three weeks of silence is a strong signal for someone who used to show up daily and means nothing at all for someone who shows up monthly — a single threshold gets one of those wrong.

The score from 0 to 100 blends five signals, each shown on screen in plain language:

SignalWeightWhat it measures
Overdue40How far past their own usual gap between visits they are.
Silence25How long they have been gone in absolute terms.
Frequency15Active days recently versus the period before.
Depth10How much they do per visit now versus before.
Friction10Errors hit, tickets filed, and one-page exits.
Rhythm is measured in active days, not sessionsA single sitting can produce several sessions as someone idles and comes back. Counting those as separate visits would put a user’s “usual return interval” at a few hours and make every genuine absence look like a hundredfold deviation.

Segments — and which ones to act on

Users are grouped by what to do about them, not by how far gone they are. The important split is at the top: two people can score identically while being very differently recoverable.

SegmentMeaningWorth reaching out?
Went quietBuilt a visiting habit, then broke it.Start here. They return at roughly 4x the rate of the group below.
Never got goingCame back once or twice, but a habit never formed.Rarely recoverable. Treat as an onboarding problem, not a win-back one.
SlippingStill active, but visiting less or doing less than they were.The cheapest group to save — they have not left yet.
ActiveBehaving normally for them.No action needed.
One and doneOne day of activity, ever.An acquisition problem. Hidden by default — they outnumber everyone else several times over.

The ranked list is ordered by risk weighted against how engaged someone was, so a long-standing regular going quiet appears above a casual visitor with the same raw score.

How accurate is it?

We validated the scoring by running it against real production data as of a past date — using only what was known then — and checking who actually came back over the following 45 days:

Predicted bandActually came back
LOW37.8%
MEDIUM25.4%
HIGH6.6%
CRITICAL3.8%

A user scored LOW was about 6.5x more likely to return than one scored HIGH or above. Every person also carries a confidence label — someone with two days of history is a guess, and the dashboard says so rather than hiding it behind a decisive-looking number.

This is a heuristic, not a trained modelIt reasons over each user’s real history the way an analyst reading a dashboard would. Nothing is invented, and no prediction is presented as more certain than the evidence behind it.

Drop-off Map

The second half of the screen answers where. For every page, it shows what share of the people who reached it never went any further — their journey ended there.

ColumnWhat it means
Reached byHow many distinct people opened this page at all.
Ended hereHow many of them were last seen on it.
Abandon rateEnded here ÷ reached by. This is the number to read.
At-riskHow many of those exits were people scoring 50+.
ErrorsErrors recorded on this page — often the reason for the leak.

A high abandon rate on a page with real volume is a leak worth fixing. Pages seen by fewer than five people are excluded, because a 100% abandon rate over three visitors is noise. Ranking is by at-risk exits rather than raw traffic — otherwise your home page wins every time and tells you nothing.

URLs are grouped automatically/orders/1841 and /orders/1842 collapse into /orders/:id, so a page that leaks badly is not split into a thousand harmless-looking rows. Query strings are dropped; the hostname is kept, so staging traffic never folds into your production numbers.

Following one person

Click any user to open their full history: every visit, the pages in each one, the errors they hit, and their exit path — the last few pages before they went quiet, newest first. Where a session replay exists for a visit, you can watch it back from that same panel.

Call identify() or you can see who is leaving but not contact themWithout it, every row on this screen reads “Anonymous” — you get an accurate list of churning users and no way to reach any of them, which makes the whole screen a report rather than a tool. One identify() call with an email puts a name and a mailto link on every person here, and it applies retroactively to activity captured before they signed in.
Anonymous visitors are tracked per browserUntil identify() is called, a person is keyed by a localStorage ID. Clearing cookies or switching device reads as a new person, which inflates the “One and done” count and understates real retention. Identifying users fixes this too — activity stitches together across their devices.

Company Churn Risk

Separate from the per-user screen above, Churn Risk scores your business as a whole — usage trend against revenue churn — and writes up what it sees. Use At-Risk Users to find people to email; use Churn Risk to judge whether the business is trending the wrong way.

At-Risk UsersChurn Risk
ScoresEach end userYour company overall
AnswersWho to contact, what to fixIs the business in trouble
Runs onPage viewsUsage trend + revenue, reasoned over by AI
AlertsEmail / Slack above a threshold you set

Churn Risk also takes a comparison window and a free-text context box, so you can tell it things the database cannot know — a pricing change, an outage, a planned migration — before it reads a deliberate drop as churn.

Events

An event is a single thing that happened — a page view, a click, an identify call, a wallet connect. Every event captured by the SDK gets enriched with the auto-captured context above and lands in your Adtivity workspace ready to query.

You don't compose events manually. The SDK produces them from data attributes on your markup and from the init* functions you call once at startup.

Companies

A company is the workspace that owns your data. One Adtivity account can have multiple companies — useful if you ship more than one product, or want a clean split between staging and production. Each company has its own API key and dashboard.

Set up a new company from Settings → Companies → New.

Users

A user is anyone whose journey Adtivity has stitched together. On first visit they exist as an anonymous ID; the moment you call identify() with a userId, email, or wallet address, anonymous activity gets merged into the known user record. See Identity for how to wire that up.

Lifecycle

Adtivity automatically classifies users by where they sit in your funnel — visitor, signup, activated, paying, churned — by reading the events you're already sending. The Lifecycle Chart on your dashboard is the canonical view; the same buckets are queryable through the MCP server.

Stripe Integration

Connect Stripe from Settings → Connections — about 90 seconds. Once connected, MRR, ARR and churn auto-populate in your dashboard and become queryable through the MCP server. No manual event tracking required for revenue.

Paystack Integration

Processing payments in Africa? Connect Paystack the same way — Settings → Connections. Stripe and Paystack can be active in the same workspace; revenue from both is unified into a single MRR view.

Wallet Monitoring — No SDK Required

You don't need the SDK to track on-chain activity. Register any wallet address in the Web3 Wallets section of your company dashboard and Adtivity monitors it at the backend level — no MetaMask, no ethers.js, no SDK integration.

Endpoint (backend)What it returns
GET /wallets/connections/{id}/analyticsTotal txns, volume, unique tokens, inflow/outflow, gas, top tokens
GET /wallets/connections/{id}/recent?hours=24Transactions in the last N hours
GET /wallets/connections/{id}/activitiesPaginated full transaction history
GET /wallets/balance?wallet_address=…&network=…Current balance and token holdings
SDK events vs wallet monitoring are separatetrackTx() and setWallet() capture user-triggered on-chain actions from your frontend. Wallet monitoring tracks the registered address itself via backend indexing — it works even if you have no SDK installed.

Privacy & Consent

Call setConsent(false) when a user opts out. This stops all future event tracking, flushes and clears the event queue, and removes persisted events from localStorage. Call setConsent(true) to re-enable.

import { setConsent } from "@adtivity/adtivity-sdk";

// User opts out
setConsent(false);

// User opts back in
setConsent(true);

You can also start with all tracking off by passing collectData: false to init().

Runtime Configuration

Swap the API key or backend URL after init — useful when users switch workspaces or when you point to a different environment at runtime.

import { setApiKey, setBaseUrl } from "@adtivity/adtivity-sdk";

// After user logs into a different workspace
setApiKey("new-api-key");

// Point to a different backend
setBaseUrl("https://your-custom-backend.com");

React Native

The SDK is React Native compatible. Because AsyncStorage reads are asynchronous, call waitUntilReady() before tracking to ensure the anonymous ID and session ID have loaded from storage.

import { init, waitUntilReady, trackEvent, setWallet } from "@adtivity/adtivity-sdk";

init({ apiKey: "your-api-key" });

await waitUntilReady();   // wait for AsyncStorage IDs to load

trackEvent("App Opened");
setWallet("0xabc...", "1");
FeatureBrowserReact Native
trackEvent
trackTx
identify
setWallet
initPageTracking
initClickTracking
initLocationTracking
waitUntilReadyno-opRequired

REST API Coming soon

A public REST surface is on the roadmap for teams that want to read Adtivity data outside of the dashboard or the MCP server — for internal tools, custom dashboards, or scheduled exports. Endpoints, auth model, and rate limits will be documented here once it lands.

Until then, the MCP server is the supported way to query your data programmatically.

SDK Reference

Quick index of every public function in @adtivity/adtivity-sdk:

ExportSignatureWhat it does
init(config) => voidInitialize the SDK. Singleton — safe to call multiple times.
trackEvent(name, props?) => voidTrack a custom event. Batched and retried automatically.
trackTx(name, props?) => voidTrack a blockchain transaction. Promotes transaction_hash and contract_address to top-level.
identify(userId, props?) => voidIdentify a user. Sent immediately, not batched.
setWallet(address, chainId?) => voidAttach wallet address and chain ID to all subsequent events. Pass null to clear.
initPageTracking() => voidEnable automatic page view tracking with UTM capture. Browser only.
initClickTracking(opts?) => voidEnable click tracking. Pass { trackAllButtons: true } for global mode (all buttons/links) or omit for opt-in mode (data-adtivity-* only). Browser only.
initLocationTracking() => voidEnable IP-based geolocation. Browser + React Native.
setConsent(bool) => voidEnable or disable all data collection. false flushes and clears the queue.
setApiKey(key) => voidUpdate API key at runtime.
setBaseUrl(url) => voidUpdate backend URL at runtime.
waitUntilReady() => Promise<void>Wait for async ID init. Required in React Native; no-op in browser.

Event Schema

Every event the SDK produces ships with the same envelope of auto-captured fields. The full reference is in Auto-Captured Data; the short version:

All events are sent as a JSON array to POST /sdk/event with an X-API-Key header. Top-level fields (wallet_address, chain_id, transaction_hash, contract_address, country, region, city, ip_address) are always promoted out of properties. Null and empty-string values are omitted.

{
  "type":              "track",
  "eventName":         "Token Swap",
  "timestamp":         "2025-06-23T12:00:00.000Z",

  "anonymous_id":      "uuid-v4",
  "session_id":        "uuid-v4",
  "user_id":           "user_abc123",

  "wallet_address":    "0xabc...def",
  "chain_id":          "1",

  "transaction_hash":  "0xdef...789",
  "contract_address":  "0x123...456",

  "country":           "US",
  "region":            "California",
  "city":             "San Francisco",
  "ip_address":        "203.0.113.1",

  "properties": {
    "url":       "https://app.example.com/swap",
    "referrer":  "https://app.example.com/",
    "userAgent": "Mozilla/5.0...",
    "amount":    "100",
    "token":     "USDC"
  }
}
FieldTypeDescription
typestringAlways “track”.
eventNamestringThe name you passed to trackEvent() or trackTx().
timestampISO 8601When the event fired.
anonymous_idstringPer-visitor UUID, persisted in localStorage / AsyncStorage.
session_idstringPer-tab / per-session identifier.
user_idstringSet after identify().
wallet_addressstringSet after setWallet().
chain_idstringChain ID passed to setWallet().
transaction_hashstringFrom trackTx() — promoted to top-level.
contract_addressstringFrom trackTx() — promoted to top-level.
country / region / city / ip_addressstringFrom initLocationTracking() — promoted to top-level.
propertiesobjectAll remaining custom and auto-captured fields (URL, referrer, user agent, element data).

Best practices

A few things worth doing on day one:

  • Name events for humans, not machines. Your dashboard, your team, and your AI all read these.
  • Identify users early. Call identify() the moment auth resolves so the anonymous → known stitch happens cleanly.
  • Turn on debug: true locally. You'll see every event in the console while you wire things up.
  • Keep the public key public, the server key private. They're separate by design.

Next: Connect MCP → · Plug in Stripe → · Web3 identity →