Skip to content

fix(integration): capture on confirmed success, and stop over-flagging person processing - #388

Open
sarahxsanders wants to merge 4 commits into
mainfrom
posthog/capture-on-success-and-server-person-processing
Open

fix(integration): capture on confirmed success, and stop over-flagging person processing#388
sarahxsanders wants to merge 4 commits into
mainfrom
posthog/capture-on-success-and-server-person-processing

Conversation

@sarahxsanders

@sarahxsanders sarahxsanders commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Two instrumentation-quality bugs from @raquelmsmith, plus the audit checks that catch them.

  1. Events fired on submit intent, so a form the server rejected still counted as a success. Fixed: completion events go in the branch that runs after the response confirms success.

  2. Server Action forms had nowhere legal to capture from. useActionState never hands the client a response, and the React rule forbids reacting to state in an effect. Fixed: capture inside the action on the server; client fallback fires off the returned success state.

  3. Backend SDKs overwrote person properties and billed as identified. Fixed: server events that only record that something happened pass $process_person_profile: false.

  4. First version of that rule went too far — it flagged signup and subscription too, leaving no person profiles at all. Fixed: person-defining events are let through and paired with identify()/$set.

  5. wizard audit events couldn't resolve a single check. Nothing seeded its ledger, so every id came back unknown. Fixed: step 1 seeds its own.

  6. audit-identify only ever penalised a missing flag, never a flag on everything. Fixed: that check reads both ways now.

Also adds capture-fires-on-success to the audits, so people can find intent-fired events in code the wizard already wrote.

Testing

Four live wizard runs (Next.js, Fastify, Django ×2) via --local-context-mill; pnpm build && pnpm test green and warlock clean. On the Django app the flag went from 13/13 captures to 3/12 — only the activity events keep it — and identify()/$set went 0 → 8.

Checklist

  • Skills build, new text reaches all 40 variants
  • pnpm test passing
  • Warlock scan clean
  • Verified end-to-end against real apps

Land PostHog/wizard#1211 first — it seeds the new check id.


Created with PostHog Desktop

… intent

The capture step told the agent to instrument "the real user action — the
click or submit handler". That draws the line at handler-vs-render and never
at intent-vs-outcome, so a completion event landed at the top of the submit
handler and a form the server rejected still counted as a success. Inflated
event counts, and the inflation lands on the metrics people steer by.

Completion events now belong in the branch that runs after the awaited
response confirms success. Capturing the attempt as well is still fine where
the drop-off matters, under its own name — one name must never mean both.

Also closes an adjacent gap on the server side. Backend SDKs process a person
profile on every capture carrying a distinct id, and the runtime metadata they
attach overwrites what the browser set on that person, so an event captured
from a Linux host rewrites a macOS user's profile. Identified events also cost
more than anonymous ones. Server-side business events now pass
$process_person_profile false unless they deliberately update the person.

audit-identify already stated both rules, but only as after-the-fact checks —
the write-time prompts never got told, so the wizard could generate code its
own audit would flag. The wording here is lifted from that audit reference so
the two surfaces agree.

Generated-By: PostHog Desktop
Task-Id: 4fd61d6e-eb4e-498e-abfd-1d0b60d95a63
…tent

Two fixes to the same skill.

The ledger was never seeded. audit-events' overview claimed the wizard
pre-seeded it, but only `audit all` gets that treatment — `wizard audit events`
resolves through the generic skill program, which seeds nothing. With no
ledger, audit_resolve_checks rejects every id as unknown, so no check in the
skill could report anything. Step 1 now seeds it itself, the way
audit-attribution already does, and the overview says so instead of the
opposite.

Adds capture-fires-on-success to audit-events and to the comprehensive audit.
It flags completion-named events that fire before the awaited response
resolves, or outside the success branch, and names the branch the call belongs
in. This is the detection side of the capture-timing rule the integration
skills now write against — it matters because fixing the write-time prompt
only helps future runs, and anyone who already ran the wizard has no other way
to find the over-reporting sitting in their data.

Scoped to completion-named events on purpose. A blanket "not in a success
branch" rule would flag every legitimate intent event and make the check noise.

Generated-By: PostHog Desktop
Task-Id: 4fd61d6e-eb4e-498e-abfd-1d0b60d95a63
The success-branch rule assumed there is always an awaited response to branch
on. With a Server Action driven by useActionState, a form action prop, or a
mutation hook, the client never gets one — the outcome arrives as state. The
existing React rule forbids reacting to state in an effect, so between the two
rules the agent had no legal placement left, in the framework the wizard is
used with most.

Capture belongs inside the action itself, on the server, right after the
mutation succeeds: that is where the outcome is known, and it needs no client
state. Staying in the browser is the fallback, and then firing off the returned
success state is correct rather than the effect anti-pattern — called out
explicitly so the two rules stop contradicting each other.

