Repository navigation
fix(signals): report the PR quota pause and unblock suggestion scans - #94327
Conversation
The self-driving credits meter charges per shipped pull request, but the scout paths read the raw quota limiter instead of the shared gate, so a paused scout run emitted no `signal_report_quota_paused` event and ignored the `self-driving-quota-enforcement` kill switch. Route both scout paths (the scheduled activity and the pre-dispatch gate behind the manual and workflow triggers) through `self_driving_quota_gate` so the pause reports itself at `stage="scout_run"` and honors the flag. Drop the quota check from scout suggestion scans: they open no pull request, so they charge nothing, and a skip still burned the team's whole refresh window because `last_requested_at` is stamped at dispatch. Rewrite the copy in the product's pull request vocabulary, and stop the quota pause overwriting the unrelated Daily report limit value. Generated-By: PostHog Desktop Task-Id: e426549b-2599-491f-9b67-1b8052418dfe
|
😎 Merged successfully - details. |
🤖 CI report
|
| Function | Location | Complexity | Limit |
|---|---|---|---|
InboxUsageWidget |
products/signals/frontend/inbox/components/shell/InboxUsageWidget.tsx:148 |
19 | 10 |
✅ Bundle size — 🟢 -121.4 KiB (-0.2%)
Uncompressed size of every built .js bundle, compared against the base branch.
Total: 68.28 MiB · 🟢 -121.4 KiB (-0.2%)
| File | Size | Δ vs base |
|---|---|---|
posthog-app/_parent/products/workflows/frontend/Workflows/WorkflowScene.js |
35.9 KiB | 🟢 -89.1 KiB (-71.3%) |
posthog-app/_parent/products/logs/frontend/LogsScene.js |
46.1 KiB | 🟢 -48.6 KiB (-51.3%) |
posthog-app/_parent/products/data_catalog/frontend/DataCatalogScene.js |
24.5 KiB | 🟢 -6.6 KiB (-21.2%) |
posthog-app/src/scenes/project-homepage/ProjectHomepage.js |
37.4 KiB | 🔺 +5.3 KiB (+16.6%) |
render-query/src/render-query/render-query.js |
21.87 MiB | 🔺 +4.0 KiB (+0.0%) |
posthog-app/src/scenes/experiments/Experiment.js |
264.9 KiB | 🔺 +3.3 KiB (+1.3%) |
posthog-app/_parent/products/signals/frontend/inbox/InboxScene.js |
760.9 KiB | 🟢 -3.2 KiB (-0.4%) |
exporter/src/exporter/scenes/ExporterNotebookScene.js |
3.46 MiB | 🔺 +2.0 KiB (+0.1%) |
posthog-app/_parent/products/data_catalog/frontend/DataCatalogMetricScene.js |
23.0 KiB | 🔺 +1.6 KiB (+7.3%) |
exporter/src/queries/schema.js |
1.20 MiB | 🔺 +1.5 KiB (+0.1%) |
posthog-app/src/queries/schema.js |
1.20 MiB | 🔺 +1.5 KiB (+0.1%) |
posthog-app/src/scenes/App.js |
38.2 KiB | 🔺 +1.2 KiB (+3.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.45 MiB · 22 files | 🔺 +317 B (+0.0%) | ███░░░░░░░ 32.3% of 4.51 MiB |
authenticated shell (every logged-in page)src/scenes/AuthenticatedShell.tsx |
8.89 MiB · 3,268 files | 🔺 +4.4 KiB (+0.0%) | █████████░ 91.5% of 9.71 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 |
| 854 B | src/scenes/ChunkLoadErrorBoundary.tsx |
| 762 B | src/index.tsx |
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
| Size | File |
|---|---|
| 307.3 KiB | ../node_modules/.pnpm/posthog-js@1.425.1_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/rrweb.js |
| 272.4 KiB | ../node_modules/.pnpm/posthog-js@1.425.1_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.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 |
| 255.2 KiB | src/taxonomy/core-filter-definitions-by-group.json |
| 154.2 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 |
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 | 🔺 +171 B (+0.0%) | ████░░░░░░ 39.8% of 5.72 MiB |
| Deferred (lazy) | 2.11 MiB · 45 files | 🔺 +124 B (+0.0%) | 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 |
|---|---|
| 754.1 KiB | dist/toolbar/toolbar-app-HCAVZOPH.css |
| 599.5 KiB | dist/toolbar/chunk-chunk-IP5OG3UU.js |
| 484.7 KiB | dist/toolbar/chunk-chunk-EL6RQSMS.js |
| 135.6 KiB | dist/toolbar/chunk-chunk-7UG5SHZR.js |
| 131.8 KiB | dist/toolbar/chunk-chunk-FDH2IBXT.js |
| 71.3 KiB | dist/toolbar/toolbar-app-P2YIMAVU.js |
| 69.0 KiB | dist/toolbar/chunk-chunk-TSAL54PB.js |
| 35.6 KiB | dist/toolbar/chunk-chunk-TX5RGCWJ.js |
| 21.0 KiB | dist/toolbar/chunk-chunk-6WYJMFDF.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 — 🟢 -1.02 MiB (-0.1%)
Total size of the built frontend/dist folder (all assets), compared against the base branch.
Total: 1458.81 MiB · 🟢 -1.02 MiB (-0.1%)
ℹ️ MCP UI apps size — 33 app(s), 17665.2 KB JS
Built size of each MCP UI app (main.js + styles.css).
| App | JS | CSS |
|---|---|---|
| debug | 600.0 KB | 195.2 KB |
| action | 458.2 KB | 195.2 KB |
| action-list | 564.9 KB | 195.2 KB |
| cohort | 457.2 KB | 195.2 KB |
| cohort-list | 563.8 KB | 195.2 KB |
| email-template | 457.0 KB | 195.2 KB |
| error-details | 472.9 KB | 195.2 KB |
| error-issue | 457.9 KB | 195.2 KB |
| error-issue-list | 564.7 KB | 195.2 KB |
| experiment | 562.0 KB | 195.2 KB |
| experiment-list | 565.6 KB | 195.2 KB |
| experiment-results | 567.1 KB | 195.2 KB |
| feature-flag | 567.6 KB | 195.2 KB |
| feature-flag-list | 571.4 KB | 195.2 KB |
| feature-flag-testing | 461.4 KB | 195.2 KB |
| inline-scan | 457.7 KB | 195.2 KB |
| insight-actors | 563.0 KB | 195.2 KB |
| invite-email-preview | 456.4 KB | 195.2 KB |
| llm-costs | 560.0 KB | 195.2 KB |
| session-recording | 459.0 KB | 195.2 KB |
| survey | 458.8 KB | 195.2 KB |
| survey-global-stats | 562.7 KB | 195.2 KB |
| survey-list | 565.5 KB | 195.2 KB |
| survey-stats | 562.7 KB | 195.2 KB |
| trace-span | 457.6 KB | 195.2 KB |
| trace-span-list | 564.7 KB | 195.2 KB |
| vision-observation-list | 563.9 KB | 195.2 KB |
| workflow | 457.5 KB | 195.2 KB |
| workflow-list | 564.2 KB | 195.2 KB |
| loops-review | 461.9 KB | 195.2 KB |
| query-results | 756.3 KB | 195.2 KB |
| render-ui | 839.5 KB | 195.2 KB |
| visual-review-snapshots | 462.0 KB | 195.2 KB |
ℹ️ ClickHouse migration SQL — 1 migration(s)
ClickHouse migration SQL per cloud environment
- unset
- all
CREATE TABLE IF NOT EXISTS sharded_billing_usage_records ( schema_version UInt8, record_id String, producer_id LowCardinality(String), team_id Int64, organization_id UUID, usage_key LowCardinality(String), unit LowCardinality(String), quantity Int64, timestamp DateTime64(6, 'UTC'), inserted_at DateTime64(6, 'UTC') , _timestamp DateTime , _offset UInt64 , _partition UInt64 ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/{shard}/posthog.sharded_billing_usage_records', '{replica}', inserted_at) PARTITION BY toYYYYMM(timestamp) ORDER BY (team_id, toDate(timestamp), producer_id, usage_key, record_id)
CREATE TABLE IF NOT EXISTS billing_usage_records ( schema_version UInt8, record_id String, producer_id LowCardinality(String), team_id Int64, organization_id UUID, usage_key LowCardinality(String), unit LowCardinality(String), quantity Int64, timestamp DateTime64(6, 'UTC'), inserted_at DateTime64(6, 'UTC') , _timestamp DateTime , _offset UInt64 , _partition UInt64 ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_billing_usage_records', cityHash64(team_id))
CREATE TABLE IF NOT EXISTS writable_billing_usage_records ( schema_version UInt8, record_id String, producer_id LowCardinality(String), team_id Int64, organization_id UUID, usage_key LowCardinality(String), unit LowCardinality(String), quantity Int64, timestamp DateTime64(6, 'UTC'), inserted_at DateTime64(6, 'UTC') , _timestamp DateTime , _offset UInt64 , _partition UInt64 ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_billing_usage_records', cityHash64(team_id))
CREATE TABLE IF NOT EXISTS kafka_billing_usage_records ( schema_version UInt8, record_id String, producer_id LowCardinality(String), team_id Int64, organization_id UUID, usage_key LowCardinality(String), unit LowCardinality(String), quantity Int64, timestamp DateTime64(6, 'UTC'), inserted_at DateTime64(6, 'UTC') ) ENGINE = Kafka(warpstream_shared, kafka_topic_list = 'clickhouse_billing_usage_records_test', kafka_group_name = 'clickhouse_billing_usage_records', kafka_format = 'JSONEachRow') SETTINGS date_time_input_format = 'best_effort'
CREATE MATERIALIZED VIEW IF NOT EXISTS billing_usage_records_mv TO writable_billing_usage_records AS SELECT schema_version, record_id, producer_id, team_id, organization_id, usage_key, unit, quantity, timestamp, inserted_at, _timestamp, _offset, _partition FROM kafka_billing_usage_records
- all
- US, EU, DEV
- data
CREATE TABLE IF NOT EXISTS sharded_billing_usage_records ( schema_version UInt8, record_id String, producer_id LowCardinality(String), team_id Int64, organization_id UUID, usage_key LowCardinality(String), unit LowCardinality(String), quantity Int64, timestamp DateTime64(6, 'UTC'), inserted_at DateTime64(6, 'UTC') , _timestamp DateTime , _offset UInt64 , _partition UInt64 ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/{shard}/posthog.sharded_billing_usage_records', '{replica}', inserted_at) PARTITION BY toYYYYMM(timestamp) ORDER BY (team_id, toDate(timestamp), producer_id, usage_key, record_id)
CREATE TABLE IF NOT EXISTS billing_usage_records ( schema_version UInt8, record_id String, producer_id LowCardinality(String), team_id Int64, organization_id UUID, usage_key LowCardinality(String), unit LowCardinality(String), quantity Int64, timestamp DateTime64(6, 'UTC'), inserted_at DateTime64(6, 'UTC') , _timestamp DateTime , _offset UInt64 , _partition UInt64 ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_billing_usage_records', cityHash64(team_id))
- small
CREATE TABLE IF NOT EXISTS writable_billing_usage_records ( schema_version UInt8, record_id String, producer_id LowCardinality(String), team_id Int64, organization_id UUID, usage_key LowCardinality(String), unit LowCardinality(String), quantity Int64, timestamp DateTime64(6, 'UTC'), inserted_at DateTime64(6, 'UTC') , _timestamp DateTime , _offset UInt64 , _partition UInt64 ) ENGINE = Distributed('posthog', 'posthog_test', 'sharded_billing_usage_records', cityHash64(team_id))
CREATE TABLE IF NOT EXISTS kafka_billing_usage_records ( schema_version UInt8, record_id String, producer_id LowCardinality(String), team_id Int64, organization_id UUID, usage_key LowCardinality(String), unit LowCardinality(String), quantity Int64, timestamp DateTime64(6, 'UTC'), inserted_at DateTime64(6, 'UTC') ) ENGINE = Kafka(warpstream_shared, kafka_topic_list = 'clickhouse_billing_usage_records_test', kafka_group_name = 'clickhouse_billing_usage_records', kafka_format = 'JSONEachRow') SETTINGS date_time_input_format = 'best_effort'
CREATE MATERIALIZED VIEW IF NOT EXISTS billing_usage_records_mv TO writable_billing_usage_records AS SELECT schema_version, record_id, producer_id, team_id, organization_id, usage_key, unit, quantity, timestamp, inserted_at, _timestamp, _offset, _partition FROM kafka_billing_usage_records
- data
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 456ea69b8d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if quota_gate.limited: | ||
| capture_signal_report_quota_paused(team, report_id=None, stage="scout_run", enforced=quota_gate.enforced) |
There was a problem hiding this comment.
Emit only one dark-launch pause per scout run
When a team is quota-limited but enforcement is disabled, this preflight captures signal_report_quota_paused and permits the manual or workflow-triggered run to dispatch. run_signals_scout_activity() then captures the same stage="scout_run", report_id=None, enforced=false event again in scout_scheduler.py:104-105, so off-schedule dark-launch runs count twice while scheduled runs count once. This skews the would-block telemetry used to evaluate rollout; emit from the preflight only when the run will not reach the activity, or otherwise deduplicate the observations.
Useful? React with 👍 / 👎.
| {dailyReportLimitReached && ( | ||
| <p className="text-xs font-medium text-danger mb-0 px-2.5 pb-1.5"> | ||
| Daily report limit reached. New reports resume at midnight in your project's timezone. |
There was a problem hiding this comment.
Avoid promising a midnight resume while quota-paused
When a project has reached both its daily report cap and its enforced pull-request quota, removing the !quotaLimited condition shows “New reports resume at midnight,” but report generation remains blocked until the billing limit is raised or resets. InboxUsageWidget.tsx:221-225 now states that latter condition on the same Settings and agent-setup surfaces, leaving users with contradictory recovery guidance. Keep the quota-aware condition or describe only the daily counter reset without promising that reports resume.
Useful? React with 👍 / 👎.
| if daily_gate.limited: | ||
| capture_signal_report_daily_limit_paused(team, report_id=None, stage="scout_run", gate=daily_gate) | ||
| if quota_limited: | ||
| if quota_gate.enforced: |
There was a problem hiding this comment.
Keep signal scouts gated until ingestion honors the flag
For a quota-limited team with enforcement disabled—or when the enforcement-flag lookup fails open—this condition now lets a signal-channel scout perform its full model-backed run. Its findings still flow through check_signals_quota_limited_activity() in temporal/buffer.py:86-105, which checks the raw quota limiter without the enforcement flag and drops the entire batch. These scouts therefore consume agent compute and can report successful runs while producing no inbox output; retain the raw scout guard until ingestion uses the same gate, or migrate both stages together.
AGENTS.md reference: products/signals/backend/scout_harness/AGENTS.md:L3-L3
Useful? React with 👍 / 👎.
|
/trunk merge |
1 similar comment
|
/trunk merge |
Problem
A project over its self-driving pull request limit loses its scout suggestion scans, which never open a pull request and never charge that limit.
run_signals_scout_activityand the manual run endpoint read the raw quota limiter, so the pause emits nosignal_report_quota_pausedevent and ignores theself-driving-quota-enforcementkill switch. Every other paused stage reports itself.last_requested_atat dispatch, so the project waits the full 7-day refresh window even after its billing period resets.Closes #94166
Changes
signal_report_quota_pausedwithstage="scout_run"and blocks only when the enforcement flag is on.select_related("organization")at the two spend-gate callers for the new capture, plus the regenerated OpenAPI descriptions inproducts/signals/frontend/generated/api.tsandservices/mcp/src/generated/signals/api.ts.No screenshot: the changed label only renders for a quota-limited organization and no story covers that state. Both strings are quoted above.
Note
Whether an over-quota project should run scouts at all stays open, and this PR does not decide it. The gate above scout runs is kept because ingestion still drops those runs' signals at the same limit (
check_signals_quota_limited_activity), so lifting it alone would spend model budget on reports that never arrive. Lifting it properly means lifting the ingestion, promotion, and summary gates too, which is a spend decision for a human.How did you test this code?
test_activity_skips_run_attributed_to_the_limit_that_firedgains a dark-launch case: a limited project with enforcement off runs, and still reports the pause. The old test read a bare boolean, so it could not express that state.test_run_over_quota_reports_the_pause_and_blocks_only_when_enforcedreplaces the old 429 test. It guards the event trail this PR is about: drop the capture fromcheck_spend_gatesand a rejected manual run leaves no record again.products.signals.backend.quota.is_team_limited, the limiter read both quota helpers bottom out on. Re-adding a quota gate to the scan in any form fails them.posthog/temporal/tests/ai/test_module_integrity.py,hogli lint:tach,hogli ci:preflight --fix(no failures), and repo-wideuv run mypy --cache-fine-grained .(clean).@posthog/quillworkspace was needed beforetsgocould resolve this component's imports at all; after that,products/signalstypechecks with zero errors.Automatic notifications
Docs update
products/signals/ARCHITECTURE.mdgains thescout_runstage name in the gate table and a paragraph on why suggestion scans are deliberately not gated.products/signals/backend/scout_harness/AGENTS.mddrops "signals quota" from thearun_scout_suggestionsgate list.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Authored by Claude Code (Opus 5) in a PostHog Desktop cloud task, acting on the linked inbox report. The PR is unassigned because
gh api userreturns 403 in this environment, so the driver's GitHub handle could not be verified, and this repo's rules forbid guessing one.Skills invoked:
/writing-user-facing-copyfor the four copy strings,/writing-testsfor the test gate,/writing-pr-descriptionsfor this body.The report's stated solution named two remedies that pull apart for scout runs: route them through the shared gate, and drop the check from paths that mint no pull requests. The split shipped here is by whether a path can reach a pull request at all. A suggestion scan cannot, so it loses the check. A scout run feeds the pipeline that does, so it keeps the check and gains the telemetry and the kill switch. Un-gating scout runs was considered and rejected for the reason in the note above; that is the one open question left for a reviewer.
Public artifact: nothing in the diff, the commit message, or this description carries material from the agent session. The report drew on a Discord thread and issue #94166; only the issue is referenced, by number.
Created with PostHog Desktop from an inbox report