Skip to content

Commit f22516c

Browse files
authored
fix(stats): scope reuse trend to public repos (#4991)
1 parent 1822fca commit f22516c

2 files changed

Lines changed: 44 additions & 9 deletions

File tree

src/services/public-reuse-rate-trend.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,17 @@
88
// is already durable, so a live weekly re-bucketing of the SAME rows can recompute any historical week correctly
99
// on every request -- no cron-miss gap risk, and no second copy of the number to keep in sync.
1010
//
11-
// DELIBERATELY GLOBAL, not scoped to the public-stats repo allowlist: unlike accuracy/handled-PR counts, a
12-
// cache-hit/miss event carries no PR content, author, or repo-specific outcome -- the aggregate reuse rate
13-
// doesn't reveal anything about any one repo's activity, and target_key isn't uniformly shaped across all eight
14-
// capabilities (some key by bare repoFullName, others by repoFullName#prNumber), so allowlist-filtering it would
15-
// need a fragile per-capability parser for no real privacy benefit.
11+
// PUBLIC-SAFE SCOPE: only events whose target_key maps to GITTENSORY_PUBLIC_STATS_REPOS are included. Most
12+
// cache keys are either a bare repoFullName or repoFullName#prNumber; anything outside that allowlist is treated
13+
// as private operational telemetry and deliberately excluded from this unauthenticated payload.
1614
//
1715
// NAMING CONVENTION, not a hardcoded capability list: every instrumented capability already follows
1816
// `github_app.<name>_cache_hit` / `github_app.<name>_cache_miss` (confirmed via a full-repo grep before writing
1917
// this), so a single LIKE-pattern query picks up all eight today AND any future capability that follows the
2018
// same convention, with zero code change here. ai_review's three additional REUSE variants (frozen/paused/
2119
// one-shot) don't fit that exact suffix -- each is a genuine "skipped a redundant AI call" event, so they're
2220
// folded into "hit" alongside the plain ai_review_cache_hit.
23-
import { safeAll } from "../review/public-stats";
21+
import { publicStatsProjects, safeAll } from "../review/public-stats";
2422
import { isoWeekStart } from "./public-quality-metrics";
2523

2624
export const PUBLIC_REUSE_RATE_TREND_WEEKS = 8;
@@ -81,7 +79,9 @@ export function buildPublicReuseRateTrend(dayRows: DayRow[], nowMs: number, week
8179
/** Day-bucketed hit/miss counts across every `github_app.<name>_cache_hit` / `_cache_miss` event, plus
8280
* ai_review's three non-suffix-conforming reuse variants (see file header). Fail-safe: degrades to [] on any
8381
* query error (safeAll), yielding under-counted weeks rather than throwing the whole public stats payload. */
84-
async function loadReuseRateDayRows(env: Env, sinceIso: string): Promise<DayRow[]> {
82+
async function loadReuseRateDayRows(env: Env, projects: string[], sinceIso: string): Promise<DayRow[]> {
83+
if (projects.length === 0) return [];
84+
const projectPlaceholders = projects.map(() => "?").join(", ");
8585
const reuseTypePlaceholders = AI_REVIEW_REUSE_EVENT_TYPES.map(() => "?").join(", ");
8686
const rows = await safeAll<{ day: string; hits: number; misses: number }>(
8787
env,
@@ -90,10 +90,12 @@ async function loadReuseRateDayRows(env: Env, sinceIso: string): Promise<DayRow[
9090
SUM(CASE WHEN event_type LIKE 'github_app.%cache_miss' THEN 1 ELSE 0 END) AS misses
9191
FROM audit_events
9292
WHERE (event_type LIKE 'github_app.%cache_hit' OR event_type LIKE 'github_app.%cache_miss' OR event_type IN (${reuseTypePlaceholders}))
93+
AND LOWER(CASE WHEN instr(target_key, '#') > 0 THEN substr(target_key, 1, instr(target_key, '#') - 1) ELSE target_key END) IN (${projectPlaceholders})
9394
AND created_at >= ?
9495
GROUP BY day`,
9596
...AI_REVIEW_REUSE_EVENT_TYPES,
9697
...AI_REVIEW_REUSE_EVENT_TYPES,
98+
...projects,
9799
sinceIso,
98100
);
99101
/* v8 ignore next -- SUM(CASE WHEN ... THEN 1 ELSE 0 END) over an existing GROUP BY day always yields a
@@ -105,7 +107,8 @@ async function loadReuseRateDayRows(env: Env, sinceIso: string): Promise<DayRow[
105107
/** Assemble the public reuse-rate trend from the SAME live audit_events ledger every instrumented capability
106108
* already writes to. */
107109
export async function loadPublicReuseRateTrend(env: Env, nowMs: number = Date.now()): Promise<PublicReuseRateTrendWeek[]> {
110+
const projects = publicStatsProjects(env);
108111
const sinceIso = new Date(Date.parse(isoWeekStart(nowMs)) - (PUBLIC_REUSE_RATE_TREND_WEEKS - 1) * MS_PER_WEEK).toISOString();
109-
const dayRows = await loadReuseRateDayRows(env, sinceIso);
112+
const dayRows = await loadReuseRateDayRows(env, projects, sinceIso);
110113
return buildPublicReuseRateTrend(dayRows, nowMs);
111114
}

test/unit/public-reuse-rate-trend.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ describe("buildPublicReuseRateTrend", () => {
7070

7171
describe("loadPublicReuseRateTrend — end-to-end over the real live audit_events ledger", () => {
7272
it("counts every github_app.*_cache_hit / *_cache_miss event, plus ai_review's three non-suffix reuse variants, as hits/misses", async () => {
73-
const env = createTestEnv();
73+
const env = createTestEnv({ GITTENSORY_PUBLIC_STATS_REPOS: "owner/repo" });
7474
const thisMonday = isoWeekStart(NOW);
7575
const thisWeekIso = `${thisMonday}T09:00:00.000Z`;
7676

@@ -94,8 +94,40 @@ describe("loadPublicReuseRateTrend — end-to-end over the real live audit_event
9494
});
9595

9696
it("returns all-zero buckets when no instrumented events exist yet", async () => {
97+
const env = createTestEnv({ GITTENSORY_PUBLIC_STATS_REPOS: "owner/repo" });
98+
const trend = await loadPublicReuseRateTrend(env, NOW);
99+
for (const week of trend) expect(week).toMatchObject({ hits: 0, misses: 0, reuseRatePct: null });
100+
});
101+
102+
it("REGRESSION: excludes cache activity outside the public stats repo allowlist", async () => {
103+
const env = createTestEnv({ GITTENSORY_PUBLIC_STATS_REPOS: "owner/repo" });
104+
const thisMonday = isoWeekStart(NOW);
105+
const thisWeekIso = `${thisMonday}T09:00:00.000Z`;
106+
107+
await recordAuditEvent(env, { eventType: "github_app.grounding_cache_hit", targetKey: "owner/repo", outcome: "completed", createdAt: thisWeekIso });
108+
await recordAuditEvent(env, { eventType: "github_app.impact_map_cache_hit", targetKey: "owner/repo#123", outcome: "completed", createdAt: thisWeekIso });
109+
await recordAuditEvent(env, { eventType: "github_app.review_memory_cache_miss", targetKey: "owner/repo#123", outcome: "completed", createdAt: thisWeekIso });
110+
await recordAuditEvent(env, { eventType: "github_app.grounding_cache_hit", targetKey: "secret/private", outcome: "completed", createdAt: thisWeekIso });
111+
await recordAuditEvent(env, { eventType: "github_app.review_memory_cache_miss", targetKey: "secret/private#7", outcome: "completed", createdAt: thisWeekIso });
112+
await recordAuditEvent(env, { eventType: "github_app.ai_review_frozen_reuse", targetKey: "secret/private#7", outcome: "completed", createdAt: thisWeekIso });
113+
114+
const trend = await loadPublicReuseRateTrend(env, NOW);
115+
const currentWeek = trend[trend.length - 1];
116+
expect(currentWeek).toMatchObject({ weekStart: thisMonday, hits: 2, misses: 1, reuseRatePct: null });
117+
});
118+
119+
it("returns all-zero buckets when the public stats repo allowlist is empty", async () => {
97120
const env = createTestEnv();
121+
const thisMonday = isoWeekStart(NOW);
122+
await recordAuditEvent(env, {
123+
eventType: "github_app.grounding_cache_hit",
124+
targetKey: "owner/repo",
125+
outcome: "completed",
126+
createdAt: `${thisMonday}T09:00:00.000Z`,
127+
});
128+
98129
const trend = await loadPublicReuseRateTrend(env, NOW);
99130
for (const week of trend) expect(week).toMatchObject({ hits: 0, misses: 0, reuseRatePct: null });
100131
});
132+
101133
});

0 commit comments

Comments
 (0)