Skip to content

feat(actions): warn when an action's event stopped arriving - #95863

Draft
posthog[bot] wants to merge 5 commits into
masterfrom
posthog-self-driving/featactions-warn-when-an-actions-event-36dd3d
Draft

feat(actions): warn when an action's event stopped arriving#95863
posthog[bot] wants to merge 5 commits into
masterfrom
posthog-self-driving/featactions-warn-when-an-actions-event-36dd3d

Conversation

@posthog

@posthog posthog Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Problem

  • An action whose event stopped arriving looks exactly like a healthy one, so people trust insights that quietly return nothing.
  • A step points at an event by name. Nothing rechecks that the name still receives data, or still has a definition after someone deletes it in data management.
  • isDefinitionStale already answers this, and the taxonomic filter already draws a "Stale" tag with it. Nothing under products/actions/ used it.

Changes

State Tag Tooltip says
Event last seen over 30 days ago Stale when PostHog last saw it, and that insights using the action stay empty
No event definition at all Not seen it was never sent or the definition was deleted, and the action matches nothing
Event arriving, or still loading nothing
  • The tag sits next to the step it affects, so a list of ten actions points at the one broken step rather than at the action.
  • Both surfaces render the same tag: the actions list Type column, and the Match Group header in the step editor.
  • Mechanical: a new actionEventHealthLogic batches every event name on a page into one event_definitions request; api.eventDefinitions.list gains the names filter the endpoint already supports.

Actions list

actions list

Step editor

step editor

Tooltip

tooltip

Not in this PR: the background health check in posthog/temporal/health_checks/, and a guard on EventDefinitionViewSet.destroy for definitions that action steps reference.

How did you test this code?

  • Screenshots above come from Storybook rendered in headless Chromium, from the new EventHealthWarning story and two new Action.stories.tsx stories.
  • New logic tests cover the three verdicts a wrong isDefinitionStale wiring would flip: fresh event silent, old event stale, unknown event missing. No existing test covered any of them.
  • Two more lock the request shape, because a warning per row is a request per row: a burst of names goes out as one request, and a resolved name is never asked for again.
  • Ran the products/actions Jest suite and hogli ci:preflight --fix. Frontend typecheck reports only errors that predate this branch, all in unrelated quill imports.

Automatic notifications

  • Publish to changelog?

Docs update

None. No documented workflow changes.

🤖 Agent context

Autonomy: Fully autonomous

  • Written by Claude Opus 5 in PostHog Desktop, from an inbox report about actions that rot silently.
  • Skills invoked: /writing-ui-components, /writing-tests, plus the frontend and copy conventions in AGENTS.md.
  • The report proposed a health column on the actions list. A column costs width in a narrow scene and cannot say which step broke, so the tag went next to the step summary instead.
  • No duplicate: gh pr list --state open --search over stale-event and action keywords found nothing covering this.
  • Public artifact: every fixture is invented. Nothing here carries material from the originating session.

Created with PostHog Desktop from this inbox report.

An action step points at an event by name. If that event stops being sent, or its
definition is deleted in data management, the action keeps looking healthy while it
matches nothing.

Show a "Stale" or "Not seen" tag next to the affected step, on the actions list and in
the step editor. Staleness reuses `isDefinitionStale`, the same helper the taxonomic
filter uses on its dropdown rows.

Generated-By: PostHog Desktop
Task-Id: 6f8463f7-4957-4a9a-9495-33c76d308a6d
@trunk-io

trunk-io Bot commented Sep 7, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@posthog

posthog Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

🦔 PostHog Review reviewed this pull request

Found 0 must fix, 4 should fix, 0 consider.

Published 4 findings (view the review).

Resolved comments: 4 fixed

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

Trunk lane — non-backend lane

This PR is assigned to the non-backend lane. It does not run backend Python tests and may merge in parallel with PRs in other lanes.

⚠️ Complexity (TypeScript) — 4 functions above the limit (max 16)

Cyclomatic complexity above the limit in changed typescript files (10 for production files, 15 for test files). Warn only: worth simplifying when you next touch these functions.

Function Location Complexity Limit
handleFetch frontend/src/lib/api.ts:7537 16 10
ActionStep products/actions/frontend/components/ActionStep.tsx:48 16 10
listLegacy frontend/src/lib/api.ts:2650 14 10
ScreenNameField products/actions/frontend/components/ActionStep.tsx:434 11 10
⚠️ Bundle size — 🔺 +2.2 KiB (+0.0%)

