-
Notifications
You must be signed in to change notification settings - Fork 3
from tracking script to sdk
When you use a Convert SDK directly instead of the classic Convert tracking script, the SDK becomes the runtime engine for the rules and goals you configured in the Convert UI — but you become the trigger and context provider. Nothing auto-detects URLs, clicks, geolocation, or visitor state for you. You provide them on every call and you decide when each conversion fires.
This page maps Convert UI concepts (goal types, audience rules, location rules, experience types) to the SDK calls and parameters you'd use to replicate them. The mapping is the same across every Convert SDK — JavaScript and PHP today, and the same shape will apply to future SDKs (iOS, Android, etc.) when they ship. Code snippets below are shown side-by-side in JavaScript and PHP where the trigger surface allows; browser-only patterns are explicitly marked.
See also: Server-Side Experimentation, Running Experiences, Tracking Conversions, Rule Evaluation.
The classic tracking script is opinionated and turnkey. It:
- Reads the URL and refires evaluation on
history.pushState/popstate. - Reads the User-Agent and infers browser, OS, and device.
- Reads cookies (geo, UTM, custom).
- Watches the DOM for click and form-submission targets matching your goal selectors.
- Auto-fires
viewExpevents when the visitor enters a matching site area. - Auto-fires
hitGoalevents when a matching trigger condition occurs. - Persists visitor identity across pages via
_conv_v.
No SDK does any of those things — by design. Each SDK exposes two surfaces and trusts you with the rest:
-
runExperience/runExperiences— given the context you supply, returns the bucketed variation (and fires an exposure event). -
trackConversion— given a goal key and any attributes you supply, fires a conversion event.
Everything between (URL change detection, click watching, geo lookup, cookie parsing, consent gating) is your job. The pages below tell you what attribute keys the SDK expects so the rules you configured in the UI still match. The keys are SDK-agnostic — they're properties of the project config you wrote in the Convert UI, not language-specific.
When you run userContext.runExperience('experience-key') (or runExperiences() to evaluate all active experiences), the SDK walks the experience's locations and audiences rule trees. For each rule, it looks up a key in the attribute object you passed and runs a comparison. If you don't pass that key, the rule can't match and the visitor isn't bucketed.
The SDK accepts two attribute objects on every runExperience(s) call:
| Attribute | What the SDK reads from it | Typical keys |
|---|---|---|
locationProperties |
Anything tied to where the visitor is — URL parts, referrer, page path, hostname, hash, querystring components. |
url, path, referrer, hostname, hash, query, etc. |
visitorProperties |
Anything tied to who the visitor is — geo, device, browser, OS, visitor type, UTM tags, custom dimensions, cookies, logged-in state, plan tier, anything you want to target on. |
country, region, device, browser, os, visitorType, utm_source, utm_medium, plan, isLoggedIn, etc. |
const variation = userContext.runExperience('homepage-hero', {
locationProperties: {
url: window.location.href,
path: window.location.pathname,
referrer: document.referrer
},
visitorProperties: {
country: 'US',
device: 'desktop',
browser: 'chrome',
visitorType: 'returning',
utm_source: new URLSearchParams(location.search).get('utm_source') ?? undefined
}
});The exact keys you need depend on the rules you configured. Inspect your project's experiences[].audiences and experiences[].locations / experiences[].site_area to see which keys each rule references, and make sure your visitorProperties / locationProperties include those keys with the right values.
These are the most common UI rule types and what to pass. If your rule references a key not listed here, just match the key name your rule uses.
| UI rule | Pass via | Typical value source |
|---|---|---|
| URL (exact / contains / regex / starts-with / ends-with) |
locationProperties.url (or path, depending on rule config) |
window.location.href or your server-side request URL |
| Hostname | locationProperties.hostname |
window.location.hostname |
| Query parameter |
locationProperties.query.<param> or split out at the top level |
parse window.location.search
|
| Referrer | locationProperties.referrer |
document.referrer |
| Country | visitorProperties.country |
Geo-IP from your CDN/edge headers (cf-ipcountry, x-vercel-ip-country, oxygen-sub-country) or a server-side geo service |
| Region / state | visitorProperties.region |
Same source as country |
| City | visitorProperties.city |
Same source as country |
| Device type |
visitorProperties.device ('desktop' / 'mobile' / 'tablet') |
UA parser (browser or server-side) |
| Browser |
visitorProperties.browser (e.g. 'chrome', 'safari', 'firefox') |
UA parser |
| Operating system |
visitorProperties.os (e.g. 'windows', 'macos', 'ios', 'android') |
UA parser |
| Visitor type (new / returning) | visitorProperties.visitorType |
Check your own visitor-ID cookie: present → 'returning', absent → 'new'
|
| UTM tag |
visitorProperties.utm_source, utm_medium, utm_campaign, utm_term, utm_content, gclid
|
Parse window.location.search on landing, persist to a cookie for later evaluation |
| Cookie value | visitorProperties.<cookieName> |
Read the cookie yourself |
| Logged-in state |
visitorProperties.isLoggedIn (boolean) |
Your auth state |
| Plan / tier / role |
visitorProperties.plan, visitorProperties.role, etc. |
Your application state |
| Custom segment | Use runCustomSegments(['segment-key'], { ...properties }) before bucketing — see Visitor Context
|
n/a |
If your rule uses a value the SDK couldn't evaluate (because the key wasn't in the attributes you passed), the SDK returns a RuleError.NeedMoreData — not a false match. That's how you tell "the visitor genuinely didn't qualify" from "you forgot to pass the key". The SDK's debug logs surface which keys were missing — see the Troubleshooting page.
URL rules in the Convert UI typically operate on a normalized URL string. Most stacks pass locationProperties.url as the full URL (window.location.href in the browser; the equivalent server-side value otherwise). Some rule configs target specific parts (path only, hostname, querystring); in those cases, pass the corresponding sub-key (path, hostname, query) so the rule has what it needs.
If your rules were written against window.location.href, pass window.location.href. If they were written against window.location.pathname, pass that. The SDK doesn't normalize for you — what's in the rule is what gets compared.
Fullstack projects support two experience types. Both use the same runExperience / runExperiences calls.
type field on ConfigExperience
|
What it does | SDK pattern |
|---|---|---|
a/b_fullstack |
Standard A/B test. Visitor is bucketed into one variation; you render or apply that variation. Goal hits attribute to the bucketed variation. |
runExperience('exp-key') → check variation.variationKey and render accordingly. |
feature_rollout |
Feature flag rollout. Variations represent feature states (enabled/disabled, or per-variant variable values). Goals are typically tied to the feature's effect rather than a variant comparison. |
runFeature('feature-key') → check feature.status ('enabled' / 'disabled') and read feature.variables. See Running Features. |
You can run both kinds in the same project. A/B tests use experiment-style reporting; feature rollouts use feature-status reporting.
In the Convert UI, goals carry conceptual trigger types — "this goal fires when the user clicks a button", "this goal fires when the user reaches /thank-you", and so on. The classic tracking script auto-watches for each trigger type.
In any SDK, all of that is your code's responsibility. There is one method — trackConversion(goalKey, attrs?) — and it fires whenever you call it. The goal's key is the only thing the SDK cares about; everything else (selector watching, URL pattern matching, timing) is the customer's trigger code.
Below are the common goal types you'd find in the UI and one-or-two patterns for replicating each. Each goal type can fire from any SDK — what changes is the surface where the trigger event happens. Where the event has both a browser and a server-side surface (e.g. URL visit → route handler, form submit → POST handler), examples are shown in both JavaScript and PHP. Some triggers are inherently single-surface (clicks are DOM-only; engagement metrics are client-only) and are marked as such, with notes on how future mobile SDKs would express the same idea.
About goal
rules: a goal in your project config may have arulesobject attached. When you calltrackConversion(key, { ruleData: {...} }), the SDK evaluates those rules against yourruleDataand only fires the conversion if they match. If the goal has no rules, it always fires when you call. Most "trigger" logic lives in your code, not in the goal config —ruleDatais for fine-grained conditional firing, not full trigger detection.
Browser-only trigger. A click is a DOM event with no native server-side analogue — the closest server-side equivalent is the action the click eventually causes (form POST, API call), in which case you'd model the goal as a form-submit or URL-visit goal instead.
UI config: a CSS selector. The classic tracking script auto-attaches a delegated click listener and calls hitGoal when a matching element is clicked.
JavaScript SDK pattern — attach the listener yourself:
document.body.addEventListener('click', (e) => {
if (e.target.closest('.signup-cta')) {
userContext.trackConversion('signup-cta-click');
}
});Delegated to document.body so it survives DOM mutations and SPA re-renders. If your variation code adds and removes the target element repeatedly, the delegated listener still works — no need to re-attach. For dynamic selectors or selector lists, replace .closest('.signup-cta') with whatever query matches your goal's selector spec.
Future mobile SDKs: the same goal-key conversion would fire from a native gesture handler (
UITapGestureRecognizeron iOS,View.OnClickListeneron Android). The SDK call shape is the same —trackConversion('signup-cta-click')— only the trigger source changes.
Browser and server both. UI config: a URL pattern. The classic tracking script fires when the visitor lands on a matching page.
Browser pattern — detect navigation and check the path. Initial-load case is easy; SPA case needs hooking the router:
function maybeFireUrlGoal() {
if (window.location.pathname === '/thank-you') {
userContext.trackConversion('purchase-confirmation-reached');
}
}
// Initial load
maybeFireUrlGoal();
// SPA: hook your router. Vanilla fallback:
window.addEventListener('popstate', maybeFireUrlGoal);
const origPush = history.pushState;
history.pushState = function (...args) {
origPush.apply(this, args);
maybeFireUrlGoal();
};For pattern-based matches (contains, regex), substitute the comparison:
if (/\/order\/[a-z0-9-]+\/confirm$/.test(window.location.pathname)) {
userContext.trackConversion('order-confirmation-reached');
}Server pattern — fire from the route handler that renders the matching path. The visitor reaches /thank-you only by hitting your server, so the route handler is the natural place:
For many URL goals on the browser side, the equivalent generalization:
const urlGoals = [
{ goalKey: 'purchase-confirmation-reached', match: (p) => p === '/thank-you' },
{ goalKey: 'pricing-page-viewed', match: (p) => p === '/pricing' },
];
function evaluateUrlGoals() {
const path = window.location.pathname;
for (const g of urlGoals) {
if (g.match(path)) userContext.trackConversion(g.goalKey);
}
}The SDK dedups goal conversions per visitor (one fire per visitor per goal unless forceMultipleTransactions: true), so calling on every navigation / request is safe — repeats are silently dropped.
Future mobile SDKs: model URL-visit goals as screen-visit goals — fire from your screen's
onAppear/onResume/ route-listener hook. SametrackConversion(goalKey)call.
Browser and server both. UI config: a CSS selector targeting a <form>. The classic tracking script auto-listens on the storefront.
Browser pattern — same idea as click, but on submit:
document.body.addEventListener('submit', (e) => {
if (e.target.matches('form.newsletter-signup')) {
userContext.trackConversion('newsletter-signup-submitted');
}
});If your form is submitted via AJAX (no actual submit event), call trackConversion directly from your submit handler instead.
Server pattern — fire from the route that processes the form POST. This is often the more reliable place because it only runs after the form actually validates and persists:
// Or in Node/Express server-side
app.post('/newsletter/subscribe', async (req, res) => {
// ... validate + persist ...
req.convertContext.trackConversion('newsletter-signup-submitted');
res.redirect('/newsletter/thanks');
});Future mobile SDKs: fire from the submit-button handler after your validation completes, same
trackConversioncall.
Browser and server both. Tracking script: customer calls _conv_q.push({ what: 'tag', params: { ID: '12345' }}) or _conv_q.push({ what: 'trigger-goal', params: { goal_id: '...' }}).
SDK equivalent: just call trackConversion directly. There is no queue, no indirection.
function onSomeBusinessEvent() {
userContext.trackConversion('business-event-goal-key');
}This is the most natural fit between the two worlds — anywhere you'd have pushed to _conv_q, you call trackConversion. The shape of the call is identical across every SDK; only the surrounding event handler differs.
Browser and server both. UI config: a goal flagged as transactional/revenue-bearing. Tracking script auto-fires when the corresponding trigger (typically a click or URL goal) fires, attaching revenue data the customer set on _conv_q or via convert.currentData.transaction.
SDK pattern: pass conversionData on the trackConversion call. The most reliable place for this is server-side (e.g. an order webhook) because client-side conversions can be blocked or lost on the navigation away from checkout.
userContext.trackConversion('purchase-completed', {
conversionData: [
{ key: 'amount', value: 99.99 },
{ key: 'productsCount', value: 3 },
{ key: 'transactionId', value: 'order-12345' }
]
});Conversion count (unique visitors who converted) is dedup'd by visitor per goal. Revenue accumulates across distinct transactionId values for the same visitor — so multiple orders from the same customer add up, but the conversion count itself is 1. For subscription renewals where you want every renewal to count as its own transaction, pass conversionSetting: { forceMultipleTransactions: true } — see Tracking Conversions.
Browser-only trigger. Engagement metrics live in the client — a server can't observe how long someone spent on a page or how far they scrolled. Future mobile SDKs would express the same idea via screen-time observers or scroll-listener APIs.
These have no built-in SDK equivalent — the tracking script auto-watches; no SDK does. Implement the threshold yourself in the browser and call trackConversion when the condition is met:
// Scrolled 75% of page goal
let fired = false;
window.addEventListener('scroll', () => {
if (fired) return;
const pct = (window.scrollY + window.innerHeight) / document.body.scrollHeight;
if (pct >= 0.75) {
userContext.trackConversion('scrolled-75-percent');
fired = true;
}
});
// 30 seconds on page goal
setTimeout(() => userContext.trackConversion('engaged-30s'), 30_000);Keep these listeners idempotent and tear them down on route changes if your stack uses SPA navigation, to avoid duplicate timers across pages.
A brief checklist of behaviors you'll re-implement (or accept as unsupported):
| Tracking-script behavior | SDK equivalent |
|---|---|
| Auto page-view events on every URL change | None — the SDK only fires exposure events on runExperience and conversion events on trackConversion. If you need a page-view-like signal, fire a dedicated custom goal manually. |
| Visitor cookie generation + 6-month persistence | You generate and persist the visitor ID. See Client-Side Experimentation and Server-Side Experimentation for cookie helpers. |
| Cross-domain visitor linking via signal redirects | You plumb the visitor ID across domains yourself (URL parameter on outbound links, or sync server-side). |
| Audience re-evaluation on SPA navigation | Call runExperience / runExperiences again with updated locationProperties after navigation. |
| Consent queue gating | You gate calls behind your consent signal. See Client-Side Experimentation > Consent. |
_conv_q API (addListener, tag, addRevenue, …) |
Call SDK methods directly — there is no queue. Event hooks are available via convertSDK.on(SystemEvents.X, ...); see Event System. |
| Visual Editor changes (DOM mutations defined in the UI) | Not delivered to Fullstack-SDK callers. Variation changes in Fullstack are fullStackFeature change types — feature variables you read in your code, not pre-baked DOM mutations. See Data Model > Variation Changes. |
| Cross-experience mutual exclusion / experiment groups | Configured in the UI; the SDK respects it when bucketing. No additional code needed. |
If your project has many goals and audiences, scattering trigger logic across components gets unwieldy fast. A central registry pattern keeps things tidy.
Browser — a single registry that binds each goal to its trigger when the SDK is ready:
const goalRegistry = {
'signup-cta-click': () =>
delegate('click', '.signup-cta'),
'newsletter-signup-submitted': () =>
delegate('submit', 'form.newsletter-signup'),
'pricing-page-viewed': () =>
onPathMatch((p) => p === '/pricing'),
'purchase-completed': () =>
onCustomEvent('checkout:completed', (e) => ({
conversionData: [
{ key: 'amount', value: e.detail.total },
{ key: 'transactionId', value: e.detail.orderId }
]
})),
};
// Bind everything once, after the SDK is ready and the context is created.
for (const [goalKey, bind] of Object.entries(goalRegistry)) {
bind((attrs) => userContext.trackConversion(goalKey, attrs));
}The helpers (delegate, onPathMatch, onCustomEvent) are whatever utilities fit your stack.
Server — a single class that owns server-side triggers, invoked from middleware, controllers, webhooks, and queue workers as appropriate:
The point is the same in both worlds: keep the mapping between UI-configured goal keys and runtime triggers in one place that mirrors the structure you can see in the Convert UI. This is roughly the responsibility the tracking script took on for you; doing it explicitly in your code makes the contract obvious.
The classic tracking script re-evaluates audiences on every URL change. No SDK does this automatically — runExperience runs when you call it.
Browser — three reasonable strategies:
- Once per session. Bucket every active experience at app boot, store the result, never re-run. Simplest, but audience rules that depend on URL won't catch SPA navigations into newly-eligible pages.
-
On every navigation. Hook your router and call
runExperiencesagain with the updatedlocationProperties.url. Catches SPA-eligible audiences correctly. Without a configuredDataStore, this re-fires the exposure event each time — the server-side metrics processor dedups by visitor+experience, so reports are unaffected, but your wire traffic is N× the visitor count. -
Configure a DataStore. With a persistent DataStore set on the SDK, the dedup is on the SDK side — re-running
runExperiencereads the cached decision and skips the wire call. This is the recommended approach if you re-run on every navigation. See Persistent DataStore.
Server — call runExperience(s) once per request that needs the decision, with the request's URL and visitor properties. The SDK's batching + per-request shutdown flush handle the event tally; a DataStore (database, cache, etc.) is what makes the dedup span across requests for the same visitor.
Pick the strategy that matches your traffic and tolerance for duplicate exposure events on the wire. None of them affect the correctness of bucketing decisions — those are deterministic per visitor on every SDK.
Copyrights © 2025 All Rights Reserved by Convert Insights, Inc.
Getting Started
JavaScript SDK
Core Concepts
- Experiences & Variations
- Feature Flags
- Bucketing Algorithm
- Rule Evaluation
- Segments
- Data Management
- Event System
- API Communication
How-To Guides
- Running Experiences
- Running Features
- Tracking Conversions
- Visitor Context
- Persistent DataStore
- Client-Side Experimentation
- Server-Side Experimentation
- Tracking Script → SDK
- Troubleshooting
Edge & Integrations
Contributing