CLItrail Docs

Website SDK

browser.js records visits to your install page and leaves the receipt the installer hook looks for. It has no dependencies, sets no cookies and never asks the visitor for a permission. The framework guides show where the tag goes in each framework.

#Add the tag

<script defer src="https://YOUR_SERVICE/browser.js"
        data-website="YOUR_WEBSITE_ID"></script>

With data-website present, the SDK starts on load and exposes the result as the promise InstallAttribution.ready. Load it after your analytics tags so it can read their IDs. The page must be served from your website's primary origin or one of its additional domains, over https (or localhost during development).

AttributeDefaultPurpose
data-websitenoneYour website ID. Starts the SDK automatically. data-project is accepted as an alias.
data-servicethe script's originYour CLItrail service address.
data-enabledtruefalse turns the SDK off: nothing is read, stored or sent.
data-modestoragehandoff writes no OPFS receipt and reads or writes no web storage; prepared commands carry a token instead, with the source the current page's URL and referrer show.
data-measurement-idnoneA GA4 measurement ID to read client and session IDs from, in addition to the ones found automatically.
data-url-idautomaticA page ID registered for the website. Most sites leave it unset and use the automatic ID.

#Start it yourself

Load browser.js without data-website and call init when your code is ready, for example after consent:

const attribution = await InstallAttribution.init({
  service: 'https://YOUR_SERVICE',
  project: 'YOUR_WEBSITE_ID',
  measurementIds: ['G-XXXXXXXXXX'],
  enabled: analyticsAllowed
});
OptionTypePurpose
servicestringRequired. Your CLItrail service address.
projectstringRequired. Your website ID.
enabledbooleanfalse returns { ok: false, reason: 'disabled' } and does nothing else.
measurementIdsstring[]GA4 measurement IDs to read. Combined with IDs from your GA4 destinations and the page's own tags, up to 10.
measurementIdstringA single GA4 measurement ID.
providersstring[]Which ad identifiers to read: google_ads, meta, tiktok, x (and ga4). Defaults to the website's setting, which starts with all five. The service keeps only the platforms that setting includes. GA4 IDs are read whenever a measurement ID is known.
modestring'handoff' writes no OPFS receipt and uses no web storage: the session source, Google's session attributes and the TikTok and X click dates come from the current page only and stay in memory.
getContextasync functionSupplies GA4 IDs yourself instead of asking gtag. Return { clientId, sessionId } for the first measurement ID, or { identities } to replace every automatic identifier. Answers after 2 seconds are ignored.
urlIdstringA registered page ID; see data-url-id.

#The result

On success, init (and InstallAttribution.ready) resolves to { ok: true, urlId, hasAnalyticsContext, expiresAt }. hasAnalyticsContext is true when a GA4 client ID was captured; expiresAt is when the receipt expires, in milliseconds. It never rejects. On failure it resolves to { ok: false, reason }:

ReasonCause
disabledenabled: false or data-enabled="false".
invalid_configurationThe website ID or URL-ID is not 1 to 64 letters, digits, _ or -.
handoff_selectedHandoff mode: no receipt was written. prepareInstall still works.
opfs_unavailableThe browser has no OPFS, or the page is not a secure context.
an error nameFor example TimeoutError, or Error when the service refused the visit. Check that the page's origin is registered.

#What the SDK reads

Only what the page can already see and, for ad platforms, only for the providers in effect: init's providers option, or else the website's setting. The service drops the identifiers of platforms the website's setting leaves out. One exception: on a landing, Google's session attributes and the TikTok and X click dates are stored before the website's setting has loaded, so only init's providers option (leaving out google_ads, tiktok or x) or handoff mode prevents that. Missing identifiers are never made up.

ProviderIdentifiersWhere from
GA4client ID, session IDgtag('get', …) for each known measurement ID. IDs are found in gtag('config', …) commands and gtag.js script URLs, or come from your GA4 destinations and options.
Google Adsgclid, wbraid, gbraid, _gcl_aw, session attributesPage URL, first-party cookie, localStorage
Metafbclid, _fbp, _fbcPage URL, first-party cookies
TikTokttclid, _ttp, and ttclid_at when the SDK saw that click landPage URL (or a ttclid cookie), first-party cookie, localStorage
Xtwclid, and twclid_at when the click’s landing time is knownPage URL, the _twclid cookie X’s pixel sets, localStorage
Session sourceutm_source, utm_medium, utm_campaign, utm_term, utm_content, utm_id, external referrerPage URL, document.referrer