Uncompressed size of every built .js bundle, compared against the base branch.

Total: 68.62 MiB · 🔺 +2.2 KiB (+0.0%)

File Size Δ vs base
exporter/src/exporter/scenes/ExporterNotebookScene.js 3.46 MiB 🔺 +2.2 KiB (+0.1%)

Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report

Eager graph — within budget

How much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy import() / React.lazy chunks are not counted.

Root Eager (shipped) Δ vs base Budget
entry (logged-out pages, app bootstrap)
src/index.tsx
1.50 MiB · 22 files no change ███░░░░░░░ 33.2% of 4.51 MiB
logged-out boot: index + App + bootApp (preloaded by every page, including /login)
src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
5.25 MiB · 668 files 🔺 +3 B (+0.0%) █████████░ 93.3% of 5.63 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
7.67 MiB · 2,677 files 🔺 +3 B (+0.0%) █████████░ 87.4% of 8.77 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/layout/navigation-3000/navigationLogic.tsx stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/scenes/dashboard/dashboardLogic.tsx stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx

Largest files eagerly shipped from src/index.tsx
Size File
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
24.6 KiB ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js
6.3 KiB ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
4.5 KiB ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js
3.9 KiB ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js
1.4 KiB ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js
1.3 KiB src/RootErrorBoundary.tsx
1000 B src/index.tsx
912 B ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js
854 B src/scenes/ChunkLoadErrorBoundary.tsx
Largest files eagerly shipped from src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
Size File
276.1 KiB ../node_modules/.pnpm/posthog-js@1.427.2_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
267.7 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
255.2 KiB src/taxonomy/core-filter-definitions-by-group.json
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
104.4 KiB src/lib/api.ts
79.1 KiB src/products.tsx
68.8 KiB src/lib/lemon-ui/icons/icons.tsx
62.5 KiB src/lib/utils/eventUsageLogic.ts
38.8 KiB ../node_modules/.pnpm/@dnd-kit+core@6.0.8_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@dnd-kit/core/dist/core.esm.js
33.9 KiB ../node_modules/.pnpm/kea@4.0.0-pre.6_patch_hash=139b8d1f1304f9d9da452a9a1244c94ea679dbcb85687d8999563146879fb6f5_react@18.3.1/node_modules/kea/lib/index.cjs.js
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
276.1 KiB ../node_modules/.pnpm/posthog-js@1.427.2_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
267.7 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
255.2 KiB src/taxonomy/core-filter-definitions-by-group.json
153.8 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
104.4 KiB src/lib/api.ts
95.8 KiB ../packages/quill/packages/quill/dist/index.js
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
90.6 KiB ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js
79.1 KiB src/products.tsx

Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479

Toolbar bundle — eager 2.28 MiB within budget

What the toolbar ships to customer pages, measured from the esbuild output (minified, post-tree-shake). The eager set is the entry plus everything statically imported from it — fetched before any feature runs; deferred chunks load lazily. The eager guardrail is 5.72 MiB. Each output file must also stay below 10 MB, where CloudFront stops compressing it. The module boundary is enforced separately by check-toolbar-graph.

Metric Size Δ vs base Budget
Eager (shipped)
entry + static imports
2.28 MiB · 18 files no change ████░░░░░░ 39.9% of 5.72 MiB
Deferred (lazy) 2.11 MiB · 45 files no change n/a — loads on demand
Loader dist/toolbar.js 1.1 KiB no change █░░░░░░░░░ 5.8% of 19.5 KiB
Largest eagerly-shipped chunks
Size File
756.3 KiB dist/toolbar/toolbar-app-IHRJKSXI.css
603.1 KiB dist/toolbar/chunk-chunk-NCE4QL3M.js
484.7 KiB dist/toolbar/chunk-chunk-D33R7XDZ.js
136.5 KiB dist/toolbar/chunk-chunk-BXI27EUT.js
131.8 KiB dist/toolbar/chunk-chunk-FDH2IBXT.js
72.2 KiB dist/toolbar/toolbar-app-CJMMARKZ.js
69.0 KiB dist/toolbar/chunk-chunk-TSAL54PB.js
35.6 KiB dist/toolbar/chunk-chunk-VLYPUESV.js
21.0 KiB dist/toolbar/chunk-chunk-LPBZV4EM.js
6.8 KiB dist/toolbar/chunk-chunk-DV7IWQNF.js

Posted automatically by check-toolbar-size · sizes are toolbar output bytes (shipped, post-tree-shake) from the esbuild metafile

