SURF-1415 feat(store): first-touch UTM persistence in the Surface tag#67
SURF-1415 feat(store): first-touch UTM persistence in the Surface tag#67paragmore wants to merge 2 commits into
Conversation
Persist landing-page attribution params (utm_*, ad click-ids) in a first-party cookie and merge them into the payload sent to form iframes, so paid-source UTMs survive internal navigation to whichever page hosts the form. Current-page params always win; utm_content alone or a same-origin referrer never claims the first-touch slot (internal CTA tags like ?utm_content=home_homepage-hero must not poison it). Write-once with a 30-day expiry, matching HubSpot Original Source semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
…in fallback - getPayload() now reads current-page params fresh so the direct notifyIframe() path and post-route-change payloads never let stale params override first-touch values - degrade oversized first-touch records (drop url/referrer, truncate values) instead of letting the browser reject the cookie - retry the cookie write host-only when the base-domain attribute is rejected (public-suffix hosts like *.github.io, IP hosts) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| record.params[key] = record.params[key].slice(0, FALLBACK_VALUE_LENGTH); | ||
| }); | ||
| serialized = JSON.stringify(record); | ||
| if (encodeURIComponent(serialized).length > MAX_ENCODED_COOKIE_BYTES) return; |
There was a problem hiding this comment.
Oversized Params Still Drop When a qualifying landing URL contains several long non-ASCII UTM values, the fallback can still leave the encoded JSON above
MAX_ENCODED_COOKIE_BYTES. This branch then returns without writing any cookie, so later pages receive no first-touch params at all. That preserves the attribution loss this size guard is meant to avoid; the fallback should keep degrading or preserve a minimal qualifying attribution record instead of silently dropping the capture.
Problem
Customers running paid campaigns lose attribution before a form is ever opened: the tag reads
window.location.searchfresh on everySTORE_UPDATE, so a visitor who lands on a paid landing page withutm_source/utm_medium/click-id params and then navigates loses them — especially when internal CTAs replace the query string with internal tags (e.g.?utm_content=home_homepage-hero). Verified in Yuma's prod data: across two LinkedIn campaign windows, zero genuine prospect submissions carried LinkedIn-paid params. Paid Social shows zero in their HubSpot; contacts land as Direct/Offline.Ticket: SURF-1415
Change
src/store/first-touch.ts(new): on page load, if the URL carries qualifying attribution params, persist{ params, url, referrer, at }in first-party cookiesurface_first_touch(30-day max-age, base-domain scoped viagetJourneyCookieDomain(), values capped at 256 chars).utm_source,utm_medium, or a click-id (gclid,fbclid,li_fat_id,msclkid,ttclid).utm_contentalone does not qualify — internal CTA tags would poison the slot.document.referreris same-origin (internal navigation), and is write-once until expiry (strict first-touch, matching HubSpot Original Source semantics).document.referreris stale on pushState so the same-origin guard can't protect that path.SurfaceStore.getPayload(): merge{ ...getFirstTouchParams(), ...this.urlParams }— current-page params win, first-touch fills gaps. Merging here (not insendPayloadToIframes()) also covers the directnotifyIframe()call on the embed partial-fill path.test/first-touch.html(new): manual test page showing the cookie and the merged payload params.Downstream (form-render
URL_DATA.params→ HubSpot UTM hidden fields) is unchanged — the merged params flow through the existing plumbing.Verification (Playwright against the built bundle)
?utm_source=linkedin&utm_medium=paid_social&utm_campaign=Test+Campaign&li_fat_id=test-123&utm_content=ad-variant-a→ cookie written with all params.?utm_content=home_homepage-hero→ merged payload keeps all LinkedIn params, current page'sutm_contentwins.?utm_source=sneaky-internal→ cookie NOT written.utm_content-only landing with no referrer → cookie NOT written.?utm_source=google&gclid=…while cookie set → stored record unchanged (write-once); merged payload shows current-page google params winning, first-touch fillingutm_medium.pnpm run typecheck+pnpm run buildclean;surface_tag.js/surface_embed_v1.jsrebuilt.Rollout note
Consumers load
@latestfrom jsDelivr — purgehttps://purge.jsdelivr.net/gh/trysurface/scripts@latest/surface_tag.min.jsafter merge to make it land same-day.🤖 Generated with Claude Code