Skip to content

feat(gtm): Spec 1E — qualified lead + drift guard (analytics-foundation 1e)#376

Merged
blove merged 7 commits into
mainfrom
gtm-spec-1e-qualified-lead-and-drift-guard
May 16, 2026
Merged

feat(gtm): Spec 1E — qualified lead + drift guard (analytics-foundation 1e)#376
blove merged 7 commits into
mainfrom
gtm-spec-1e-qualified-lead-and-drift-guard

Conversation

@blove
Copy link
Copy Markdown
Contributor

@blove blove commented May 16, 2026

Summary

Spec 1E closes analytics-foundation. Two deliverables carved out of Spec 1D:

  • `marketing:lead_qualified` server-side, fired from `/api/leads/route.ts` after the existing `captureLeadConversion` call when the enterprise gate passes: non-personal email domain + non-empty company + valid email. Properties: `{email_domain, company, source_page, track: 'enterprise'}`. `distinctId` matches the SHA-256 hash used by `captureLeadConversion` so PostHog stitches both events to the same person.
  • Code → taxonomy drift guard (`tools/posthog/code-taxonomy.spec.ts`). Regex scanner over `apps/` + `libs/` for event names fired via `posthog.capture`, `track`, `captureServerEvent`, or `analyticsEvents.`. Asserts every name is in `docs/gtm/taxonomy.md`. Catches the kind of rename drift that slipped through during Spec 1C (`recipe_start` → `recipe_opened`).

The qualification rules and the personal-email blocklist (19 free-mail domains) live in `@ngaf/telemetry/shared` so any future consumer can reuse them. No taxonomy changes — `marketing:lead_qualified` was already listed in `taxonomy.md` from Spec 0.

Spec & Plan

  • Spec: `docs/superpowers/specs/gtm/2026-05-16-analytics-foundation-1e-qualified-lead-and-drift-guard-design.md`
  • Plan: `docs/superpowers/plans/gtm/2026-05-16-analytics-foundation-1e-qualified-lead-and-drift-guard.md`

Notable

  • Implicit enterprise track. The `LeadForm` only lives on pricing/contact pages, so `track: 'enterprise'` is stamped as a property rather than collected as a UI field. If a future developer-track lead form ships, the qualifier needs a `track` parameter.
  • Privacy preserved. The event carries `email_domain` only — raw emails never leave the server boundary, and the distinctId is the SHA-256 hash matching `captureLeadConversion`.
  • Drift scanner is regex-based, not AST-based. Misses template-literal event names by design (taxonomy.md already forbids dynamic names). Catches >95% of real drift.

Test plan

  • `nx run telemetry:test` — green
  • `nx run posthog-tools:test` — 38 existing + 1 new (drift guard) tests pass
  • `cd apps/website && npx vitest run src/lib/analytics/server.spec.ts` — 5/5 pass (happy path, personal-domain, missing/blank company, malformed email)
  • Synthetic-drift sanity: temporarily adding `posthog.capture('marketing:never_exists', {})` makes the drift guard fail with a clear message; reverting restores green
  • Post-merge manual smoke: submit a real LeadForm with a corporate email and verify `marketing:lead_qualified` lands in PostHog with matching distinct_id to `marketing:lead_form_success`

🤖 Generated with Claude Code

blove and others added 7 commits May 16, 2026 10:32
…uard)

Closes analytics-foundation. Two deliverables carved out of Spec 1D:
- marketing:lead_qualified server-side, fired from /api/leads when the
  enterprise gate passes (non-personal email domain + non-empty company).
  Personal-email blocklist lives in @ngaf/telemetry/shared.
- Code → taxonomy drift guard (mirror of the existing insights guard).
  Regex scanner over apps/ + libs/ asserts every fired event name is
  documented in docs/gtm/taxonomy.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… lead + drift guard)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
19 free-mail domains in a ReadonlySet plus a case-insensitive predicate.
Used by the website's lead-qualification gate to filter out personal
email submissions before firing marketing:lead_qualified.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ared

Both the website (lead-qualification gate, this PR) and any future
consumer can import the blocklist + predicate from the published lib.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Symbolic ref for the new server-side event. Wiring lands in Tasks 1.2
and 1.3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…e leads

captureLeadQualified gates on getEmailDomain(email) being present,
isPersonalEmailDomain(domain) being false, and toSafeAnalyticsString(company, 200)
being non-empty. When all three pass, fires marketing:lead_qualified with
properties { email_domain, company, source_page, track: 'enterprise' }.

Wired from /api/leads/route.ts immediately after captureLeadConversion.
Five unit tests cover the gate matrix.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New node:test script that scans apps/ + libs/ for event-name literals
fired via posthog.capture(...), track(...), captureServerEvent({event}),
or analyticsEvents.<key>. Asserts every name is documented in
docs/gtm/taxonomy.md. Mirror of the existing insights → taxonomy guard.

Catches the kind of drift that slipped through during Spec 1C
(cockpit:recipe_start → recipe_opened rename).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cacheplane Ready Ready Preview, Comment May 16, 2026 7:36pm

Request Review

@blove blove merged commit 310b67e into main May 16, 2026
13 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant