mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-17 17:51:27 +08:00
merge: incorporate origin/main into community-mode branch
Resolved 10 conflicted files:
- VERSION/package.json: kept 0.12.0.0 (feature branch version)
- CHANGELOG.md: preserved both branch entry and main's new entries
- supabase/config.sh: kept GSTACK_WEB_URL, accepted TELEMETRY_ENDPOINT removal
- bin/gstack-{community-dashboard,telemetry-log,telemetry-sync,update-check}:
took main's improved versions (edge function approach, safe cursor, UUID gen)
- supabase/functions/community-pulse: took main's count-based approach
- test/telemetry.test.ts: took main's structure with fingerprint field name
Post-merge fixes:
- Removed shadowed local RESOLVERS/functions in gen-skill-docs.ts (main's
resolver imports now take precedence for tier-based preamble, coverage gates)
- Added 3 missing E2E_TIERS entries (ship-plan-*, review-plan-completion)
- Updated telemetry test to match current prompt text
- Regenerated all SKILL.md files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
// gstack community-pulse edge function
|
||||
// Returns weekly active installation count for preamble display.
|
||||
// Cached for 1 hour via Cache-Control header.
|
||||
// Returns aggregated community stats for the dashboard:
|
||||
// weekly active count, top skills, crash clusters, version distribution.
|
||||
// Uses server-side cache (community_pulse_cache table) to prevent DoS.
|
||||
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
||||
|
||||
const CACHE_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
Deno.serve(async () => {
|
||||
const supabase = createClient(
|
||||
Deno.env.get("SUPABASE_URL") ?? "",
|
||||
@@ -11,68 +14,123 @@ Deno.serve(async () => {
|
||||
);
|
||||
|
||||
try {
|
||||
// Count unique update checks in the last 7 days (install base proxy)
|
||||
// Check cache first
|
||||
const { data: cached } = await supabase
|
||||
.from("community_pulse_cache")
|
||||
.select("data, refreshed_at")
|
||||
.eq("id", 1)
|
||||
.single();
|
||||
|
||||
if (cached?.refreshed_at) {
|
||||
const age = Date.now() - new Date(cached.refreshed_at).getTime();
|
||||
if (age < CACHE_MAX_AGE_MS) {
|
||||
return new Response(JSON.stringify(cached.data), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "public, max-age=3600",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Cache is stale or missing — recompute
|
||||
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
||||
const twoWeeksAgo = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
// This week's unique installs (by install_fingerprint, filtered to source=live)
|
||||
const { data: thisWeekData } = await supabase
|
||||
// Weekly active (update checks this week)
|
||||
const { count: thisWeek } = await supabase
|
||||
.from("update_checks")
|
||||
.select("install_fingerprint")
|
||||
.eq("source", "live")
|
||||
.gte("checked_at", weekAgo);
|
||||
|
||||
// Last week's unique installs (for change %)
|
||||
const { data: lastWeekData } = await supabase
|
||||
// Last week (for change %)
|
||||
const { count: lastWeek } = await supabase
|
||||
.from("update_checks")
|
||||
.select("install_fingerprint")
|
||||
.eq("source", "live")
|
||||
.gte("checked_at", twoWeeksAgo)
|
||||
.lt("checked_at", weekAgo);
|
||||
|
||||
let current = new Set((thisWeekData ?? []).map((e: { install_fingerprint: string }) => e.install_fingerprint).filter(Boolean)).size;
|
||||
let previous = new Set((lastWeekData ?? []).map((e: { install_fingerprint: string }) => e.install_fingerprint).filter(Boolean)).size;
|
||||
|
||||
// Fallback: if no fingerprinted data, count distinct sessions from telemetry_events
|
||||
if (current === 0) {
|
||||
const { data: thisWeekSessions } = await supabase
|
||||
.from("telemetry_events")
|
||||
.select("session_id")
|
||||
.eq("event_type", "skill_run")
|
||||
.eq("source", "live")
|
||||
.gte("event_timestamp", weekAgo);
|
||||
|
||||
const { data: lastWeekSessions } = await supabase
|
||||
.from("telemetry_events")
|
||||
.select("session_id")
|
||||
.eq("event_type", "skill_run")
|
||||
.eq("source", "live")
|
||||
.gte("event_timestamp", twoWeeksAgo)
|
||||
.lt("event_timestamp", weekAgo);
|
||||
|
||||
current = new Set((thisWeekSessions ?? []).map((e: { session_id: string }) => e.session_id)).size;
|
||||
previous = new Set((lastWeekSessions ?? []).map((e: { session_id: string }) => e.session_id)).size;
|
||||
}
|
||||
const current = thisWeek ?? 0;
|
||||
const previous = lastWeek ?? 0;
|
||||
const changePct = previous > 0
|
||||
? Math.round(((current - previous) / previous) * 100)
|
||||
: 0;
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
weekly_active: current,
|
||||
change_pct: changePct,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "public, max-age=3600", // 1 hour cache
|
||||
},
|
||||
// Top skills (last 7 days)
|
||||
const { data: skillRows } = await supabase
|
||||
.from("telemetry_events")
|
||||
.select("skill")
|
||||
.eq("event_type", "skill_run")
|
||||
.gte("event_timestamp", weekAgo)
|
||||
.not("skill", "is", null)
|
||||
.limit(1000);
|
||||
|
||||
const skillCounts: Record<string, number> = {};
|
||||
for (const row of skillRows ?? []) {
|
||||
if (row.skill) {
|
||||
skillCounts[row.skill] = (skillCounts[row.skill] ?? 0) + 1;
|
||||
}
|
||||
);
|
||||
}
|
||||
const topSkills = Object.entries(skillCounts)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 10)
|
||||
.map(([skill, count]) => ({ skill, count }));
|
||||
|
||||
// Crash clusters (top 5)
|
||||
const { data: crashes } = await supabase
|
||||
.from("crash_clusters")
|
||||
.select("error_class, gstack_version, total_occurrences, identified_users")
|
||||
.limit(5);
|
||||
|
||||
// Version distribution (last 7 days)
|
||||
const versionCounts: Record<string, number> = {};
|
||||
const { data: versionRows } = await supabase
|
||||
.from("telemetry_events")
|
||||
.select("gstack_version")
|
||||
.eq("event_type", "skill_run")
|
||||
.gte("event_timestamp", weekAgo)
|
||||
.limit(1000);
|
||||
|
||||
for (const row of versionRows ?? []) {
|
||||
if (row.gstack_version) {
|
||||
versionCounts[row.gstack_version] = (versionCounts[row.gstack_version] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
const topVersions = Object.entries(versionCounts)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 5)
|
||||
.map(([version, count]) => ({ version, count }));
|
||||
|
||||
const result = {
|
||||
weekly_active: current,
|
||||
change_pct: changePct,
|
||||
top_skills: topSkills,
|
||||
crashes: crashes ?? [],
|
||||
versions: topVersions,
|
||||
};
|
||||
|
||||
// Upsert cache
|
||||
await supabase
|
||||
.from("community_pulse_cache")
|
||||
.upsert({
|
||||
id: 1,
|
||||
data: result,
|
||||
refreshed_at: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify(result), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "public, max-age=3600",
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return new Response(
|
||||
JSON.stringify({ weekly_active: 0, change_pct: 0 }),
|
||||
JSON.stringify({ weekly_active: 0, change_pct: 0, top_skills: [], crashes: [], versions: [] }),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
||||
Reference in New Issue
Block a user