HubSpot Trial Attribution #
Classification: Internal-Only
HubSpot attributes a contact’s traffic source (paid, organic, email, …) from
the hubspotutk visitor cookie its tracking script sets on www.gotofu.com.
That history only attaches to a contact when the visitor converts through a
HubSpot form. The trial CTA sends visitors to Clerk’s hosted
accounts.gotofu.com/sign-up, where no script can run, so without this
integration every trial contact showed up unattributed (ENG-7499).
How it works #
www.gotofu.comsetshubspotutkfor the whole.gotofu.comdomain.- The user signs up on the Clerk portal and lands on
app.gotofu.com/onboarding. - The webapp loads the same HubSpot tracking script (
HubSpotTrackinginapps/webapp/src/shared/components/hubspot-tracking.tsx), so the session continues into the app. - On the first onboarding visit of a self-serve variant (
fullorprefilled),useHubSpotTrialAttributionPOSTs to/api/onboarding/hubspot-attribution. - The route reads
hubspotutkfrom the request cookies, looks up the user’s email in Clerk and submits HubSpot’s hidden trial-signup form through the Forms API withcontext.hutk. HubSpot then merges the visitor history into the new contact. - On success the route stamps
hubspot_trial_attributed_aton the user’s ClerkpublicMetadata, so the form is never submitted twice for one user.
Sales-led onboarding (entities-only) and the OAuth return leg
(integrations-only) never submit.
Cookie consent #
Cookiebot runs in auto-blocking mode. The HubSpot script is tagged
data-cookieconsent="marketing" everywhere except /onboarding and
/sign-up, where it is ignore. That exemption is the agreed policy for the
trial flow: the source has to be tied to the account before the user has
answered the banner. After onboarding the marketing category governs it again,
and the user can change it under Account → Cookie Consent.
Cookiebot only blocks scripts that have not loaded yet, so a “reject” after
onboarding is also pushed into HubSpot directly: HubSpotTracking listens to
the Cookiebot events and, when marketing is refused, queues doNotTrack and
revokeCookieConsent (which deletes hubspotutk and the other HubSpot
cookies). Opting back in queues doNotTrack with track: true.
The attribution route honours an earlier refusal too: if the request carries
Cookiebot’s CookieConsent cookie with marketing:false, it answers
{ status: "skipped", reason: "marketing_consent_declined" } and never calls
HubSpot.
Configuration #
| Variable | Where | Purpose |
|---|---|---|
NEXT_PUBLIC_HUBSPOT_PORTAL_ID |
webapp build | Portal whose tracking script to load. Unset disables the script. |
HUBSPOT_PORTAL_ID |
webapp runtime | Portal for the Forms API submission. |
HUBSPOT_TRIAL_SIGNUP_FORM_ID |
webapp runtime | GUID of the hidden trial-signup form. |
HUBSPOT_FORMS_API_BASE_URL |
webapp runtime, optional | Defaults to https://api-eu1.hsforms.com (EU portal). |
If either runtime variable is unset the route answers { status: "disabled" }
and logs a warning, so a non-production environment never writes to the
marketing portal.
Troubleshooting #
- Contact created but unattributed: the response logged
hasVisitorCookie: false. The cookie was missing on the request, usually because Cookiebot onwww.gotofu.comblocked HubSpot before the user clicked the CTA, or the user came straight to the signup URL. HubSpot trial signup submission failedin the logs: the status and HubSpot’s body are attached. A400withINVALID_EMAILor a missing required field points at the form definition in HubSpot.- Nothing submitted: check the user’s Clerk
publicMetadataforhubspot_trial_attributed_at; a stamped user is skipped by design. Amarketing_consent_declinedresponse means the browser already carried a Cookiebot refusal, also by design.