feat(signals): Spawn fresh report on recurrence - #71981
Conversation
…recurrence Resolved inbox reports no longer reopen. When a new signal would have grouped into an already-resolved report, we start a fresh report instead and point it at the resolved one via grouped_from_resolved_report. The research agent for that new report is handed the resolved report's title/summary so it can judge whether the recurrence is a regression, a new dimension of the same issue, or distinct. Generated-By: PostHog Code Task-Id: 5056349e-21fd-45da-a06b-026d62c95f20
|
Hey @Twixes! 👋 It looks like your git author email on this PR isn't your
You can fix it for this repo with: git config user.email "you@posthog.com"Or set it globally with |
🤖 CI report
|
| File | Size | Δ vs base |
|---|---|---|
render-query/src/render-query/render-query.js |
21.33 MiB | 🔺 +8.6 KiB (+0.0%) |
posthog-app/_parent/products/posthog_ai/frontend/components/tool/builtinToolRenderers.js |
9.6 KiB | 🔺 +3.7 KiB (+61.9%) |
posthog-app/_parent/products/stamphog/frontend/scenes/StamphogScene/StamphogScene.js |
11.8 KiB | 🔺 +1.9 KiB (+19.7%) |
posthog-app/src/scenes/surveys/wizard/SurveyWizard.js |
68.6 KiB | 🟢 -1.5 KiB (-2.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.23 MiB · 22 files | 🔺 +564 B (+0.0%) | ███░░░░░░░ 28.6% of 4.29 MiB |
authenticated shell (every logged-in page)src/scenes/AuthenticatedShell.tsx |
8.15 MiB · 2,988 files | 🔺 +1.1 KiB (+0.0%) | █████████░ 88.1% of 9.25 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/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers 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 |
| 912 B | ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js |
| 789 B | src/scenes/ChunkLoadErrorBoundary.tsx |
| 762 B | src/index.tsx |
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
| Size | File |
|---|---|
| 281.3 KiB | ../node_modules/.pnpm/posthog-js@1.404.0/node_modules/posthog-js/dist/rrweb.js |
| 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 |
| 235.5 KiB | src/taxonomy/core-filter-definitions-by-group.json |
| 223.2 KiB | ../node_modules/.pnpm/posthog-js@1.404.0/node_modules/posthog-js/dist/module.js |
| 164.0 KiB | src/queries/validators.js |
| 154.3 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 |
| 105.8 KiB | src/lib/api.ts |
| 93.3 KiB | ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js |
| 93.2 KiB | ../packages/quill/packages/quill/dist/index.js |
Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479
✅ Dist folder size — 🔺 +355.6 KiB (+0.0%)
Total size of the built frontend/dist folder (all assets), compared against the base branch.
Total: 1336.91 MiB · 🔺 +355.6 KiB (+0.0%)
⚠️ Playwright — 1 failed
🎭 Playwright report · View test results →
❌ 1 failed test:
- level facet passes severityLevels to API (chromium)
These issues are not necessarily caused by your changes.
Annoyed by this section? Help fix flakies and failures and it will go green!
⚠️ Backend coverage — 89.0% of changed backend lines covered — 11 uncovered
🧪 Backend test coverage
Patch coverage — changed backend lines (products + core): ██████████████████░░ 89.0% (95 / 106)
| File | Patch | Uncovered changed lines |
|---|---|---|
products/signals/backend/temporal/agentic/report.py |
46.7% | 152–155, 158, 164–166 |
products/signals/backend/temporal/grouping.py |
86.7% | 758–759 |
products/signals/backend/artefact_schemas.py |
88.9% | 468 |
🤖 Agents: add a test covering the lines above, or note why under "How did you test this code?". Machine-readable gap list: the patch-coverage artifact on this run (gh run download 29598089691 -n patch-coverage), or the coverage-data block at the end of this comment.
Per-product line coverage (touched products)
| Product | Coverage | Lines |
|---|---|---|
platform_features |
██░░░░░░░░░░░░░░░░░░ 12.1% |
7 / 58 |
batch_exports |
████████░░░░░░░░░░░░ 39.7% |
8,416 / 21,225 |
demo |
███████████░░░░░░░░░ 56.2% |
1,497 / 2,663 |
warehouse_sources_queue |
████████████░░░░░░░░ 59.2% |
148 / 250 |
tasks |
█████████████░░░░░░░ 67.0% |
25,933 / 38,686 |
data_tools |
██████████████░░░░░░ 70.0% |
63 / 90 |
ai_gateway |
███████████████░░░░░ 75.0% |
9 / 12 |
signals |
████████████████░░░░ 79.3% |
19,298 / 24,350 |
data_modeling |
████████████████░░░░ 80.0% |
4,834 / 6,045 |
cdp |
████████████████░░░░ 80.7% |
3,118 / 3,864 |
wizard |
████████████████░░░░ 82.5% |
772 / 936 |
notebooks |
█████████████████░░░ 84.8% |
6,903 / 8,141 |
cohorts |
█████████████████░░░ 86.2% |
4,065 / 4,717 |
agent_platform |
█████████████████░░░ 86.4% |
3,807 / 4,405 |
actions |
█████████████████░░░ 86.6% |
717 / 828 |
product_tours |
█████████████████░░░ 87.5% |
1,266 / 1,447 |
exports |
██████████████████░░ 88.4% |
6,933 / 7,842 |
visual_review |
██████████████████░░ 88.5% |
5,565 / 6,287 |
business_knowledge |
██████████████████░░ 88.5% |
4,400 / 4,969 |
conversations |
██████████████████░░ 89.0% |
16,183 / 18,186 |
mcp_analytics |
██████████████████░░ 89.2% |
2,514 / 2,819 |
dashboards |
██████████████████░░ 89.3% |
5,841 / 6,540 |
engineering_analytics |
██████████████████░░ 89.3% |
5,407 / 6,053 |
error_tracking |
██████████████████░░ 89.9% |
9,758 / 10,854 |
alerts |
██████████████████░░ 89.9% |
4,045 / 4,499 |
early_access_features |
██████████████████░░ 90.1% |
1,031 / 1,144 |
streamlit_apps |
██████████████████░░ 90.4% |
2,501 / 2,767 |
slack_app |
██████████████████░░ 90.6% |
9,018 / 9,955 |
links |
██████████████████░░ 90.6% |
183 / 202 |
marketing_analytics |
██████████████████░░ 90.8% |
11,514 / 12,684 |
stamphog |
██████████████████░░ 91.0% |
3,993 / 4,387 |
product_analytics |
██████████████████░░ 91.3% |
5,757 / 6,304 |
mcp_store |
██████████████████░░ 91.8% |
3,685 / 4,012 |
data_warehouse |
██████████████████░░ 92.4% |
18,626 / 20,158 |
notifications |
███████████████████░ 92.7% |
1,031 / 1,112 |
web_analytics |
███████████████████░ 92.7% |
13,535 / 14,597 |
workflows |
███████████████████░ 92.8% |
5,482 / 5,909 |
ai_observability |
███████████████████░ 92.8% |
14,916 / 16,077 |
surveys |
███████████████████░ 93.0% |
5,724 / 6,157 |
posthog_ai |
███████████████████░ 93.2% |
1,325 / 1,421 |
approvals |
███████████████████░ 93.3% |
3,395 / 3,640 |
reminders |
███████████████████░ 93.4% |
468 / 501 |
tracing |
███████████████████░ 93.4% |
2,545 / 2,724 |
managed_migrations |
███████████████████░ 93.8% |
1,220 / 1,300 |
legal_documents |
███████████████████░ 94.1% |
1,568 / 1,667 |
endpoints |
███████████████████░ 94.1% |
8,606 / 9,143 |
revenue_analytics |
███████████████████░ 94.5% |
3,598 / 3,809 |
skills |
███████████████████░ 94.5% |
2,881 / 3,049 |
messaging |
███████████████████░ 94.5% |
2,530 / 2,677 |
review_hog |
███████████████████░ 94.6% |
6,789 / 7,174 |
logs |
███████████████████░ 95.2% |
9,439 / 9,911 |
experiments |
███████████████████░ 95.7% |
24,417 / 25,527 |
replay_vision |
███████████████████░ 95.8% |
13,776 / 14,383 |
growth |
███████████████████░ 95.8% |
2,837 / 2,960 |
annotations |
███████████████████░ 96.2% |
732 / 761 |
feature_flags |
███████████████████░ 96.3% |
16,196 / 16,822 |
user_interviews |
███████████████████░ 96.4% |
2,242 / 2,325 |
warehouse_sources |
███████████████████░ 96.5% |
284,730 / 295,203 |
access_control |
███████████████████░ 96.8% |
849 / 877 |
customer_analytics |
███████████████████░ 97.2% |
7,482 / 7,700 |
data_catalog |
███████████████████░ 97.4% |
2,303 / 2,365 |
analytics_platform |
████████████████████ 98.0% |
2,102 / 2,145 |
metrics |
████████████████████ 98.2% |
2,404 / 2,448 |
pulse |
████████████████████ 98.4% |
2,017 / 2,049 |
live_debugger |
████████████████████ 99.2% |
613 / 618 |
field_notes |
████████████████████ 99.4% |
158 / 159 |
Report-only. Patch coverage = changed backend lines covered vs origin/master. Sorted lowest first.
Known gaps: lines covered only by Temporal tests show as uncovered; core line numbers may drift if master changed the same file.
⚠️ MCP snapshots — 1 updated (1 modified, 0 added, 0 deleted)
Snapshots: MCP unit test snapshots updated
Changes: 1 snapshots (1 modified, 0 added, 0 deleted)
What this means:
- Snapshots have been automatically updated to match current output
Next steps:
- Review the changes to ensure they're intentional
- If unexpected, investigate what caused the output to change
⚠️ Django migration SQL — 1 new migration to review
We've detected new migrations on this PR. Review the SQL output for each migration:
products/signals/backend/migrations/0063_alter_signalreportartefact_type.py
BEGIN;
--
-- Alter field type on signalreportartefact
--
-- (no-op)
COMMIT;Last updated: 2026-07-17 17:04 UTC (8d6d7da)
✅ Django migration risk — migration analysis complete
We've analyzed your migrations for potential risks.
Summary: 0 Safe | 1 Needs Review | 0 Blocked
⚠️ Needs Review
May have performance impact
signals.0063_alter_signalreportartefact_type
└─ #1 ⚠️ AlterField
Field alteration may cause table locks or data loss (check if changing type or constraints)
model: signalreportartefact, field: type, field_type: CharField
Last updated: 2026-07-17 17:05 UTC (8d6d7da)
There was a problem hiding this comment.
This review was skipped because it would exceed your organization's monthly flex usage limit. Raise the limit in billing settings or wait until the next billing period resets limits.
Generated-By: PostHog Code Task-Id: 5056349e-21fd-45da-a06b-026d62c95f20
… model field Replace the grouped_from_resolved_report FK with a symmetric related_report artefact (recurrence_of / recurred_as) written on both reports. Keeps the recurrence linkage without a schema-level model field, and generalizes to a reusable "related report" artefact type. The research agent resolves the recurrence_of link to load the resolved report as context. Generated-By: PostHog Code Task-Id: 5056349e-21fd-45da-a06b-026d62c95f20
🦔 Hogbox preview · ✅ ready▶ Open the preview
commit |
| "title_change": TitleChange, | ||
| "summary_change": SummaryChange, | ||
| "code_review": CodeReview, | ||
| "related_report": RelatedReport, |
There was a problem hiding this comment.
Medium: Related-report links are writable by callers
Registering this type without adding it to NON_WRITABLE_ARTEFACT_TYPES lets any authenticated team member create or modify a recurrence_of link. _load_resolved_report_context trusts the first such link, so a caller can inject an arbitrary report's title and summary into research; because report_id only requires a nonempty string, a non-UUID value also raises during the UUID query and causes both activity attempts to fail. Treat these pipeline-generated links as read-only, validate the identifier as a UUID, and verify that the referenced report is resolved before using it.
There was a problem hiding this comment.
I don't think it's a security worry for this artefact type to be writable - it would make sense, for instance, for an agent to decide this and mark the connection via artefact creation tool. Sure, it's a surface for injecting content, but only for agents that already inject by design.
PR overviewThis pull request adds support for spawning a fresh signal report when a recurrence is detected, including schema/model changes for linking the new report back to the original report context. There are still two open security concerns around recurrence report linking. Authenticated team members can currently create or alter related-report links in ways that may inject arbitrary report context into downstream research or create cross-team report relationships, so the main remaining risk is improper trust and tenant scoping of these generated links. No issues have been addressed yet, so the PR still needs targeted validation and access-control fixes before merge. Open issues (2)
Fixed/addressed: 0 · PR risk: 7/10 |
Per review, collapse the directional recurrence_of/recurred_as pair into one symmetric related_to artefact (report_id only) written on both reports — created_at ordering captures direction, so no relationship label is needed. The research reader identifies the recurrence source as whichever linked report is resolved. Regenerated OpenAPI types accordingly. Generated-By: PostHog Code Task-Id: 5056349e-21fd-45da-a06b-026d62c95f20
|
Yes — Veria identified the same underlying trust-boundary issue. The current |
Move the symmetric back-link from a one-off grouping helper into SignalReportArtefact.add_log, and route append()'s log types through add_log, so a related_to A→B always records B→A no matter which write path creates it. The reverse row is written via _create to avoid recursion. Generated-By: PostHog Code Task-Id: 5056349e-21fd-45da-a06b-026d62c95f20
| # row belongs to the same tenant. | ||
| cls._create( | ||
| team_id=team_id, | ||
| report_id=content.report_id, |
There was a problem hiding this comment.
Medium: Cross-team report mutation
An authenticated user can link their report to a known report UUID from another team. This reverse insert copies the caller's team_id while pointing its foreign key at the other team's report, creating cross-tenant state and affecting unscoped consumers such as the report's artefact_count. Before either insert, resolve the target report with id=content.report_id, team_id=team_id and reject foreign or deleted reports; perform both inserts in one transaction.
Addresses review findings on the user-driven resolve API. Four bot findings argued that resolve-then-refund reopens "repeatable free work" because resolved reports re-promote to candidate on new signals. They don't: RESOLVED -> CANDIDATE is absent from transition_to, and a signal matching a resolved report spawns a fresh report instead. ARCHITECTURE.md still documented the old behaviour (resolved -> candidate, RESOLVED re-promotion under the re-research cap), which #71981 removed — corrected, along with the code comments that repeated the claim. The defensible version of that concern was real and is now fixed: the refund guard keyed on SUPPRESSED, but a refunded merged-PR report deliberately stays RESOLVED, so it could be snoozed to POTENTIAL and re-enter research. The guard now refuses any refunded report going back to POTENTIAL, whatever its current status. Also: - Resolve out of the archive now requires the report to have been researched before it was archived, so candidate -> suppressed -> resolved can't land a report in RESOLVED with no title or summary. - Resolve attribution is resolved once per request instead of once per report, collapsing bulk_state's repeated X-PostHog-Task-Id lookup into one query. - output.pr_merged is now webhook-only: set_task_run_output no longer accepts it from callers, since signals reads it to decide refund finality and it was writable with a plain task:write token. - Reports expose implementation_pr_merged, so the inbox PR badge and the refund navigation read the real merge state instead of inferring "merged" from RESOLVED — which a direct resolve makes wrong. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Problem
In the Signals product, a resolved inbox report reopens itself whenever a new matching signal arrives — it re-promotes straight back into research. We want resolved reports to be terminal (one report ↔ one shipped fix). But a recurring issue still deserves attention, and its research should benefit from knowing the same thing was resolved before (it might be a regression, or a new dimension of the same issue).
Raised in this Slack thread.
Changes
assign_and_emit_signal_activitystarts a fresh report instead.resolved → candidatepipeline transition so resolved is enforced as terminal at the model level.related_toartefact type — an untyped link (just a report id) written symmetrically on both reports, so it's discoverable from either side and the grouping dataset can be reconstructed later, without a model field. Direction isn't tracked (the two rows' timestamps capture it). It's a general link type, usable beyond this flow.## Previously resolved reportblock telling the agent to judge regression vs. new dimension vs. distinct.related_tolabel to the activity-log renderer. Resolved reports were already terminal in the UI (no reopen/restore/snooze), so no other UI change was needed.How did you test this code?
The sandbox has no Postgres, so DB-backed tests were written but run in CI; pure-function tests ran green locally.
test_artefact_schemas.py(ran locally): the registry-parity test enforces the newrelated_totype is registered consistently across the model enum and content-schema map.test_research_prompt.py(ran locally): asserts the## Previously resolved reportblock appears when resolved context is passed and is absent otherwise.test_assign_and_emit_signal.py:test_resolved_match_spawns_new_report_and_leaves_resolved_untouched— a signal matching a resolved report creates a new POTENTIAL report, writes the symmetricrelated_toartefacts, and leaves the resolved report's status/count untouched. Narrowed the three READY/RESOLVED re-promotion tests to READY only.Docs update
Updated
products/signals/backend/report_generation/AGENTS.mdto document the artefact-based context path.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Authored by the PostHog Slack app (Claude Code). Skills invoked:
/django-migrationsand/writing-tests. Iterations, per reviewer feedback in the thread: (1) agrouped_from_resolved_reportself-FK → (2) a directionalrelated_reportartefact pair → (3) a single untypedrelated_toartefact (timestamps capture direction; the resolved link is identified by the linked report's status). The reopen decision lives in theassign_and_emit_signal_activityTemporal activity, so it covers both the sequential and parallel grouping paths with no workflow versioning/patch.Created with PostHog from a Slack thread