Adds pseudonymous lead attribution to any website or landing page from a single script tag. A random token appears in that site's Google Analytics; the person behind it is resolved from our own database. No personal data ever reaches Google.
Visitor lands on a page carrying the snippet
|
+-- t.js mints a 22-character random token (128 bits)
|
+-- t.js finds the Calendly or HubSpot embed and threads the token through it
|
+-- visitor books or submits
|
+-- token + details -> our database (personal data stays here)
+-- token only -> dataLayer -> GTM -> GA4 (no personal data)
The token is a random label. It is not an encrypted or hashed version of anything, so there is nothing to decode and no key that could leak. It means nothing to Google and nothing to anyone reading the URL. Only our database can turn it back into a person.
Two steps. Ten minutes, once per site.
In the site's <head>, before any Calendly or
HubSpot embed markup:
<script src="https://track.siliconpathlaw.com/t.js?v=1"
data-site="pk_yoursite_xxxxxxxxxxxx"></script>
That is the whole code change. No per-page work, and a new landing page on an already-registered site needs nothing at all.
Each site needs its own data-site key. Ask a developer to run
php tools/track-newsite.php <slug> "<Name>" <domain>,
which prints the key, the SQL to register the site, and this checklist filled in.
No, and it does not need to be. It ships in the page source on every site. What protects the endpoint is a server-side check that the request really came from that site's registered domain, so a copied key cannot be used from anywhere else.
See section 4. It has to be done in each site's own container.
| On the page | What happens |
|---|---|
| Calendly inline widget | Token appended to the booking URL as utm_content |
| Calendly popup or badge | Same |
| Calendly started from JavaScript | Same, by wrapping Calendly's own init call |
| HubSpot form | Name, email and phone captured on submit |
| Nothing recognised | Token is recorded, nothing else happens. No errors |
Nothing is ever read from inside the Calendly window. That is another origin and cannot be read, which is why the token is passed in rather than details being pulled out.
For a custom form the script does not recognise:
// on successful submit
window.splTrack.lead({ name: 'A Name', email: 'a@example.com' });
// on a confirmed booking or purchase
window.splTrack.conversion();
// the current token, if you need it
window.splTrack.token();
Every site has its own GTM container and its own GA4 property, so this cannot be done once centrally. The names below are identical for every site, so it is the same checklist each time, never a judgement call.
DL - lead_token ·
Data Layer Variable Name: lead_token · Version 2
CE - booking_complete ·
Event name: booking_complete
booking_completelead_token = {{DL - lead_token}}CE - booking_complete
Lead Tokenlead_tokenGA4 custom dimensions are not retroactive. Events collected before the dimension exists are never associated with it, and that data cannot be recovered. Register the dimension first.
| Trap | Why it matters |
|---|---|
| Choosing User scope | User scope keeps only the most recent value per user, so a second booking overwrites the first. It must be Event scope. |
Mapping the token to user_id |
That turns pseudonymous event tagging into cross-session user identification, which is a different thing under GDPR. Never do it. |
| Judging it by the standard reports on day one | Reports lag 24 to 48 hours. Use DebugView to confirm arrival immediately. |
In this order. The third is the one that is easy to skip and must not be.
window.splTrack.token() returns 22 characters.
utm_content=
followed by that same token. Inspect the iframe element, not the fallback link.
booking_complete arrives with
lead_token populated.
SELECT * FROM leads ORDER BY created_at DESC LIMIT 5;
It is the only check that proves the token actually links to a person. Everything before it can pass while that link is silently missing.
Marketing sends a token from Analytics. One query answers it:
SELECT s.slug AS site, l.page, l.name, l.email, l.phone, l.created_at
FROM leads l
JOIN sites s ON s.id = l.site_id
WHERE l.token = 'PASTE_TOKEN_HERE';
The result includes which site produced it, so no guessing is needed first.
-- recent leads for one site
SELECT * FROM leads_eb1a ORDER BY created_at DESC LIMIT 50;
-- totals per site this month
SELECT s.slug, COUNT(*) AS leads
FROM leads l JOIN sites s ON s.id = l.site_id
WHERE l.created_at >= '2026-08-01'
GROUP BY s.slug ORDER BY leads DESC;
-- which landing pages convert
SELECT page, COUNT(*) AS leads FROM leads
WHERE site_id = 2 GROUP BY page ORDER BY leads DESC;
-- tokens issued but never completed (visitor left without booking)
SELECT COUNT(*) FROM leads WHERE email IS NULL AND name IS NULL;
Symptom first, since that is what you have.
t.js.data-site present and does it start with pk_?domain in the sites table exactly right? A request from an unregistered domain is refused.active = 1?204 for both success and refusal on purpose, so the browser cannot tell you which happened. The database is the only way to know.src: does it carry utm_content?Expected, and harmless. The first row is written when the page loads, the second when the booking completes. They share a token, so they are one row that was updated, not two leads.
| Where | What is held |
|---|---|
| Google Analytics | The token only. No name, email, phone, or hash of any of them |
| Our database | Name, email, phone, page, timestamp, and the token |
| Calendly | The token in the booking's UTM data, alongside whatever the visitor typed into Calendly itself |