Skip to main content
sdk

Track from the Web

draft
Audience: developerUpdated 2026-07-26

This tutorial gets the Binoban Web SDK sending its first identify and track events from your site.

Before you begin

You need:

  • A website you can add a <script> tag to (or a build where you can install an npm package).
  • Your Binoban source credentialsapiKey and sourceIdentifier — from your Binoban workspace. (New to credentials? See the Quick Start.)
apiHost is optional on the Web SDK

Unlike the Native SDK, the Web SDK does not require an explicit apiHost — it falls back to a default host if you omit host.apiPath. Set it explicitly for on-prem or air-gapped deployments, or if your workspace uses a non-default host.

Step 1 — Add the Web SDK to your site

Where your credentials come from

The snippet below is the current one — copy it from here. Your apiKey and sourceIdentifier are issued per source in the Product Workspace, under Data → Data Sources, where you can also copy them prefilled into a configuration block.

To add the Binoban snippet to your app:

Paste the snippet into the <head> tag of your site to install Binoban.


<script type="text/javascript">
(function () {
// define the key where the global SDK object will be accessible
// you can safely set this to be something else if need be
window.__GLOBAL_CDXP_SDK_KEY = "Binoban"

// Create a queue, but don't obliterate an existing one!
var _SDK = window[__GLOBAL_CDXP_SDK_KEY] = window[__GLOBAL_CDXP_SDK_KEY] || [];

// If the real sdk object is already on the page return.
if (_SDK.initialize) return;

// If the snippet was invoked already show an error.
if (_SDK.invoked) {
if (window.console && console.error) {
console.error("SDK snippet included twice.");
}
return;
}

// Invoked flag, to make sure the snippet
// is never invoked twice.
_SDK.invoked = true;

// A list of the methods to stub.
_SDK.methods = [
"trackSubmit",
"trackClick",
"trackLink",
"trackForm",
"pageview",
"identify",
"reset",
"group",
"track",
"ready",
"alias",
"debug",
"page",
"screen",
"once",
"off",
"on",
"addSourceMiddleware",
"addIntegrationMiddleware",
"setAnonymousId",
"addDestinationMiddleware",
"register",
"loadDisplayAds"
];

// Query param names dropped under the SDK's 'full' privacy level (see
// DEFAULT_QUERY_DENYLIST in packages/browser/src/core/privacy/defaults.ts).
// Kept in sync by hand -- duplicated here, not imported, because this
// is a plain inline snippet.
var BPC_DENYLIST = ["token", "code", "jwt", "access_token", "id_token", "refresh_token", "auth", "authorization", "session", "sid", "secret", "password", "passwd", "pwd", "api_key", "apikey", "sig", "signature", "state"];

// The snippet runs before the SDK has loaded, so before the configured
// privacy policy is known. It only drops what EVERY policy level would
// drop: denylisted param names and the URL fragment. Everything else --
// including non-credential params like utm_*/btid/urid that a 'full'
// destination is designed to keep -- is left for the SDK to filter
// again at drain time, once the configured policy is known.
var bpcSanitize = function (url) {
try {
if (!url) return "";
var parsed = new URL(url);
var search = parsed.search;
var parts = (search.charAt(0) === "?" ? search.slice(1) : search).split("&");
var out = [];
for (var j = 0; j < parts.length; j++) {
if (!parts[j]) continue;
var k = parts[j].split("=")[0].toLowerCase();
if (BPC_DENYLIST.indexOf(k) === -1) out.push(parts[j]);
}
return parsed.origin + parsed.pathname + (out.length ? "?" + out.join("&") : "");
} catch (err) {
return "";
}
};

// Define a factory to create stubs. These are placeholders
// for methods in sdk so that you never have to wait
// for it to load to actually record data. The `method` is
// stored as the first argument, so we can replay the data.
_SDK.factory = function (e) {
return function () {
if (window[__GLOBAL_CDXP_SDK_KEY].initialized) {
// Sometimes users assigned SDK to a variable before SDK is done loading, resulting in a stale reference.
// If so, proxy any calls to the 'real' CDXP instance.
return window[__GLOBAL_CDXP_SDK_KEY][e].apply(window[__GLOBAL_CDXP_SDK_KEY], arguments);
}
var args = Array.prototype.slice.call(arguments);

// Add buffered page context object so page information is always up-to-date.
// Query params are denylist-filtered here as well as in the SDK: the
// SDK sanitizes this payload again when it drains the queue, so this
// is defense in depth, not the policy itself.
if (["track", "screen", "alias", "group", "page", "identify"].indexOf(e) > -1) {
var c = document.querySelector("link[rel='canonical']");
var u = bpcSanitize(location.href);
var qIndex = u.indexOf("?");
var s = qIndex > -1 ? u.slice(qIndex) : "";
args.push({
__t: "bpc",
c: c && c.getAttribute("href") || undefined,
p: location.pathname,
u: u,
s: s,
t: document.title,
r: document.referrer ? bpcSanitize(document.referrer) : ""
});
}

args.unshift(e);
_SDK.push(args);
return _SDK;
};
};


// For each of our methods, generate a queueing stub.
for (var i = 0; i < _SDK.methods.length; i++) {
var key = _SDK.methods[i];
_SDK[key] = _SDK.factory(key);
}

// Define a method to load SDK from our CDN,
// and that will be sure to only ever load it once.
_SDK.load = function (options) {
// Create an async script element based on your key.
var t = document.createElement("script");
t.type = "text/javascript";
t.async = true;
t.setAttribute("data-global-sdk-key", __GLOBAL_CDXP_SDK_KEY)
t.src = options.host.sdkPath;

// Insert our script next to the first script element.
var first = document.getElementsByTagName("script")[0];
first.parentNode.insertBefore(t, first);
_SDK._apiKey = options?.credentials.apiKey
_SDK._sourceIdentifier = options?.credentials.sourceIdentifier
_SDK._loadOptions = options;
};

// Add a version to keep track of what's in the wild.
_SDK.SNIPPET_VERSION = "1.3.0";

// set your unique AnonymousId here.
// note that this id should be unique for each device (browser)
// _SDK.setAnonymousId("YOUR_UNIQUE_ANONYMOUS_ID");

// Load SDK with your key, which will automatically
// load the tools you've enabled for your account. Boosh!
_SDK.load({
credentials: {
apiKey: "PROJECT_API_KEY",
sourceIdentifier: "PROJECT_SOURCE_IDENTIFIER"
},
host:{
rtbPath: "YOUR_BINOBAN_SERVER_RTB_URL",
sdkPath: "YOUR_BINOBAN_SERVER_SDK_URL",
apiPath: "YOUR_BINOBAN_SERVER_API_URL",
},
});

// Make the first page call to load the integrations. If
// you'd like to manually name or tag the page, edit or
// move this call however you'd like.
// _SDK.page();
})();
</script>

