Skip to content

feat(flags): read last_called_at sync calls from flag_evaluations behind a setting - #110150

Merged
trunk-io[bot] merged 5 commits into
masterfrom
haacked/last-called-sync-source
Oct 2, 2026
Merged

trunk-io[bot] merged 5 commits into
masterfrom
haacked/last-called-sync-source

Conversation

@haacked

@haacked haacked commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Problem

  • Flags in organizations that store flag calls only in flag_evaluations never get a last_called_at.
  • Stale-flag detection then marks every 100% rollout in those organizations stale after 30 days, and never marks a partial rollout stale.
  • New organizations will soon store $feature_flag_called only in flag_evaluations, while sync_feature_flag_last_called reads only distributed_events_recent.

Changes

  • A new setting, FEATURE_FLAG_LAST_CALLED_AT_SYNC_SOURCE, picks the table the sync reads: events (default) or flag_evaluations.
  • With flag_evaluations, the sync reads every team's calls from that one table, grouped on its typed flag_key column.
  • With flag_evaluations, the scan window ends 15 minutes before now instead of 60 seconds (FEATURE_FLAG_LAST_CALLED_AT_SYNC_FLAG_EVALUATIONS_BUFFER_SECONDS).
  • That table stamps inserted_at with the Kafka message time, so the buffer must also cover ClickHouse consumer lag. Otherwise a late row lands behind the checkpoint.
  • The extra delay does not affect stale detection, which uses a 30-day threshold.
  • The lock, the inserted_at checkpoint, time chunks, the row limit, the batched update and the metrics are unchanged.
  • Both sources share the existing checkpoint key. last_called_at only moves forward, so a small gap or overlap at the switch has no lasting effect.
  • An unknown value logs a warning on each run and reads events. The task does not fail the deploy over this knob.
  • A $feature_flag of JSON null stores the string 'null' in flag_key. The new query keeps it, so a flag keyed null still gets calls.
  • No migration, no API change, and no generated types. Behavior changes only when the setting is flag_evaluations.

Warning

Ingestion forks flag calls into flag_evaluations only on the real-time lanes. With the new source, calls on the historical and async lanes no longer move last_called_at.

Rollout: Cloud sets FEATURE_FLAG_LAST_CALLED_AT_SYNC_SOURCE=flag_evaluations after deploy, US first, then EU. Self-hosted keeps events, since it does not fill flag_evaluations.

Warning

After the flip, disabling the flag_evaluations ingestion fork also stops last_called_at, and the checkpoint moves past the missed calls. The charts change that sets the source also adds a step to the fork's produce-failure runbook: switch the sync source back to events before disabling the fork.

Note

A ClickHouse consumer stall longer than 15 minutes still drops calls behind the checkpoint. Only flags with no calls after the stall keep a stale last_called_at. A column that ClickHouse stamps at write time would close this gap, and it is a follow-up.

The flag_evaluations query was run on the production ClickHouse clusters that Celery queries, in US and EU, and returned results in both.

One 5-minute chunk, the most recent window a normal run reads, took about 2 seconds end to end in each region. The query timeout that fails a run is 120 seconds.

Unchecked: a catch-up run after a long gap reads windows up to 6 hours old. Those parts have merged more, because inserted_at is not in the table's sort key, so a chunk there may read more.

How did you test this code?

Ran hogli test products/feature_flags/backend/test/test_feature_flag_sync.py locally against real ClickHouse.

Test rationale: Every existing sync test mocks the ClickHouse query, so none catches a wrong table, column or filter. TestSyncFeatureFlagLastCalledSource runs the real query.

  • test_reads_flag_calls_from_the_configured_source puts one call only in events_recent and one only in flag_evaluations. It fails if a source reads the wrong table, or if the unknown-value fallback breaks.
  • Its calls happened two days ago but arrived ten minutes ago, so a filter on timestamp instead of inserted_at fails it.
  • test_call_without_a_flag_key_does_not_take_the_row_limit runs per source with a row limit of 1. It fails if the empty-key filter is dropped.
  • test_call_inside_the_buffer_waits_for_a_later_run stamps a call 5 minutes ago. The flag_evaluations case fails when its buffer is set back to 60 seconds.
  • A temporary pytest plugin rewrote the task SQL to check each test fails for its bug. The old events-only query fails both flag_evaluations cases.

Not checked: the warning log line for an unknown value, and metrics on the new path.

Release status

  • No feature flag controls this change
  • This change is behind a feature flag and is not available to users
  • This change makes a previously flagged feature available to everyone

Automatic notifications

  • Publish to changelog?

Docs update

None. No doc under docs/ covers the sync source.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: Claude Code, Claude Opus 5.5 (claude-opus-5-5)

  • Built from an implementation plan that set the setting, the query shape and the test scope.
  • Skills invoked: /django-migrations, /writing-tests, /writing-user-facing-copy, /writing-pr-descriptions, /simplify, /reviewing-with-coderabbit.
  • A test writer wrote the tests from the plan alone, without seeing the implementation. Two parameterized cases on mocked tests were dropped because they assert nothing that differs by source.
  • CodeRabbit CLI ran with --deep against master. It reported no findings.
  • Decisions along the way: reading both tables every run was rejected, to keep one query per chunk. The setting stays an env var, like the other FEATURE_FLAG_LAST_CALLED_AT_SYNC_* settings, rather than an instance setting.
  • An earlier commit changed the last_called_at help text, with a migration and regenerated types. That change was dropped, because the old text is still accurate.
  • Duplicate search found feat(flags): read flag last_called_at for data freshness #110143, which reads last_called_at but does not touch the sync.
  • No customer or session material is in the diff or this description.

https://claude.ai/code/session_01PV6xJfFwzQEXqiTE2d1t4i

@haacked haacked self-assigned this Oct 1, 2026
@trunk-io

trunk-io Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

😎 Merged successfully - details.

@haacked
haacked requested a balanced review from Copilot October 1, 2026 16:08
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

🤖 CI report

⚠️ Trunk lane — backend Python lane

This PR is assigned to the backend Python lane. It runs backend Python tests and may merge in parallel with PRs in other lanes.

✅ Complexity (TypeScript) — clean

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.

✅ Duplication (Python) — clean

New Python code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

✅ Duplication (TypeScript) — clean

New TypeScript code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

🚨 Comment density — 12% of added code lines are comments (17 of 147)

This section warns when comments are more than 3% of the code lines a PR adds, and alerts above 6%. Before agent-assisted PRs, the typical share was about 2%. Only full-line comments count. Docstrings, generated files, snapshots, migrations, and workflow files are left out.

Comments that restate the code, record how the change came about, or narrate the next line add noise for the next reader. Keep the comments that explain a reason the code cannot show, and remove the rest. See .agents/skills/writing-code-comments/SKILL.md for the house rules.

Files with the most added comment lines:

File Comment lines Added lines
posthog/settings/feature_flags.py 9 14
posthog/tasks/tasks.py 4 33
products/feature_flags/backend/test/test_feature_flag_sync.py 4 100

This check does not block merging. It updates on every push and clears when the share drops.

⚠️ Bundle size — 🔺 +51.2 KiB (+0.1%)

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

Total: 70.09 MiB · 🔺 +51.2 KiB (+0.1%)

