# Worked example: Acme Reports event list review

Acme Reports is a fictional B2B reporting product. Mixpanel is the product analytics tool and GA4 measures acquisition. A developer asked for review of the list in `example-acme-events-input.txt` before adding more events.

The stack before the review:

- Web app: the Google tag sends GA4's own `page_view`. Product events go to Segment as Track calls. The web source feeds Mixpanel and Segment's GA4 Cloud destination, which sends to GA4 through the Measurement Protocol.
- GA4 Cloud has one Custom Event mapping, triggered on `Sign Up`, `Report Created` and `plan_selected_pro`, with that mapping's Lowercase Event Name field set to false. GA4 therefore receives `Sign_Up`, `Report_Created` and `plan_selected_pro`. None of them is a key event (checked in the GA4 key events list).
- iOS app and backend: send to Mixpanel directly.

## 1. Lint the raw list

```bash
python3 scripts/lint_event_names.py assets/example-acme-events-input.txt
```

Output of `scripts/lint_event_names.py` on this file, run from the skill folder. It matches the self-test counts in `linter-rules.md`; rerun both after changing the script or its lists.

```text
ERROR N03 'Sign Up': not lowercase snake_case: uppercase letter, space
ERROR N03 'Clicked Export Button': not lowercase snake_case: uppercase letter, space
WARN  N15 'Clicked Export Button': UI gesture or element; name the business fact: clicked, button
WARN  N11 'report_exported_pdf': action 'pdf' does not look past tense
ERROR N03 'reportExported': not lowercase snake_case: uppercase letter
ERROR N03 'Report Created': not lowercase snake_case: uppercase letter, space
ERROR N08 'page_view': GA4 reserved event name
WARN  N11 'page_view': action 'view' does not look past tense
WARN  N11 'plan_selected_pro': action 'pro' does not look past tense
WARN  N13 'onboarding_step_3_completed': token '3' contains a digit; steps, versions, dates and IDs belong in properties
ERROR N04 'workspace_created_successfully_from_template_gallery': 52 characters; GA4 does not log event names over 40 and will not report a key event over 40
WARN  N07 'workspace_created_successfully_from_template_gallery': 6 tokens; a value or UI location probably belongs in a property
WARN  N11 'workspace_created_successfully_from_template_gallery': action 'gallery' does not look past tense
WARN  N16 'workspace_created_successfully_from_template_gallery': filler word: successfully
ERROR N09 'ga_report_shared': starts with a Firebase and GA4 reserved prefix (firebase_, google_, ga_)
ERROR N17 'report_created / Report Created': same name after normalising (report_created)
WARN  N19 'subscription_canceled, subscription_cancelled': mixes 'canceled' and 'cancelled'; keep one spelling in the vocabulary
WARN  N19 'signup_completed, Sign Up': mixes 'signup' and 'sign_up'; keep one spelling in the vocabulary

18 checked, 8 errors, 10 warnings
```

## 2. Apply the manual rules

Asking each producer when the event fires found what the script cannot:

- `Sign Up` (web) fires on the button click. `signup_completed` (iOS) fires when the account screen loads after signup. `user_registered` (backend) fires when the user row is committed, for every signup on every platform, and always has. Three names, two moments (R01, R02). Only the backend event is the fact.
- `Clicked Export Button` (web) is the intent. `report_exported_pdf` (web) and `reportExported` (iOS) are the same outcome from two clients, with the format in one name (R03, R04). The web app offered only PDF export before this release.
- `Report Created` (web) fires when the save request returns success. `report_created` (backend) fires when any report row is committed, from the web editor or the API.
- `page_view` is a custom event the web app sends to Mixpanel when a report page opens. It means "report viewed", not Google's page view.
- `member_invited` and `invite_accepted` use two words for one object (R05).
- `ga_report_shared` carries the destination in the name; the fact is "report shared".
- `subscription_cancelled` (web) fires on the cancel button; `subscription_canceled` (backend) fires when the subscription actually ends. Two facts, not two spellings of one.

## 3. Run the overlap test before choosing any history method

Evidence for 2026-10-05 to 2026-10-11 (one full week): daily counts from Mixpanel against the `users`, `reports` and `export_jobs` tables, then a join on `user_id`, `report_id` and `export_job_id`.

- `signup_completed` and `user_registered`: every `user_id` in `signup_completed` also appears in `user_registered`. `user_registered` alone is within 0.3% of new user rows every day; the sum is 22% to 26% above. Overlapping. The authoritative series is `user_registered`, which has always fired.
- `Report Created` and `report_created`: 61% of `report_id` values appear under both names. `report_created` alone is within 0.5% of new report rows. Overlapping; authoritative `report_created`.
- `report_exported_pdf` and `reportExported`: no `export_job_id` under both names, and the sum is within 1% of completed export jobs. Disjoint with each other. The new `report_export_succeeded` comes from the backend, so after its deploy it records the same exports as any client still sending an old name: overlapping from the boundary on.

## 4. Decisions