info

Replace PROJECT_SOURCE_IDENTIFIER and PROJECT_API_KEY in the snippet, with values on related source in your Binoban Project. Values of host object including YOUR_BINOBAN_SERVER_RTB_URL, YOUR_BINOBAN_SERVER_SDK_URL, YOUR_BINOBAN_SERVER_API_URL will provided for you by our support team.

info

When you use Web SDK in device-mode, the source's Keys is public, because it runs in a user's browser and can be accessed using the browser's developer tools. If this is not acceptable to your organization, you can explore other Binoban Sources which collect data from a server-based installation, and which are not accessible to the user.

That snippet loads Binoban.js onto the page asynchronously, so it won't affect your page load speed. Once the snippet is running on your site, calls to identify, track, and page start flowing to your Binoban workspace immediately — there's no separate destination or integration to turn on.

Page data and privacy

Every identify, track, and page call the snippet queues also carries a buffered page-context object (the u, s, r, p, t, c fields you can see pushed in the snippet above). That object is not forwarded as-is — the SDK sanitizes it before anything leaves the browser.

The bundle you were given is currently built with full

The privacy level has a build-time default baked into each hosted bundle, and every bundle Binoban ships today is built with full — so pasting the snippet below unchanged gives you full, not sanitized. Under full the SDK drops only a fixed denylist of credential-shaped parameter names, so something like ?sso_ticket=abc123 is collected.

To get the stricter behaviour described in this section, set privacy: { pageContext: "sanitized" } in your _SDK.load({...}) options (shown below) — a load option always wins over the bundle's build-time default.