Dist folder size — 🔺 +27.5 KiB (+0.0%)

Total size of the built frontend/dist folder (all assets), compared against the base branch.

Total: 1457.46 MiB · 🔺 +27.5 KiB (+0.0%)

stamphog[bot]
stamphog Bot previously approved these changes Sep 7, 2026

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved.

Additive frontend-only feature (new warning component + batching logic) with no data model, API contract, auth, billing, or CI changes; covered by new unit tests and stories, so it's outside risky territory and safe on its own merits despite lacking a human review.

Gate mechanics and policy version
Gate Result
prerequisites all clear
deny-list no deny categories matched
size 275L, 7F substantive, 343L/8F incl. docs/generated/snapshots — within ceiling
tier T1-agent / T1d-complex (343L, 8F, two-areas, feat)
stamphog 2.0.0b4 .stamphog/policy.yml @ 2c9456b · reviewed head 2c9456b

@posthog

posthog Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

👋 Visual changes detected for this PR.

Review and approve in PostHog Visual Review

If these changes are unexpected, they may be caused by a flaky test or a broken snapshot on master. Don't approve — rerun the job or wait for a fix.

@trunk-io

trunk-io Bot commented Sep 7, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@posthog

posthog Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

PostHog Review alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review

Found 4 should fix.

Comment thread frontend/src/lib/api.ts
Comment thread products/actions/frontend/components/EventHealthWarning.tsx
Comment thread products/actions/frontend/logics/actionEventHealthLogic.ts Outdated
Comment thread products/actions/frontend/logics/actionEventHealthLogic.ts Outdated
The `names` filter on `event_definitions` splits every value it receives on
commas, so an event name that contains one is queried as two separate names and
never comes back. The health logic pre-filled each requested name with `null`,
so such a name kept its `null` and drew a "Not seen" tag claiming the action
matches nothing, on a step that matches fine.

The logic now leaves those names out of the batch, so no verdict is reached and
no tag is drawn. Changing the endpoint's documented comma-separated contract is
out of scope here.

Generated-By: PostHog Desktop
Task-Id: d85c83d9-9ca8-483b-a793-1e66cdc9b620
@stamphog
stamphog Bot dismissed their stale review September 7, 2026 04:19

New commits were pushed — dismissing the stamphog approval from an earlier head. This PR no longer qualifies for automatic review.

posthog Bot added 3 commits September 7, 2026 04:22
Action match groups are OR-ed at query time, so one group whose event stopped
arriving does not stop the other groups from matching. Both tooltip branches
claimed the whole action matches nothing, which is false on any multi-group
action.

The stale branch also claimed insights stay empty. An event is stale once
PostHog last saw it more than 30 days ago, so its history is still there and an
insight whose date range covers that history still returns rows.

The tooltip now speaks about the match group it sits next to, says the group
stops matching new events, and says recent results can be incomplete instead of
empty.

Generated-By: PostHog Desktop
Task-Id: d85c83d9-9ca8-483b-a793-1e66cdc9b620
Event names come from the customer, so one can be `constructor`, `toString`, or
`__proto__`. Held in normal objects those names broke three ways at once:

- `name in values.definitions` was true before anything resolved, because `in`
  reads the prototype chain, so the name was dropped from the batch and never
  requested.
- `eventHealthIssues[name]` returned the inherited value, which is truthy, so
  the component drew a "Not seen" tag on a healthy event.
- `resolved['__proto__'] = definition` re-parented the map instead of adding a
  key, so `__proto__` could never resolve and went out on every later burst.

Both maps are now created with `Object.create(null)`, which fixes the two reads
and the write together, so `in` needs no change.

Generated-By: PostHog Desktop
Task-Id: d85c83d9-9ca8-483b-a793-1e66cdc9b620
The loader put every unresolved event name into one GET. A page holds 50
actions, each action holds any number of match groups, and an event name holds
up to 400 characters, so a mature project could build a request line past the
~4k a proxy accepts. Nothing wrote to `definitions` on failure while
`requestedNames` kept every name, so every tag on the page stayed absent and
the next burst rebuilt the same oversized URL.

`propertyDefinitionsModel` batches against the same limit with a count cap of
50. A count cap does not bound this URL, because one 400-character name costs
as much as thirteen short ones, so the batch is split by encoded length
instead. Requests run one after another and merge into the same map, so a burst
that fits in one URL still goes out as one request.

Generated-By: PostHog Desktop
Task-Id: d85c83d9-9ca8-483b-a793-1e66cdc9b620
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.

0 participants