@adtivity/adtivity-sdk
v1.8.0
Published
Adtivity SDK for web and mobile applications (React Native) to track user behavior and analytics.
Maintainers
Readme
Adtivity SDK
Lightweight analytics SDK for web and React Native apps — with built-in Web3/blockchain support.
Installation
npm install @adtivity/adtivity-sdkFor React Native, also install the async storage peer dependency:
npm install @react-native-async-storage/async-storageQuick Start
import { init, trackEvent } from '@adtivity/adtivity-sdk'
init({ apiKey: 'your-api-key' })
trackEvent('Page Loaded', { section: 'home' })Entry points
The SDK ships as three separate bundles. Import only what you use — session replay and error tracking are not included in the main bundle, so they cost you nothing unless you ask for them.
| Import | What it gives you | Size (gzipped) |
|---|---|---|
| @adtivity/adtivity-sdk | Events, click/page/location tracking, identity, Web3 | ~8.7 KB |
| @adtivity/adtivity-sdk/session-replay | DOM recording and playback | ~6.6 KB |
| @adtivity/adtivity-sdk/errors | Error tracking + the end-user report widget | ~5.5 KB |
All three share the same session and anonymous IDs, so a recording, an error, and an event from the same visit line up in your dashboard automatically.
Session Replay
Records the DOM so you can watch a session back — mouse, clicks, scrolls, input, navigation and errors.
import { SessionReplay } from '@adtivity/adtivity-sdk/session-replay'
const replay = new SessionReplay({ apiKey: 'your-api-key' })
replay.start()You don't need to set an endpoint: replay sends to the same API base URL
init() already resolved.
Masking. Password and credit-card inputs are masked automatically. Add your
own selectors for anything else, or mark elements in your markup with
data-adtv-mask (masks text) or data-adtv-block (replaces the element).
new SessionReplay({
apiKey: 'your-api-key',
maskSelectors: ['input[type=email]', '[data-sensitive]'],
blockSelectors: ['.customer-details'],
sampleRate: 0.25, // record 25% of sessions
captureNetwork: false, // fetch/XHR method, url, status, duration
})| Option | Default | Description |
|---|---|---|
| sampleRate | 1 | Fraction of sessions to record |
| maskSelectors | [] | Extra selectors whose text is masked |
| blockSelectors | [] | Selectors replaced with a placeholder |
| captureNetwork | false | Record fetch/XHR results |
| 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 one recording |
replay.pause() / replay.resume() / replay.stop() control it at runtime.
Don't record screens showing other people's data. If your app has an admin area displaying your own customers' information, don't start the recorder there. Masking is for fields; whole screens are better handled by not recording them.
Error Tracking
One call captures uncaught exceptions and unhandled promise rejections, then groups them into issues in your dashboard.
import { initErrorTracking } from '@adtivity/adtivity-sdk/errors'
initErrorTracking({ apiKey: 'your-api-key' })Rejected promises never reach window.onerror — every failed await and
rejected fetch lands somewhere else entirely. On most modern apps those are the
majority of real errors, so they're captured separately.
Grouping. Raw errors are a firehose; one broken component can throw
thousands of times. Errors are collapsed into issues by normalizing variable
data out of the message (User 4821 not found and User 9134 not found are one
issue) and by preferring the first stack frame that is your own code (the same
generic message thrown from two different places stays two issues). Deploy-hashed
filenames group stably across releases.
Identical errors are also collapsed client-side before sending — a render loop throwing 5,000 times sends one entry.
initErrorTracking({
apiKey: 'your-api-key',
release: '2.4.1',
beforeSend: (error) => !error.message.includes('chrome-extension'),
})| Option | Default | Description |
|---|---|---|
| widget | true | Show the end-user report widget on error |
| release | — | App version, to attribute issues to a deploy |
| maxErrorsPerPage | 25 | Hard cap per page load |
| beforeSend | — | Return false to drop an error |
| flushInterval | 4000 | How often queued errors are sent (ms) |
Report caught errors yourself with captureError:
import { captureError } from '@adtivity/adtivity-sdk/errors'
try {
await checkout()
} catch (err) {
captureError(err, { step: 'payment' })
}Support Widget
When an error occurs, a small panel appears bottom-right letting the user say what happened. The report lands in your Support Inbox linked to both the error and the session replay of what they were doing.
It's on by default with initErrorTracking(). Pass widget: false for silent
collection only.
It never shows technical detail — no message, no stack, no filename. Your
users can't act on a TypeError, and showing your internals to anyone who can
trigger an error is a leak. That all goes to your dashboard instead.
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,
})It shows once per page load (a page that throws repeatedly shouldn't nag), renders in a shadow root so your CSS and its CSS can't collide, and confirms to the user even if the upload fails — showing an error inside an error reporter is a bleak experience.
Identifying users
Call identify() when someone signs in. Pass an email — this is the
difference between knowing that a user is churning and being able to contact
them.
import { identify, resetIdentity } from '@adtivity/adtivity-sdk'
identify('user_5150', {
email: '[email protected]',
name: 'Dana Okafor',
plan: 'pro', // anything else is kept as a trait
})
// on sign-out
resetIdentity()email and name are stored against the person rather than only recorded as
event properties. Everything else you pass is kept as traits, so it shows up
next to a failing session.
Activity captured before sign-in is attributed retroactively. Identification
almost always happens mid-session — someone browses anonymously, hits the error
that made them give up, and only then signs in. Matching on the anonymous ID
means that earlier error is still tied to a real person, so you don't have to
call identify() early to benefit from it.
Call resetIdentity() on logout. Identity is persisted to localStorage so
it survives a refresh — a logged-in user who reloads shouldn't silently go
anonymous. The cost of that is the next person on a shared browser inheriting
the last one's identity if you never clear it.
Once identified, the person appears against:
| Surface | What you get | |---|---| | Error Tracking | Every person who hit an issue, so you can email those affected rather than knowing only that "12 users" were | | Session Replay | Recordings listed by person, not anonymous ID | | Support Widget | The reporter's email pre-filled, so a logged-in user who leaves it blank is still reachable |
Identity is scoped per company: two Adtivity customers can each have a user with the same email without colliding.
Everything together
'use client'
import { useEffect } from 'react'
import { init, initClickTracking, initPageTracking, identify } 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 })
// Once you know who the user is — errors and recordings become contactable
if (user) identify(user.id, { email: user.email, name: user.name })
const replay = new SessionReplay({ apiKey: API_KEY, sampleRate: 0.5 })
replay.start()
return () => replay.stop()
}, [])
return null
}Initialization
import { init } from '@adtivity/adtivity-sdk'
// Simple
init('your-api-key')
// With full config
init({
apiKey: 'your-api-key',
apiBaseUrl: 'https://your-backend.com', // optional; has a default
debug: true, // logs to console
batchSize: 10, // flush after N events (default: 10)
flushInterval: 5000, // flush every N ms (default: 5000)
maxRetries: 3,
retryDelayMs: 1000,
collectData: true, // set false to start with consent off
})Tracking
Custom events
import { trackEvent } from '@adtivity/adtivity-sdk'
trackEvent('Purchase Completed', {
product_id: 'prod_123',
amount: 49.99,
currency: 'USD',
})User identification
import { identify } from '@adtivity/adtivity-sdk'
identify('user_abc', {
email: '[email protected]',
plan: 'pro',
})Sent immediately (not batched). Links all subsequent events to this user ID.
Web3 / Blockchain
Connect a wallet
Call setWallet when the user connects their wallet. The address and chain ID will be automatically attached to every subsequent event.
import { setWallet } from '@adtivity/adtivity-sdk'
// On wallet connect
setWallet('0xabc...def', '1') // address, chainId (chainId optional)
// On wallet disconnect
setWallet(null)Track transactions
import { trackTx } from '@adtivity/adtivity-sdk'
trackTx('Token Swap', {
transaction_hash: '0xdef...789',
contract_address: '0x123...456',
amount: '100',
token: 'USDC',
})transaction_hash and contract_address are promoted to top-level fields in the API payload. wallet_address and chain_id (set via setWallet) are included automatically.
Full Web3 example
import { init, setWallet, identify, trackTx, trackEvent } from '@adtivity/adtivity-sdk'
init({ apiKey: 'your-api-key' })
// User connects wallet
async function onWalletConnect(address: string, chainId: string) {
setWallet(address, chainId)
identify(address) // treat wallet address as the user ID
trackEvent('Wallet Connected', { chainId })
}
// User submits a transaction
async function onTransactionSent(hash: string) {
trackTx('NFT Minted', {
transaction_hash: hash,
contract_address: '0x...',
token_id: '42',
})
}
// User disconnects
function onWalletDisconnect() {
setWallet(null)
trackEvent('Wallet Disconnected')
}Auto-Trackers
These are opt-in. Call them once after init.
Page views
Tracks initial load and SPA navigation (history API + popstate).
import { initPageTracking } from '@adtivity/adtivity-sdk'
initPageTracking()Captured fields: title, pathname, query, url, timestamp.
Clicks
Tracks clicks on any element with a data-adtivity-* attribute.
import { initClickTracking } from '@adtivity/adtivity-sdk'
initClickTracking()| Attribute | Event prefix | Example event name |
|---|---|---|
| data-adtivity-track | none | "Hero CTA" |
| data-adtivity-button-track | Button: | "Button: Sign Up" |
| data-adtivity-link-track | Link: | "Link: View Pricing" |
<button data-adtivity-button-track="Sign Up">Sign Up</button>
<a href="/pricing" data-adtivity-link-track="View Pricing">Pricing</a>
<!-- Attach extra properties as JSON -->
<button
data-adtivity-button-track="Add to Cart"
data-adtivity-props='{"product_id": "123", "price": 29.99}'>
Add to Cart
</button>Geolocation
Fetches country, region, city, and IP via ipapi.co on init.
import { initLocationTracking } from '@adtivity/adtivity-sdk'
initLocationTracking()Privacy & Consent
import { setConsent } from '@adtivity/adtivity-sdk'
// Disable tracking and clear all queued events
setConsent(false)
// Re-enable after user gives consent
setConsent(true)Runtime Configuration
import { setApiKey, setBaseUrl } from '@adtivity/adtivity-sdk'
setApiKey('new-api-key')
setBaseUrl('https://new-backend.com')React Native
The SDK works in React Native out of the box. IDs are loaded from AsyncStorage asynchronously, so wait for the SDK to be ready before tracking:
import { init, waitUntilReady, trackEvent } from '@adtivity/adtivity-sdk'
init({ apiKey: 'your-api-key' })
await waitUntilReady()
trackEvent('App Opened')Page tracking and click tracking are browser-only and will no-op in React Native.
API Reference
| Export | Description |
|---|---|
| init(config) | Initialize the SDK |
| trackEvent(name, props?) | Track a custom event |
| trackTx(name, props?) | Track a blockchain transaction |
| identify(userId, props?) | Identify a user |
| setWallet(address, chainId?) | Set wallet address and chain for all subsequent events |
| initPageTracking() | Enable automatic page view tracking |
| initClickTracking() | Enable automatic click tracking |
| initLocationTracking() | Enable automatic geolocation tracking |
| setConsent(bool) | Enable or disable data collection |
| setApiKey(key) | Update the API key at runtime |
| setBaseUrl(url) | Update the backend URL at runtime |
| waitUntilReady() | Wait for async ID initialization (React Native) |
Event Payload Shape
Every event sent to the server looks like:
{
"type": "track",
"eventName": "Token Swap",
"timestamp": "2025-01-14T12:00:00.000Z",
"anonymous_id": "uuid-v4",
"session_id": "uuid-v4",
"user_id": "user_abc",
"wallet_address": "0xabc...def",
"chain_id": "1",
"transaction_hash": "0xdef...789",
"contract_address": "0x123...456",
"country": "US",
"region": "California",
"city": "San Francisco",
"properties": {
"amount": "100",
"token": "USDC"
}
}License
MIT
