Skip to content

analytics: scope /facts/status to the caller's organisation - #16

Merged
robinbowes merged 2 commits into
mainfrom
fix/analytics-facts-status-tenant
Nov 3, 2026
Merged

robinbowes merged 2 commits into
mainfrom
fix/analytics-facts-status-tenant

Conversation

@robinbowes

Copy link
Copy Markdown
Contributor

Summary

 checkFactTableFreshness(db, tenantId, now?)
-  select max(fact_refreshed_at) from <fact table>
-  select max(<source ts>) from <source table>
+  select max(fact_refreshed_at) from <fact table>  where tenant_id = $tenantId
+  select max(<source ts>)       from <source table> where tenant_id = $tenantId

 GET /api/analytics/facts/status   (admin+)
-  checkFactTableFreshness(ctx.db)
+  checkFactTableFreshness(ctx.db, ctx.tenantId)

With the kit's default TENANT_SCOPE_MODE=off nothing else narrows these queries, so an organisation's admin saw the newest activity timestamp of every organisation, and a busy tenant could hide another's stale fact table. A fact table's source.table must now carry tenant_id, as the fact table already does (registry doc, fact-tables/CLAUDE.md). The contract does not change: a source without the column fails loudly with column "tenant_id" does not exist.

Also: the README's "Releasing" section now describes this monorepo's release (--plugin for every plugin, stamps-only notes, README install refs, release-check --tag, tag the release PR's merge commit).

Part of 3.8.1. The release waits for this PR; the chunk-size warning is going to an issue rather than into 3.8.1.

Evidence

Scratch kit worktree on kit main (0.24.0), analytics installed from this branch.

  • Before:

    • checkFactTableFreshness sees only the caller's tenant: expected 1793727335269 to be 1793727335266, which is the other tenant's refreshedAt.
    • GET /facts/status reports the caller's organisation, not another one's backlog: another organisation's unbuilt 5-hour-old activity made the caller's table lag.

    After: both pass; 49/49 analytics API tests; pnpm gate exit 0 (688 API tests, 1867 others).

Merge Danger

Door: two-way

No schema or data change; reverting restores the old query.

Blast Radius: freshness

/facts/status, the dashboard list's freshness badges and rocketflare analytics check-facts now report the signed-in organisation only. An app that contributes a fact table whose source has no tenant_id gets a 500 from /facts/status until it adds one (the porting note says so). Apps calling checkFactTableFreshness directly must pass the tenant id.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com

robinbowes and others added 2 commits November 3, 2026 17:38
checkFactTableFreshness read MAX() over every tenant, so one
organisation's admin saw when any other organisation last had activity,
and a busy tenant could hide another's stale fact table. Both queries
now filter on tenant_id, and a fact table's source must carry it, as
the fact table already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Releasing section described a single-plugin repository. In this
one, release.mjs needs every plugin named with --plugin, refuses an
empty unreleased.md, and leaves the README install refs to be moved by
hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@robinbowes
robinbowes merged commit 0e7b046 into main Nov 3, 2026
29 of 30 checks passed
@robinbowes robinbowes mentioned this pull request Nov 3, 2026
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