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.

Event Fires when Parameters
hire_query_landing A visitor lands with a non-empty ?q= (or legacy ?query=) query_length
hire_email_submitted A work email is accepted and a code is sent chat_id, user_data
hire_email_verified The code is verified and the session is created chat_id, user_data
hire_candidates_shown Candidate results are first shown for a chat chat_id, candidate_count, is_broadened, user_data
hire_booking_opened The contact request succeeds and the Cal.com embed appears chat_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.

Where Name Notes
GitHub environments production / staging HIRE_GTM_ID A variable, not a secret — a container id ships in the HTML
Railway (set by CI) NEXT_PUBLIC_GTM_ID Renamed on the way through .github/workflows/hire-deploy.yml
Local NEXT_PUBLIC_GTM_ID Optional; 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 action Source event Category Primary?
Hire — Email Submitted hire_email_submitted Submit lead form Secondary
Hire — Candidates Shown hire_candidates_shown Page view Secondary
Hire — Talk To Team hire_booking_opened Book appointment Primary

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.