- `user_registered`: keep as canonical; producer backend.
  - Mixpanel receives it from the backend source only.
  - GA4 receives it as `sign_up`. Producer for GA4: the browser, which sends a `user_registered` Track call after the registration success response. The GA4 Cloud destination's Sign Up action forwards it, and the web source's Mixpanel mapping excludes it, so Mixpanel keeps one producer. Both producers fire after the same commit. Recorded M04 exception: the trigger matches Google's meaning.
- `signup_completed`: an overlapping duplicate. iOS 5.8.0 stops sending it and sends nothing in its place, because the backend already records every signup. Mixpanel reports that use it are repointed to `user_registered`; then its display name becomes "Signup Completed (duplicate, retired)" and it is hidden. No Lexicon merge. Events from older iOS versions are excluded from every metric.
- `Sign Up`: retire at the web release. It counted clicks, a different moment, so its history is joined to nothing. Its Mixpanel reports are repointed to `user_registered`, with a note that earlier click counts are not comparable, then it is hidden.
- `Clicked Export Button`: replace with `report_export_requested` (export job accepted). Add `report_export_succeeded` and `report_export_failed`, joined by `export_job_id`; `export_format` carries `pdf` or `csv`. `report_exported_pdf` and `reportExported` map to `report_export_succeeded` (backend).
- `Report Created`: an overlapping duplicate of `report_created` (backend, authoritative). The web stops sending it at the release; reports are repointed, then it is hidden. No merge.
- `report_generation_failed`: keep; add `report_generation_requested` and `report_generation_succeeded`.
- `page_view`: rename to `report_viewed`. GA4 keeps its own `page_view` from the Google tag.
- `plan_selected_pro`: `plan_selected` with `plan_code`.
- `onboarding_step_3_completed`: `onboarding_step_completed` with `step_number` and `step_key`.
- `member_invited`, `invite_accepted`: `member_invitation_sent`, `member_invitation_accepted`.
- `workspace_created_successfully_from_template_gallery`: `workspace_created` with `creation_source: template_gallery`. GA4 receives it from the browser after the success response, routed like `sign_up`.
- `ga_report_shared`: `report_shared`.
- `subscription_cancelled` (button): `subscription_cancellation_requested`, fired when the owner confirms. `subscription_canceled` (backend): keep. US spelling recorded in the vocabulary.
- Key events: whether `sign_up` becomes a GA4 key event or feeds Google Ads is the conversion owner's decision. The conversion-goals-and-key-events skill is not installed, so the question goes to the user and is recorded as open in `decisions.md`. Nothing is marked as a key event in this review.

## 5. Lint the new names and their destination mapping

```bash
python3 scripts/lint_event_names.py assets/example-acme-mapping.csv --kind mapping \
  --objects assets/example-acme-objects.txt --actions assets/actions.txt
```

Expected: 23 checked, 0 errors, 2 warnings, each resolved with a reason in the registry:

- N05 on `subscription_cancellation_requested` (35 characters): within GA4's 40, and a shorter name would break the shared `subscription_cancellation` object.
- M04 on `ga4:sign_up`: the trigger is the committed registration, which matches Google's meaning; `method` is mapped.

## 6. Plan the renames that touch history

Boundaries: backend deploy 2026-11-03T08:00:00Z, web release 2026-11-03T09:00:00Z, iOS 5.8.0.

- Mixpanel, `signup_completed` and `Report Created`: history method hide. Totals across all history come from `user_registered` and `report_created` alone, so nothing is double counted before or after the boundary.
- Mixpanel, exports: date split. Before the backend boundary, read `report_exported_pdf` plus `reportExported` (disjoint with each other). After it, read `report_export_succeeded` only; old names sent after the boundary, including by iOS versions below 5.8.0, are excluded. Annotate that history before the boundary has no web CSV exports.
- Mixpanel, `page_view` becomes `report_viewed`, and the other renamed events (`plan_selected_pro`, `onboarding_step_3_completed`, `member_invited`, `invite_accepted`, `workspace_created_successfully_from_template_gallery`, `ga_report_shared`): date split, old name before each producer's boundary and new name after. The old `page_view` gets the display name "Report Viewed (legacy)" so nobody confuses it with GA4's page view. No merges, so no further overlap evidence is needed.
- Mixpanel, `subscription_cancelled` to `subscription_cancellation_requested`: a semantic change (button click to confirmed cancellation). The old series is read on its own and the break is annotated; the two are never joined.
- GA4: before the release it received `Sign_Up` (a click), `Report_Created` and `plan_selected_pro`; after it, `sign_up` (the committed registration), `workspace_created` and `plan_selected`. `Sign_Up` to `sign_up` is a semantic break as well as a rename: annotate it in reports and never chain the two into one trend. `Report_Created` stops. Explorations and audiences that use any of the three are listed with owners and read the old name before 2026-11-03T09:00:00Z and the new one after.
- GA4 verification: Segment's GA4 Cloud destination sends through the Measurement Protocol, so Modify event cannot bridge these names and DebugView does not show them. Check each new payload with the Measurement Protocol validation server, then confirm the names in Realtime.
- After the releases, each action reaches each destination under one name, and no metric adds a hidden duplicate to its authoritative series.

The resulting registry is `example-acme-registry.yaml`; the destination mapping for the lint script is `example-acme-mapping.csv`.
