fix(lifecycle_emails): map experiment action_taken to friendly text - #74
Merged
Merged
Conversation
Same bug class as worker#73 (make_permanent_endpoint): the worker email
template was reading params["action_taken"] from the dashboard's
experiments.converted call and inlining the raw snake_case identifier
straight into the body. Users would receive
"We noticed you checkout_started — nice."
in their inbox — exposing the dashboard's internal action constant as
if it were a past-tense English phrase.
Fix mirrors friendlyMakePermanentSource: a friendlyExperimentAction
helper maps each emit-site value to a user-facing phrase; unknown
values render as empty so the surrounding template clause disappears
entirely (better than leaking a future snake_case enum). Same treatment
for the {{ .Experiment }} field (the upgrade_button id was leaking into
"Experiment: upgrade_button (B)").
Mappings shipped:
- action_taken
- "checkout_started" → "started the checkout flow"
- "overview_upgrade_clicked" → "clicked Upgrade on the dashboard"
- experiment
- "upgrade_button" → "the Upgrade button rollout"
- "onboarding_v2" → "the new onboarding flow"
Registry-iterating regression tests (CLAUDE.md rule 18) enumerate every
emit site live in dashboard + instanode-web today; a future emitter that
adds a value without a friendly mapping fails the test before reaching
production. Plus a safety-net test asserting an unknown action drops
the clause entirely rather than leaking the raw enum.
Coverage block:
Symptom: "We noticed you checkout_started — nice." in email
Enumeration: grep -rn '"action_taken"' api/ worker/ ; grep -rn 'action:\s*'"'"'[a-z_]' dashboard/src instanode-web/src
Sites found: 2 emit values (checkout_started, overview_upgrade_clicked) from 4 call sites
Sites touched: 2 (both mapped in friendlyExperimentAction)
Coverage test: TestRenderExperimentConversion_ActionIsFriendly enumerates emit values; TestRenderExperimentConversion_UnknownActionDropsClause guards the safety net
Live verified: awaiting user verification — emit path requires a real /experiments/converted call from a logged-in dashboard session; local renderer test green (ok instant.dev/worker/internal/jobs 1.349s)
mastermanas805
force-pushed
the
fix/email-experiment-friendly-action
branch
from
May 31, 2026 05:48
2575df8 to
fc6efbf
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Same bug class as worker#73 (
make_permanent_endpoint): the worker email template was readingparams["action_taken"]from the dashboard's/experiments/convertedcall and inlining the raw snake_case identifier straight into the user-facing body.Users would receive:
— exposing the dashboard's internal action constant + the experiments registry id as if they were English phrases.
What changed
friendlyExperimentAction()helper (mirrorsfriendlyMakePermanentSource) maps every dashboard/instanode-web emit value to a user-facing phrase. Unknown values → empty string → template clause drops entirely.friendlyExperimentName()helper does the same for{{ .Experiment }}.renderExperimentConversionroutes both fields through the helpers.Mappings shipped:
action_takencheckout_startedaction_takenoverview_upgrade_clickedexperimentupgrade_buttonexperimentonboarding_v2Regression guards (CLAUDE.md rule 18 — registry-iterating)
TestRenderExperimentConversion_ActionIsFriendly— enumerates every action value emitted bydashboard/src/components/UpgradeButton.tsx,dashboard/src/pages/OverviewPage.tsx, and theinstanode-websiblings. A future emitter that adds a value without a friendly mapping fails this test before the broken copy reaches a real inbox.TestRenderExperimentConversion_UnknownActionDropsClause— safety net: a brand-new unmapped action drops the "We noticed you X — nice." clause entirely rather than leaking the raw enum.TestRenderExperimentConversion_ExperimentNameIsFriendly— same shape for the experiment-id field.Coverage block
Test plan
make gategreen locallygo test ./internal/jobs/ -run 'TestRenderExperimentConversion'→ PASS (3/3)go test ./internal/jobs/ -run 'TestLifecycleEmail|TestEveryEmailKindHasAGoRenderer'→ still green (regression unchanged)/experiments/convertedfrom the dashboard UpgradeButton, confirm the resulting email body says "started the checkout flow" not "checkout_started"