File Size Δ vs base
posthog-app/_parent/products/posthog_ai/frontend/scenes/TaskTracker/components/ArtifactObjectEmbed.js 19.5 KiB 🔺 +19.5 KiB (new)
exporter/_parent/products/posthog_ai/frontend/scenes/TaskTracker/components/ArtifactObjectEmbed.js 6.8 KiB 🔺 +6.8 KiB (new)
posthog-app/_parent/products/workflows/frontend/Broadcasts/BroadcastScene.js 81.6 KiB 🔺 +6.5 KiB (+8.7%)
exporter/_parent/products/posthog_ai/frontend/scenes/TaskTracker/TaskTracker.js 43.4 KiB 🔺 +6.3 KiB (+17.1%)
posthog-app/_parent/products/posthog_ai/frontend/scenes/TaskTracker/TaskTracker.js 51.2 KiB 🔺 +6.3 KiB (+14.0%)
posthog-app/_parent/products/tasks/frontend/spaces/SpaceScene.js 66.6 KiB 🔺 +4.2 KiB (+6.8%)
exporter/src/exporter/scenes/ExporterNotebookScene.js 3.70 MiB 🔺 +1.8 KiB (+0.0%)
render-query/src/render-query/render-query.js 20.22 MiB 🔺 +1.8 KiB (+0.0%)

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.61 MiB · 22 files 🔺 +225 B (+0.0%) █████████░ 87.5% of 1.84 MiB
logged-out boot: index + App + bootApp (preloaded by every page, including /login)
src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
3.56 MiB · 630 files 🔺 +237 B (+0.0%) █████████░ 88.5% of 4.03 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
7.60 MiB · 2,452 files 🔺 +629 B (+0.0%) █████████░ 91.1% of 8.34 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
🟢 src/lib/lemon-ui/LemonMarkdown/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/components/RichContentEditor/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/components/CodeSnippet/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/taxonomy/core-filter-definitions-by-group.json 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
🟢 [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/index.tsx
1.3 KiB src/RootErrorBoundary.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
306.2 KiB ../node_modules/.pnpm/posthog-js@1.435.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
217.3 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
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
100.5 KiB src/lib/api.ts
91.6 KiB src/products.tsx
69.4 KiB src/lib/lemon-ui/icons/icons.tsx
40.1 KiB src/lib/utils/eventUsageLogic.ts
38.7 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
28.5 KiB src/scenes/scenes.ts
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
306.2 KiB ../node_modules/.pnpm/posthog-js@1.435.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
279.9 KiB src/taxonomy/core-filter-definitions-by-group.json
217.3 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
153.7 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.5 KiB ../packages/quill/packages/quill/dist/index.js
100.5 KiB src/lib/api.ts
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
91.6 KiB src/products.tsx
90.6 KiB ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/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

✅ Toolbar bundle — eager 2.19 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.19 MiB · 19 files 🔺 +237 B (+0.0%) ████░░░░░░ 38.2% of 5.72 MiB
Deferred (lazy) 2.10 MiB · 44 files 🔺 +28 B (+0.0%) n/a — loads on demand
Loader dist/toolbar.js 1.2 KiB no change █░░░░░░░░░ 6.0% of 19.5 KiB
Largest eagerly-shipped chunks
Size File
824.6 KiB dist/toolbar/toolbar-app-6MIBOQDK.css
656.8 KiB dist/toolbar/chunk-chunk-K7TY7VDS.js
259.4 KiB dist/toolbar/chunk-chunk-VEMKODO5.js
138.2 KiB dist/toolbar/chunk-chunk-2726HMK6.js
131.8 KiB dist/toolbar/chunk-chunk-FDH2IBXT.js
75.2 KiB dist/toolbar/toolbar-app-UDGNCPET.js
69.0 KiB dist/toolbar/chunk-chunk-TSAL54PB.js
35.6 KiB dist/toolbar/chunk-chunk-OLBPSHBJ.js
21.0 KiB dist/toolbar/chunk-chunk-GKFTYVXL.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 — 🔺 +460.1 KiB (+0.0%)

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

Total: 962.09 MiB · 🔺 +460.1 KiB (+0.0%)

ℹ️ MCP UI apps size — 33 app(s), 17633.1 KB JS

Built size of each MCP UI app (main.js + styles.css).

App JS CSS
debug 597.9 KB 199.3 KB
action 454.1 KB 199.3 KB
action-list 564.2 KB 199.3 KB
cohort 453.1 KB 199.3 KB
cohort-list 563.2 KB 199.3 KB
email-template 452.9 KB 199.3 KB
error-details 469.6 KB 199.3 KB
error-issue 454.5 KB 199.3 KB
error-issue-list 564.8 KB 199.3 KB
experiment 561.3 KB 199.3 KB
experiment-list 564.9 KB 199.3 KB
experiment-results 566.3 KB 199.3 KB
feature-flag 566.8 KB 199.3 KB
feature-flag-list 570.5 KB 199.3 KB
feature-flag-testing 457.3 KB 199.3 KB
inline-scan 453.6 KB 199.3 KB
insight-actors 562.3 KB 199.3 KB
invite-email-preview 452.3 KB 199.3 KB
llm-costs 559.3 KB 199.3 KB
session-recording 455.3 KB 199.3 KB
survey 454.7 KB 199.3 KB
survey-global-stats 561.9 KB 199.3 KB
survey-list 564.9 KB 199.3 KB
survey-stats 561.9 KB 199.3 KB
trace-span 453.5 KB 199.3 KB
trace-span-list 564.1 KB 199.3 KB
vision-observation-list 563.3 KB 199.3 KB
workflow 453.4 KB 199.3 KB
workflow-list 563.5 KB 199.3 KB
loops-review 457.8 KB 199.3 KB
query-results 774.1 KB 199.3 KB
render-ui 858.1 KB 199.3 KB
visual-review-snapshots 457.9 KB 199.3 KB
⚠️ Playwright — 1 flaky

🎭 Playwright report · View test results →

⚠️ 1 flaky test:

  • Edit mode button enters and exits edit mode (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 — 85.0% of changed backend lines covered — 10 uncovered

🧪 Backend test coverage

Patch coverage — changed backend lines (products + core): █████████████████░░░ 85.0% (60 / 70)

File Patch Uncovered changed lines
posthog/tasks/tasks.py 0.0% 1299, 1317–1320, 1380, 1385, 1421, 1437, 1452

🤖 Agents: add a test only if an uncovered line exposes a realistic regression that existing tests miss. Otherwise explain why no new test is needed under "How did you test this code?". Gap list: the patch-coverage artifact on this run (gh run download 36940776791 -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
demo ███████████░░░░░░░░░ 53.4% 1,445 / 2,707
data_tools ████████████░░░░░░░░ 61.2% 90 / 147
warehouse_sources_queue █████████████░░░░░░░ 65.7% 1,608 / 2,446
ai_gateway ███████████████░░░░░ 75.0% 9 / 12
aeo ███████████████░░░░░ 76.3% 617 / 809
batch_exports ████████████████░░░░ 81.2% 21,572 / 26,551
apm █████████████████░░░ 84.1% 1,306 / 1,553
cdp ██████████████████░░ 88.3% 4,559 / 5,164
ml_inference ██████████████████░░ 88.8% 539 / 607
mcp_analytics ██████████████████░░ 89.2% 5,038 / 5,651
product_tours ██████████████████░░ 89.3% 1,340 / 1,500
dashboards ██████████████████░░ 89.6% 6,924 / 7,727
notebooks ██████████████████░░ 90.2% 15,304 / 16,971
data_warehouse ██████████████████░░ 90.3% 14,298 / 15,839
signals ██████████████████░░ 90.4% 59,629 / 65,990
cohorts ██████████████████░░ 90.5% 8,534 / 9,434
streamlit_apps ██████████████████░░ 90.8% 2,684 / 2,956
managed_warehouse ██████████████████░░ 91.0% 10,252 / 11,263
data_modeling ██████████████████░░ 91.2% 10,562 / 11,584
tasks ██████████████████░░ 91.3% 78,944 / 86,502
today ██████████████████░░ 91.4% 894 / 978
exports ██████████████████░░ 91.7% 9,684 / 10,566
business_knowledge ██████████████████░░ 92.0% 8,446 / 9,180
engineering_analytics ██████████████████░░ 92.2% 11,497 / 12,475
ai_training ██████████████████░░ 92.2% 356 / 386
conversations ███████████████████░ 92.6% 29,137 / 31,466
early_access_features ███████████████████░ 92.6% 1,339 / 1,446
managed_migrations ███████████████████░ 92.7% 1,581 / 1,705
stamphog ███████████████████░ 92.8% 8,109 / 8,742
visual_review ███████████████████░ 92.9% 9,534 / 10,265
canvas ███████████████████░ 92.9% 7,155 / 7,703
approvals ███████████████████░ 93.0% 3,974 / 4,271
mcp_registry ███████████████████░ 93.1% 1,670 / 1,794
notifications ███████████████████░ 93.2% 1,144 / 1,228
error_tracking ███████████████████░ 93.2% 16,370 / 17,557
surveys ███████████████████░ 93.4% 6,644 / 7,113
slack_app ███████████████████░ 93.6% 14,560 / 15,557
autoresearch ███████████████████░ 93.6% 8,837 / 9,442
context_layer ███████████████████░ 93.8% 3,415 / 3,639
web_analytics ███████████████████░ 93.9% 23,680 / 25,229
billing_alerts ███████████████████░ 94.1% 2,094 / 2,226
mcp_store ███████████████████░ 94.3% 8,959 / 9,501
ai_observability ███████████████████░ 94.6% 24,974 / 26,409
alerts ███████████████████░ 94.6% 9,267 / 9,796
wizard ███████████████████░ 94.7% 6,150 / 6,496
workflows ███████████████████░ 94.7% 15,159 / 16,006
reminders ███████████████████░ 94.8% 760 / 802
review_hog ███████████████████░ 95.0% 11,750 / 12,362
annotations ███████████████████░ 95.1% 817 / 859
endpoints ███████████████████░ 95.1% 9,222 / 9,694
customer_analytics ███████████████████░ 95.2% 25,991 / 27,301
legal_documents ███████████████████░ 95.2% 2,311 / 2,427
marketing_analytics ███████████████████░ 95.3% 19,435 / 20,398
posthog_ai ███████████████████░ 95.3% 2,491 / 2,614
actions ███████████████████░ 95.5% 756 / 792
logs ███████████████████░ 95.5% 15,468 / 16,200
experiments ███████████████████░ 95.5% 32,931 / 34,481
data_catalog ███████████████████░ 95.6% 4,402 / 4,606
tracing ███████████████████░ 95.6% 3,536 / 3,699
replay_vision ███████████████████░ 95.6% 29,355 / 30,692
growth ███████████████████░ 95.7% 11,381 / 11,888
skills ███████████████████░ 95.8% 6,972 / 7,274
messaging ███████████████████░ 95.9% 3,824 / 3,989
product_analytics ███████████████████░ 96.0% 28,521 / 29,696
revenue_analytics ███████████████████░ 96.4% 1,889 / 1,959
user_interviews ███████████████████░ 96.5% 2,870 / 2,974
feature_flags ███████████████████░ 96.6% 26,872 / 27,815
access_control ███████████████████░ 96.7% 7,738 / 8,005
warehouse_sources ███████████████████░ 97.3% 468,809 / 481,716
data_quality ████████████████████ 97.5% 7,701 / 7,895
links ████████████████████ 97.9% 234 / 239
security ████████████████████ 98.0% 1,283 / 1,309
metrics ████████████████████ 98.0% 4,252 / 4,338
analytics_platform ████████████████████ 98.3% 2,784 / 2,833
pulse ████████████████████ 98.5% 2,046 / 2,078
live_debugger ████████████████████ 99.2% 626 / 631
field_notes ████████████████████ 99.4% 172 / 173

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.

⚠️ Django migration SQL — 1 new migration to review

We've detected new migrations on this PR. Review the SQL output for each migration:

products/feature_flags/backend/migrations/0023_alter_featureflag_last_called_at_help_text.py

/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/anyio/from_thread.py:119: SyntaxWarning: 'return' in a 'finally' block
  return result
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/structlog/stdlib.py:1166: UserWarning: Remove `format_exc_info` from your processor chain if you want pretty exceptions.
  ed = p(logger, meth_name, ed)  # type: ignore[arg-type]
2026-10-01T17:01:45.533706Z [error    ] Path must be a valid database or directory containing databases. [posthog.exceptions_capture] pid=8122 tid=140698970577792
Traceback (most recent call last):
  File "/home/runner/work/posthog/posthog/posthog/geoip.py", line 15, in <module>
    geoip: Optional[GeoIP2] = GeoIP2(cache=8)
                              ~~~~~~^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/django/contrib/gis/geoip2.py", line 116, in __init__
    raise GeoIP2Exception(
        "Path must be a valid database or directory containing databases."
    )
django.contrib.gis.geoip2.GeoIP2Exception: Path must be a valid database or directory containing databases.
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/sshtunnel.py:1040: SyntaxWarning: 'return' in a 'finally' block
  return (ssh_host,
/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/site-packages/langchain_core/_api/deprecation.py:27: UserWarning: Core Pydantic V1 functionality isn't compatible with Python 3.14 or greater.
  from pydantic.v1.fields import FieldInfo as FieldInfoV1
System check identified some issues:

WARNINGS:
?: (axes.W001) You are using the django-axes cache handler for login attempt tracking. Your cache configuration is however invalid and will not work correctly with django-axes. This can leave security holes in your login systems as attempts are not tracked correctly. Reconfigure settings.AXES_CACHE and settings.CACHES per django-axes configuration documentation.
?: (staticfiles.W004) The directory '/home/runner/work/posthog/posthog/frontend/dist' in the STATICFILES_DIRS setting does not exist.
BEGIN;
--
-- Alter field last_called_at on featureflag
--
-- (no-op)
COMMIT;

Last updated: 2026-10-01 17:02 UTC (f8ce8a2)

✅ Django migration risk — no migrations to analyze

No Django migrations need risk analysis.

@posthog

posthog Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

🦔 PostHog Review reviewed this pull request

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

Published 1 finding (view the review).

Resolved comments: 4 declined, 2 left for you

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The shared checkpoint skips existing flag-evaluation history during rollout, leaving affected flags incorrectly stale.

Review effort: Balanced
Findings: 1 High severity

Open (1)
What changed in this PR

Adds a configurable ClickHouse source for feature flag last_called_at synchronization.

Changes:

  • Adds events/flag_evaluations source selection with fallback logging.
  • Queries typed flag_key values from flag_evaluations.
  • Adds real ClickHouse coverage and updates generated API documentation.
File Description
services/​mcp/​src/​api/​generated.ts Updates generated field documentation.
products/​feature_flags/​frontend/​generated/​api.schemas.ts Updates generated frontend schema documentation.
products/​feature_flags/​backend/​test/​test_feature_flag_sync.py Tests source selection and key filtering.
products/​feature_flags/​backend/​models/​feature_flag.py Updates last_called_at help text.
products/​feature_flags/​backend/​migrations/​max_migration.txt Advances the migration marker.
products/​feature_flags/​backend/​migrations/​0023_alter_featureflag_last_called_at_help_text.py Records the help-text change.
posthog/​tasks/​tasks.py Selects and queries the configured source.
posthog/​settings/​feature_flags.py Defines the source setting and operational caveats.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread posthog/tasks/tasks.py Resolved
@greptile-apps

greptile-apps Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Retrigger

[High risk] Adds a configurable data source to the feature flag sync task.

The PR is not safe to merge until the missed-call paths and required production setting wiring are addressed.

Reviews (1) · Last reviewed commit: "feat(flags): read last_called_at sync ca..."

Comment thread posthog/tasks/tasks.py Resolved
Comment thread posthog/tasks/tasks.py Resolved
Comment thread posthog/settings/feature_flags.py Resolved
@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The sync task now reads flag calls from either events or flag_evaluations. Settings define the source, default, and source-specific time buffer. Both sources use the shared checkpoint. Tests cover source fallback, query behavior with empty flag keys, and scan timing.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to 1bc93

Enabling the new flag_evaluations source can leave some flags with stale "last called" times. Switching back to the events source does not repair them. The default stays on events, so this behavior affects only deployments that enable the new source. The checkpoint behavior should be resolved, or explicitly accepted, before Cloud is switched over.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 1bc93

The default behavior remains unchanged, and the inspected update path preserves team and flag identity. The main risk is operational: selecting a source that lacks calls, or falls too far behind, can leave usage timestamps inaccurate across teams. Switching back does not automatically recover missed windows. The inspected downstream effects are stale-flag status and cleanup guidance, not access-control decisions.

Retained concerns

  • Low · reliability · inferred: Selecting flag_evaluations globally can advance the shared checkpoint past calls missing from that source or arriving beyond its buffer. Switching back to events does not automatically replay those windows, weakening rollback recovery and potentially leaving misleading usage and cleanup guidance across teams. Actual exposure depends on deployment routing and producer coverage.
Security review details

Security Blast Radius

  • inferred — The source choice applies to the whole task run rather than individual teams. A producer-coverage mismatch can therefore affect usage metadata for multiple teams served by that task deployment, while updates remain tied to each team's exact flag identity.

Trust Boundaries and Controls

  • observed — The producer mapping preserves processed-event team identity. At the privileged relational writer, exact team/flag lookup constrains which rows receive timestamps. The alternate SQL interpolates a predefined table identifier, while scan bounds and limits are passed as query parameters rather than event-derived SQL.

Resilience and Maintainability Implications

  • observed — The ingestion fork deliberately continues after certain production failures. Its ClickHouse materialized view uses Kafka message time when inserted_at is absent. Consequently, a successful synchronization query is not proof of complete input, and the fixed delay protects only bounded consumer lag.

Hardening Proposals

  • proposed — Make complete team and lane coverage an explicit activation prerequisite, monitor producer failures and consumer lag separately from scan success, and define a bounded checkpoint-rewind or replay procedure for missed windows. Where a reliable producer watermark is available, consider using it to prevent progress beyond confirmed ingestion.
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description is complete and stand-alone. It clearly states the problem, user-visible changes, rollout risks, testing scope, release status, documentation status, and agent context. It also identif…
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (2)
posthog/tasks/tasks.py-1301-1301 (1)

1301-1301: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Move the new import to module scope.

Move FLAG_EVALUATIONS_TABLE beside the module-level imports.

As per coding guidelines: “Always place imports at the top of the file (module level), never inside functions or methods (local imports).”

Source: Coding guidelines

services/mcp/src/api/generated.ts-48015-48015 (1)

48015-48015: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The description omits the flag_evaluations coverage limit, so both fields can imply that last_called_at reflects calls that the selected source does not contain.

  • services/mcp/src/api/generated.ts#L48015-L48015: Clarify in the source help text that flag_evaluations reflects only calls forked into that table on real-time ingestion lanes, then regenerate this API description.
  • services/mcp/src/api/generated.ts#L48418-L48418: Regenerate this API description from the clarified source help text.
🧹 Nitpick comments (1)
posthog/tasks/tasks.py (1)

1440-1440: 🩺 Stability & Availability | 🔵 Trivial

Ensure flag_evaluations is readable on every offline query host before enabling this source.

When FEATURE_FLAG_LAST_CALLED_AT_SYNC_SOURCE is "flag_evaluations", the task queries the Distributed flag_evaluations table. If the selected host or its remote tables are unavailable, the first chunk can fail and leave the Redis checkpoint unchanged. Ensure the table and its remote-table configuration exist on each host selected by the offline workload.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 93c09ff6-67f0-4ed3-bce9-0d4e8b33688a

📥 Commits

Reviewing files that changed from the base of the PR and between 77cd868 and ee51fad.

⛔ Files ignored due to path filters (1)
  • products/feature_flags/frontend/generated/api.schemas.ts is excluded by !**/generated/**
📒 Files selected for processing (7)
  • posthog/settings/feature_flags.py
  • posthog/tasks/tasks.py
  • products/feature_flags/backend/migrations/0023_alter_featureflag_last_called_at_help_text.py
  • products/feature_flags/backend/migrations/max_migration.txt
  • products/feature_flags/backend/models/feature_flag.py
  • products/feature_flags/backend/test/test_feature_flag_sync.py
  • services/mcp/src/api/generated.ts

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 8 remain after this review.

@posthog

posthog Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

PostHog Review

Found 1 should fix.

Comment thread posthog/tasks/tasks.py Resolved
@posthog posthog Bot removed the reviewhog label Oct 1, 2026
@trunk-io

trunk-io Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Static Badge   Static Badge   Static Badge

Failed Test Failure Summary Logs
Dashboard compact cards and inline editing › Edit mode button enters and exits edit mode The test exceeded the maximum allowed time without completing. Logs ↗︎

View Full Report ↗︎ ⋅ Docs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 1a05e57b-7b13-4d7c-a3d5-73fd95f4c2ba

📥 Commits

Reviewing files that changed from the base of the PR and between ee51fad and f8ce8a2.

📒 Files selected for processing (1)
  • posthog/settings/feature_flags.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 5 remain after this review.

Comment thread posthog/settings/feature_flags.py Resolved

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: c1178565-4a85-441c-9b58-47486de83bf8

📥 Commits

Reviewing files that changed from the base of the PR and between 9fc2f61 and 1bc93f1.

📒 Files selected for processing (2)
  • posthog/settings/feature_flags.py
  • posthog/tasks/tasks.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 7 remain after this review.

Comment thread posthog/tasks/tasks.py
Comment on lines +1303 to +1304
# Both sources share this checkpoint, so a source switch resumes where the old source stopped.
# last_called_at only moves forward, so a call that both sources read at the switch changes nothing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Keep independent progress for each source.

While flag_evaluations is selected, the shared checkpoint can advance past calls from historical and async lanes, which are absent from that table. Switching to events then starts after the checkpoint, so those calls are never replayed and last_called_at can remain stale until another call arrives. Switching back before disabling the fork does not recover calls skipped during earlier runs. Maintain source-specific checkpoints or backfill the skipped event interval before resuming.

The PR objective states that historical and async lane calls are not written to flag_evaluations.

@haacked
haacked marked this pull request as ready for review October 1, 2026 23:25
@haacked haacked added the stamphog Request AI approval (no full review) label Oct 1, 2026
@parameterai

parameterai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Risk: No findings

This PR adds a FEATURE_FLAG_LAST_CALLED_AT_SYNC_SOURCE setting that switches the sync_feature_flag_last_called Celery task between the existing distributed_events_recent query and a new flag_evaluations query, with a larger buffer for the latter's Kafka-stamped inserted_at. The change is a maintenance/data-pipeline toggle with no new auth or input surface: the only SQL interpolation is a module-level constant table name, all query values are parameterized, the source setting is validated against a two-value allowlist with an events fallback, and the Postgres update remains scoped per (team_id, flag_key) with NUL-byte key filtering intact. No security issues found.

Sentinel reviewed 1bc93f1 · Review settings

@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested a review from a team October 1, 2026 23:26
@posthog-project-board-bot posthog-project-board-bot Bot moved this to In Review in Feature Flags Oct 1, 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.

The review did not complete.

Stamphog hit an error and produced no verdict for this commit.

Push a new commit to try again.

@haacked haacked added stamphog Request AI approval (no full review) and removed stamphog Request AI approval (no full review) labels Oct 2, 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.

The new source setting defaults to the existing behavior, so nothing changes until it is flipped. It only affects a background sync read path and adds real-ClickHouse tests. The rollout caveats about the shared checkpoint are documented in the PR and settings comments, and the change is easy to revert.

  • Author wrote 33% of the modified lines and has 52 merged PRs in these paths (familiarity MODERATE).
  • coderabbitai[bot] reviewed the current head.
  • CodeRabbit's unresolved inline thread says the checkpoint is shared across sources. The PR documents this tradeoff and defaults to the old behavior, so it is a rollout caveat and not a blocker.
  • Minor: the new function-local import in tasks.py follows the surrounding local-import pattern, but CodeRabbit suggested moving it to module scope.
Gate mechanics and policy version
Gate Result
prerequisites ✓ all clear
deny-list ✓ no deny categories matched
size ✓ 55L, 2F substantive, 176L/3F incl. docs/generated/snapshots — within ceiling
tier ✓ T1-agent / T1c-medium (176L, 3F, two-areas, feat)
stamphog 2.3.1 .stamphog/policy.yml @ 1bc93f1 · reviewed head 1bc93f1

@trunk-io
trunk-io Bot merged commit fe2d6a9 into master Oct 2, 2026
@trunk-io
trunk-io Bot deleted the haacked/last-called-sync-source branch October 2, 2026 00:47
@github-project-automation github-project-automation Bot moved this from Approved to Done in Feature Flags Oct 2, 2026
@deployment-status-posthog

deployment-status-posthog Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-10-02 01:02 UTC Run
prod-us ✅ Deployed 2026-10-02 01:13 UTC Run
prod-eu ✅ Deployed 2026-10-02 01:14 UTC Run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature/feature-flags Feature Tag: Feature flags stamphog Request AI approval (no full review) team/feature-flags

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

2 participants