@runchr/gstack-antigravity 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @runchr/gstack-antigravity might be problematic. Click here for more details.
- package/.agents/rules/ETHOS.md +129 -0
- package/.agents/rules/global-gstack.md +117 -0
- package/.agents/rules/persona-gstack-autoplan.md +14 -0
- package/.agents/rules/persona-gstack-benchmark.md +14 -0
- package/.agents/rules/persona-gstack-browse.md +14 -0
- package/.agents/rules/persona-gstack-canary.md +14 -0
- package/.agents/rules/persona-gstack-careful.md +14 -0
- package/.agents/rules/persona-gstack-codex.md +14 -0
- package/.agents/rules/persona-gstack-cso.md +14 -0
- package/.agents/rules/persona-gstack-design-consultation.md +14 -0
- package/.agents/rules/persona-gstack-design-review.md +14 -0
- package/.agents/rules/persona-gstack-document-release.md +14 -0
- package/.agents/rules/persona-gstack-freeze.md +14 -0
- package/.agents/rules/persona-gstack-gstack-upgrade.md +14 -0
- package/.agents/rules/persona-gstack-guard.md +14 -0
- package/.agents/rules/persona-gstack-investigate.md +14 -0
- package/.agents/rules/persona-gstack-land-and-deploy.md +14 -0
- package/.agents/rules/persona-gstack-office-hours.md +14 -0
- package/.agents/rules/persona-gstack-plan-ceo-review.md +14 -0
- package/.agents/rules/persona-gstack-plan-design-review.md +14 -0
- package/.agents/rules/persona-gstack-plan-eng-review.md +14 -0
- package/.agents/rules/persona-gstack-qa-only.md +14 -0
- package/.agents/rules/persona-gstack-qa.md +14 -0
- package/.agents/rules/persona-gstack-retro.md +14 -0
- package/.agents/rules/persona-gstack-review.md +14 -0
- package/.agents/rules/persona-gstack-setup-browser-cookies.md +14 -0
- package/.agents/rules/persona-gstack-setup-deploy.md +14 -0
- package/.agents/rules/persona-gstack-ship.md +14 -0
- package/.agents/rules/persona-gstack-unfreeze.md +14 -0
- package/.agents/rules/persona-gstack.md +40 -0
- package/.agents/rules/recursive-identities.md +22 -0
- package/.agents/workflows/autoplan.md +30 -0
- package/.agents/workflows/benchmark.md +31 -0
- package/.agents/workflows/browse.md +26 -0
- package/.agents/workflows/canary.md +33 -0
- package/.agents/workflows/careful.md +22 -0
- package/.agents/workflows/codex.md +36 -0
- package/.agents/workflows/cso.md +29 -0
- package/.agents/workflows/design-consultation.md +28 -0
- package/.agents/workflows/design-review.md +28 -0
- package/.agents/workflows/document-release.md +32 -0
- package/.agents/workflows/freeze.md +17 -0
- package/.agents/workflows/gstack-upgrade.md +54 -0
- package/.agents/workflows/gstack.md +56 -0
- package/.agents/workflows/guard.md +18 -0
- package/.agents/workflows/investigate.md +37 -0
- package/.agents/workflows/land-and-deploy.md +35 -0
- package/.agents/workflows/office-hours.md +27 -0
- package/.agents/workflows/plan-ceo-review.md +34 -0
- package/.agents/workflows/plan-design-review.md +31 -0
- package/.agents/workflows/plan-eng-review.md +28 -0
- package/.agents/workflows/qa-only.md +28 -0
- package/.agents/workflows/qa.md +73 -0
- package/.agents/workflows/retro.md +34 -0
- package/.agents/workflows/review.md +30 -0
- package/.agents/workflows/setup-browser-cookies.md +15 -0
- package/.agents/workflows/setup-cookies.md +8 -0
- package/.agents/workflows/setup-deploy.md +21 -0
- package/.agents/workflows/ship.md +93 -0
- package/.agents/workflows/unfreeze.md +12 -0
- package/LICENSE +22 -0
- package/README.md +189 -0
- package/README_KO.md +191 -0
- package/bin/install.js +105 -0
- package/gstack-origin/.agents/skills/gstack/SKILL.md +651 -0
- package/gstack-origin/.agents/skills/gstack-autoplan/SKILL.md +678 -0
- package/gstack-origin/.agents/skills/gstack-benchmark/SKILL.md +482 -0
- package/gstack-origin/.agents/skills/gstack-browse/SKILL.md +511 -0
- package/gstack-origin/.agents/skills/gstack-canary/SKILL.md +486 -0
- package/gstack-origin/.agents/skills/gstack-careful/SKILL.md +50 -0
- package/gstack-origin/.agents/skills/gstack-cso/SKILL.md +607 -0
- package/gstack-origin/.agents/skills/gstack-design-consultation/SKILL.md +615 -0
- package/gstack-origin/.agents/skills/gstack-design-review/SKILL.md +988 -0
- package/gstack-origin/.agents/skills/gstack-document-release/SKILL.md +604 -0
- package/gstack-origin/.agents/skills/gstack-freeze/SKILL.md +67 -0
- package/gstack-origin/.agents/skills/gstack-guard/SKILL.md +62 -0
- package/gstack-origin/.agents/skills/gstack-investigate/SKILL.md +415 -0
- package/gstack-origin/.agents/skills/gstack-land-and-deploy/SKILL.md +873 -0
- package/gstack-origin/.agents/skills/gstack-office-hours/SKILL.md +986 -0
- package/gstack-origin/.agents/skills/gstack-plan-ceo-review/SKILL.md +1268 -0
- package/gstack-origin/.agents/skills/gstack-plan-design-review/SKILL.md +668 -0
- package/gstack-origin/.agents/skills/gstack-plan-eng-review/SKILL.md +826 -0
- package/gstack-origin/.agents/skills/gstack-qa/SKILL.md +1006 -0
- package/gstack-origin/.agents/skills/gstack-qa-only/SKILL.md +626 -0
- package/gstack-origin/.agents/skills/gstack-retro/SKILL.md +1065 -0
- package/gstack-origin/.agents/skills/gstack-review/SKILL.md +704 -0
- package/gstack-origin/.agents/skills/gstack-setup-browser-cookies/SKILL.md +325 -0
- package/gstack-origin/.agents/skills/gstack-setup-deploy/SKILL.md +450 -0
- package/gstack-origin/.agents/skills/gstack-ship/SKILL.md +1312 -0
- package/gstack-origin/.agents/skills/gstack-unfreeze/SKILL.md +36 -0
- package/gstack-origin/.agents/skills/gstack-upgrade/SKILL.md +220 -0
- package/gstack-origin/.env.example +5 -0
- package/gstack-origin/.github/workflows/skill-docs.yml +17 -0
- package/gstack-origin/AGENTS.md +49 -0
- package/gstack-origin/ARCHITECTURE.md +359 -0
- package/gstack-origin/BROWSER.md +271 -0
- package/gstack-origin/CHANGELOG.md +800 -0
- package/gstack-origin/CLAUDE.md +284 -0
- package/gstack-origin/CONTRIBUTING.md +370 -0
- package/gstack-origin/ETHOS.md +129 -0
- package/gstack-origin/LICENSE +21 -0
- package/gstack-origin/README.md +228 -0
- package/gstack-origin/SKILL.md +657 -0
- package/gstack-origin/SKILL.md.tmpl +281 -0
- package/gstack-origin/TODOS.md +564 -0
- package/gstack-origin/VERSION +1 -0
- package/gstack-origin/autoplan/SKILL.md +689 -0
- package/gstack-origin/autoplan/SKILL.md.tmpl +416 -0
- package/gstack-origin/benchmark/SKILL.md +489 -0
- package/gstack-origin/benchmark/SKILL.md.tmpl +233 -0
- package/gstack-origin/bin/dev-setup +68 -0
- package/gstack-origin/bin/dev-teardown +56 -0
- package/gstack-origin/bin/gstack-analytics +191 -0
- package/gstack-origin/bin/gstack-community-dashboard +113 -0
- package/gstack-origin/bin/gstack-config +38 -0
- package/gstack-origin/bin/gstack-diff-scope +71 -0
- package/gstack-origin/bin/gstack-global-discover.ts +591 -0
- package/gstack-origin/bin/gstack-repo-mode +93 -0
- package/gstack-origin/bin/gstack-review-log +9 -0
- package/gstack-origin/bin/gstack-review-read +12 -0
- package/gstack-origin/bin/gstack-slug +15 -0
- package/gstack-origin/bin/gstack-telemetry-log +158 -0
- package/gstack-origin/bin/gstack-telemetry-sync +127 -0
- package/gstack-origin/bin/gstack-update-check +196 -0
- package/gstack-origin/browse/SKILL.md +517 -0
- package/gstack-origin/browse/SKILL.md.tmpl +141 -0
- package/gstack-origin/browse/bin/find-browse +21 -0
- package/gstack-origin/browse/bin/remote-slug +14 -0
- package/gstack-origin/browse/scripts/build-node-server.sh +48 -0
- package/gstack-origin/browse/src/browser-manager.ts +634 -0
- package/gstack-origin/browse/src/buffers.ts +137 -0
- package/gstack-origin/browse/src/bun-polyfill.cjs +109 -0
- package/gstack-origin/browse/src/cli.ts +420 -0
- package/gstack-origin/browse/src/commands.ts +111 -0
- package/gstack-origin/browse/src/config.ts +150 -0
- package/gstack-origin/browse/src/cookie-import-browser.ts +417 -0
- package/gstack-origin/browse/src/cookie-picker-routes.ts +207 -0
- package/gstack-origin/browse/src/cookie-picker-ui.ts +541 -0
- package/gstack-origin/browse/src/find-browse.ts +61 -0
- package/gstack-origin/browse/src/meta-commands.ts +269 -0
- package/gstack-origin/browse/src/platform.ts +17 -0
- package/gstack-origin/browse/src/read-commands.ts +335 -0
- package/gstack-origin/browse/src/server.ts +369 -0
- package/gstack-origin/browse/src/snapshot.ts +398 -0
- package/gstack-origin/browse/src/url-validation.ts +91 -0
- package/gstack-origin/browse/src/write-commands.ts +352 -0
- package/gstack-origin/browse/test/bun-polyfill.test.ts +72 -0
- package/gstack-origin/browse/test/commands.test.ts +1836 -0
- package/gstack-origin/browse/test/config.test.ts +250 -0
- package/gstack-origin/browse/test/cookie-import-browser.test.ts +397 -0
- package/gstack-origin/browse/test/cookie-picker-routes.test.ts +205 -0
- package/gstack-origin/browse/test/find-browse.test.ts +50 -0
- package/gstack-origin/browse/test/fixtures/basic.html +33 -0
- package/gstack-origin/browse/test/fixtures/cursor-interactive.html +22 -0
- package/gstack-origin/browse/test/fixtures/dialog.html +15 -0
- package/gstack-origin/browse/test/fixtures/empty.html +2 -0
- package/gstack-origin/browse/test/fixtures/forms.html +55 -0
- package/gstack-origin/browse/test/fixtures/qa-eval-checkout.html +108 -0
- package/gstack-origin/browse/test/fixtures/qa-eval-spa.html +98 -0
- package/gstack-origin/browse/test/fixtures/qa-eval.html +51 -0
- package/gstack-origin/browse/test/fixtures/responsive.html +49 -0
- package/gstack-origin/browse/test/fixtures/snapshot.html +55 -0
- package/gstack-origin/browse/test/fixtures/spa.html +24 -0
- package/gstack-origin/browse/test/fixtures/states.html +17 -0
- package/gstack-origin/browse/test/fixtures/upload.html +25 -0
- package/gstack-origin/browse/test/gstack-config.test.ts +125 -0
- package/gstack-origin/browse/test/gstack-update-check.test.ts +467 -0
- package/gstack-origin/browse/test/handoff.test.ts +235 -0
- package/gstack-origin/browse/test/path-validation.test.ts +63 -0
- package/gstack-origin/browse/test/platform.test.ts +37 -0
- package/gstack-origin/browse/test/snapshot.test.ts +467 -0
- package/gstack-origin/browse/test/test-server.ts +57 -0
- package/gstack-origin/browse/test/url-validation.test.ts +72 -0
- package/gstack-origin/canary/SKILL.md +493 -0
- package/gstack-origin/canary/SKILL.md.tmpl +220 -0
- package/gstack-origin/careful/SKILL.md +59 -0
- package/gstack-origin/careful/SKILL.md.tmpl +57 -0
- package/gstack-origin/careful/bin/check-careful.sh +112 -0
- package/gstack-origin/codex/SKILL.md +677 -0
- package/gstack-origin/codex/SKILL.md.tmpl +356 -0
- package/gstack-origin/conductor.json +6 -0
- package/gstack-origin/cso/SKILL.md +615 -0
- package/gstack-origin/cso/SKILL.md.tmpl +376 -0
- package/gstack-origin/design-consultation/SKILL.md +625 -0
- package/gstack-origin/design-consultation/SKILL.md.tmpl +369 -0
- package/gstack-origin/design-review/SKILL.md +998 -0
- package/gstack-origin/design-review/SKILL.md.tmpl +262 -0
- package/gstack-origin/docs/images/github-2013.png +0 -0
- package/gstack-origin/docs/images/github-2026.png +0 -0
- package/gstack-origin/docs/skills.md +877 -0
- package/gstack-origin/document-release/SKILL.md +613 -0
- package/gstack-origin/document-release/SKILL.md.tmpl +357 -0
- package/gstack-origin/freeze/SKILL.md +82 -0
- package/gstack-origin/freeze/SKILL.md.tmpl +80 -0
- package/gstack-origin/freeze/bin/check-freeze.sh +68 -0
- package/gstack-origin/gstack-upgrade/SKILL.md +226 -0
- package/gstack-origin/gstack-upgrade/SKILL.md.tmpl +224 -0
- package/gstack-origin/guard/SKILL.md +82 -0
- package/gstack-origin/guard/SKILL.md.tmpl +80 -0
- package/gstack-origin/investigate/SKILL.md +435 -0
- package/gstack-origin/investigate/SKILL.md.tmpl +196 -0
- package/gstack-origin/land-and-deploy/SKILL.md +880 -0
- package/gstack-origin/land-and-deploy/SKILL.md.tmpl +575 -0
- package/gstack-origin/office-hours/SKILL.md +996 -0
- package/gstack-origin/office-hours/SKILL.md.tmpl +624 -0
- package/gstack-origin/package.json +55 -0
- package/gstack-origin/plan-ceo-review/SKILL.md +1277 -0
- package/gstack-origin/plan-ceo-review/SKILL.md.tmpl +838 -0
- package/gstack-origin/plan-design-review/SKILL.md +676 -0
- package/gstack-origin/plan-design-review/SKILL.md.tmpl +314 -0
- package/gstack-origin/plan-eng-review/SKILL.md +836 -0
- package/gstack-origin/plan-eng-review/SKILL.md.tmpl +279 -0
- package/gstack-origin/qa/SKILL.md +1016 -0
- package/gstack-origin/qa/SKILL.md.tmpl +316 -0
- package/gstack-origin/qa/references/issue-taxonomy.md +85 -0
- package/gstack-origin/qa/templates/qa-report-template.md +126 -0
- package/gstack-origin/qa-only/SKILL.md +633 -0
- package/gstack-origin/qa-only/SKILL.md.tmpl +101 -0
- package/gstack-origin/retro/SKILL.md +1072 -0
- package/gstack-origin/retro/SKILL.md.tmpl +833 -0
- package/gstack-origin/review/SKILL.md +849 -0
- package/gstack-origin/review/SKILL.md.tmpl +259 -0
- package/gstack-origin/review/TODOS-format.md +62 -0
- package/gstack-origin/review/checklist.md +190 -0
- package/gstack-origin/review/design-checklist.md +132 -0
- package/gstack-origin/review/greptile-triage.md +220 -0
- package/gstack-origin/scripts/analytics.ts +190 -0
- package/gstack-origin/scripts/dev-skill.ts +82 -0
- package/gstack-origin/scripts/eval-compare.ts +96 -0
- package/gstack-origin/scripts/eval-list.ts +116 -0
- package/gstack-origin/scripts/eval-select.ts +86 -0
- package/gstack-origin/scripts/eval-summary.ts +187 -0
- package/gstack-origin/scripts/eval-watch.ts +172 -0
- package/gstack-origin/scripts/gen-skill-docs.ts +2414 -0
- package/gstack-origin/scripts/skill-check.ts +167 -0
- package/gstack-origin/setup +269 -0
- package/gstack-origin/setup-browser-cookies/SKILL.md +330 -0
- package/gstack-origin/setup-browser-cookies/SKILL.md.tmpl +74 -0
- package/gstack-origin/setup-deploy/SKILL.md +459 -0
- package/gstack-origin/setup-deploy/SKILL.md.tmpl +220 -0
- package/gstack-origin/ship/SKILL.md +1457 -0
- package/gstack-origin/ship/SKILL.md.tmpl +528 -0
- package/gstack-origin/supabase/config.sh +10 -0
- package/gstack-origin/supabase/functions/community-pulse/index.ts +59 -0
- package/gstack-origin/supabase/functions/telemetry-ingest/index.ts +135 -0
- package/gstack-origin/supabase/functions/update-check/index.ts +37 -0
- package/gstack-origin/supabase/migrations/001_telemetry.sql +89 -0
- package/gstack-origin/test/analytics.test.ts +277 -0
- package/gstack-origin/test/codex-e2e.test.ts +197 -0
- package/gstack-origin/test/fixtures/coverage-audit-fixture.ts +76 -0
- package/gstack-origin/test/fixtures/eval-baselines.json +7 -0
- package/gstack-origin/test/fixtures/qa-eval-checkout-ground-truth.json +43 -0
- package/gstack-origin/test/fixtures/qa-eval-ground-truth.json +43 -0
- package/gstack-origin/test/fixtures/qa-eval-spa-ground-truth.json +43 -0
- package/gstack-origin/test/fixtures/review-eval-design-slop.css +86 -0
- package/gstack-origin/test/fixtures/review-eval-design-slop.html +41 -0
- package/gstack-origin/test/fixtures/review-eval-enum-diff.rb +30 -0
- package/gstack-origin/test/fixtures/review-eval-enum.rb +27 -0
- package/gstack-origin/test/fixtures/review-eval-vuln.rb +14 -0
- package/gstack-origin/test/gemini-e2e.test.ts +173 -0
- package/gstack-origin/test/gen-skill-docs.test.ts +1049 -0
- package/gstack-origin/test/global-discover.test.ts +187 -0
- package/gstack-origin/test/helpers/codex-session-runner.ts +282 -0
- package/gstack-origin/test/helpers/e2e-helpers.ts +239 -0
- package/gstack-origin/test/helpers/eval-store.test.ts +548 -0
- package/gstack-origin/test/helpers/eval-store.ts +689 -0
- package/gstack-origin/test/helpers/gemini-session-runner.test.ts +104 -0
- package/gstack-origin/test/helpers/gemini-session-runner.ts +201 -0
- package/gstack-origin/test/helpers/llm-judge.ts +130 -0
- package/gstack-origin/test/helpers/observability.test.ts +283 -0
- package/gstack-origin/test/helpers/session-runner.test.ts +96 -0
- package/gstack-origin/test/helpers/session-runner.ts +357 -0
- package/gstack-origin/test/helpers/skill-parser.ts +206 -0
- package/gstack-origin/test/helpers/touchfiles.ts +260 -0
- package/gstack-origin/test/hook-scripts.test.ts +373 -0
- package/gstack-origin/test/skill-e2e-browse.test.ts +293 -0
- package/gstack-origin/test/skill-e2e-deploy.test.ts +279 -0
- package/gstack-origin/test/skill-e2e-design.test.ts +614 -0
- package/gstack-origin/test/skill-e2e-plan.test.ts +538 -0
- package/gstack-origin/test/skill-e2e-qa-bugs.test.ts +194 -0
- package/gstack-origin/test/skill-e2e-qa-workflow.test.ts +412 -0
- package/gstack-origin/test/skill-e2e-review.test.ts +535 -0
- package/gstack-origin/test/skill-e2e-workflow.test.ts +586 -0
- package/gstack-origin/test/skill-e2e.test.ts +3325 -0
- package/gstack-origin/test/skill-llm-eval.test.ts +787 -0
- package/gstack-origin/test/skill-parser.test.ts +179 -0
- package/gstack-origin/test/skill-routing-e2e.test.ts +605 -0
- package/gstack-origin/test/skill-validation.test.ts +1520 -0
- package/gstack-origin/test/telemetry.test.ts +278 -0
- package/gstack-origin/test/touchfiles.test.ts +262 -0
- package/gstack-origin/unfreeze/SKILL.md +40 -0
- package/gstack-origin/unfreeze/SKILL.md.tmpl +38 -0
- package/package.json +38 -0
- package/scripts/install-antigravity-skill.ps1 +33 -0
- package/scripts/install-antigravity-skill.sh +41 -0
- package/scripts/sync-gstack-origin.ps1 +37 -0
- package/scripts/sync-gstack-origin.sh +35 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// gstack telemetry-ingest edge function
|
|
2
|
+
// Validates and inserts a batch of telemetry events.
|
|
3
|
+
// Called by bin/gstack-telemetry-sync.
|
|
4
|
+
|
|
5
|
+
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
|
6
|
+
|
|
7
|
+
interface TelemetryEvent {
|
|
8
|
+
v: number;
|
|
9
|
+
ts: string;
|
|
10
|
+
event_type: string;
|
|
11
|
+
skill: string;
|
|
12
|
+
session_id?: string;
|
|
13
|
+
gstack_version: string;
|
|
14
|
+
os: string;
|
|
15
|
+
arch?: string;
|
|
16
|
+
duration_s?: number;
|
|
17
|
+
outcome: string;
|
|
18
|
+
error_class?: string;
|
|
19
|
+
used_browse?: boolean;
|
|
20
|
+
sessions?: number;
|
|
21
|
+
installation_id?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const MAX_BATCH_SIZE = 100;
|
|
25
|
+
const MAX_PAYLOAD_BYTES = 50_000; // 50KB
|
|
26
|
+
|
|
27
|
+
Deno.serve(async (req) => {
|
|
28
|
+
if (req.method !== "POST") {
|
|
29
|
+
return new Response("POST required", { status: 405 });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check payload size
|
|
33
|
+
const contentLength = parseInt(req.headers.get("content-length") || "0");
|
|
34
|
+
if (contentLength > MAX_PAYLOAD_BYTES) {
|
|
35
|
+
return new Response("Payload too large", { status: 413 });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const body = await req.json();
|
|
40
|
+
const events: TelemetryEvent[] = Array.isArray(body) ? body : [body];
|
|
41
|
+
|
|
42
|
+
if (events.length > MAX_BATCH_SIZE) {
|
|
43
|
+
return new Response(`Batch too large (max ${MAX_BATCH_SIZE})`, { status: 400 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const supabase = createClient(
|
|
47
|
+
Deno.env.get("SUPABASE_URL") ?? "",
|
|
48
|
+
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? ""
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Validate and transform events
|
|
52
|
+
const rows = [];
|
|
53
|
+
const installationUpserts: Map<string, { version: string; os: string }> = new Map();
|
|
54
|
+
|
|
55
|
+
for (const event of events) {
|
|
56
|
+
// Required fields
|
|
57
|
+
if (!event.ts || !event.gstack_version || !event.os || !event.outcome) {
|
|
58
|
+
continue; // skip malformed
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Validate schema version
|
|
62
|
+
if (event.v !== 1) continue;
|
|
63
|
+
|
|
64
|
+
// Validate event_type
|
|
65
|
+
const validTypes = ["skill_run", "upgrade_prompted", "upgrade_completed"];
|
|
66
|
+
if (!validTypes.includes(event.event_type)) continue;
|
|
67
|
+
|
|
68
|
+
rows.push({
|
|
69
|
+
schema_version: event.v,
|
|
70
|
+
event_type: event.event_type,
|
|
71
|
+
gstack_version: String(event.gstack_version).slice(0, 20),
|
|
72
|
+
os: String(event.os).slice(0, 20),
|
|
73
|
+
arch: event.arch ? String(event.arch).slice(0, 20) : null,
|
|
74
|
+
event_timestamp: event.ts,
|
|
75
|
+
skill: event.skill ? String(event.skill).slice(0, 50) : null,
|
|
76
|
+
session_id: event.session_id ? String(event.session_id).slice(0, 50) : null,
|
|
77
|
+
duration_s: typeof event.duration_s === "number" ? event.duration_s : null,
|
|
78
|
+
outcome: String(event.outcome).slice(0, 20),
|
|
79
|
+
error_class: event.error_class ? String(event.error_class).slice(0, 100) : null,
|
|
80
|
+
used_browse: event.used_browse === true,
|
|
81
|
+
concurrent_sessions: typeof event.sessions === "number" ? event.sessions : 1,
|
|
82
|
+
installation_id: event.installation_id ? String(event.installation_id).slice(0, 64) : null,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Track installations for upsert
|
|
86
|
+
if (event.installation_id) {
|
|
87
|
+
installationUpserts.set(event.installation_id, {
|
|
88
|
+
version: event.gstack_version,
|
|
89
|
+
os: event.os,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (rows.length === 0) {
|
|
95
|
+
return new Response(JSON.stringify({ inserted: 0 }), {
|
|
96
|
+
status: 200,
|
|
97
|
+
headers: { "Content-Type": "application/json" },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Insert events
|
|
102
|
+
const { error: insertError } = await supabase
|
|
103
|
+
.from("telemetry_events")
|
|
104
|
+
.insert(rows);
|
|
105
|
+
|
|
106
|
+
if (insertError) {
|
|
107
|
+
return new Response(JSON.stringify({ error: insertError.message }), {
|
|
108
|
+
status: 500,
|
|
109
|
+
headers: { "Content-Type": "application/json" },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Upsert installations (update last_seen)
|
|
114
|
+
for (const [id, data] of installationUpserts) {
|
|
115
|
+
await supabase
|
|
116
|
+
.from("installations")
|
|
117
|
+
.upsert(
|
|
118
|
+
{
|
|
119
|
+
installation_id: id,
|
|
120
|
+
last_seen: new Date().toISOString(),
|
|
121
|
+
gstack_version: data.version,
|
|
122
|
+
os: data.os,
|
|
123
|
+
},
|
|
124
|
+
{ onConflict: "installation_id" }
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return new Response(JSON.stringify({ inserted: rows.length }), {
|
|
129
|
+
status: 200,
|
|
130
|
+
headers: { "Content-Type": "application/json" },
|
|
131
|
+
});
|
|
132
|
+
} catch {
|
|
133
|
+
return new Response("Invalid request", { status: 400 });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// gstack update-check edge function
|
|
2
|
+
// Logs an install ping and returns the current latest version.
|
|
3
|
+
// Called by bin/gstack-update-check as a parallel background request.
|
|
4
|
+
|
|
5
|
+
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
|
6
|
+
|
|
7
|
+
const CURRENT_VERSION = Deno.env.get("GSTACK_CURRENT_VERSION") || "0.6.4.1";
|
|
8
|
+
|
|
9
|
+
Deno.serve(async (req) => {
|
|
10
|
+
if (req.method !== "POST") {
|
|
11
|
+
return new Response(CURRENT_VERSION, { status: 200 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const { version, os } = await req.json();
|
|
16
|
+
|
|
17
|
+
if (!version || !os) {
|
|
18
|
+
return new Response(CURRENT_VERSION, { status: 200 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const supabase = createClient(
|
|
22
|
+
Deno.env.get("SUPABASE_URL") ?? "",
|
|
23
|
+
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? ""
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Log the update check (fire-and-forget)
|
|
27
|
+
await supabase.from("update_checks").insert({
|
|
28
|
+
gstack_version: String(version).slice(0, 20),
|
|
29
|
+
os: String(os).slice(0, 20),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return new Response(CURRENT_VERSION, { status: 200 });
|
|
33
|
+
} catch {
|
|
34
|
+
// Always return the version, even if logging fails
|
|
35
|
+
return new Response(CURRENT_VERSION, { status: 200 });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
-- gstack telemetry schema
|
|
2
|
+
-- Tables for tracking usage, installations, and update checks.
|
|
3
|
+
|
|
4
|
+
-- Main telemetry events (skill runs, upgrades)
|
|
5
|
+
CREATE TABLE telemetry_events (
|
|
6
|
+
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
7
|
+
received_at TIMESTAMPTZ DEFAULT now(),
|
|
8
|
+
schema_version INTEGER NOT NULL DEFAULT 1,
|
|
9
|
+
event_type TEXT NOT NULL DEFAULT 'skill_run',
|
|
10
|
+
gstack_version TEXT NOT NULL,
|
|
11
|
+
os TEXT NOT NULL,
|
|
12
|
+
arch TEXT,
|
|
13
|
+
event_timestamp TIMESTAMPTZ NOT NULL,
|
|
14
|
+
skill TEXT,
|
|
15
|
+
session_id TEXT,
|
|
16
|
+
duration_s NUMERIC,
|
|
17
|
+
outcome TEXT NOT NULL,
|
|
18
|
+
error_class TEXT,
|
|
19
|
+
used_browse BOOLEAN DEFAULT false,
|
|
20
|
+
concurrent_sessions INTEGER DEFAULT 1,
|
|
21
|
+
installation_id TEXT -- nullable, only for "community" tier
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
-- Index for skill_sequences view performance
|
|
25
|
+
CREATE INDEX idx_telemetry_session_ts ON telemetry_events (session_id, event_timestamp);
|
|
26
|
+
-- Index for crash clustering
|
|
27
|
+
CREATE INDEX idx_telemetry_error ON telemetry_events (error_class, gstack_version) WHERE outcome = 'error';
|
|
28
|
+
|
|
29
|
+
-- Retention tracking per installation
|
|
30
|
+
CREATE TABLE installations (
|
|
31
|
+
installation_id TEXT PRIMARY KEY,
|
|
32
|
+
first_seen TIMESTAMPTZ DEFAULT now(),
|
|
33
|
+
last_seen TIMESTAMPTZ DEFAULT now(),
|
|
34
|
+
gstack_version TEXT,
|
|
35
|
+
os TEXT
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
-- Install pings from update checks
|
|
39
|
+
CREATE TABLE update_checks (
|
|
40
|
+
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
41
|
+
checked_at TIMESTAMPTZ DEFAULT now(),
|
|
42
|
+
gstack_version TEXT NOT NULL,
|
|
43
|
+
os TEXT NOT NULL
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
-- RLS: anon key can INSERT and SELECT (all telemetry data is anonymous)
|
|
47
|
+
ALTER TABLE telemetry_events ENABLE ROW LEVEL SECURITY;
|
|
48
|
+
CREATE POLICY "anon_insert_only" ON telemetry_events FOR INSERT WITH CHECK (true);
|
|
49
|
+
CREATE POLICY "anon_select" ON telemetry_events FOR SELECT USING (true);
|
|
50
|
+
|
|
51
|
+
ALTER TABLE installations ENABLE ROW LEVEL SECURITY;
|
|
52
|
+
CREATE POLICY "anon_insert_only" ON installations FOR INSERT WITH CHECK (true);
|
|
53
|
+
CREATE POLICY "anon_select" ON installations FOR SELECT USING (true);
|
|
54
|
+
-- Allow upsert (update last_seen)
|
|
55
|
+
CREATE POLICY "anon_update_last_seen" ON installations FOR UPDATE USING (true) WITH CHECK (true);
|
|
56
|
+
|
|
57
|
+
ALTER TABLE update_checks ENABLE ROW LEVEL SECURITY;
|
|
58
|
+
CREATE POLICY "anon_insert_only" ON update_checks FOR INSERT WITH CHECK (true);
|
|
59
|
+
CREATE POLICY "anon_select" ON update_checks FOR SELECT USING (true);
|
|
60
|
+
|
|
61
|
+
-- Crash clustering view
|
|
62
|
+
CREATE VIEW crash_clusters AS
|
|
63
|
+
SELECT
|
|
64
|
+
error_class,
|
|
65
|
+
gstack_version,
|
|
66
|
+
COUNT(*) as total_occurrences,
|
|
67
|
+
COUNT(DISTINCT installation_id) as identified_users, -- community tier only
|
|
68
|
+
COUNT(*) - COUNT(installation_id) as anonymous_occurrences, -- events without installation_id
|
|
69
|
+
MIN(event_timestamp) as first_seen,
|
|
70
|
+
MAX(event_timestamp) as last_seen
|
|
71
|
+
FROM telemetry_events
|
|
72
|
+
WHERE outcome = 'error' AND error_class IS NOT NULL
|
|
73
|
+
GROUP BY error_class, gstack_version
|
|
74
|
+
ORDER BY total_occurrences DESC;
|
|
75
|
+
|
|
76
|
+
-- Skill sequence co-occurrence view
|
|
77
|
+
CREATE VIEW skill_sequences AS
|
|
78
|
+
SELECT
|
|
79
|
+
a.skill as skill_a,
|
|
80
|
+
b.skill as skill_b,
|
|
81
|
+
COUNT(DISTINCT a.session_id) as co_occurrences
|
|
82
|
+
FROM telemetry_events a
|
|
83
|
+
JOIN telemetry_events b ON a.session_id = b.session_id
|
|
84
|
+
AND a.skill != b.skill
|
|
85
|
+
AND a.event_timestamp < b.event_timestamp
|
|
86
|
+
WHERE a.event_type = 'skill_run' AND b.event_type = 'skill_run'
|
|
87
|
+
GROUP BY a.skill, b.skill
|
|
88
|
+
HAVING COUNT(DISTINCT a.session_id) >= 10
|
|
89
|
+
ORDER BY co_occurrences DESC;
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import { parseJSONL, filterByPeriod, formatReport } from '../scripts/analytics';
|
|
3
|
+
import type { AnalyticsEvent } from '../scripts/analytics';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as os from 'os';
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
|
|
9
|
+
const TMP_DIR = path.join(os.tmpdir(), 'analytics-test');
|
|
10
|
+
const SCRIPT = path.resolve(import.meta.dir, '../scripts/analytics.ts');
|
|
11
|
+
|
|
12
|
+
function writeTempJSONL(name: string, lines: string[]): string {
|
|
13
|
+
fs.mkdirSync(TMP_DIR, { recursive: true });
|
|
14
|
+
const p = path.join(TMP_DIR, name);
|
|
15
|
+
fs.writeFileSync(p, lines.join('\n') + '\n');
|
|
16
|
+
return p;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Run the analytics script with a custom JSONL file by overriding the path.
|
|
21
|
+
* We test the exported functions directly for unit tests, and use this
|
|
22
|
+
* helper for integration-style checks.
|
|
23
|
+
*/
|
|
24
|
+
function runScript(jsonlPath: string | null, extraArgs: string = ''): string {
|
|
25
|
+
// We test via the exported functions; for CLI integration we read the file
|
|
26
|
+
// and run the pipeline manually to avoid needing to override the hardcoded path.
|
|
27
|
+
if (jsonlPath === null) {
|
|
28
|
+
return 'No analytics data found.';
|
|
29
|
+
}
|
|
30
|
+
if (!fs.existsSync(jsonlPath)) {
|
|
31
|
+
return 'No analytics data found.';
|
|
32
|
+
}
|
|
33
|
+
const content = fs.readFileSync(jsonlPath, 'utf-8').trim();
|
|
34
|
+
if (!content) {
|
|
35
|
+
return 'No analytics data found.';
|
|
36
|
+
}
|
|
37
|
+
const events = parseJSONL(content);
|
|
38
|
+
if (events.length === 0) {
|
|
39
|
+
return 'No analytics data found.';
|
|
40
|
+
}
|
|
41
|
+
// Parse period from extraArgs
|
|
42
|
+
let period = 'all';
|
|
43
|
+
const match = extraArgs.match(/--period\s+(\S+)/);
|
|
44
|
+
if (match) period = match[1];
|
|
45
|
+
const filtered = filterByPeriod(events, period);
|
|
46
|
+
return formatReport(filtered, period);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
fs.mkdirSync(TMP_DIR, { recursive: true });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
fs.rmSync(TMP_DIR, { recursive: true, force: true });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('parseJSONL', () => {
|
|
58
|
+
test('parses valid JSONL lines', () => {
|
|
59
|
+
const content = [
|
|
60
|
+
'{"skill":"ship","ts":"2026-03-18T15:30:00Z","repo":"my-app"}',
|
|
61
|
+
'{"skill":"qa","ts":"2026-03-18T16:00:00Z","repo":"my-api"}',
|
|
62
|
+
].join('\n');
|
|
63
|
+
const events = parseJSONL(content);
|
|
64
|
+
expect(events).toHaveLength(2);
|
|
65
|
+
expect(events[0].skill).toBe('ship');
|
|
66
|
+
expect(events[1].skill).toBe('qa');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('skips malformed lines', () => {
|
|
70
|
+
const content = [
|
|
71
|
+
'{"skill":"ship","ts":"2026-03-18T15:30:00Z","repo":"my-app"}',
|
|
72
|
+
'not valid json',
|
|
73
|
+
'{broken',
|
|
74
|
+
'',
|
|
75
|
+
'{"skill":"qa","ts":"2026-03-18T16:00:00Z","repo":"my-api"}',
|
|
76
|
+
].join('\n');
|
|
77
|
+
const events = parseJSONL(content);
|
|
78
|
+
expect(events).toHaveLength(2);
|
|
79
|
+
expect(events[0].skill).toBe('ship');
|
|
80
|
+
expect(events[1].skill).toBe('qa');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('returns empty array for empty string', () => {
|
|
84
|
+
expect(parseJSONL('')).toHaveLength(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('skips objects missing ts field', () => {
|
|
88
|
+
const content = '{"skill":"ship","repo":"my-app"}\n';
|
|
89
|
+
const events = parseJSONL(content);
|
|
90
|
+
expect(events).toHaveLength(0);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('filterByPeriod', () => {
|
|
95
|
+
const now = new Date();
|
|
96
|
+
const daysAgo = (n: number) => new Date(now.getTime() - n * 24 * 60 * 60 * 1000).toISOString();
|
|
97
|
+
|
|
98
|
+
const events: AnalyticsEvent[] = [
|
|
99
|
+
{ skill: 'ship', ts: daysAgo(1), repo: 'app' },
|
|
100
|
+
{ skill: 'qa', ts: daysAgo(3), repo: 'app' },
|
|
101
|
+
{ skill: 'review', ts: daysAgo(10), repo: 'app' },
|
|
102
|
+
{ skill: 'retro', ts: daysAgo(40), repo: 'app' },
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
test('period "all" returns all events', () => {
|
|
106
|
+
expect(filterByPeriod(events, 'all')).toHaveLength(4);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('period "7d" returns only last 7 days', () => {
|
|
110
|
+
const filtered = filterByPeriod(events, '7d');
|
|
111
|
+
expect(filtered).toHaveLength(2);
|
|
112
|
+
expect(filtered[0].skill).toBe('ship');
|
|
113
|
+
expect(filtered[1].skill).toBe('qa');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('period "30d" returns last 30 days', () => {
|
|
117
|
+
const filtered = filterByPeriod(events, '30d');
|
|
118
|
+
expect(filtered).toHaveLength(3);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('invalid period string returns all events', () => {
|
|
122
|
+
expect(filterByPeriod(events, 'bogus')).toHaveLength(4);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('formatReport', () => {
|
|
127
|
+
test('includes header and period label', () => {
|
|
128
|
+
const report = formatReport([], 'all');
|
|
129
|
+
expect(report).toContain('gstack skill usage analytics');
|
|
130
|
+
expect(report).toContain('Period: all time');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('shows "last 7 days" for 7d period', () => {
|
|
134
|
+
const report = formatReport([], '7d');
|
|
135
|
+
expect(report).toContain('Period: last 7 days');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('shows "last 30 days" for 30d period', () => {
|
|
139
|
+
const report = formatReport([], '30d');
|
|
140
|
+
expect(report).toContain('Period: last 30 days');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('counts skill invocations correctly', () => {
|
|
144
|
+
const events: AnalyticsEvent[] = [
|
|
145
|
+
{ skill: 'ship', ts: '2026-03-18T15:30:00Z', repo: 'app' },
|
|
146
|
+
{ skill: 'ship', ts: '2026-03-18T16:00:00Z', repo: 'app' },
|
|
147
|
+
{ skill: 'qa', ts: '2026-03-18T16:30:00Z', repo: 'app' },
|
|
148
|
+
];
|
|
149
|
+
const report = formatReport(events);
|
|
150
|
+
expect(report).toContain('/ship');
|
|
151
|
+
expect(report).toContain('2 invocations');
|
|
152
|
+
expect(report).toContain('/qa');
|
|
153
|
+
expect(report).toContain('1 invocation');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('groups by repo', () => {
|
|
157
|
+
const events: AnalyticsEvent[] = [
|
|
158
|
+
{ skill: 'ship', ts: '2026-03-18T15:30:00Z', repo: 'app-a' },
|
|
159
|
+
{ skill: 'qa', ts: '2026-03-18T16:00:00Z', repo: 'app-a' },
|
|
160
|
+
{ skill: 'ship', ts: '2026-03-18T16:30:00Z', repo: 'app-b' },
|
|
161
|
+
];
|
|
162
|
+
const report = formatReport(events);
|
|
163
|
+
expect(report).toContain('app-a: ship(1) qa(1)');
|
|
164
|
+
expect(report).toContain('app-b: ship(1)');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('counts hook fire events separately', () => {
|
|
168
|
+
const events: AnalyticsEvent[] = [
|
|
169
|
+
{ skill: 'ship', ts: '2026-03-18T15:30:00Z', repo: 'app' },
|
|
170
|
+
{ skill: 'careful', ts: '2026-03-18T16:00:00Z', repo: 'app', event: 'hook_fire', pattern: 'rm_recursive' },
|
|
171
|
+
{ skill: 'careful', ts: '2026-03-18T16:30:00Z', repo: 'app', event: 'hook_fire', pattern: 'rm_recursive' },
|
|
172
|
+
{ skill: 'careful', ts: '2026-03-18T17:00:00Z', repo: 'app', event: 'hook_fire', pattern: 'git_force_push' },
|
|
173
|
+
];
|
|
174
|
+
const report = formatReport(events);
|
|
175
|
+
expect(report).toContain('Safety Hook Events');
|
|
176
|
+
expect(report).toContain('rm_recursive');
|
|
177
|
+
expect(report).toContain('2 fires');
|
|
178
|
+
expect(report).toContain('git_force_push');
|
|
179
|
+
expect(report).toContain('1 fire');
|
|
180
|
+
expect(report).toContain('Total: 1 skill invocation, 3 hook fires');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('handles mixed events correctly', () => {
|
|
184
|
+
const events: AnalyticsEvent[] = [
|
|
185
|
+
{ skill: 'ship', ts: '2026-03-18T15:30:00Z', repo: 'my-app' },
|
|
186
|
+
{ skill: 'ship', ts: '2026-03-18T15:35:00Z', repo: 'my-app' },
|
|
187
|
+
{ skill: 'qa', ts: '2026-03-18T16:00:00Z', repo: 'my-api' },
|
|
188
|
+
{ skill: 'careful', ts: '2026-03-18T16:30:00Z', repo: 'my-app', event: 'hook_fire', pattern: 'rm_recursive' },
|
|
189
|
+
];
|
|
190
|
+
const report = formatReport(events);
|
|
191
|
+
// Skills counted correctly (hook_fire events excluded from skill counts)
|
|
192
|
+
expect(report).toContain('Total: 3 skill invocations, 1 hook fire');
|
|
193
|
+
// Both sections present
|
|
194
|
+
expect(report).toContain('Top Skills');
|
|
195
|
+
expect(report).toContain('Safety Hook Events');
|
|
196
|
+
expect(report).toContain('By Repo');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('integration via runScript helper', () => {
|
|
201
|
+
test('missing file → "No analytics data found."', () => {
|
|
202
|
+
const output = runScript(path.join(TMP_DIR, 'nonexistent.jsonl'));
|
|
203
|
+
expect(output).toBe('No analytics data found.');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('null path → "No analytics data found."', () => {
|
|
207
|
+
const output = runScript(null);
|
|
208
|
+
expect(output).toBe('No analytics data found.');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('empty file → "No analytics data found."', () => {
|
|
212
|
+
const p = writeTempJSONL('empty.jsonl', ['']);
|
|
213
|
+
// Overwrite with truly empty content
|
|
214
|
+
fs.writeFileSync(p, '');
|
|
215
|
+
const output = runScript(p);
|
|
216
|
+
expect(output).toBe('No analytics data found.');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('all malformed lines → "No analytics data found."', () => {
|
|
220
|
+
const p = writeTempJSONL('bad.jsonl', [
|
|
221
|
+
'not json',
|
|
222
|
+
'{broken',
|
|
223
|
+
'42',
|
|
224
|
+
]);
|
|
225
|
+
const output = runScript(p);
|
|
226
|
+
expect(output).toBe('No analytics data found.');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('normal aggregation produces correct output', () => {
|
|
230
|
+
const p = writeTempJSONL('normal.jsonl', [
|
|
231
|
+
'{"skill":"ship","ts":"2026-03-18T15:30:00Z","repo":"my-app"}',
|
|
232
|
+
'{"skill":"ship","ts":"2026-03-18T15:35:00Z","repo":"my-app"}',
|
|
233
|
+
'{"skill":"qa","ts":"2026-03-18T16:00:00Z","repo":"my-app"}',
|
|
234
|
+
'{"skill":"review","ts":"2026-03-18T16:30:00Z","repo":"my-api"}',
|
|
235
|
+
]);
|
|
236
|
+
const output = runScript(p);
|
|
237
|
+
expect(output).toContain('/ship');
|
|
238
|
+
expect(output).toContain('2 invocations');
|
|
239
|
+
expect(output).toContain('/qa');
|
|
240
|
+
expect(output).toContain('1 invocation');
|
|
241
|
+
expect(output).toContain('/review');
|
|
242
|
+
expect(output).toContain('Total: 4 skill invocations, 0 hook fires');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('period filtering (7d) only includes recent entries', () => {
|
|
246
|
+
const now = new Date();
|
|
247
|
+
const recent = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString();
|
|
248
|
+
const old = new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString();
|
|
249
|
+
|
|
250
|
+
const p = writeTempJSONL('period.jsonl', [
|
|
251
|
+
`{"skill":"ship","ts":"${recent}","repo":"app"}`,
|
|
252
|
+
`{"skill":"qa","ts":"${old}","repo":"app"}`,
|
|
253
|
+
]);
|
|
254
|
+
const output = runScript(p, '--period 7d');
|
|
255
|
+
expect(output).toContain('Period: last 7 days');
|
|
256
|
+
expect(output).toContain('/ship');
|
|
257
|
+
expect(output).toContain('Total: 1 skill invocation, 0 hook fires');
|
|
258
|
+
// qa should be filtered out
|
|
259
|
+
expect(output).not.toContain('/qa');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('hook fire events counted in full pipeline', () => {
|
|
263
|
+
const p = writeTempJSONL('hooks.jsonl', [
|
|
264
|
+
'{"skill":"ship","ts":"2026-03-18T15:30:00Z","repo":"app"}',
|
|
265
|
+
'{"event":"hook_fire","skill":"careful","pattern":"rm_recursive","ts":"2026-03-18T16:00:00Z","repo":"app"}',
|
|
266
|
+
'{"event":"hook_fire","skill":"careful","pattern":"rm_recursive","ts":"2026-03-18T16:30:00Z","repo":"app"}',
|
|
267
|
+
'{"event":"hook_fire","skill":"careful","pattern":"git_force_push","ts":"2026-03-18T17:00:00Z","repo":"app"}',
|
|
268
|
+
]);
|
|
269
|
+
const output = runScript(p);
|
|
270
|
+
expect(output).toContain('Safety Hook Events');
|
|
271
|
+
expect(output).toContain('rm_recursive');
|
|
272
|
+
expect(output).toContain('2 fires');
|
|
273
|
+
expect(output).toContain('git_force_push');
|
|
274
|
+
expect(output).toContain('1 fire');
|
|
275
|
+
expect(output).toContain('Total: 1 skill invocation, 3 hook fires');
|
|
276
|
+
});
|
|
277
|
+
});
|