The page URL and referrer are sent too; the service keeps only their origin and path. The SDK also sends a few coarse details its own request headers may hide, for the browser and system breakdowns and reconstruction: the low-entropy user-agent client hints and, in Chromium browsers, the CPU architecture and bitness; whether the browser is Brave; the number of touch points (an iPad asking for the desktop site); the time zone and the language. See What is collected for what is stored.

#Google session attributes

When a landing URL carries a gad_ parameter, gclid or gbraid, the SDK builds Google's session attributes: every gad_* parameter, session_start_time_usec and landing_page_user_agent, encoded as base64url JSON. The landing URL and referrer are left out, as Google asks when they cannot be sent in full. The value is stored in localStorage under google_session_attributes, the key Google's own helper uses, unless init's providers option leaves out google_ads (the website's setting has not loaded yet at that point). Its session_start_time_usec is the session source's landing time, which tells the landing session from later ones that merely reuse the stored value. In handoff mode it is built for the current page only and never stored. It is sent only when google_ads is among the providers in effect, and kept only when the website's setting includes it.

#TikTok and X click dates

TikTok's pixel keeps the latest ttclid in a cookie without a date. When a landing URL carries ttclid, the SDK stores { ttclid, at } in localStorage under clitrail_ttclid (unless init's providers option leaves out tiktok, or in handoff mode), and a later visit whose ttclid matches sends ttclid_at to the service. X's twclid works the same way, under clitrail_twclid; when X's own _twclid cookie carries the landing time, that time is used. The dates place each click for channels and destinations and are never sent to TikTok or X.

Follow your site's analytics preferences. Two patterns work:

  • Start after consent. Load browser.js without data-website and call InstallAttribution.init({ …, enabled: true }) once the visitor agrees.
  • Turn it off. data-enabled="false" or enabled: false stops all reading, writing and requests.

If a visitor changes their choice after a command was prepared, prepare the command again: prepareInstall({ enabled: false, … }) returns your original command unchanged.

#Global Privacy Control

When the browser sends Global Privacy Control (navigator.globalPrivacyControl === true), the SDK adds gpc: true to the visit and to handoff requests. The service stores the flag with the visit and never shares that visit with an ad platform:

  • Google Ads, Meta, TikTok and X Ads never receive a touch recorded under GPC. If no other touch remains in the journey, nothing is sent to them for that install.
  • GA4 and webhooks are your own measurement and still receive the install. Webhook touches carry "gpc": true.
  • A later handoff request with GPC marks an existing visit too. A request without GPC never clears the flag.
  • The dashboard marks these visits with a GPC badge in the visit list and in each install's journey.

#Custom context

Attach product-specific values, such as a plan or an experiment variant, with an adapter. Adapters are read when the visit is recorded, so register them before calling init, or in a script that runs right after the tag.

InstallAttribution.registerAdapter('experiment', async () => ({
  variant: 'b',
  plan: 'team'
}));

Names are 1 to 64 letters, digits, _ or -. Each adapter has 1 second to answer. The service keeps up to 10 adapters per visit, with up to 10 string fields each, of up to 256 characters. Webhooks with Include captured identifiers receive them under identifiers.custom.

#Your Content Security Policy

If your site sends a CSP, allow your CLItrail service for scripts and requests:

script-src 'self' https://YOUR_SERVICE;
connect-src 'self' https://YOUR_SERVICE;
worker-src 'self' blob:;

worker-src blob: is needed only for Safari before version 26, which writes OPFS files from a small worker.

#Browser storage

WhereNameContent
OPFSopfs-install-attribution.<website>.<url id>.jsonThe marker. Expires after 30 days.
sessionStorageclitrail_sourceThe tab session's source: referrer, UTM values and landing time.
localStoragegoogle_session_attributesGoogle's session attributes, when a Google Ads click landed.
localStorageclitrail_ttclidThe last TikTok click that landed and when: { ttclid, at }.
localStorageclitrail_twclidThe last X click that landed and when: { twclid, at }.

The session source is replaced when a page URL carries a utm_ or gad_ parameter or a click ID, or when the referrer is another origin. Your other domains count as another origin. In handoff mode (data-mode="handoff" or init's mode: 'handoff') the SDK writes no OPFS receipt and neither reads nor writes sessionStorage or localStorage; it still reads the ad and analytics cookies your own tags set. A handoff then carries only what the current page's URL and referrer show, so a campaign that landed on an earlier page of the tab is lost after internal navigation. Blocked storage never breaks the page; the SDK records less.

The SDK is 18.0 KB, or 6.6 KB compressed with gzip.