Skip to content

feat(observability): WS4 funnel custom events → New Relic (InstantFunnel)#259

Merged
mastermanas805 merged 3 commits into
masterfrom
ws4-funnel-analytics-events
Jun 5, 2026
Merged

feat(observability): WS4 funnel custom events → New Relic (InstantFunnel)#259
mastermanas805 merged 3 commits into
masterfrom
ws4-funnel-analytics-events

Conversation

@mastermanas805

Copy link
Copy Markdown
Member

What

Wires common/analyticsevent (merged in common #44) into the api so the conversion funnel (anonymous→provision→claim→paid) is recorded as a per-entity New Relic custom event (InstantFunnel), alongside the existing aggregate Prometheus counter instant_conversion_funnel_total. Closes the WS4 gap from OBSERVABILITY-AND-INTELLIGENCE-PLAN.md — the backend→NR custom-event bridge now emits at the funnel points.

Prometheus answers "how many"; the NR custom event answers "for which entity / cohort" — the KPIs anon→claimed (>2%) and claimed→paid (>20%) need a stable key (fingerprint bucket / teamId), which a counter can't carry.

Emitter construction / config

  • Package-level analyticsevent.Emitter in handlers (atomic, boxed for type-stable atomic.Value), default = noop.
  • Router builds it once at boot (wireAnalyticsEmitter) from ANALYTICS_BACKEND (default "noop" = inert; "newrelic" reuses the api's existing *newrelic.Application — no second NR connection).
  • Fail-open: the analyticsevent Wrap swallows panics and PII-sanitizes (allowlist) before any backend sees the attrs; emit calls add no error handling that could fail the request path.
  • noop-default IS the flag protection — inert in any env where NR isn't configured, so no separate feature flag is needed.

Funnel emit points wired (10; ADD, never replace the Prom counter)

Step Site
provision db.go / cache.go / nosql.go / vector.go / queue.go / storage.go / webhook.goNewX anon path
claim onboarding.go Claim
landing onboarding.go StartLanding
paid billing.go handleSubscriptionCharged

Enumeration: rg -F 'metrics.ConversionFunnel.WithLabelValues' → 10 sites; all 10 touched.

Attributes are PII-safe + low-cardinality: funnelStep, serviceName, tier, env, hashed fingerprint, opaque teamId. No raw email/token/connection string (allowlist backstop in the wrapper).

Observability (rule 25)

  • New Prom counter instant_analytics_emit_failed_total{reason} (nil_app = NR unconfigured) via the nr failure hook.
  • docs/OBSERVABILITY-FUNNEL-EVENTS.md documents the InstantFunnel event contract, NRQL starters, and the cohort='synthetic' exclusion for funnel analysis. The NR alert + dashboard tile live in the separate infra repo (no auto-apply).

Tests (test names)

  • internal/handlers/analytics_test.go: TestGetAnalyticsEmitter_DefaultsToNoop, TestSetAnalyticsEmitter_NilIgnored, TestRecordFunnelEvent_EmitsFunnelEventWithStepAndAttrs, TestRecordFunnelEvent_OmitsEmptyOptionalAttrs, TestRecordFunnelEvent_EachCanonicalStep, TestRecordFunnelEvent_DoesNotEmitPII, TestFunnelAttrs_ToMap_OnlyAllowlistedKeys, TestFunnelStepsMatchCanonical
  • internal/router/analytics_wiring_test.go: TestWireAnalyticsEmitter_DefaultNoop, TestWireAnalyticsEmitter_UnknownBackendDegradesToNoop, TestWireAnalyticsEmitter_NewRelicNilAppFiresFailureHook
  • internal/handlers/billing_funnel_event_test.go: TestBillingWebhook_SubscriptionCharged_EmitsPaidFunnelEvent (DB-backed, real webhook path)

Gate

make gate: build + vet clean; all touched-package tests pass. The handlers -p 1 run reds only on pre-existing NATS/customer-DB environmental flakes (TestDBNew 503 = customer-DB provisioner down, TestQueue "NATS health check failed — is the NATS pod running?", etc.) — verified identical on clean origin/master with these files stashed. CI (which has those services) is authoritative.

🤖 Generated with Claude Code

…nel)

Wire common/analyticsevent into the api so the conversion funnel
(anonymous→provision→claim→paid) is recorded as a per-entity New Relic
custom event (InstantFunnel) alongside the existing aggregate Prometheus
counter instant_conversion_funnel_total. Closes the WS4 gap: backend→NR
custom-event bridge now emits at the funnel points.

Emitter:
- Package-level analyticsevent.Emitter in handlers (atomic, boxed for
  type-stable atomic.Value), default = noop. Router builds it once at boot
  (wireAnalyticsEmitter) from ANALYTICS_BACKEND (default "noop" = INERT;
  "newrelic" reuses the api's existing *newrelic.Application). Fail-open:
  the analyticsevent wrapper swallows panics + sanitizes PII (allowlist).
  noop-default is the flag protection — no separate feature flag.

Emit sites (10, alongside — not replacing — the Prom counter):
- provision: db/cache/nosql/vector/queue/storage/webhook NewX (anon path)
- claim:     onboarding.Claim (anon→claimed)
- landing:   onboarding.StartLanding (top of funnel)
- paid:      billing.handleSubscriptionCharged (claimed→paid)

Attributes are PII-safe + low-cardinality: funnelStep, service, tier, env,
hashed fingerprint, opaque teamId. No raw email/token/connection string.

Observability (rule 25): new Prom counter
instant_analytics_emit_failed_total{reason} (nil_app = NR unconfigured) via
the nr failure hook; docs/OBSERVABILITY-FUNNEL-EVENTS.md documents the
InstantFunnel event + NRQL + the cohort='synthetic' exclusion. Alert +
dashboard tile live in the infra repo (no auto-apply).

Tests: recording-emitter assertions per step+attrs, noop-default no-error,
PII-not-emitted, registry-iterating allowlist guard, wire-contract step
guard, NR nil-app failure-hook → counter, and a DB-backed paid-funnel
event test through the real webhook path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mastermanas805 mastermanas805 enabled auto-merge (squash) June 5, 2026 02:14
mastermanas805 and others added 2 commits June 5, 2026 07:45
…e 69)

The 100%-patch gate flagged analytics.go:69 (the NewNoop() fallback) uncovered —
SetAnalyticsEmitter ignores nil so the existing tests always store a non-nil
box. Add an in-package test storing emitterBox{e:nil} directly to exercise the
fallback branch. analytics.go getAnalyticsEmitter now 100%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mastermanas805 mastermanas805 merged commit 94cc58c into master Jun 5, 2026
18 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