Under privacy.pageContext: "sanitized", the SDK:

  • Collects the page URL as origin + pathname + an allowlisted set of query parameters — every other query parameter is dropped.
  • Reduces document.referrer to its origin only.
  • Never collects the URL fragment (#…), for either the page URL or the referrer, at any privacy level.

The default query allowlist is any parameter prefixed with utm_, plus btid and urid (used for ad attribution). Anything else in the query string — session tokens, auth codes, internal identifiers you never opted in — is stripped before the event is sent.

Configure this with a privacy block alongside credentials and host in the same _SDK.load({...}) call you already pasted in Step 1:

_SDK.load({
credentials: {
apiKey: "PROJECT_API_KEY",
sourceIdentifier: "PROJECT_SOURCE_IDENTIFIER"
},
host: {
rtbPath: "YOUR_BINOBAN_SERVER_RTB_URL",
sdkPath: "YOUR_BINOBAN_SERVER_SDK_URL",
apiPath: "YOUR_BINOBAN_SERVER_API_URL",
},
privacy: {
pageContext: "sanitized",
queryAllowlist: ["promo_code"],
referrer: "origin",
},
});

queryAllowlist and queryDenylist extend the built-in defaults — they don't replace them. Adding promo_code above still keeps every utm_* parameter plus btid and urid.

If your workspace genuinely needs the full query string, set privacy: { pageContext: "full" }. This restores unrestricted query-string collection, minus a fixed denylist of obviously credential-shaped parameter names (token, jwt, access_token, session, password, and similar names). Treat full as an opt-in you can justify, not a default — any parameter not on that denylist, including identifiers that don't look like credentials, is sent exactly as it appears in the URL. full also implies referrer: "full" (the referrer keeps its path and query, minus the same denylist) unless you set privacy.referrer yourself.

full is an opt-out, not a safety net

The denylist only catches parameter names someone thought of. It exists so a workspace that opts out of sanitized isn't completely unprotected — it is not a substitute for sanitized.

One more detail if you're reading the snippet closely: the snippet itself does a small amount of filtering (denylisted names and the fragment) before the SDK has even loaded, because at that point it doesn't yet know which pageContext level your workspace is configured for. That's a defense-in-depth floor, not the real policy — the SDK re-applies your configured privacy settings when it drains the queue, and that's what actually determines what ships to your workspace.

Finally: none of this is a substitute for keeping credentials out of your URLs in the first place. The SDK is far from the only thing that reads location.href on your page — browser history, browser extensions, the Referer header sent on any outbound link, and every other script running on the page see it too. An analytics SDK can't safely call history.replaceState() on your URL to scrub it after the fact without risking a collision with your own router or other code reading the query string. If a URL on your site can carry a token, auth code, or session identifier, scrub it in your application, regardless of these settings.

Knowing who each user is on your site is the foundation everything downstream builds on. Read on about the Identify method.

Step 2 — Identify users

info

For any of the different methods described in this quickstart, you can replace the properties and traits in the code samples with variables that represent the data collected.

The Identify method is how you tell Binoban who the current user is. It includes a unique User ID, and any optional traits you know about them.

You don't need to call Identify for anonymous visitors to your site. Binoban automatically assigns them an anonymousId, so just calling page and track works just fine without Identify.

Here's what a basic call to Identify might look like:

Binoban.identify("jdUserId", {
name: "John Doe",
email: "jdoe@example.com",
});

This identifies John by his unique User ID (in this case, jdUserId, which is what you know him by in your database) and labels him with name and email traits.

When you actually put that code on your site, you need to replace those hard-coded trait values with the variables that represent the details of the currently logged-in user.

To do that, Binoban recommends that you use a backend template to inject an Identify call into the footer of every page of your site where the user is logged in. That way, no matter what page the user first lands on, they will always be identified. You don't need to call Identify if your unique identifier (userId) is not known.

Depending on your templating language, your actual Identify call might look something like this:

Binoban.identify('{{user.id}}', {
name: '{{user.fullname}}',
email: '{{user.email}}'
});

With that call in your page footer, you successfully identify every user that visits your site.

Identities are only half the picture — the Web SDK also records the actions each user performs. If you're looking for a complete event-tracking setup, keep reading...

Step 3 — Track actions

The Track method is how you tell Binoban about the actions your users are performing on your site. Every action triggers what's called an "event", which can also have associated properties. You can read more about Track in the Web SDK reference → Track.

Here's what a call to a Track call might look like when a user signs up:

Binoban.track("signed_up", {
plan: "Enterprise",
});

That tells Binoban that your user triggered the signed_up event, and chose your hypothetical 'Enterprise' plan. Properties can be anything you want to record, for example:

Binoban.track("article_bookmarked", {
title: "Snow Fall",
subtitle: "The Avalanche at Tunnel Creek",
author: "John Branch",
});

If you're just getting started, some of the events you should track are events that indicate the success of your site, like signed_up, product_added, or article_bookmarked.

Event names are validated

Event names must match ^[a-zA-Z0-9_\-.]*$ — no spaces. Use snake_case (signed_up, not Signed Up); a space anywhere in the name rejects the whole call. See Track for the full rule.

To get started, Binoban recommends that you track just a few important events. You can always add more later.

After you add a few Track calls, you successfully installed Binoban.js tracking.

Step 4 — Track page views

The snippet already fires a page() call on load. Call it again whenever the URL changes without a full page load, for example on route changes in a single-page app:

Binoban.page();

Pass a name if you want to distinguish the view:

Binoban.page("Pricing");

Page calls carry url, title, referrer, and path automatically. For the full field list and default behavior, see the Web SDK reference → Page.

Step 5 — Verify it's working

Open your browser's network tab and reload the page — you should see a POST to …/api/tracker/sdk/web returning 200 for each page, identify, and track call. Unlike the Tracking REST API, this endpoint carries no {sourceIdentifier} path segment — the apiKey in the load snippet identifies the source. You can also run Binoban.debug(true) in the console to log every event Binoban.js sends before it leaves the browser. See Debug Mode for more ways to inspect events.

What you've done

You added the snippet, identified a user, tracked an action, and verified delivery — the full Web tracking loop.

Next steps

  • Full method reference → the Web SDK referencetrackLink/trackForm helpers, utility methods, retries and batching, UTM tracking, and the cookies the SDK sets.
  • Standardize your events → the ecommerce event catalogue to make sure your products and checkout experience are instrumented properly.
  • Model identity well across platformsIdentity strategy.
  • Before production → the Go-live checklist.