Skip to content

Hire Conversion Tracking

apps/hire reports five funnel events to Google Tag Manager. GTM turns them into GA4 events and Google Ads conversions. This page is the runbook for the console-side configuration, which lives outside the repository and cannot be recreated from the code alone.

Everything is pushed onto window.dataLayer by apps/hire/lib/analytics.ts.

EventFires whenParameters
hire_query_landingA visitor lands with a non-empty ?q= (or legacy ?query=)query_length
hire_email_submittedA work email is accepted and a code is sentchat_id, user_data
hire_email_verifiedThe code is verified and the session is createdchat_id, user_data
hire_candidates_shownCandidate results are first shown for a chatchat_id, candidate_count, is_broadened, user_data
hire_booking_openedThe contact request succeeds and the Cal.com embed appearschat_id, user_data

The GTM snippet also pushes landing_page_location and landing_page_referrer before anything else. The chat hook rewrites the URL to /chat/<id> as soon as it has read ?q=, dropping gclid, _gl and every utm_* with it; gtm.js loads asynchronously and may not have run by then, so that snapshot is the only copy of the landing URL the container can rely on.

No raw PII reaches the dataLayer. The ?q= text is free-form user input, so only its length is reported. An email only ever travels as user_data.sha256_email_address — the SHA-256 hex digest of the trimmed, lowercased address, which is the form Google Ads enhanced conversions expect.

Analytics can never break a chat. Every push no-ops when GTM has not created a dataLayer, and swallows its own errors. crypto.subtle is only available in a secure context, so on a plain-http origin events still fire — just without user_data.

WhereNameNotes
GitHub environments production / stagingHIRE_GTM_IDA variable, not a secret — a container id ships in the HTML
Railway (set by CI)NEXT_PUBLIC_GTM_IDRenamed on the way through .github/workflows/hire-deploy.yml
LocalNEXT_PUBLIC_GTM_IDOptional; see apps/hire/.env.example

Leaving it unset loads no container at all — no script, no noscript iframe, no dataLayer. That is what keeps local dev and staging out of the production property.

Two things to remember:

  • NEXT_PUBLIC_* is inlined at build time. Changing HIRE_GTM_ID requires a redeploy, not just a variable edit. It works at all only because CI sets Railway variables before railway up triggers the build.
  • A value that is not shaped like GTM-XXXXXXX is rejected and the container is simply not loaded, rather than interpolated into the inline script.

Use the same container as the marketing site (teros-dev/www, which reads PUBLIC_GTM_ID). Cross-domain measurement, the GA4 configuration tag, and the Ads tags then live in one place instead of being duplicated and drifting.

  • Variables — Data Layer Variables for chat_id, candidate_count, is_broadened, query_length, and user_data (as one object).
  • Triggers — one Custom Event trigger per event name above, matched exactly.
  • Tags — a Google tag (GA4) on initialisation; one GA4 Event tag per trigger; one Google Ads Conversion Tracking tag per conversion action, each with Enhanced Conversions on and its User-Provided Data variable pointed at user_data; a Conversion Linker on all pages.
  • Cross-domain — in the Google tag’s configuration settings list teros.dev, www.teros.dev, and hire.teros.dev.

Verify in Preview mode against both domains before publishing a version.

  • Admin → Data Streams → Configure tag settings → Configure your domains: add teros.dev, www.teros.dev, hire.teros.dev. Without this the hop from the marketing site starts a new session attributed to a referral.
  • Custom definitions: register chat_id, candidate_count, is_broadened, and query_length as event-scoped dimensions/metrics, or they are collected but not reportable.
  • Key events: mark hire_email_submitted, hire_email_verified, hire_candidates_shown, and hire_booking_opened. hire_query_landing stays a plain event — it is funnel entry, not a conversion.

Three conversion actions, fired by the GTM tags:

Conversion actionSource eventCategoryPrimary?
Hire — Email Submittedhire_email_submittedSubmit lead formSecondary
Hire — Candidates Shownhire_candidates_shownPage viewSecondary
Hire — Talk To Teamhire_booking_openedBook appointmentPrimary

Only the primary should drive bidding; the others exist to show where the funnel leaks. Set counting to “One” on all three — one visitor submitting twice is one lead.

Turn on Enhanced conversions for web with source “Google Tag Manager” and accept the customer-data terms.

Do not also import the same GA4 key events as Ads conversion actions. Running GTM-fired Ads tags and GA4 imports for the same moment double-counts every conversion. The GTM tags are the ones that carry the hashed email.

The marketing site sends visitors to https://hire.teros.dev/?q=... from src/components/home/RoleInputHero.astro. That navigation must forward gclid, gbraid, wbraid and any utm_* from the current URL, because a programmatic window.location.href assignment is not decorated by GA4’s automatic cross-domain linker. Without it, the ad click is lost at the domain boundary even though both domains report to the same property.

Google Consent Mode v2 is not implemented. There is no consent banner and no denied-by-default signalling, which leaves an EEA compliance gap. Track it separately.