Quickstart
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.localNEXT_PUBLIC_ADTIVITY_API_KEY=your_key_hereVite 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
bashnpm install @adtivity/adtivity-sdkInitialise it once, where your app boots
Three calls. init() connects, and the two trackers start collecting page views and clicks with no further wiring.
"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.
NEXT_PUBLIC_* variables are baked in at build time, so changing one needs a redeploy, not a restart.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.
NPM SDK
Drop @adtivity/adtivity-sdk into your web app. Auto-tracks page views, clicks, geolocation and identity with a single init().
MCP Server
Point Claude, Cursor or any MCP-compatible agent at Adtivity. Query your metrics in plain English — no SDK, no SQL.
Connect MCP →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.
claude mcp add adtivity -- npx adtivity-mcpTo use it across all your projects, add it globally:
claude mcp add --scope user adtivity -- npx adtivity-mcpCursor / Claude Desktop / Windsurf
Add to ~/.cursor/mcp.json (or the project-level .cursor/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:
| Tool | What it does |
|---|---|
adtivity_detect_framework | Reads package.json — identifies Next.js App/Pages Router, React, Vue, Express, or React Native and finds the right entry file. |
adtivity_install_sdk | Runs npm/yarn/pnpm/bun install @adtivity/adtivity-sdk using your detected package manager. |
adtivity_write_init_code | Creates 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_monitoring | Switches 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_identify | Returns 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_event | Returns a ready-to-paste trackEvent() snippet for any named action — signup, upgrade, checkout, and more. |
adtivity_verify_integration | Checks 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:
npx adtivity-mcpThe 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:
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:
| Prompt | What 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. |
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.
package.json, install the SDK, create the right files, and patch your layout automatically.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 install @adtivity/adtivity-sdkReact Native — also install the async storage peer dependency:
npm install @react-native-async-storage/async-storageThe 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.
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 SDKimport {
init,
initClickTracking,
initPageTracking,
initLocationTracking,
} from "@adtivity/adtivity-sdk";Initialise inside useEffectuseEffect(() => {
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
| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | — | Required. Your project key from Settings → API Keys. |
apiBaseUrl | string | Adtivity default | Override the backend endpoint. |
debug | boolean | false | Logs all SDK activity to the console. Use during development. |
batchSize | number | 10 | Events to collect before an automatic flush. |
flushInterval | number | 5000 | Milliseconds between automatic flushes. |
maxRetries | number | 3 | How many times to retry a failed batch. |
retryDelayMs | number | 1000 | Base delay for exponential backoff retries (doubles each attempt). |
collectData | boolean | true | Master 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:
| Mode | How to enable | What gets tracked |
|---|---|---|
| Global | initClickTracking({ trackAllButtons: true }) | Every button, a, input[submit], and [role=button] click. Event name is derived automatically from aria-label → innerText → 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. |
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.
<div data-adtivity-track="feature-card-click">
Click me
</div>Buttons
For <button> elements, use the more specific data-adtivity-button-track.
<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.
<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.
<button
data-adtivity-button-track="add-to-cart"
data-adtivity-props='{"productId": "123", "price": 49.99}'>
Add to Cart
</button>signup-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.
| Trigger | When it fires |
|---|---|
| Initial load | When the user first lands on your site. |
history.pushState | Client-side navigation in React Router, Next.js App Router, etc. |
history.replaceState | URL replacement without adding a history entry. |
popstate | Browser back / forward navigation. |
Properties captured on every page view:
| Property | Description |
|---|---|
pageTitle | document.title |
path | window.location.pathname |
query | window.location.search |
fullUrl | window.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.
Parameters captured
| Parameter | Property name | Example |
|---|---|---|
utm_source | utm_source | google, twitter, newsletter |
utm_medium | utm_medium | cpc, email, organic |
utm_campaign | utm_campaign | summer_sale, launch_2026 |
utm_term | utm_term | analytics+tool |
utm_content | utm_content | hero-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.
| Field | Description |
|---|---|
country | ISO country code, e.g. “US”. |
region | State or province. |
city | City-level location. |
ip_address | Visitor'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.
// 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.
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.
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.
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
| Surface | What you see |
|---|---|
| Error Tracking | Every person who hit an issue, so you can email the ones affected instead of knowing only that “12 users” were. |
| Session Replay | Recordings listed by person rather than by anonymous ID. |
| Support Inbox | The reporter’s email is filled in from their identity, so a logged-in user who leaves the field blank is still reachable. |
| At-Risk Users | The 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. |
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.
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).
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.
| Group | Fields |
|---|---|
| Time & URL | ISO 8601 timestamp · current URL with query · referrer |
| Device | User agent · browser · device type · OS |
| Location | Country · region · city · anonymised IP (needs initLocationTracking) |
| Identity | Anonymous ID · session ID · user ID & wallet (if identify'd) |
| Element | Element 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.
bashnpm install @adtivity/adtivity-sdkjavascriptimport { SessionReplay } from "@adtivity/adtivity-sdk/session-replay"
const replay = new SessionReplay({
apiKey: "YOUR_API_KEY",
})
replay.start()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.
javascriptnew 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.
| Option | Default | What it does |
|---|---|---|
sampleRate | 1 | Fraction of sessions to record. 0.1 records one in ten. |
maskSelectors | [] | Extra selectors whose text is masked. |
blockSelectors | [] | Selectors replaced with a grey placeholder. |
captureNetwork | false | Record fetch/XHR method, URL, status, duration. |
captureErrors | true | Mark JS errors on the replay timeline. |
idleTimeout | 5 min | End the session after this much inactivity. |
maxSessionDuration | 30 min | Hard cap on a single recording. |
Pausing and resuming
Stop recording around a screen you would rather not capture, then pick up again.
javascriptreplay.pause() // stop capturing, keep the session open
replay.resume() // takes a fresh snapshot and continues
replay.stop() // end the session and flushError Tracking
One call captures uncaught exceptions and unhandled promise rejections, batches them, and groups them into issues in your dashboard.
javascriptimport { initErrorTracking } from "@adtivity/adtivity-sdk/errors"
initErrorTracking({ apiKey: "YOUR_API_KEY" })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 foundandUser 9134 not foundare 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 undefinedgets thrown from dozens of unrelated places — those are different bugs, and are kept apart. Frames insidenode_modulesare 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.
javascriptimport { captureError } from "@adtivity/adtivity-sdk/errors"
try {
await checkout()
} catch (err) {
captureError(err, { step: "payment" })
}Options
| Option | Default | What it does |
|---|---|---|
widget | true | Show the report widget when an error occurs. |
release | — | Your app version, so issues can be attributed to a deploy. |
maxErrorsPerPage | 25 | Hard cap per page load. Protects a struggling page from a flood of requests. |
beforeSend | — | Return false to drop an error before it is sent. |
flushInterval | 4000 | How often queued errors are sent, in ms. |
Filter out noise you cannot fix — a third-party script, a browser extension:
javascriptinitErrorTracking({
apiKey: "YOUR_API_KEY",
release: "2.4.1",
beforeSend: (error) => !error.message.includes("chrome-extension"),
})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.
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:
javascriptinitErrorTracking({
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,
})| Behaviour | Why |
|---|---|
| Shows once per page load | A page that throws repeatedly must not nag the person using it. |
| Renders in a shadow root | Your CSS cannot affect it and it cannot affect yours — no inherited resets or z-index fights. |
| Confirms even if the upload fails | A failed report is our problem, not the user’s. Showing an error inside an error reporter is a bleak experience. |
| Excluded from session replay | The 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:
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()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.
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:
| Signal | Weight | What it measures |
|---|---|---|
| Overdue | 40 | How far past their own usual gap between visits they are. |
| Silence | 25 | How long they have been gone in absolute terms. |
| Frequency | 15 | Active days recently versus the period before. |
| Depth | 10 | How much they do per visit now versus before. |
| Friction | 10 | Errors hit, tickets filed, and one-page exits. |
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.
| Segment | Meaning | Worth reaching out? |
|---|---|---|
| Went quiet | Built a visiting habit, then broke it. | Start here. They return at roughly 4x the rate of the group below. |
| Never got going | Came back once or twice, but a habit never formed. | Rarely recoverable. Treat as an onboarding problem, not a win-back one. |
| Slipping | Still active, but visiting less or doing less than they were. | The cheapest group to save — they have not left yet. |
| Active | Behaving normally for them. | No action needed. |
| One and done | One 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 band | Actually came back |
|---|---|
| LOW | 37.8% |
| MEDIUM | 25.4% |
| HIGH | 6.6% |
| CRITICAL | 3.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.
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.
| Column | What it means |
|---|---|
| Reached by | How many distinct people opened this page at all. |
| Ended here | How many of them were last seen on it. |
| Abandon rate | Ended here ÷ reached by. This is the number to read. |
| At-risk | How many of those exits were people scoring 50+. |
| Errors | Errors 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.
/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.
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.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 Users | Churn Risk | |
|---|---|---|
| Scores | Each end user | Your company overall |
| Answers | Who to contact, what to fix | Is the business in trouble |
| Runs on | Page views | Usage trend + revenue, reasoned over by AI |
| Alerts | — | Email / 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}/analytics | Total txns, volume, unique tokens, inflow/outflow, gas, top tokens |
GET /wallets/connections/{id}/recent?hours=24 | Transactions in the last N hours |
GET /wallets/connections/{id}/activities | Paginated full transaction history |
GET /wallets/balance?wallet_address=…&network=… | Current balance and token holdings |
trackTx() 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");| Feature | Browser | React Native |
|---|---|---|
trackEvent | ✓ | ✓ |
trackTx | ✓ | ✓ |
identify | ✓ | ✓ |
setWallet | ✓ | ✓ |
initPageTracking | ✓ | — |
initClickTracking | ✓ | — |
initLocationTracking | ✓ | ✓ |
waitUntilReady | no-op | Required |
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:
| Export | Signature | What it does |
|---|---|---|
init | (config) => void | Initialize the SDK. Singleton — safe to call multiple times. |
trackEvent | (name, props?) => void | Track a custom event. Batched and retried automatically. |
trackTx | (name, props?) => void | Track a blockchain transaction. Promotes transaction_hash and contract_address to top-level. |
identify | (userId, props?) => void | Identify a user. Sent immediately, not batched. |
setWallet | (address, chainId?) => void | Attach wallet address and chain ID to all subsequent events. Pass null to clear. |
initPageTracking | () => void | Enable automatic page view tracking with UTM capture. Browser only. |
initClickTracking | (opts?) => void | Enable click tracking. Pass { trackAllButtons: true } for global mode (all buttons/links) or omit for opt-in mode (data-adtivity-* only). Browser only. |
initLocationTracking | () => void | Enable IP-based geolocation. Browser + React Native. |
setConsent | (bool) => void | Enable or disable all data collection. false flushes and clears the queue. |
setApiKey | (key) => void | Update API key at runtime. |
setBaseUrl | (url) => void | Update 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"
}
}| Field | Type | Description |
|---|---|---|
type | string | Always “track”. |
eventName | string | The name you passed to trackEvent() or trackTx(). |
timestamp | ISO 8601 | When the event fired. |
anonymous_id | string | Per-visitor UUID, persisted in localStorage / AsyncStorage. |
session_id | string | Per-tab / per-session identifier. |
user_id | string | Set after identify(). |
wallet_address | string | Set after setWallet(). |
chain_id | string | Chain ID passed to setWallet(). |
transaction_hash | string | From trackTx() — promoted to top-level. |
contract_address | string | From trackTx() — promoted to top-level. |
country / region / city / ip_address | string | From initLocationTracking() — promoted to top-level. |
properties | object | All 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: truelocally. 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 →