Skip to content

Insights opt-in cross-tenant benchmarks - #1925

Merged
mishrasanjeev merged 21 commits into
mainfrom
feat/insights-phase5-benchmarks
Nov 5, 2026
Merged

mishrasanjeev merged 21 commits into
mainfrom
feat/insights-phase5-benchmarks

Conversation

@mishrasanjeev

@mishrasanjeev mishrasanjeev commented Nov 4, 2026 •

Copy link
Copy Markdown
Owner

Insights Phase 5, release 3 of 5. Stacked on #1924 (merge that first; it is contained here).

Opt-in cross-tenant benchmarks, as the PRD decided: opt-in only, and a cohort of at least 20 organisations.

What a tenant sees

  • A benchmark card on the Executive, Finance and Operations views (administrators, CFO, COO). It shows the organisation's run success rate, cost per run (USD), approvals per 100 runs and average run time for the last 30 complete days. Each is set against the 25th, 50th and 75th percentiles of all opted-in organisations and of its plan's cohort, with where it sits.
  • An organisation that has not opted in sees why the card is empty. An active human administrator opts in or out from the card (PUT /insights/benchmarks/opt-in, insights_admin family, audited).

Privacy, enforced in the database (migration v6z117)

  • insights_benchmark_optins (forced RLS): one row per tenant that took part.
  • public.insights_benchmark_cohorts(p_from, p_to, p_min_tenants, p_min_runs): SECURITY DEFINER, owned by the v6z107 platform role, search_path = pg_catalog.
    • It counts only opted-in, live tenants' production rollups that were measured.
    • It skips tenants with fewer than 100 runs and returns only cohorts of at least 20, whatever floors are passed (GREATEST).
    • It returns percentiles only, never a tenant's figure or id.
  • public.insights_benchmark_participation() returns the opted-in count, which the Platform view shows.
  • insights_benchmarks stores the published figures with no tenant id; a check refuses fewer than 20 tenants.
  • Grants follow v6z114: column grants only, a FOR SELECT policy for the role, EXECUTE for the installer and agenticorg_app only, and no membership left with the installer. scripts/create_app_role.sql allow-lists both functions.
  • A nightly task (04:10 UTC) publishes the cohorts.

Verification

  • Postgres, NOBYPASSRLS caller:
    • The caller reads no other tenant's rows directly.
    • Excluded tenants are not counted: one that never opted in, one deleted, one under 100 runs, demo-only, shadow-only, and rows outside the window.
    • The percentiles match Python's quartiles.
    • The 3-tenant plan cohort is not returned, and smaller floors passed by the caller are ignored.
    • A range over 92 days is refused, and the participation count is exact.
    • compute publishes without tenant ids.
    • Cards show the position against each band, the not-opted-in explanation, and only all where the plan's cohort is too small.
    • Opting in and out is audited and invisible to other tenants.
    • The migration rolls back and reapplies, with the owner and PUBLIC privileges checked.
  • Unit: window, positions, readers, floors in the SQL against the module, routes and scopes, nightly schedule and the off switch; the platform summary's count.
  • UI: card states and Hindi; Insights suite; tsc, eslint.

Insights writes mapped to the administrator's scope at the route, so a CFO
could not acknowledge an alert, save a view or start a background export,
and an auditor could not sign an evidence pack (403 Missing scope).

Roles that read Insights now hold insights:write and the family's writes
take it; handlers still decide who may act. BI logins and the money-saved
changes move to the insights_admin family, whose writes stay admin-only at
the route too.
Auditors keep audit:read and insights:read only. Exports, saved views,
the last visit and evidence-pack signatures change no organisation state,
so they move to the insights_personal family, which needs insights:read.
Acknowledging an alert stays an insights:write action.
…s one

Administrators and the CFO add, change and delete budgets for the
organisation, a company or a cost centre at /dashboard/insights/budgets
(API /insights/budgets); auditors read them. Each shows this period's
ledger spend against its limit. Every change is a signed audit row.

The alert sweep raises cost.forecast_over_budget when the month's
forecast, in USD, is above a tenant-wide monthly budget.

The budget evaluator no longer posts slack or webhook alerts to the
platform-wide URLs, and escapes the budget name in its email.
A view_pptx background export renders a landing view as a deck: title,
tiles table, a native chart with its text summary for each chart and the
chart's data as a table. Metric and explorer exports also download as
Parquet with typed columns and the definitions and filters as metadata.

Migration v6z116 widens the insights_exports kind and format checks.
The shared export menu offers Parquet, so the platform export endpoint
accepts it (it renders through the same export module).
Administrators opt the organisation in; an opted-in organisation sees its
run success rate, cost per run, approvals per 100 runs and average run time
against the 25th, 50th and 75th percentiles of opted-in cohorts.

Migration v6z117 adds the opt-in table (forced RLS), the published cohort
table (no tenant ids) and SECURITY DEFINER functions that enforce the
20-tenant and 100-run floors in the database. A nightly task publishes the
cohorts; the platform view shows the opted-in count.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-04T13:21:06.680647Z 01bf342 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 01bf342de9

ℹ️ 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".

Comment on lines +264 to +265
published = (
(await session.execute(select(InsightsBenchmark).where(InsightsBenchmark.cohort.in_(cohorts)))).scalars().all()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Align the comparison window with the published benchmarks

Between midnight UTC and the 04:10 publication task, this query returns yesterday's published bands while own_figures() uses today's newly shifted 30-day window. The response labels both with today's window and calculates positions across different periods. A failed publication leaves this mismatch in place indefinitely because neither window_from nor window_to is checked. Use the published window for the organisation's figures, or withhold bands that do not match the requested window.

Useful? React with 👍 / 👎.

Comment thread core/insights/slides.py
number = _number(row.get("value", row.get("count")))
if number is None:
continue
labels.append(str(row.get("label") or row.get("name") or row.get("key") or row.get("t") or ""))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve metric-specific labels in PowerPoint charts

For the Finance deck, resolve_cost_by_agent() returns labels in agent_name and resolve_cost_by_model() returns them in model, but neither field is read here. Consequently, both spend breakdowns export with blank category labels; table_rows() repeats the same omission, so the accompanying tables cannot identify the agents or models either. Handle the actual metric row shapes in both helpers, matching the label selection already used by ui/src/lib/insights/chartData.ts.

Useful? React with 👍 / 👎.

Comment thread core/insights/budgets.py
Comment on lines +326 to +330
row.company_id = _uuid(changes["company_id"], "invalid_company", "company_id")
if "cost_center_id" in changes:
row.cost_center_id = _uuid(changes["cost_center_id"], "invalid_cost_center", "cost_center_id")
if "company_id" in changes or "cost_center_id" in changes:
await _check_scope(session, tenant_id, row.company_id, row.cost_center_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate budget scope before assigning foreign keys

When a PATCH supplies a nonexistent company or cost-centre UUID, these assignments dirty the persistent budget before _check_scope() executes its ORM SELECT. Both production session factories use SQLAlchemy's default autoflush, so that SELECT first attempts the UPDATE and raises a foreign-key IntegrityError, rather than returning the intended BudgetError and 404. Validate the proposed scope before assigning it, or suppress autoflush during validation, so invalid scope changes do not become unhandled 500 responses.

Useful? React with 👍 / 👎.

The full suite left the shared app without the Insights routes in CI, so
the test now reads each router and checks api/main.py mounts them.
@mishrasanjeev
mishrasanjeev merged commit 6fef142 into main Nov 5, 2026
28 checks passed
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.

1 participant