Splits the React commandment back into two entries instead of one overloaded
line, so the original useEffect rule reads unchanged and the timing rule can
carry its own exception.

The audit checks pick up the same shape. A capture in the submit path of an
action-driven form has no await beside it to notice, so it would otherwise slip
past a reviewer looking only for misplaced awaits.

Generated-By: PostHog Desktop
Task-Id: 4fd61d6e-eb4e-498e-abfd-1d0b60d95a63
…up and billing

A live run against a Django SaaS app put `$process_person_profile: false` on
all 13 server captures, including user_registered, subscription_started,
subscription_plan_changed and subscription_canceled, with no identify() call
anywhere. The project would create no person profiles at all — no cohorts, no
person-property filters, no lifecycle insights, no funnel from first visit to
paying. Cheaper events, and nothing left to analyse.

The wording invited it. "Server-side business events that are not meant to
update person properties, which is most of them" reads as a default, and
"business event" covers a subscription as readily as a cron job.

The rule now splits on what the event is for. Events that only record that
something happened — a row written, a job run, a webhook received — take the
flag. Events that decide who the person is or what they are worth do not, and
are paired with the identify()/$set that records the change. Blanketing every
capture is called out as its own mistake.

audit-identify graded the same way in one direction only: it penalised a
missing flag and never a flag on everything, and its own worked example was
subscription_upgraded — an event that should reach the person. That check now
reads both ways.

Generated-By: PostHog Desktop
Task-Id: 4fd61d6e-eb4e-498e-abfd-1d0b60d95a63
@sarahxsanders
sarahxsanders requested a review from a team as a code owner September 4, 2026 21:24
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

🧙 Wizard CI

Run the Wizard CI and test your changes against wizard-workbench example apps by replying with a GitHub comment using one of the following commands:

Test all apps:

  • /wizard-ci all

Test all apps in a directory:

  • /wizard-ci ai-observability
  • /wizard-ci basic-integration
  • /wizard-ci mcp-analytics
  • /wizard-ci replay-vision
  • /wizard-ci revenue
  • /wizard-ci self-driving
  • /wizard-ci warehouse
  • /wizard-ci warehouse-seeded

Test an individual app:

  • /wizard-ci ai-observability/anthropic
  • /wizard-ci ai-observability/google-adk
  • /wizard-ci ai-observability/groq
Show more apps
  • /wizard-ci ai-observability/manual-capture
  • /wizard-ci ai-observability/openai
  • /wizard-ci ai-observability/openai-agents
  • /wizard-ci ai-observability/opentelemetry
  • /wizard-ci ai-observability/vercel-ai
  • /wizard-ci basic-integration/android
  • /wizard-ci basic-integration/angular
  • /wizard-ci basic-integration/astro
  • /wizard-ci basic-integration/django
  • /wizard-ci basic-integration/fastapi
  • /wizard-ci basic-integration/flask
  • /wizard-ci basic-integration/flutter
  • /wizard-ci basic-integration/javascript-node
  • /wizard-ci basic-integration/javascript-web
  • /wizard-ci basic-integration/laravel
  • /wizard-ci basic-integration/next-js
  • /wizard-ci basic-integration/nuxt
  • /wizard-ci basic-integration/python
  • /wizard-ci basic-integration/rails
  • /wizard-ci basic-integration/react-native
  • /wizard-ci basic-integration/react-router
  • /wizard-ci basic-integration/sveltekit
  • /wizard-ci basic-integration/swift
  • /wizard-ci basic-integration/tanstack-router
  • /wizard-ci basic-integration/tanstack-start
  • /wizard-ci basic-integration/vue
  • /wizard-ci mcp-analytics/custom-dispatcher
  • /wizard-ci mcp-analytics/typescript-sdk
  • /wizard-ci replay-vision/javascript-node
  • /wizard-ci replay-vision/next-js
  • /wizard-ci replay-vision/react-vite
  • /wizard-ci revenue/stripe
  • /wizard-ci self-driving/astro
  • /wizard-ci self-driving/fastapi
  • /wizard-ci self-driving/nuxt
  • /wizard-ci self-driving/react-router
  • /wizard-ci self-driving/sveltekit
  • /wizard-ci warehouse/monorepo-env
  • /wizard-ci warehouse/multi-source-next
  • /wizard-ci warehouse/stripe-node
  • /wizard-ci warehouse/zero-source
  • /wizard-ci warehouse-seeded/next-stripe
  • /wizard-ci warehouse-seeded/next-stripe-declined

Test against a wizard branch:

  • /wizard-ci all wizard:my-branch

Add wizard:<branch> to any command above to pin the wizard branch. It defaults to main.

Results will be posted here when complete.

@edwinyjlim

Copy link
Copy Markdown
Member

@sarahxsanders looks solid to me. let's run a few CI jobs beginning of next week

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.

2 participants