Skip to content

The web SDK

Owner: Web / marketing Endpoint: POST /v1/collect Status: live in dev. SDK is about 1.5 KB gzipped, against a 3 KB budget.


<script src="https://t.makeabeeline.com/sdk.js" defer></script>
<script>
window.addEventListener("load", function () {
window.stitch = window.stitchInit({ collectorHost: "t.makeabeeline.com" });
});
</script>

Each property uses the collector on its own registrable domain. This is not a style preference:

SiteCollector
makeabeeline.comt.makeabeeline.com
app.makeabeeline.comt.makeabeeline.com
beelineequitynow.comt.beelineequitynow.com

Why a shared collector would break one brand entirely

Section titled “Why a shared collector would break one brand entirely”

If beelineequitynow.com posted to t.makeabeeline.com, that collector would be third-party to that origin. Safari would cap or block its cookie, and attribution on that entire brand would silently degrade — no error, no alert, just a slowly growing unattributed bucket.

The cookie is also set by the Worker over an HTTP header, never document.cookie: Safari’s ITP caps JS-set cookies at 7 days regardless of domain, against an acquisition lookback of 365 days.


  • Page views on load
  • SPA route changes — pushState, replaceState and popstate are hooked and emit a virtual pageview. app.makeabeeline.com is a single-page app, so without this the entire application funnel would be invisible
  • UTM parameters and click IDs — gclid, gbraid, wbraid, fbclid, ttclid, msclkid, li_fat_id, and since plan 33 Taboola’s tblci, Outbrain’s dicbo (and obclid), X’s twclid and Pinterest’s epik
  • Vendor pixel cookies — _fbp, _fbc, _ttp and _gcl_aw are read when the Meta, TikTok or Google pixels have set them and sent with each event; they are what those platforms match a conversion on when the click id is gone. Stitch sets none of them
  • Batching and retry — events flush on a 5-second interval and on pagehide via sendBeacon, with a localStorage retry buffer if a beacon is refused
window.stitch.track("calculator_used", { term: 30, product: "dscr" });

Most page events do not need this. Stitch maps URLs to event names through a config table, so adding an event is a database row rather than a deploy — marketing can add one without engineering. Use track() for things a URL cannot express: button clicks, form field completion, scroll depth.

A lead’s conversion goes to the ad platforms twice on purpose: once from the page by the platform’s own pixel, which carries the browser signals Stitch never stores, and once from Stitch’s server seconds later, which carries the value and lands whether or not the browser was blocked. The platform keeps one, if both carry the same id. The SDK mints it:

const id = window.stitch?.submission?.(); // one UUID per page, stable; undefined when the SDK is blocked
if (id) {
form.querySelector("[name=stitch_submission_id]").value = id; // a hidden field in the lead form
fbq("track", "Lead", {}, { eventID: id }); // Meta pixel, no value
ttq.track("SubmitForm", {}, { event_id: id }); // TikTok pixel, no value
}

Guard it as shown: with the SDK blocked, an unguarded window.stitch.submission() throws inside the submit handler, and input.value = undefined writes the string "undefined" into the field. Stitch ignores that string, null, the field’s own name and any id another lead already carries — a second lead posting the same page’s id gets no submission id rather than vanishing into the first lead’s event — so leave the field empty when there is no id.

The form handler passes the hidden field on with the lead, and whichever system emits the lead’s created event carries it as lead.attrs.submission_id (see lead-events.md); Stitch uses it as the lead’s event id. The application event stays server-only: its value is the value model’s, which the page does not know.

window.stitch.consent({ analytics: true, advertising: false });

Call it from your CMP’s change callback. Global Privacy Control is picked up on its own — from the request header at the edge, and from navigator.globalPrivacyControl as a backstop — so a browser sending GPC is honoured whether or not this is ever called.

Full detail, including the “Do Not Sell or Share” endpoint and what actually changes when someone refuses, is in consent.md.

  • It cannot read the anon_id. The _bid cookie is HttpOnly. An anon_id readable by page JS is a shareable identifier — paste that link into Slack and five people merge into one person. Server-side handlers read it from the request instead (see identity.md).
  • It does not identify people. Identification is server-to-server, via /v1/identify.
  • It does not count conversions. A /thank-you page hit is evidence someone probably submitted a form; the lead is whatever the system of record says it is (lead-events.md).
  • It is not a consent banner and will not become one. It reads a signal and enforces it; asking the question is a CMP’s job. See consent.md.

Links from makeabeeline.com to beelineequitynow.com cross a registrable domain boundary, so the cookie does not follow. Ask the collector for a short-lived signed token and append it:

const res = await fetch(
"https://t.makeabeeline.com/v1/decorate?to=beelineequitynow.com",
{ credentials: "include" }
);
if (res.status === 200) {
const { token } = await res.json();
link.href += (link.href.includes("?") ? "&" : "?") + "_bx=" + token;
}

The token is HMAC-signed and expires in 300 seconds. Both properties matter: an unsigned id in a URL is forgeable, and a long-lived one in a bookmarked link could merge strangers months later. The receiving Worker validates it and strips _bx from the URL via history.replaceState.

Only the opaque anon_id ever travels in a URL. Never email, phone or any other PII.

Terminal window
curl -sS -i -X POST https://stitch-collector-dev.magic-blocks.workers.dev/v1/collect \
-H "content-type: application/json" \
-H "origin: https://makeabeeline.com" \
-d '[{"event_id":"smoke-1","ts":1772539200000,"name":"page_view","url":"https://makeabeeline.com/rates?gclid=abc","referrer":null,"props":{}}]'

Expect 204, and a Set-Cookie with Domain=.makeabeeline.com, HttpOnly, Secure, SameSite=Lax and Max-Age=34560000.