diff --git a/.github/workflows/selfhost.yml b/.github/workflows/selfhost.yml index f8ff53ee7b..ced861712c 100644 --- a/.github/workflows/selfhost.yml +++ b/.github/workflows/selfhost.yml @@ -140,6 +140,6 @@ jobs: if [ "$ok" != "1" ]; then echo "::error::container did not become healthy"; docker logs gt; exit 1; fi curl -sf http://127.0.0.1:8787/health | grep -q '"status":"ok"' curl -sf http://127.0.0.1:8787/ready | grep -q '"ok":true' - curl -sf http://127.0.0.1:8787/metrics | grep -q 'gittensory_uptime_seconds' + curl -sf http://127.0.0.1:8787/metrics | grep -q 'loopover_uptime_seconds' docker logs gt 2>&1 | grep -q 'selfhost_migrations_applied' echo "self-host smoke test passed" diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-configuration.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-configuration.tsx index a1425e55b7..1bee52a51e 100644 --- a/apps/gittensory-ui/src/routes/docs.self-hosting-configuration.tsx +++ b/apps/gittensory-ui/src/routes/docs.self-hosting-configuration.tsx @@ -289,7 +289,7 @@ GITHUB_METADATA_CACHE_TTL_SECONDS=600`} above so operators can keep repeated policy reads hot without broadening stale cache risk. Live CI status, check-run, check-suite, pull/issue subresources, pull mergeability, token minting, rate-limit, and collaborator-permission endpoints are never served from this cache. - Prometheus exports gittensory_github_response_cache_total, and the bundled + Prometheus exports loopover_github_response_cache_total, and the bundled self-host Grafana dashboard includes the hit/miss/coalesced/error breakdown. diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx index 50ad3918c2..02b12dae85 100644 --- a/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx +++ b/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx @@ -110,12 +110,12 @@ docker compose --profile postgres --profile observability --profile backup up -d Check sync health with chronyc sources (or ntpq -p on an{" "} ntpd host) — every configured source should show a nonzero Reach{" "} value; Reach: 0 means that source has never successfully synced. The{" "} - gittensory_clock_skew_seconds gauge on the Clock Sync (NTP){" "} - row of the main Grafana dashboard tracks the live drift between this process and GitHub's - server time, sampled from the Date header of the GitHub App's own - installation-token mint calls — no extra network probe required. The bundled Prometheus - rules alert at 60s (warning) and 120s (critical) drift, both well under the margin that - actually breaks JWT auth. + loopover_clock_skew_seconds gauge on the Clock Sync (NTP) row + of the main Grafana dashboard tracks the live drift between this process and GitHub's server + time, sampled from the Date header of the GitHub App's own installation-token + mint calls — no extra network probe required. The bundled Prometheus rules alert at 60s + (warning) and 120s (critical) drift, both well under the margin that actually breaks JWT + auth.

Alerting — required for a 24/7 deployment

@@ -650,7 +650,7 @@ SENTRY_RELEASE=gittensory-selfhost@2026.07.05 Check Grafana dead-job panel, /metrics{" "} - gittensory_jobs_dead_total, queue logs; replay from DLQ dashboard only + loopover_jobs_dead_total, queue logs; replay from DLQ dashboard only after fixing root cause diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-troubleshooting.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-troubleshooting.tsx index d7e4d0a8b7..04ad7b5906 100644 --- a/apps/gittensory-ui/src/routes/docs.self-hosting-troubleshooting.tsx +++ b/apps/gittensory-ui/src/routes/docs.self-hosting-troubleshooting.tsx @@ -36,7 +36,7 @@ function SelfHostingTroubleshooting() { @@ -122,18 +122,17 @@ rees_analyzer_config_invalid`}

GitHub rate-limit responses or admission deferrals

Two independent signals cover this:{" "} - gittensory_github_rest_rate_limit_responses_total counts actual 403/429 - responses from GitHub, and the{" "} - gittensory_jobs_rate_limit_admission_deferred_total /{" "} - gittensory_jobs_rate_limit_budget_deferred_total /{" "} - gittensory_jobs_rate_limited_by_type_total counters track jobs the queue itself + loopover_github_rest_rate_limit_responses_total counts actual 403/429 responses + from GitHub, and the loopover_jobs_rate_limit_admission_deferred_total /{" "} + loopover_jobs_rate_limit_budget_deferred_total /{" "} + loopover_jobs_rate_limited_by_type_total counters track jobs the queue itself held back before making a request, to avoid tripping a limit. All three job-side counters carry the same three labels — kind (webhook or{" "} background), key_scope (installation,{" "} @@ -145,20 +144,20 @@ docker compose logs gittensory | grep selfhost_job_dead`} A short burst of deferrals is expected and self-resolving: the queue is deliberately trading a few seconds of delay to avoid a real 429. Treat it as a real problem only once it's sustained — which is exactly what{" "} - GittensoryGitHubRateLimitResponses (real 403/429s observed) and{" "} - GittensoryQueueRateLimitDeferralsHigh (a sustained deferral rate, not a blip) - are tuned to alert on, rather than firing on every brief admission hold. + LoopoverGitHubRateLimitResponses (real 403/429s observed) and{" "} + LoopoverQueueRateLimitDeferralsHigh (a sustained deferral rate, not a blip) are + tuned to alert on, rather than firing on every brief admission hold.

If a single key_scope=installation pool is consistently the bottleneck, the fix @@ -168,33 +167,33 @@ sum(rate(gittensory_github_rest_rate_limit_responses_total[10m]))`}

Low GitHub response-cache hit rate

- gittensory_github_response_cache_total (REST) and{" "} - gittensory_github_graphql_cache_total (GraphQL) both carry a{" "} - result label — hit, miss, set,{" "} - coalesced, bypassed, or error — and a{" "} - class label identifying the endpoint family. A healthy cache should show most - traffic as hit for endpoints that are read repeatedly in one review/maintenance - pass (PR reads, check-run lookups); a low hit rate on those specific classes, not the - overall average, is the useful signal. + loopover_github_response_cache_total (REST) and{" "} + loopover_github_graphql_cache_total (GraphQL) both carry a result{" "} + label — hit, miss, set, coalesced,{" "} + bypassed, or error — and a class label identifying + the endpoint family. A healthy cache should show most traffic as hit for + endpoints that are read repeatedly in one review/maintenance pass (PR reads, check-run + lookups); a low hit rate on those specific classes, not the overall average, is the useful + signal.

Qdrant / vector-store errors

- gittensory_qdrant_errors_total carries an op label ( + loopover_qdrant_errors_total carries an op label ( upsert, query, or delete) so you can tell whether - indexing or retrieval is failing. GittensoryQdrantErrorRateHigh fires on a + indexing or retrieval is failing. LoopoverQdrantErrorRateHigh fires on a sustained error ratio, not an isolated blip.

    @@ -226,14 +225,14 @@ curl -X DELETE "$QDRANT_URL/collections/gittensory"`}

    Orb export or relay problems

    - For brokered self-host deployments, gittensory_orb_events_exported_total and{" "} - gittensory_orb_export_errors_total track the hourly outcome-export loop;{" "} - GittensoryOrbExportErrorRateHigh fires on a sustained error ratio there. The + For brokered self-host deployments, loopover_orb_events_exported_total and{" "} + loopover_orb_export_errors_total track the hourly outcome-export loop;{" "} + LoopoverOrbExportErrorRateHigh fires on a sustained error ratio there. The pull-mode relay loop (for installations receiving events outbound from Orb) reports through{" "} - gittensory_orb_relay_drains_total (result=events when it drained - something, result=empty otherwise) and{" "} - gittensory_orb_webhook_total (event + result labels) - for what happened to each relayed event once enqueued locally. + loopover_orb_relay_drains_total (result=events when it drained + something, result=empty otherwise) and loopover_orb_webhook_total{" "} + (event + result labels) for what happened to each relayed event + once enqueued locally.

    If exports are failing but the relay itself looks healthy, the export loop's Sentry @@ -257,7 +256,7 @@ curl -X DELETE "$QDRANT_URL/collections/gittensory"`}

  • Pull mode — logged at warn and non-fatal: the drain loop ( - gittensory_orb_relay_drains_total) keeps retrying on its own schedule + loopover_orb_relay_drains_total) keeps retrying on its own schedule regardless, so a transient failure here recovers on its own once the broker is reachable again. A registration failure that never clears across many retries still points at{" "} ORB_ENROLLMENT_SECRET being wrong, revoked, or not yet provisioned @@ -274,9 +273,9 @@ curl -X DELETE "$QDRANT_URL/collections/gittensory"`}

    Each AI provider (self-host AI_PROVIDER entries) has its own circuit breaker: after 3 consecutive failures it stops attempting real calls to that provider for 60 seconds, - recorded as gittensory_ai_provider_circuit_open_total{'{provider="..."}'}{" "} + recorded as loopover_ai_provider_circuit_open_total{'{provider="..."}'}{" "} (skipped calls) alongside{" "} - gittensory_ai_provider_failures_total{'{provider="..."}'} (real failures). It + loopover_ai_provider_failures_total{'{provider="..."}'} (real failures). It self-heals automatically — there is no manual reset — but it will reopen immediately if the underlying problem is still there.

    @@ -293,9 +292,9 @@ curl -X DELETE "$QDRANT_URL/collections/gittensory"`} unreachable from the container.
  • - GittensoryAiProviderCircuitOpen fires on any circuit-open event in a - 15-minute window — a single trip during a real but brief outage is expected; a rule that - keeps firing across multiple windows points at the persistent case above. + LoopoverAiProviderCircuitOpen fires on any circuit-open event in a 15-minute + window — a single trip during a real but brief outage is expected; a rule that keeps + firing across multiple windows points at the persistent case above.
@@ -326,7 +325,7 @@ npm run test:smoke:observability:metrics`}
  • If the app's own /metrics check fails, that is unrelated to the OTEL - collector — check the app container directly (docker compose logs gittensory + collector — check the app container directly (docker compose logs loopover ).
  • diff --git a/docker-compose.yml b/docker-compose.yml index 77d1994363..d3cb984346 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,7 +38,7 @@ # (chrony/ntpd), not just one. A single dead NTP source silently drifts the clock with no local # symptom until GitHub App JWT auth starts failing ("Bad credentials") once the drift exceeds the # JWT's 60s skew tolerance. See docs/self-hosting/operations → "Host clock sync (NTP)" and the -# gittensory_clock_skew_seconds Grafana panel/alert (--profile observability) for live drift. +# loopover_clock_skew_seconds Grafana panel/alert (--profile observability) for live drift. # Bounded container logging (#audit-rate-headroom): every service below defaults to Docker's # json-file driver, which has NO size cap on its own -- a long-running 24/7 stack can fill the @@ -1160,7 +1160,7 @@ services: - -c - "apk add --no-cache busybox-extras && sh /scripts/backup-metrics.sh" healthcheck: - test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9101/metrics | grep -q '^gittensory_backup_latest_timestamp_seconds'"] + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9101/metrics | grep -q '^loopover_backup_latest_timestamp_seconds'"] interval: 30s retries: 5 diff --git a/grafana/dashboards/ai-usage.json b/grafana/dashboards/ai-usage.json index b01363369e..a220fe2c91 100644 --- a/grafana/dashboards/ai-usage.json +++ b/grafana/dashboards/ai-usage.json @@ -1,7 +1,7 @@ { - "uid": "gittensory-ai-usage", - "title": "Gittensory - AI usage", - "tags": ["gittensory", "ai"], + "uid": "loopover-ai-usage", + "title": "LoopOver - AI usage", + "tags": ["loopover", "ai"], "timezone": "browser", "schemaVersion": 39, "version": 1, @@ -14,7 +14,7 @@ "name": "provider", "label": "Provider", "type": "query", - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "query": { "queryType": "table", "queryText": "SELECT DISTINCT provider FROM ai_usage_events WHERE provider IS NOT NULL ORDER BY provider", "rawQueryText": "SELECT DISTINCT provider FROM ai_usage_events WHERE provider IS NOT NULL ORDER BY provider" }, "includeAll": true, "allValue": "$__all", @@ -27,7 +27,7 @@ "name": "feature", "label": "Feature", "type": "query", - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "query": { "queryType": "table", "queryText": "SELECT DISTINCT feature FROM ai_usage_events ORDER BY feature", "rawQueryText": "SELECT DISTINCT feature FROM ai_usage_events ORDER BY feature" }, "includeAll": true, "allValue": "$__all", @@ -40,7 +40,7 @@ "name": "model", "label": "Model", "type": "query", - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "query": { "queryType": "table", "queryText": "SELECT DISTINCT model FROM ai_usage_events WHERE (${provider:sqlstring} = '$__all' OR provider = ${provider:sqlstring}) ORDER BY model", @@ -85,7 +85,7 @@ "title": "Total AI events", "description": "Every ai_usage_events row with a non-null provider (i.e. a real, attributed AI call) in the selected time range, filtered by Provider/Feature/Model.", "gridPos": { "h": 5, "w": 6, "x": 0, "y": 1 }, - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "fieldConfig": { "defaults": { "unit": "short", "color": { "mode": "fixed", "fixedColor": "blue" } } }, "options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "none", "justifyMode": "center" }, "targets": [ @@ -102,7 +102,7 @@ "type": "stat", "title": "Total tokens", "gridPos": { "h": 5, "w": 6, "x": 6, "y": 1 }, - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "fieldConfig": { "defaults": { "unit": "short", "color": { "mode": "fixed", "fixedColor": "purple" } } }, "options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "none", "justifyMode": "center" }, "targets": [ @@ -119,7 +119,7 @@ "type": "stat", "title": "Total cost", "gridPos": { "h": 5, "w": 6, "x": 12, "y": 1 }, - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "fieldConfig": { "defaults": { "unit": "currencyUSD", "decimals": 4, "color": { "mode": "fixed", "fixedColor": "green" } } }, "options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "none", "justifyMode": "center" }, "targets": [ @@ -137,7 +137,7 @@ "title": "Events missing real usage", "description": "Completed AI calls (status='ok') with no real usage captured (no provider attribution, or every numeric field still at its zero default). Only scoped by Feature + time range — not by Provider/Model, since a NULL provider is exactly the kind of row this panel exists to surface, and filtering it out by provider would hide the very rows it's meant to catch. Excludes quota_exceeded/disabled/unavailable (no call was attempted) and ai_key_change (not an AI call).", "gridPos": { "h": 5, "w": 6, "x": 18, "y": 1 }, - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "fieldConfig": { "defaults": { "unit": "short", "color": { "mode": "fixed", "fixedColor": "orange" } } }, "options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "none", "justifyMode": "center" }, "targets": [ @@ -161,7 +161,7 @@ "title": "Tokens by feature, over time", "description": "Stacked daily token totals per feature within the selected Provider/Model filter — a spike in one feature's stack is the signal to investigate.", "gridPos": { "h": 8, "w": 12, "x": 0, "y": 7 }, - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "fieldConfig": { "defaults": { "unit": "short", "custom": { "drawStyle": "bars", "fillOpacity": 70, "lineWidth": 1, "stacking": { "mode": "normal" } } } }, "options": { "legend": { "showLegend": true, "placement": "bottom" }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -180,7 +180,7 @@ "type": "piechart", "title": "Cost share by feature", "gridPos": { "h": 8, "w": 6, "x": 12, "y": 7 }, - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "fieldConfig": { "defaults": { "unit": "currencyUSD" } }, "options": { "pieType": "donut", "legend": { "showLegend": true, "placement": "right", "values": ["percent", "value"] }, "reduceOptions": { "calcs": ["lastNotNull"] }, "displayLabels": ["percent"] }, "targets": [ @@ -197,7 +197,7 @@ "type": "piechart", "title": "Events by provider", "gridPos": { "h": 8, "w": 6, "x": 18, "y": 7 }, - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "fieldConfig": { "defaults": { "unit": "short" } }, "options": { "pieType": "donut", "legend": { "showLegend": true, "placement": "right", "values": ["percent", "value"] }, "reduceOptions": { "calcs": ["lastNotNull"] }, "displayLabels": ["percent"] }, "targets": [ @@ -214,7 +214,7 @@ "type": "table", "title": "Usage by provider + model + effort", "gridPos": { "h": 8, "w": 24, "x": 0, "y": 15 }, - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "options": { "showHeader": true, "cellHeight": "sm", "sortBy": [{ "displayName": "events", "desc": true }] }, "fieldConfig": { "defaults": { "custom": { "filterable": true } } }, "targets": [ @@ -237,7 +237,7 @@ "type": "table", "title": "Top repos by AI usage", "gridPos": { "h": 10, "w": 12, "x": 0, "y": 24 }, - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "options": { "showHeader": true, "cellHeight": "sm", "sortBy": [{ "displayName": "events", "desc": true }] }, "fieldConfig": { "defaults": { "custom": { "filterable": true } } }, "targets": [ @@ -254,7 +254,7 @@ "type": "table", "title": "Recent AI events", "gridPos": { "h": 10, "w": 12, "x": 12, "y": 24 }, - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "options": { "showHeader": true, "cellHeight": "sm", "sortBy": [{ "displayName": "created_at", "desc": true }] }, "fieldConfig": { "defaults": { "custom": { "filterable": true } } }, "targets": [ @@ -276,12 +276,12 @@ "id": 15, "type": "timeseries", "title": "Cumulative AI cost (USD) by provider", - "description": "gittensory's own monotonic cost counter (gittensory_ai_cost_usd_total) — continuously scraped, so it draws a clean cumulative line. Resets to 0 on a stack restart.", + "description": "loopover's own monotonic cost counter (loopover_ai_cost_usd_total) — continuously scraped, so it draws a clean cumulative line. Resets to 0 on a stack restart.", "gridPos": { "h": 8, "w": 12, "x": 0, "y": 35 }, "datasource": { "type": "prometheus", "uid": "prometheus" }, "fieldConfig": { "defaults": { "unit": "currencyUSD", "color": { "mode": "palette-classic" }, "custom": { "lineWidth": 2, "fillOpacity": 10 } } }, "options": { "legend": { "showLegend": true, "placement": "bottom" }, "tooltip": { "mode": "multi" } }, - "targets": [{ "refId": "A", "expr": "sum by (provider) (gittensory_ai_cost_usd_total) or vector(0)", "legendFormat": "{{provider}}" }] + "targets": [{ "refId": "A", "expr": "sum by (provider) (loopover_ai_cost_usd_total) or vector(0)", "legendFormat": "{{provider}}" }] }, { "id": 16, @@ -294,7 +294,7 @@ "targets": [ { "refId": "A", - "expr": "sum by (provider) ((rate(gittensory_ai_input_tokens_total[5m]) + rate(gittensory_ai_output_tokens_total[5m])) * 60)", + "expr": "sum by (provider) ((rate(loopover_ai_input_tokens_total[5m]) + rate(loopover_ai_output_tokens_total[5m])) * 60)", "legendFormat": "{{provider}}" } ] @@ -308,8 +308,8 @@ "fieldConfig": { "defaults": { "unit": "short", "color": { "mode": "palette-classic" }, "custom": { "lineWidth": 2, "fillOpacity": 10 } } }, "options": { "legend": { "showLegend": true, "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "targets": [ - { "refId": "A", "expr": "sum by (model, effort) (increase(gittensory_ai_requests_total[1h]))", "legendFormat": "{{model}} · {{effort}}" }, - { "refId": "B", "expr": "sum by (primary, fallback) (increase(gittensory_ai_review_model_fallback_total[1h]))", "legendFormat": "fallback {{primary}}→{{fallback}}" } + { "refId": "A", "expr": "sum by (model, effort) (increase(loopover_ai_requests_total[1h]))", "legendFormat": "{{model}} · {{effort}}" }, + { "refId": "B", "expr": "sum by (primary, fallback) (increase(loopover_ai_review_model_fallback_total[1h]))", "legendFormat": "fallback {{primary}}→{{fallback}}" } ] }, { @@ -321,8 +321,8 @@ "fieldConfig": { "defaults": { "unit": "short", "color": { "mode": "palette-classic" }, "custom": { "lineWidth": 2, "fillOpacity": 10 } } }, "options": { "legend": { "showLegend": true, "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "targets": [ - { "refId": "A", "expr": "sum by (provider, kind) (gittensory_ai_input_tokens_total)", "legendFormat": "{{provider}} {{kind}} in" }, - { "refId": "B", "expr": "sum by (provider, kind) (gittensory_ai_output_tokens_total)", "legendFormat": "{{provider}} {{kind}} out" } + { "refId": "A", "expr": "sum by (provider, kind) (loopover_ai_input_tokens_total)", "legendFormat": "{{provider}} {{kind}} in" }, + { "refId": "B", "expr": "sum by (provider, kind) (loopover_ai_output_tokens_total)", "legendFormat": "{{provider}} {{kind}} out" } ] }, { @@ -337,7 +337,7 @@ "targets": [ { "refId": "A", - "expr": "sum by (model, effort) (increase(gittensory_ai_requests_total{provider=\"codex\"}[$__rate_interval]))", + "expr": "sum by (model, effort) (increase(loopover_ai_requests_total{provider=\"codex\"}[$__rate_interval]))", "legendFormat": "{{model}} / {{effort}}" } ] @@ -351,8 +351,8 @@ "fieldConfig": { "defaults": { "unit": "short", "custom": { "drawStyle": "bars", "fillOpacity": 70, "lineWidth": 1, "stacking": { "mode": "normal" } } } }, "options": { "legend": { "showLegend": true, "placement": "bottom" }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ - { "refId": "A", "expr": "sum by (kind) (increase(gittensory_ai_input_tokens_total{provider=\"codex\"}[$__rate_interval]))", "legendFormat": "input {{kind}}" }, - { "refId": "B", "expr": "sum by (kind) (increase(gittensory_ai_output_tokens_total{provider=\"codex\"}[$__rate_interval]))", "legendFormat": "output {{kind}}" } + { "refId": "A", "expr": "sum by (kind) (increase(loopover_ai_input_tokens_total{provider=\"codex\"}[$__rate_interval]))", "legendFormat": "input {{kind}}" }, + { "refId": "B", "expr": "sum by (kind) (increase(loopover_ai_output_tokens_total{provider=\"codex\"}[$__rate_interval]))", "legendFormat": "output {{kind}}" } ] }, { diff --git a/grafana/dashboards/browserless-metrics.json b/grafana/dashboards/browserless-metrics.json index 0b224780b7..74cfc5f1fc 100644 --- a/grafana/dashboards/browserless-metrics.json +++ b/grafana/dashboards/browserless-metrics.json @@ -114,7 +114,7 @@ ], "refresh": "30s", "schemaVersion": 38, - "tags": ["gittensory", "browserless", "observability"], + "tags": ["loopover", "browserless", "observability"], "templating": { "list": [ { "current": {}, "hide": 0, "includeAll": false, "name": "DS_PROMETHEUS", "options": [], "query": "prometheus", "refresh": 1, "type": "datasource" } @@ -123,7 +123,7 @@ "time": { "from": "now-6h", "to": "now" }, "timepicker": {}, "timezone": "browser", - "title": "Gittensory — Browserless (visual review)", - "uid": "gittensory-browserless", + "title": "LoopOver — Browserless (visual review)", + "uid": "loopover-browserless", "version": 1 } diff --git a/grafana/dashboards/github-prs.json b/grafana/dashboards/github-prs.json index b40eed4948..69b9fb0f80 100644 --- a/grafana/dashboards/github-prs.json +++ b/grafana/dashboards/github-prs.json @@ -1,7 +1,7 @@ { - "uid": "gittensory-github", - "title": "Gittensory — Upstream PRs & issues (GitHub)", - "tags": ["gittensory", "github", "maintainer"], + "uid": "loopover-github", + "title": "LoopOver — Upstream PRs & issues (GitHub)", + "tags": ["loopover", "github", "maintainer"], "timezone": "browser", "schemaVersion": 39, "version": 1, @@ -14,7 +14,7 @@ "name": "scope", "label": "Scope", "type": "query", - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "description": "Dynamic, never hardcoded: built from the repositories tracked in this self-hoster's own reporting DB. Each option is an explicit repo:/ GitHub search qualifier; the dashboard deliberately does not synthesize an org-wide \"All repos\" option because the GitHub datasource token may be able to read private repositories that are not tracked locally.", "query": { "queryType": "table", diff --git a/grafana/dashboards/gittensory.json b/grafana/dashboards/gittensory.json index d0dc0964ff..0a65bcaa1d 100644 --- a/grafana/dashboards/gittensory.json +++ b/grafana/dashboards/gittensory.json @@ -60,7 +60,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_uptime_seconds", + "expr": "loopover_uptime_seconds", "legendFormat": "uptime" } ] @@ -100,7 +100,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_queue_pending", + "expr": "loopover_queue_pending", "legendFormat": "pending" } ] @@ -140,7 +140,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_queue_dead", + "expr": "loopover_queue_dead", "legendFormat": "dead" } ] @@ -176,7 +176,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_jobs_processed_persisted_total", + "expr": "loopover_jobs_processed_persisted_total", "legendFormat": "processed" } ] @@ -216,7 +216,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_webhook_dedup_total", + "expr": "loopover_webhook_dedup_total", "legendFormat": "deduped" } ] @@ -255,7 +255,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum(gittensory_qdrant_errors_total) or vector(0)", + "expr": "sum(loopover_qdrant_errors_total) or vector(0)", "legendFormat": "errors" } ] @@ -291,12 +291,12 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "rate(gittensory_http_requests_total[2m])", + "expr": "rate(loopover_http_requests_total[2m])", "legendFormat": "requests/s" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "rate(gittensory_webhook_dedup_total[2m])", + "expr": "rate(loopover_webhook_dedup_total[2m])", "legendFormat": "dedup/s" } ] @@ -325,12 +325,12 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_queue_pending", + "expr": "loopover_queue_pending", "legendFormat": "pending" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_queue_dead", + "expr": "loopover_queue_dead", "legendFormat": "dead-letter" } ] @@ -366,22 +366,22 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "rate(gittensory_jobs_processed_persisted_total[2m])", + "expr": "rate(loopover_jobs_processed_persisted_total[2m])", "legendFormat": "processed/s" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "rate(gittensory_jobs_enqueued_persisted_total[2m])", + "expr": "rate(loopover_jobs_enqueued_persisted_total[2m])", "legendFormat": "enqueued/s" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "rate(gittensory_jobs_failed_persisted_total[2m])", + "expr": "rate(loopover_jobs_failed_persisted_total[2m])", "legendFormat": "failed/s" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "rate(gittensory_jobs_dead_persisted_total[2m])", + "expr": "rate(loopover_jobs_dead_persisted_total[2m])", "legendFormat": "dead/s" } ] @@ -412,7 +412,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "rate(gittensory_jobs_failed_persisted_total[5m]) / (rate(gittensory_jobs_processed_persisted_total[5m]) + rate(gittensory_jobs_failed_persisted_total[5m]) + 0.0001)", + "expr": "rate(loopover_jobs_failed_persisted_total[5m]) / (rate(loopover_jobs_processed_persisted_total[5m]) + rate(loopover_jobs_failed_persisted_total[5m]) + 0.0001)", "legendFormat": "failure %" } ] @@ -448,7 +448,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "rate(gittensory_qdrant_queries_total[2m])", + "expr": "rate(loopover_qdrant_queries_total[2m])", "legendFormat": "queries/s" } ] @@ -477,12 +477,12 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "rate(gittensory_qdrant_upserts_total[2m])", + "expr": "rate(loopover_qdrant_upserts_total[2m])", "legendFormat": "upserts/s" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "rate(gittensory_qdrant_errors_total[2m])", + "expr": "rate(loopover_qdrant_errors_total[2m])", "legendFormat": "errors/s" } ] @@ -491,7 +491,7 @@ "collapsed": false, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 32 }, "id": 104, - "title": "Gittensory Orb", + "title": "LoopOver Orb", "type": "row" }, { @@ -525,7 +525,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_orb_events_exported_total or vector(0)", + "expr": "loopover_orb_events_exported_total or vector(0)", "legendFormat": "exported" } ] @@ -564,7 +564,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_orb_export_errors_total or vector(0)", + "expr": "loopover_orb_export_errors_total or vector(0)", "legendFormat": "errors" } ] @@ -593,12 +593,12 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "rate(gittensory_orb_events_exported_total[5m]) or vector(0)", + "expr": "rate(loopover_orb_events_exported_total[5m]) or vector(0)", "legendFormat": "exported/s" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum by (result) (rate(gittensory_orb_webhook_total[5m])) or vector(0)", + "expr": "sum by (result) (rate(loopover_orb_webhook_total[5m])) or vector(0)", "legendFormat": "{{result}} webhooks/s" } ] @@ -650,7 +650,7 @@ "type": "loki", "uid": "loki" }, - "expr": "{service=\"gittensory\"} | json eventf=\"event\", errf=\"error\" | line_format \"{{.eventf}}{{if .errf}} — {{.errf}}{{end}}\"", + "expr": "{service=\"loopover\"} | json eventf=\"event\", errf=\"error\" | line_format \"{{.eventf}}{{if .errf}} — {{.errf}}{{end}}\"", "queryType": "range", "refId": "A" } @@ -691,7 +691,7 @@ "type": "loki", "uid": "loki" }, - "expr": "{service=\"gittensory\"} | json eventf=\"event\", errf=\"error\" | errf != \"\" | line_format \"{{.eventf}} — {{.errf}}\"", + "expr": "{service=\"loopover\"} | json eventf=\"event\", errf=\"error\" | errf != \"\" | line_format \"{{.eventf}} — {{.errf}}\"", "queryType": "range", "refId": "A" } @@ -818,7 +818,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum by (status) (rate(gittensory_http_requests_total[5m]))", + "expr": "sum by (status) (rate(loopover_http_requests_total[5m]))", "legendFormat": "{{status}}", "refId": "A" } @@ -868,7 +868,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "histogram_quantile(0.50, sum by (le) (rate(gittensory_http_request_duration_seconds_bucket[5m])))", + "expr": "histogram_quantile(0.50, sum by (le) (rate(loopover_http_request_duration_seconds_bucket[5m])))", "legendFormat": "p50", "refId": "A" }, @@ -877,7 +877,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "histogram_quantile(0.95, sum by (le) (rate(gittensory_http_request_duration_seconds_bucket[5m])))", + "expr": "histogram_quantile(0.95, sum by (le) (rate(loopover_http_request_duration_seconds_bucket[5m])))", "legendFormat": "p95", "refId": "B" }, @@ -886,7 +886,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "histogram_quantile(0.99, sum by (le) (rate(gittensory_http_request_duration_seconds_bucket[5m])))", + "expr": "histogram_quantile(0.99, sum by (le) (rate(loopover_http_request_duration_seconds_bucket[5m])))", "legendFormat": "p99", "refId": "C" } @@ -953,7 +953,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum(rate(gittensory_http_requests_total{status=\"5xx\"}[5m])) / clamp_min(sum(rate(gittensory_http_requests_total[5m])), 1e-9)", + "expr": "sum(rate(loopover_http_requests_total{status=\"5xx\"}[5m])) / clamp_min(sum(rate(loopover_http_requests_total[5m])), 1e-9)", "legendFormat": "5xx ratio", "refId": "A" } @@ -1157,7 +1157,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum by (result) (rate(gittensory_github_response_cache_total[5m]))", + "expr": "sum by (result) (rate(loopover_github_response_cache_total[5m]))", "legendFormat": "{{result}}", "refId": "A" } @@ -1208,7 +1208,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum by (class, result) (gittensory_github_response_cache_total)", + "expr": "sum by (class, result) (loopover_github_response_cache_total)", "legendFormat": "{{class}} {{result}}", "refId": "A" } @@ -1259,7 +1259,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum by (remaining_bucket, key_scope) (rate(gittensory_github_rest_rate_limit_observations_total[5m])) or vector(0)", + "expr": "sum by (remaining_bucket, key_scope) (rate(loopover_github_rest_rate_limit_observations_total[5m])) or vector(0)", "legendFormat": "{{key_scope}} {{remaining_bucket}} remaining", "refId": "A" }, @@ -1268,7 +1268,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum by (status, retry, key_scope) (rate(gittensory_github_rest_rate_limit_responses_total[5m])) or vector(0)", + "expr": "sum by (status, retry, key_scope) (rate(loopover_github_rest_rate_limit_responses_total[5m])) or vector(0)", "legendFormat": "{{key_scope}} {{status}} {{retry}}", "refId": "B" } @@ -1319,7 +1319,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum by (kind, key_scope, job_type) (rate(gittensory_jobs_rate_limit_admission_deferred_total[5m])) or vector(0)", + "expr": "sum by (kind, key_scope, job_type) (rate(loopover_jobs_rate_limit_admission_deferred_total[5m])) or vector(0)", "legendFormat": "admission {{kind}} {{key_scope}} {{job_type}}", "refId": "A" }, @@ -1328,7 +1328,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum by (kind, key_scope, job_type) (rate(gittensory_jobs_rate_limit_budget_deferred_total[5m])) or vector(0)", + "expr": "sum by (kind, key_scope, job_type) (rate(loopover_jobs_rate_limit_budget_deferred_total[5m])) or vector(0)", "legendFormat": "budget {{kind}} {{key_scope}} {{job_type}}", "refId": "B" }, @@ -1337,7 +1337,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum by (kind, key_scope, job_type) (rate(gittensory_jobs_rate_limited_by_type_total[5m])) or vector(0)", + "expr": "sum by (kind, key_scope, job_type) (rate(loopover_jobs_rate_limited_by_type_total[5m])) or vector(0)", "legendFormat": "limited {{kind}} {{key_scope}} {{job_type}}", "refId": "C" } @@ -1552,7 +1552,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "(time() - max(gittensory_backup_latest_timestamp_seconds{target=~\"postgres|sqlite\"})) and max(gittensory_backup_latest_timestamp_seconds{target=~\"postgres|sqlite\"}) > 0", + "expr": "(time() - max(loopover_backup_latest_timestamp_seconds{target=~\"postgres|sqlite\"})) and max(loopover_backup_latest_timestamp_seconds{target=~\"postgres|sqlite\"}) > 0", "legendFormat": "age", "refId": "A" } @@ -1823,7 +1823,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "(time() - gittensory_backup_latest_timestamp_seconds{target=~\"postgres|sqlite|qdrant\"}) and gittensory_backup_latest_timestamp_seconds{target=~\"postgres|sqlite|qdrant\"} > 0", + "expr": "(time() - loopover_backup_latest_timestamp_seconds{target=~\"postgres|sqlite|qdrant\"}) and loopover_backup_latest_timestamp_seconds{target=~\"postgres|sqlite|qdrant\"} > 0", "legendFormat": "{{target}} age seconds", "refId": "A" }, @@ -1832,7 +1832,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_backup_files{target=~\"postgres|sqlite|qdrant\"} or vector(0)", + "expr": "loopover_backup_files{target=~\"postgres|sqlite|qdrant\"} or vector(0)", "legendFormat": "{{target}} files", "refId": "B" } @@ -1878,7 +1878,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_queue_live_pending", + "expr": "loopover_queue_live_pending", "legendFormat": "live pending" } ] @@ -1914,7 +1914,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_queue_maintenance_pending", + "expr": "loopover_queue_maintenance_pending", "legendFormat": "maintenance pending" } ] @@ -1950,7 +1950,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_queue_oldest_live_pending_age_seconds", + "expr": "loopover_queue_oldest_live_pending_age_seconds", "legendFormat": "oldest live age" } ] @@ -1986,7 +1986,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_queue_oldest_maintenance_pending_age_seconds", + "expr": "loopover_queue_oldest_maintenance_pending_age_seconds", "legendFormat": "oldest maintenance age" } ] @@ -2022,7 +2022,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_host_load_avg1_per_core", + "expr": "loopover_host_load_avg1_per_core", "legendFormat": "load/core" } ] @@ -2057,7 +2057,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_github_branch_protection_permission_denied_total or vector(0)", + "expr": "loopover_github_branch_protection_permission_denied_total or vector(0)", "legendFormat": "permission denied" } ] @@ -2082,12 +2082,12 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_queue_live_pending", + "expr": "loopover_queue_live_pending", "legendFormat": "live" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_queue_maintenance_pending", + "expr": "loopover_queue_maintenance_pending", "legendFormat": "maintenance" } ] @@ -2112,7 +2112,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum by (reason, job_type) (rate(gittensory_jobs_maintenance_admission_deferred_by_reason_total[5m])) or vector(0)", + "expr": "sum by (reason, job_type) (rate(loopover_jobs_maintenance_admission_deferred_by_reason_total[5m])) or vector(0)", "legendFormat": "{{reason}} {{job_type}}", "refId": "A" } @@ -2139,7 +2139,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum by (reason, job_type) (rate(gittensory_jobs_maintenance_admission_granted_under_pressure_total[5m])) or vector(0)", + "expr": "sum by (reason, job_type) (rate(loopover_jobs_maintenance_admission_granted_under_pressure_total[5m])) or vector(0)", "legendFormat": "{{reason}} {{job_type}}", "refId": "A" } @@ -2166,7 +2166,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum(rate(gittensory_jobs_maintenance_admission_deferred_total[5m])) or vector(0)", + "expr": "sum(rate(loopover_jobs_maintenance_admission_deferred_total[5m])) or vector(0)", "legendFormat": "deferred", "refId": "A" } @@ -2209,7 +2209,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum(gittensory_jobs_maintenance_trickle_admitted_persisted_total) or vector(0)", + "expr": "sum(loopover_jobs_maintenance_trickle_admitted_persisted_total) or vector(0)", "legendFormat": "trickle-admitted" } ] @@ -2244,7 +2244,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum(gittensory_orb_relay_register_total{result=\"failed\"}) or vector(0)", + "expr": "sum(loopover_orb_relay_register_total{result=\"failed\"}) or vector(0)", "legendFormat": "register failed" } ] @@ -2279,7 +2279,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum(gittensory_installation_health_broker_probe_total{result=\"failed\"}) or vector(0)", + "expr": "sum(loopover_installation_health_broker_probe_total{result=\"failed\"}) or vector(0)", "legendFormat": "broker probe failed" } ] @@ -2314,7 +2314,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum(gittensory_agent_action_permission_denied_total) or vector(0)", + "expr": "sum(loopover_agent_action_permission_denied_total) or vector(0)", "legendFormat": "permission denied" } ] @@ -2339,13 +2339,13 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum by (actionClass) (rate(gittensory_agent_action_permission_denied_total[5m]))", + "expr": "sum by (actionClass) (rate(loopover_agent_action_permission_denied_total[5m]))", "legendFormat": "{{actionClass}} denied", "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum by (actionClass) (rate(gittensory_agent_action_permission_denied_suppressed_total[5m]))", + "expr": "sum by (actionClass) (rate(loopover_agent_action_permission_denied_suppressed_total[5m]))", "legendFormat": "{{actionClass}} suppressed-repeat", "refId": "B" } @@ -2371,7 +2371,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum by (mode, result) (rate(gittensory_orb_relay_register_total[5m]))", + "expr": "sum by (mode, result) (rate(loopover_orb_relay_register_total[5m]))", "legendFormat": "{{mode}} {{result}}", "refId": "A" } @@ -2403,13 +2403,13 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_orb_relay_register_consecutive_failures or vector(0)", + "expr": "loopover_orb_relay_register_consecutive_failures or vector(0)", "legendFormat": "consecutive registration failures", "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_orb_relay_drain_seconds_since_last or vector(0)", + "expr": "loopover_orb_relay_drain_seconds_since_last or vector(0)", "legendFormat": "seconds since last pull-mode drain", "refId": "B" } @@ -2441,7 +2441,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_queue_runnable_now", + "expr": "loopover_queue_runnable_now", "legendFormat": "runnable now" } ] @@ -2464,7 +2464,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_queue_live_runnable_now", + "expr": "loopover_queue_live_runnable_now", "legendFormat": "live runnable now" } ] @@ -2487,7 +2487,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_queue_processing", + "expr": "loopover_queue_processing", "legendFormat": "processing" } ] @@ -2523,7 +2523,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_queue_oldest_live_runnable_age_seconds", + "expr": "loopover_queue_oldest_live_runnable_age_seconds", "legendFormat": "oldest live runnable age" } ] @@ -2546,7 +2546,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_jobs_foreground_liveness_released_total or vector(0)", + "expr": "loopover_jobs_foreground_liveness_released_total or vector(0)", "legendFormat": "released" } ] @@ -2567,7 +2567,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum by (reason) (rate(gittensory_jobs_foreground_liveness_released_by_reason_total[15m])) or vector(0)", + "expr": "sum by (reason) (rate(loopover_jobs_foreground_liveness_released_by_reason_total[15m])) or vector(0)", "legendFormat": "{{reason}}", "refId": "A" } @@ -2611,7 +2611,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_queue_backlog_convergence_pending", + "expr": "loopover_queue_backlog_convergence_pending", "legendFormat": "backlog pending" } ] @@ -2647,7 +2647,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_queue_fresh_intake_pending", + "expr": "loopover_queue_fresh_intake_pending", "legendFormat": "fresh pending" } ] @@ -2664,12 +2664,12 @@ "tooltip": { "mode": "multi", "sort": "desc" } }, "title": "GitHub REST Rate Limit Remaining (by scope)", - "description": "The NEWEST observed x-ratelimit-remaining per key_scope -- unlike gittensory_github_rest_rate_limit_observations_total (a bucketed rate() counter), this is the actual current remaining count.", + "description": "The NEWEST observed x-ratelimit-remaining per key_scope -- unlike loopover_github_rest_rate_limit_observations_total (a bucketed rate() counter), this is the actual current remaining count.", "type": "timeseries", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_github_rest_rate_limit_remaining", + "expr": "loopover_github_rest_rate_limit_remaining", "legendFormat": "{{key_scope}}", "refId": "A" } @@ -2691,7 +2691,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum by (lane) (rate(gittensory_jobs_claimed_by_lane_total[5m])) or vector(0)", + "expr": "sum by (lane) (rate(loopover_jobs_claimed_by_lane_total[5m])) or vector(0)", "legendFormat": "{{lane}}", "refId": "A" } @@ -2713,7 +2713,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_queue_backlog_by_repo", + "expr": "loopover_queue_backlog_by_repo", "format": "table", "instant": true, "legendFormat": "__auto", @@ -2768,7 +2768,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "abs(gittensory_clock_skew_seconds)", + "expr": "abs(loopover_clock_skew_seconds)", "legendFormat": "skew" } ] @@ -2803,7 +2803,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_ai_review_onmerge_clamped_total or vector(0)", + "expr": "loopover_ai_review_onmerge_clamped_total or vector(0)", "legendFormat": "clamped" } ] @@ -2831,12 +2831,12 @@ "tooltip": { "mode": "multi", "sort": "desc" } }, "title": "Ops Anomaly Detections (review burst / review failure burst)", - "description": "runOpsAlerts' hourly scan over gittensory's own outcome data, labeled by kind (review_burst / review_failure_burst) and repo. Any nonzero value here means the scan flagged a stuck-CI finalize loop or retry storm -- see the ops_anomaly structured log for the human-readable detail line.", + "description": "runOpsAlerts' hourly scan over loopover's own outcome data, labeled by kind (review_burst / review_failure_burst) and repo. Any nonzero value here means the scan flagged a stuck-CI finalize loop or retry storm -- see the ops_anomaly structured log for the human-readable detail line.", "type": "timeseries", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum by (kind, repo) (increase(gittensory_ops_anomaly_total[1h])) or vector(0)", + "expr": "sum by (kind, repo) (increase(loopover_ops_anomaly_total[1h])) or vector(0)", "legendFormat": "{{kind}} {{repo}}", "refId": "A" } @@ -2858,12 +2858,12 @@ "tooltip": { "mode": "multi", "sort": "desc" } }, "title": "Published Review Comments (rate)", - "description": "Published review comments/summaries per repo -- the gittensory review pipeline's actual output rate.", + "description": "Published review comments/summaries per repo -- the loopover review pipeline's actual output rate.", "type": "timeseries", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum by (repo) (rate(gittensory_reviews_published_total[5m])) or vector(0)", + "expr": "sum by (repo) (rate(loopover_reviews_published_total[5m])) or vector(0)", "legendFormat": "{{repo}}", "refId": "A" } @@ -2890,7 +2890,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum by (conclusion) (rate(gittensory_gate_decisions_total[5m])) or vector(0)", + "expr": "sum by (conclusion) (rate(loopover_gate_decisions_total[5m])) or vector(0)", "legendFormat": "{{conclusion}}", "refId": "A" } @@ -2935,7 +2935,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_d1_database_size_bytes", + "expr": "loopover_d1_database_size_bytes", "legendFormat": "size", "refId": "A" } @@ -2973,7 +2973,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_signal_snapshots_rows_per_key", + "expr": "loopover_signal_snapshots_rows_per_key", "legendFormat": "rows/key", "refId": "A" } @@ -3010,7 +3010,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum(gittensory_d1_probe_errors_total) or vector(0)", + "expr": "sum(loopover_d1_probe_errors_total) or vector(0)", "legendFormat": "errors", "refId": "A" } @@ -3037,13 +3037,13 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_d1_database_size_bytes", + "expr": "loopover_d1_database_size_bytes", "legendFormat": "database bytes", "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "gittensory_d1_table_row_count", + "expr": "loopover_d1_table_row_count", "legendFormat": "{{table}} rows", "refId": "B" } @@ -3052,7 +3052,7 @@ ], "refresh": "30s", "schemaVersion": 38, - "tags": ["gittensory", "self-host"], + "tags": ["loopover", "self-host"], "templating": { "list": [ { @@ -3070,7 +3070,7 @@ "time": { "from": "now-1h", "to": "now" }, "timepicker": {}, "timezone": "browser", - "title": "Gittensory Self-Host", - "uid": "gittensory-selfhost", + "title": "LoopOver Self-Host", + "uid": "loopover-selfhost", "version": 10 } diff --git a/grafana/dashboards/gpu-metrics.json b/grafana/dashboards/gpu-metrics.json index 36685d17b7..7629ebeb3f 100644 --- a/grafana/dashboards/gpu-metrics.json +++ b/grafana/dashboards/gpu-metrics.json @@ -95,7 +95,7 @@ "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum by (provider, request_kind) (rate(gittensory_ai_provider_request_duration_seconds_count[5m]))", + "expr": "sum by (provider, request_kind) (rate(loopover_ai_provider_request_duration_seconds_count[5m]))", "legendFormat": "{{provider}} / {{request_kind}}", "refId": "A" } @@ -110,9 +110,9 @@ "title": "AI Request Latency (p50 / p95 / p99)", "type": "timeseries", "targets": [ - { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "expr": "histogram_quantile(0.50, sum by (le) (rate(gittensory_ai_provider_request_duration_seconds_bucket[5m])))", "legendFormat": "p50", "refId": "A" }, - { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "expr": "histogram_quantile(0.95, sum by (le) (rate(gittensory_ai_provider_request_duration_seconds_bucket[5m])))", "legendFormat": "p95", "refId": "B" }, - { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "expr": "histogram_quantile(0.99, sum by (le) (rate(gittensory_ai_provider_request_duration_seconds_bucket[5m])))", "legendFormat": "p99", "refId": "C" } + { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "expr": "histogram_quantile(0.50, sum by (le) (rate(loopover_ai_provider_request_duration_seconds_bucket[5m])))", "legendFormat": "p50", "refId": "A" }, + { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "expr": "histogram_quantile(0.95, sum by (le) (rate(loopover_ai_provider_request_duration_seconds_bucket[5m])))", "legendFormat": "p95", "refId": "B" }, + { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "expr": "histogram_quantile(0.99, sum by (le) (rate(loopover_ai_provider_request_duration_seconds_bucket[5m])))", "legendFormat": "p99", "refId": "C" } ] }, { @@ -122,12 +122,12 @@ "id": 13, "options": { "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom" }, "tooltip": { "mode": "multi", "sort": "desc" } }, "title": "AI Request Error Rate", - "description": "Excludes expected embedding-routing fallbacks (those never increment this counter -- see gittensory_ai_provider_request_errors_total's HELP text).", + "description": "Excludes expected embedding-routing fallbacks (those never increment this counter -- see loopover_ai_provider_request_errors_total's HELP text).", "type": "timeseries", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "sum by (provider, request_kind) (rate(gittensory_ai_provider_request_errors_total[5m]))", + "expr": "sum by (provider, request_kind) (rate(loopover_ai_provider_request_errors_total[5m]))", "legendFormat": "{{provider}} / {{request_kind}}", "refId": "A" } @@ -181,7 +181,7 @@ ], "refresh": "30s", "schemaVersion": 38, - "tags": ["gittensory", "gpu"], + "tags": ["loopover", "gpu"], "templating": { "list": [ { "current": {}, "hide": 0, "includeAll": false, "name": "DS_PROMETHEUS", "options": [], "query": "prometheus", "refresh": 1, "type": "datasource" } @@ -190,7 +190,7 @@ "time": { "from": "now-6h", "to": "now" }, "timepicker": {}, "timezone": "browser", - "title": "Gittensory — GPU Metrics", - "uid": "gittensory-gpu", + "title": "LoopOver — GPU Metrics", + "uid": "loopover-gpu", "version": 1 } diff --git a/grafana/dashboards/infra-health.json b/grafana/dashboards/infra-health.json index 9de1da964a..ababbea96c 100644 --- a/grafana/dashboards/infra-health.json +++ b/grafana/dashboards/infra-health.json @@ -282,7 +282,7 @@ ], "refresh": "30s", "schemaVersion": 38, - "tags": ["gittensory", "infra", "observability"], + "tags": ["loopover", "infra", "observability"], "templating": { "list": [ { "current": {}, "hide": 0, "includeAll": false, "name": "DS_PROMETHEUS", "options": [], "query": "prometheus", "refresh": 1, "type": "datasource" } @@ -291,7 +291,7 @@ "time": { "from": "now-6h", "to": "now" }, "timepicker": {}, "timezone": "browser", - "title": "Gittensory — Infra Health (Host/Container/Redis/Qdrant)", - "uid": "gittensory-infra-health", + "title": "LoopOver — Infra Health (Host/Container/Redis/Qdrant)", + "uid": "loopover-infra-health", "version": 1 } diff --git a/grafana/dashboards/maintainer-reviews.json b/grafana/dashboards/maintainer-reviews.json index 8049fb689b..e1e935e5fd 100644 --- a/grafana/dashboards/maintainer-reviews.json +++ b/grafana/dashboards/maintainer-reviews.json @@ -1,7 +1,7 @@ { - "uid": "gittensory-maintainer", - "title": "Gittensory — Reviews & PRs (maintainer)", - "tags": ["gittensory", "maintainer"], + "uid": "loopover-maintainer", + "title": "LoopOver — Reviews & PRs (maintainer)", + "tags": ["loopover", "maintainer"], "timezone": "browser", "schemaVersion": 39, "version": 7, @@ -15,7 +15,7 @@ "name": "repo", "label": "Repo", "type": "query", - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "query": { "queryType": "table", "queryText": "SELECT DISTINCT repo FROM review_targets ORDER BY repo", "rawQueryText": "SELECT DISTINCT repo FROM review_targets ORDER BY repo" }, "includeAll": true, "allValue": "$__all", @@ -41,64 +41,64 @@ "id": 2, "title": "PRs tracked", "description": "Every PR the pipeline has recorded in this window, bot-authored PRs (e.g. release-automation commits) excluded -- not the same set as the public homepage's 'PRs reviewed' counter. This includes drafts, filtered-out, and otherwise-skipped PRs the bot never posted an actual review comment on; the homepage counts only PRs that got a published review surface. Expect this to read higher than the homepage number, by design -- they answer different questions (everything the pipeline saw vs. everything it actually reviewed).", - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "gridPos": { "h": 4, "w": 4, "x": 0, "y": 1 }, "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "blue" }, "unit": "short" }, "overrides": [] }, "options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "value" }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS prs FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS prs FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS prs FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS prs FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] }, { "type": "stat", "id": 3, "title": "Merged", - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "gridPos": { "h": 4, "w": 4, "x": 4, "y": 1 }, "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "green" }, "unit": "short" }, "overrides": [] }, "options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "value" }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS merged FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='merged' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS merged FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='merged' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS merged FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='merged' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS merged FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='merged' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] }, { "type": "stat", "id": 4, "title": "Closed", - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "gridPos": { "h": 4, "w": 4, "x": 8, "y": 1 }, "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "red" }, "unit": "short" }, "overrides": [] }, "options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "value" }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS closed FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='closed' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS closed FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='closed' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS closed FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='closed' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS closed FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='closed' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] }, { "type": "stat", "id": 5, "title": "Manual review", "description": "Counts PRs whose most recent status update falls in this window AND is currently 'manual' -- a live snapshot, not a lifetime tally. A PR that entered manual review earlier in the window and was later merged or closed (also within the window) is counted only once, under Merged or Closed, not here. If you actively resolve your manual-review queue, this number will read lower than your felt sense of how many PRs passed through manual review.", - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "gridPos": { "h": 4, "w": 4, "x": 12, "y": 1 }, "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "orange" }, "unit": "short" }, "overrides": [] }, "options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "value" }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS manual FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (status='manual' OR verdict='manual') AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS manual FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (status='manual' OR verdict='manual') AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS manual FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (status='manual' OR verdict='manual') AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS manual FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (status='manual' OR verdict='manual') AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] }, { "type": "stat", "id": 6, "title": "Approved (pending merge)", "description": "Counts PRs whose most recent status update falls in this window AND is currently 'commented' (the gate decided to merge but the PR hasn't merged yet) -- the same in-window-latest-state semantics as 'Manual review'. A PR that was approved and later actually merged or closed within the same window shows only under Merged/Closed, so this under-reports total approval activity for anyone who resolves their queue promptly.", - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "gridPos": { "h": 4, "w": 4, "x": 16, "y": 1 }, "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "blue" }, "unit": "short" }, "overrides": [] }, "options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "value" }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS commented FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='commented' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS commented FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='commented' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS commented FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='commented' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS commented FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='commented' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] }, { "type": "stat", "id": 7, "title": "Ignored (no gate decision yet)", "description": "Counts PRs whose most recent status update falls in this window AND the gate has never recorded a decision for (status='manual' with no hold/close verdict behind it -- distinct from a PR the gate actively held or closed). Redefined 2026-07 (was 'status=ignored OR verdict=ignore', values a #3511/#3535 migration to review_audit-based gate decisions stopped producing years ago -- the export can no longer set either).", - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "gridPos": { "h": 4, "w": 4, "x": 20, "y": 1 }, "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "purple" }, "unit": "short" }, "overrides": [] }, "options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "value" }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS ignored FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='manual' AND verdict IS NULL AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS ignored FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='manual' AND verdict IS NULL AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS ignored FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='manual' AND verdict IS NULL AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS ignored FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='manual' AND verdict IS NULL AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] }, { "type": "row", @@ -111,40 +111,40 @@ "type": "stat", "id": 12, "title": "Issues opened", - "description": "Local, webhook-observed count via the gittensory-db issues table, same consistency model as the PR panels above (#3716's option 1) -- not a live GitHub-API census, so it inherits the same webhook-completeness caveat the PR data already has. Switched 2026-07 from the grafana-github-datasource live-API path (#3716's option 2), which hardcoded org:JSONbored (broken for any other self-hoster) and silently hit GitHub Search's 1000-result cap with no disclosure on this dashboard.", - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "description": "Local, webhook-observed count via the loopover-db issues table, same consistency model as the PR panels above (#3716's option 1) -- not a live GitHub-API census, so it inherits the same webhook-completeness caveat the PR data already has. Switched 2026-07 from the grafana-github-datasource live-API path (#3716's option 2), which hardcoded org:JSONbored (broken for any other self-hoster) and silently hit GitHub Search's 1000-result cap with no disclosure on this dashboard.", + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "gridPos": { "h": 4, "w": 8, "x": 0, "y": 6 }, "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "blue" } } }, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "center", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false } }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS opened FROM issues WHERE (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(created_at) >= ${__from:date:seconds} AND unixepoch(created_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS opened FROM issues WHERE (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(created_at) >= ${__from:date:seconds} AND unixepoch(created_at) < ${__to:date:seconds}" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS opened FROM issues WHERE (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(created_at) >= ${__from:date:seconds} AND unixepoch(created_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS opened FROM issues WHERE (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(created_at) >= ${__from:date:seconds} AND unixepoch(created_at) < ${__to:date:seconds}" }] }, { "type": "stat", "id": 13, "title": "Issues closed", - "description": "Local, webhook-observed count via the gittensory-db issues table -- see 'Issues opened' for the 2026-07 switch from the live GitHub-API path. Uses the issue's last-updated timestamp as the closing-transition signal (no separate closed_at column exists), the same approach the PR panels above already use for their own updated_at-based windowing.", - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "description": "Local, webhook-observed count via the loopover-db issues table -- see 'Issues opened' for the 2026-07 switch from the live GitHub-API path. Uses the issue's last-updated timestamp as the closing-transition signal (no separate closed_at column exists), the same approach the PR panels above already use for their own updated_at-based windowing.", + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "gridPos": { "h": 4, "w": 8, "x": 8, "y": 6 }, "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "red" } } }, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "center", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false } }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS closed FROM issues WHERE state='closed' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS closed FROM issues WHERE state='closed' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS closed FROM issues WHERE state='closed' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS closed FROM issues WHERE state='closed' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] }, { "type": "stat", "id": 14, "title": "Issues open", "description": "Current open-issue backlog from the local, webhook-observed issues table -- a state snapshot, not filtered by the dashboard time window, same semantics as the original live-API version of this panel.", - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "gridPos": { "h": 4, "w": 8, "x": 16, "y": 6 }, "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "orange" } } }, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "center", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false } }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS open FROM issues WHERE state='open' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring})", "rawQueryText": "SELECT count(*) AS open FROM issues WHERE state='open' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring})" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS open FROM issues WHERE state='open' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring})", "rawQueryText": "SELECT count(*) AS open FROM issues WHERE state='open' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring})" }] }, { "type": "table", "id": 8, "title": "Pull requests (latest 1000)", - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "gridPos": { "h": 16, "w": 24, "x": 0, "y": 10 }, "fieldConfig": { "defaults": { "custom": { "align": "auto", "cellOptions": { "type": "auto" }, "filterable": true, "inspect": false } }, @@ -172,27 +172,27 @@ ] }, "options": { "showHeader": true, "cellHeight": "sm", "footer": { "show": false }, "sortBy": [{ "displayName": "updated_at", "desc": true }] }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT repo, number, submitter AS author, status, verdict, title, updated_at FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} ORDER BY updated_at DESC LIMIT 1000", "rawQueryText": "SELECT repo, number, submitter AS author, status, verdict, title, updated_at FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} ORDER BY updated_at DESC LIMIT 1000" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT repo, number, submitter AS author, status, verdict, title, updated_at FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} ORDER BY updated_at DESC LIMIT 1000", "rawQueryText": "SELECT repo, number, submitter AS author, status, verdict, title, updated_at FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} ORDER BY updated_at DESC LIMIT 1000" }] }, { "type": "timeseries", "id": 9, "title": "Reviews per day", - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 26 }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "drawStyle": "bars", "fillOpacity": 60, "lineWidth": 1, "showPoints": "never", "stacking": { "mode": "none" } }, "unit": "short" }, "overrides": [] }, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": false }, "tooltip": { "mode": "single", "sort": "none" } }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "time series", "timeColumns": ["time"], "queryText": "SELECT date(updated_at) AS time, count(*) AS reviews FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} GROUP BY date(updated_at) ORDER BY time", "rawQueryText": "SELECT date(updated_at) AS time, count(*) AS reviews FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} GROUP BY date(updated_at) ORDER BY time" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "refId": "A", "queryType": "time series", "timeColumns": ["time"], "queryText": "SELECT date(updated_at) AS time, count(*) AS reviews FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} GROUP BY date(updated_at) ORDER BY time", "rawQueryText": "SELECT date(updated_at) AS time, count(*) AS reviews FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} GROUP BY date(updated_at) ORDER BY time" }] }, { "type": "piechart", "id": 10, "title": "By verdict", - "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 26 }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "unit": "short" }, "overrides": [] }, "options": { "legend": { "displayMode": "list", "placement": "right", "showLegend": true, "values": ["value", "percent"] }, "pieType": "donut", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": true }, "tooltip": { "mode": "single", "sort": "none" } }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT verdict, count(*) AS c FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND verdict IS NOT NULL AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} GROUP BY verdict ORDER BY c DESC", "rawQueryText": "SELECT verdict, count(*) AS c FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND verdict IS NOT NULL AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} GROUP BY verdict ORDER BY c DESC" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "loopover-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT verdict, count(*) AS c FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND verdict IS NOT NULL AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} GROUP BY verdict ORDER BY c DESC", "rawQueryText": "SELECT verdict, count(*) AS c FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND verdict IS NOT NULL AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} GROUP BY verdict ORDER BY c DESC" }] } ] } diff --git a/grafana/dashboards/rees-metrics.json b/grafana/dashboards/rees-metrics.json index 6937b29fd4..460da78a4d 100644 --- a/grafana/dashboards/rees-metrics.json +++ b/grafana/dashboards/rees-metrics.json @@ -86,7 +86,7 @@ ], "refresh": "30s", "schemaVersion": 38, - "tags": ["gittensory", "rees", "observability"], + "tags": ["loopover", "rees", "observability"], "templating": { "list": [ { "current": {}, "hide": 0, "includeAll": false, "name": "DS_PROMETHEUS", "options": [], "query": "prometheus", "refresh": 1, "type": "datasource" } @@ -95,7 +95,7 @@ "time": { "from": "now-6h", "to": "now" }, "timepicker": {}, "timezone": "browser", - "title": "Gittensory — REES (review-enrichment)", - "uid": "gittensory-rees", + "title": "LoopOver — REES (review-enrichment)", + "uid": "loopover-rees", "version": 1 } diff --git a/grafana/dashboards/resource-hub.json b/grafana/dashboards/resource-hub.json index 5822d043c7..0f5c3fa99b 100644 --- a/grafana/dashboards/resource-hub.json +++ b/grafana/dashboards/resource-hub.json @@ -1,7 +1,7 @@ { - "uid": "gittensory-hub", - "title": "Gittensory — Resource hub", - "tags": ["gittensory", "hub"], + "uid": "loopover-hub", + "title": "LoopOver — Resource hub", + "tags": ["loopover", "hub"], "timezone": "browser", "schemaVersion": 39, "version": 1, @@ -55,7 +55,7 @@ "gridPos": { "h": 13, "w": 12, "x": 0, "y": 0 }, "options": { "mode": "markdown", - "content": "## 🧠 AI providers\n- **Claude Code / Codex** — subscription CLIs, the review brain. Health: app boot log `selfhost_ai_provider`.\n- **Ollama** — embeddings (`bge-m3`) for RAG · [API root](http://localhost:11434) · `docker exec loopover-ollama-1 ollama list`\n\n## 🔎 Vector store (RAG)\n- **Qdrant** — [Dashboard](http://localhost:6333/dashboard) · [Collections API](http://localhost:6333/collections)\n- Embeddings indexed per repo; see the *RAG indexing* doc.\n\n## ⚙️ Engine / API\n- **gittensory app** — [/ ](http://localhost:8787/) · [/ready](http://localhost:8787/ready) · [/metrics](http://localhost:8787/metrics)\n- Internal jobs: `POST /v1/internal/jobs/rag-index` (bearer `INTERNAL_JOB_TOKEN`)\n\n## 💾 Data\n- **SQLite** (default) at `/data/loopover.sqlite` — private to the app/exporter. Grafana reads `/reporting/loopover-reporting.sqlite`, a redacted reporting snapshot.\n- **Postgres/pgvector** (optional `--profile postgres`).\n\n> Links assume the default published ports on the Docker host. If you run Grafana on a remote host, replace `localhost` with that host, and publish the service `ports:` you want to reach." + "content": "## 🧠 AI providers\n- **Claude Code / Codex** — subscription CLIs, the review brain. Health: app boot log `selfhost_ai_provider`.\n- **Ollama** — embeddings (`bge-m3`) for RAG · [API root](http://localhost:11434) · `docker exec loopover-ollama-1 ollama list`\n\n## 🔎 Vector store (RAG)\n- **Qdrant** — [Dashboard](http://localhost:6333/dashboard) · [Collections API](http://localhost:6333/collections)\n- Embeddings indexed per repo; see the *RAG indexing* doc.\n\n## ⚙️ Engine / API\n- **loopover app** — [/ ](http://localhost:8787/) · [/ready](http://localhost:8787/ready) · [/metrics](http://localhost:8787/metrics)\n- Internal jobs: `POST /v1/internal/jobs/rag-index` (bearer `INTERNAL_JOB_TOKEN`)\n\n## 💾 Data\n- **SQLite** (default) at `/data/loopover.sqlite` — private to the app/exporter. Grafana reads `/reporting/loopover-reporting.sqlite`, a redacted reporting snapshot.\n- **Postgres/pgvector** (optional `--profile postgres`).\n\n> Links assume the default published ports on the Docker host. If you run Grafana on a remote host, replace `localhost` with that host, and publish the service `ports:` you want to reach." } }, @@ -66,7 +66,7 @@ "gridPos": { "h": 13, "w": 12, "x": 12, "y": 0 }, "options": { "mode": "markdown", - "content": "## 📊 Dashboards\n- **[Upstream PRs & issues (GitHub)](/d/gittensory-github)** — live, accurate census + open-PR triage (GitHub API).\n- **[Reviews & PRs (maintainer)](/d/gittensory-maintainer)** — gittensory's own review activity + reviewed-PR log.\n- **[AI usage](/d/gittensory-ai-usage)** — durable cross-provider ai_usage_events (filterable by provider/feature/model), live Prometheus counters, and Claude Code's own OTEL session telemetry, all in one place.\n- **[Gittensory (infra)](/d/gittensory)** — queue, jobs, HTTP, GitHub API cache/rate limits.\n- **[GPU metrics](/d/gittensory-gpu)** — utilization/VRAM for a self-hosted Ollama GPU box.\n- **[Infra health](/d/gittensory-infra-health)** — host CPU/mem/disk/network (node-exporter), per-container resource usage (cAdvisor), Redis, Qdrant, and whether the observability stack itself is up.\n- **[REES (review-enrichment)](/d/gittensory-rees)** — request outcomes/latency and per-analyzer run/timeout/degrade rates for the optional `--profile rees` service.\n- **[Browserless (visual review)](/d/gittensory-browserless)** — queue depth, concurrency, and rejection/error/timeout rate for the optional `--profile visual-review` screenshot service.\n- **[Sentry issues](/d/gittensory-sentry)** — recent unresolved issues, top issues by event count, and error-volume trend, queried live from Sentry (`scripts/setup-sentry-datasource.sh`). The plain link below still opens Sentry itself for actions this read-only view can't do (resolving/assigning issues).\n\n## 📈 Metrics & logs\n- **Prometheus** — [targets](http://localhost:9090/targets) · [graph](http://localhost:9090)\n- **Alertmanager** — [alerts](http://localhost:9093)\n- **Loki** — query in [Explore](/explore) (pick the *Loki* datasource), e.g. `{compose_service=\"gittensory\"}`\n- **Sentry** — release/source-map enriched errors. Edit the dashboard link if your project URL differs.\n\n## 🩺 Quick health checks\n| What | Where |\n|---|---|\n| App serving | `GET /ready` → 200 |\n| AI wired | boot log `selfhost_ai_provider` |\n| Embeds wired | boot log `selfhost_embed_provider` |\n| Vectors wired | boot log `selfhost_vectorize` |\n| Token spend | **[AI usage](/d/gittensory-ai-usage)** dashboard |\n\n## 📚 Docs\n- [Maintainer self-hosting](https://gittensory.aethereal.dev/docs/maintainer-self-hosting) — setup, configuration, AI, REES, RAG, operations, and troubleshooting." + "content": "## 📊 Dashboards\n- **[Upstream PRs & issues (GitHub)](/d/loopover-github)** — live, accurate census + open-PR triage (GitHub API).\n- **[Reviews & PRs (maintainer)](/d/loopover-maintainer)** — loopover's own review activity + reviewed-PR log.\n- **[AI usage](/d/loopover-ai-usage)** — durable cross-provider ai_usage_events (filterable by provider/feature/model), live Prometheus counters, and Claude Code's own OTEL session telemetry, all in one place.\n- **[LoopOver (infra)](/d/loopover-selfhost)** — queue, jobs, HTTP, GitHub API cache/rate limits.\n- **[GPU metrics](/d/loopover-gpu)** — utilization/VRAM for a self-hosted Ollama GPU box.\n- **[Infra health](/d/loopover-infra-health)** — host CPU/mem/disk/network (node-exporter), per-container resource usage (cAdvisor), Redis, Qdrant, and whether the observability stack itself is up.\n- **[REES (review-enrichment)](/d/loopover-rees)** — request outcomes/latency and per-analyzer run/timeout/degrade rates for the optional `--profile rees` service.\n- **[Browserless (visual review)](/d/loopover-browserless)** — queue depth, concurrency, and rejection/error/timeout rate for the optional `--profile visual-review` screenshot service.\n- **[Sentry issues](/d/loopover-sentry)** — recent unresolved issues, top issues by event count, and error-volume trend, queried live from Sentry (`scripts/setup-sentry-datasource.sh`). The plain link below still opens Sentry itself for actions this read-only view can't do (resolving/assigning issues).\n\n## 📈 Metrics & logs\n- **Prometheus** — [targets](http://localhost:9090/targets) · [graph](http://localhost:9090)\n- **Alertmanager** — [alerts](http://localhost:9093)\n- **Loki** — query in [Explore](/explore) (pick the *Loki* datasource), e.g. `{compose_service=\"loopover\"}`\n- **Sentry** — release/source-map enriched errors. Edit the dashboard link if your project URL differs.\n\n## 🩺 Quick health checks\n| What | Where |\n|---|---|\n| App serving | `GET /ready` → 200 |\n| AI wired | boot log `selfhost_ai_provider` |\n| Embeds wired | boot log `selfhost_embed_provider` |\n| Vectors wired | boot log `selfhost_vectorize` |\n| Token spend | **[AI usage](/d/loopover-ai-usage)** dashboard |\n\n## 📚 Docs\n- [Maintainer self-hosting](https://gittensory.aethereal.dev/docs/maintainer-self-hosting) — setup, configuration, AI, REES, RAG, operations, and troubleshooting." } } ] diff --git a/grafana/dashboards/sentry-issues.json b/grafana/dashboards/sentry-issues.json index 1ba9e63ba1..1ea7e36d26 100644 --- a/grafana/dashboards/sentry-issues.json +++ b/grafana/dashboards/sentry-issues.json @@ -84,7 +84,7 @@ ], "refresh": "5m", "schemaVersion": 38, - "tags": ["gittensory", "sentry", "observability"], + "tags": ["loopover", "sentry", "observability"], "templating": { "list": [ { "current": {}, "hide": 0, "includeAll": false, "name": "DS_SENTRY", "options": [], "query": "grafana-sentry-datasource", "refresh": 1, "type": "datasource" } @@ -93,7 +93,7 @@ "time": { "from": "now-24h", "to": "now" }, "timepicker": {}, "timezone": "browser", - "title": "Gittensory — Sentry issues", - "uid": "gittensory-sentry", + "title": "LoopOver — Sentry issues", + "uid": "loopover-sentry", "version": 1 } diff --git a/grafana/provisioning/dashboards/provider.yml b/grafana/provisioning/dashboards/provider.yml index 80cbbbc20f..2c8f679a1c 100644 --- a/grafana/provisioning/dashboards/provider.yml +++ b/grafana/provisioning/dashboards/provider.yml @@ -1,7 +1,7 @@ apiVersion: 1 providers: - - name: gittensory - folder: Gittensory + - name: loopover + folder: LoopOver type: file disableDeletion: true editable: false diff --git a/grafana/provisioning/datasources/sqlite.yml b/grafana/provisioning/datasources/sqlite.yml index 701127e3ad..44f98332e4 100644 --- a/grafana/provisioning/datasources/sqlite.yml +++ b/grafana/provisioning/datasources/sqlite.yml @@ -4,7 +4,7 @@ apiVersion: 1 datasources: - name: LoopoverDB type: frser-sqlite-datasource - uid: gittensory-db + uid: loopover-db access: proxy editable: false jsonData: diff --git a/prometheus/rules/alerts.yml b/prometheus/rules/alerts.yml index defa84a204..a1655f6f32 100644 --- a/prometheus/rules/alerts.yml +++ b/prometheus/rules/alerts.yml @@ -1,9 +1,9 @@ -# Prometheus alerting rules for the gittensory self-host stack (#980 observability). +# Prometheus alerting rules for the loopover self-host stack (#980 observability). # # Mounted into the prometheus container at /etc/prometheus/rules/alerts.yml and loaded # via the `rule_files: ["/etc/prometheus/rules/*.yml"]` glob in prometheus.yml. # -# Every rule below is grounded ONLY in metrics the gittensory app actually exports at +# Every rule below is grounded ONLY in metrics the loopover app actually exports at # GET /metrics, plus the synthetic `up` metric Prometheus emits per scrape target. # # Thresholds are sane defaults for a SMALL single-host self-host. Tune the numbers in @@ -20,9 +20,9 @@ groups: # ── Scrape target liveness ──────────────────────────────────────────────── - - name: gittensory-availability + - name: loopover-availability rules: - - alert: GittensoryTargetDown + - alert: LoopoverTargetDown # `up` is 0 when Prometheus cannot scrape /metrics (process down, crash-loop, # network partition, or wrong port). Scoped to our job so node/other targets # don't trip this. 2m absorbs a single missed scrape + a restart. @@ -31,16 +31,16 @@ groups: labels: severity: critical annotations: - summary: "gittensory target {{ $labels.instance }} is down" + summary: "loopover target {{ $labels.instance }} is down" description: "Prometheus has failed to scrape {{ $labels.instance }} (job {{ $labels.job }}) for over 2m. The app is unreachable or not serving /metrics." - runbook: "Check `docker compose ps` and `docker compose logs gittensory`. Look for a missing selfhost_listening log line or a crash-loop (restart count climbing)." + runbook: "Check `docker compose ps` and `docker compose logs loopover`. Look for a missing selfhost_listening log line or a crash-loop (restart count climbing)." # ── Job queue / worker health ───────────────────────────────────────────── - - name: gittensory-jobs + - name: loopover-jobs rules: - - alert: GittensoryHighJobFailureRatio + - alert: LoopoverHighJobFailureRatio # Fraction of jobs ATTEMPTED (failed + succeeded) that failed over the last 10m. - # gittensory_jobs_processed_total only increments on success, so the denominator + # loopover_jobs_processed_total only increments on success, so the denominator # must be failed+processed, not processed alone (#3892) -- dividing by processed # alone computes failed:success and can read as high as 100% at a true 50% failure # rate. The `> 0` guard on the ratio avoids a 0/0 = NaN (which never fires but is @@ -49,49 +49,49 @@ groups: # problem. Mirrors the Grafana "Job Failure Rate" panel's formula. expr: | ( - sum(rate(gittensory_jobs_failed_total[10m])) + sum(rate(loopover_jobs_failed_total[10m])) / ( - sum(rate(gittensory_jobs_failed_total[10m])) + sum(rate(loopover_jobs_failed_total[10m])) + - sum(rate(gittensory_jobs_processed_total[10m])) + sum(rate(loopover_jobs_processed_total[10m])) ) > 0 ) > 0.10 for: 15m labels: severity: warning annotations: - summary: "gittensory job failure ratio above 10%" + summary: "loopover job failure ratio above 10%" description: "{{ $value | humanizePercentage }} of jobs attempted in the last 10m failed (sustained 15m). Expected: well under 10%." runbook: "Tail logs for level=error job events (e.g. selfhost_cron_error). A spike usually means a bad upstream (GitHub API / AI provider / DB) or a poison payload — check what changed." - - alert: GittensoryDeadLetterJobsGrowing + - alert: LoopoverDeadLetterJobsGrowing # Jobs exhausted all retries and landed in the dead-letter queue. ANY new dead # job in 15m is worth a look — these are lost work that won't self-heal. # `increase(...) > 0` fires on the first dead job in the window. - expr: increase(gittensory_jobs_dead_total[15m]) > 0 + expr: increase(loopover_jobs_dead_total[15m]) > 0 for: 5m labels: severity: warning annotations: - summary: "gittensory dead-letter jobs increasing" + summary: "loopover dead-letter jobs increasing" description: "{{ $value | printf \"%.0f\" }} job(s) moved to the dead-letter queue in the last 15m. These exhausted all retries and need manual attention." - runbook: "Inspect gittensory_queue_dead gauge and the dead-letter records. Decide whether to fix-and-requeue or discard. A steady climb points at a systematic failure, not a blip." + runbook: "Inspect loopover_queue_dead gauge and the dead-letter records. Decide whether to fix-and-requeue or discard. A steady climb points at a systematic failure, not a blip." - - alert: GittensoryDeadLetterBacklogHigh + - alert: LoopoverDeadLetterBacklogHigh # Standing size of the dead-letter queue. Distinct from the rate alert above: # this catches a large backlog that built up before alerting was watching. # 50 dead jobs is a generous default for a small host — tune down if you expect ~0. - expr: gittensory_queue_dead > 50 + expr: loopover_queue_dead > 50 for: 10m labels: severity: warning annotations: - summary: "gittensory dead-letter backlog above 50" + summary: "loopover dead-letter backlog above 50" description: "The dead-letter queue holds {{ $value | printf \"%.0f\" }} jobs (sustained 10m). Work is accumulating with no automatic recovery." runbook: "Drain or triage the dead-letter queue. If it only ever grows, fix the root cause before requeuing or you'll just re-fill it." - - alert: GittensoryDeadLetterBacklogPersisting + - alert: LoopoverDeadLetterBacklogPersisting # The two rules above have a coverage gap: the rate alert (increase() over 15m) # only fires in a narrow window around a job's transition INTO dead, and the # backlog alert only trips above 50. A single dead job — the common case, e.g. one @@ -99,78 +99,78 @@ groups: # 15m window closes, invisible to both. This is a LEVEL check with no count floor: # any non-zero dead-letter count sustained for an hour pages, regardless of how it # got there or how large it is. - expr: gittensory_queue_dead > 0 + expr: loopover_queue_dead > 0 for: 1h labels: severity: warning annotations: - summary: "gittensory dead-letter queue not empty for over an hour" + summary: "loopover dead-letter queue not empty for over an hour" description: "{{ $value | printf \"%.0f\" }} job(s) have sat in the dead-letter queue for over an hour with no automatic recovery." runbook: "Check whether the underlying bug is already fixed and redeployed — if so the job(s) can be requeued; otherwise triage before requeuing." # ── Queue backlog (live processing pressure) ─────────────────────────────── - - name: gittensory-queue + - name: loopover-queue rules: - - alert: GittensoryQueueBacklogHigh + - alert: LoopoverQueueBacklogHigh # Pending (not-yet-processed) jobs. A sustained high pending count means workers # can't keep up with enqueue rate. 100 is a starting line for a single-node host; # raise it if your normal steady-state pending depth is higher. - expr: gittensory_queue_pending > 100 + expr: loopover_queue_pending > 100 for: 10m labels: severity: warning annotations: - summary: "gittensory queue backlog above 100" + summary: "loopover queue backlog above 100" description: "{{ $value | printf \"%.0f\" }} jobs pending for over 10m — the worker is falling behind the enqueue rate." - runbook: "Compare rate(gittensory_jobs_enqueued_total) vs rate(gittensory_jobs_processed_total). If enqueue > processed persistently, the worker is the bottleneck (slow AI/DB, or it's stuck). Check worker logs." + runbook: "Compare rate(loopover_jobs_enqueued_total) vs rate(loopover_jobs_processed_total). If enqueue > processed persistently, the worker is the bottleneck (slow AI/DB, or it's stuck). Check worker logs." # ── Maintenance-job backpressure (#selfhost-runtime-pressure) ────────────── - - name: gittensory-maintenance-pressure + - name: loopover-maintenance-pressure rules: - - alert: GittensoryLiveQueueStarved + - alert: LoopoverLiveQueueStarved # Live/foreground work (webhooks, per-PR re-gates) is the whole point of the review stack -- it # should clear in seconds to low minutes. A sustained old live job means something OTHER than # maintenance admission is the bottleneck (AI latency, GitHub, DB, or genuine host CPU pressure), # since maintenance-admission.ts already yields to live work at claim time. - expr: gittensory_queue_oldest_live_pending_age_seconds > 300 + expr: loopover_queue_oldest_live_pending_age_seconds > 300 for: 5m labels: severity: warning annotations: - summary: "gittensory live/webhook work is stuck behind something other than maintenance" - description: "The oldest live-priority queue job has been pending for {{ $value | printf \"%.0f\" }}s. Maintenance admission already yields to live work, so check AI latency, GitHub rate limits, DB pressure, or host CPU (gittensory_host_load_avg1_per_core)." - runbook: "Open the Runtime Pressure & Maintenance row. If gittensory_host_load_avg1_per_core is elevated, a co-located CI runner or other host process is starving the app -- see docker-compose.yml's runner isolation guidance." + summary: "loopover live/webhook work is stuck behind something other than maintenance" + description: "The oldest live-priority queue job has been pending for {{ $value | printf \"%.0f\" }}s. Maintenance admission already yields to live work, so check AI latency, GitHub rate limits, DB pressure, or host CPU (loopover_host_load_avg1_per_core)." + runbook: "Open the Runtime Pressure & Maintenance row. If loopover_host_load_avg1_per_core is elevated, a co-located CI runner or other host process is starving the app -- see docker-compose.yml's runner isolation guidance." - - alert: GittensoryLiveQueueNoRunnableWork + - alert: LoopoverLiveQueueNoRunnableWork # #selfhost-queue-liveness: the precise "is the foreground lane actually stuck" signal, distinct from - # GittensoryLiveQueueStarved's age-based check -- a large live-pending count on its own is NORMAL (a + # LoopoverLiveQueueStarved's age-based check -- a large live-pending count on its own is NORMAL (a # legitimate burst, or work intentionally staggered/rate-deferred); this only fires when there is # PENDING live work AND NOTHING runnable right now, sustained. That combination is exactly the # production incident this invariant exists to make impossible: hundreds of pending contributor-PR- # review jobs, zero processing, zero runnable, previously requiring manual intervention to unstick. - expr: gittensory_queue_live_pending > 0 and gittensory_queue_live_runnable_now == 0 + expr: loopover_queue_live_pending > 0 and loopover_queue_live_runnable_now == 0 for: 3m labels: severity: critical annotations: - summary: "gittensory foreground/live queue has pending work but nothing runnable" - description: "{{ $value | printf \"%.0f\" }} live-priority job(s) are pending but gittensory_queue_live_runnable_now has read 0 for over 3m -- contributor PR review work is not making progress." - runbook: "Open the Foreground Liveness row. Check gittensory_jobs_rate_limit_admission_deferred_total / gittensory_jobs_rate_limit_budget_deferred_total for a stuck GitHub rate-limit bucket, and gittensory_jobs_foreground_liveness_released_total for whether the liveness sweep is already recovering it (releases should show up within FOREGROUND_LIVENESS_CHECK_INTERVAL_MS, default 1m). If releases are firing repeatedly with no lasting effect, the underlying rate-limit exhaustion is the real bottleneck, not the queue." + summary: "loopover foreground/live queue has pending work but nothing runnable" + description: "{{ $value | printf \"%.0f\" }} live-priority job(s) are pending but loopover_queue_live_runnable_now has read 0 for over 3m -- contributor PR review work is not making progress." + runbook: "Open the Foreground Liveness row. Check loopover_jobs_rate_limit_admission_deferred_total / loopover_jobs_rate_limit_budget_deferred_total for a stuck GitHub rate-limit bucket, and loopover_jobs_foreground_liveness_released_total for whether the liveness sweep is already recovering it (releases should show up within FOREGROUND_LIVENESS_CHECK_INTERVAL_MS, default 1m). If releases are firing repeatedly with no lasting effect, the underlying rate-limit exhaustion is the real bottleneck, not the queue." - - alert: GittensoryForegroundLivenessReleasing + - alert: LoopoverForegroundLivenessReleasing # Informational: the liveness sweep IS working (foreground work would otherwise be stuck), but a # sustained rate means something keeps re-deferring the SAME class of work past the trickle ceiling -- # worth investigating even though the invariant is holding. - expr: rate(gittensory_jobs_foreground_liveness_released_total[15m]) > 0 + expr: rate(loopover_jobs_foreground_liveness_released_total[15m]) > 0 for: 15m labels: severity: warning annotations: - summary: "gittensory foreground-liveness sweep is repeatedly force-releasing deferred work" + summary: "loopover foreground-liveness sweep is repeatedly force-releasing deferred work" description: "The foreground-liveness sweep has released stale-deferred jobs for over 15m — the invariant is holding, but something is repeatedly pushing foreground work past FOREGROUND_LIVENESS_MAX_DEFER_MS." - runbook: "Check gittensory_jobs_rate_limit_admission_deferred_total / gittensory_jobs_rate_limit_budget_deferred_total by key_scope for a chronically exhausted GitHub REST budget (often installation-scoped after a burst)." + runbook: "Check loopover_jobs_rate_limit_admission_deferred_total / loopover_jobs_rate_limit_budget_deferred_total by key_scope for a chronically exhausted GitHub REST budget (often installation-scoped after a burst)." - - alert: GittensoryMaintenanceStarved + - alert: LoopoverMaintenanceStarved # Maintenance admission (maintenance-admission.ts) has TWO age escapes (#selfhost-maintenance-self-pin): # a short maintenanceDrainAgeMs trickle that lets old jobs through even while `maintenance_pending_high` # is breached (bounded further by the queue's own background concurrency cap), and the ultimate @@ -178,50 +178,50 @@ groups: # so under correct operation this should never sit much past that outer ceiling, and in practice the # drain escape should keep it well under it. A value well beyond 4h means either an escape isn't # triggering (a bug) or the host is so overloaded even a force-admitted job can't be claimed/processed. - expr: gittensory_queue_oldest_maintenance_pending_age_seconds > 21600 + expr: loopover_queue_oldest_maintenance_pending_age_seconds > 21600 for: 15m labels: severity: warning annotations: - summary: "gittensory maintenance work has not run in over 6h" + summary: "loopover maintenance work has not run in over 6h" description: "The oldest maintenance-lane queue job has been pending for {{ $value | printf \"%.0f\" }}s, past the default trickle ceiling. Contributor evidence, RAG indexing, drift scans, and similar sweeps are stale." - runbook: "Check gittensory_jobs_maintenance_admission_deferred_by_reason_total for the dominant defer reason and gittensory_jobs_maintenance_admission_granted_under_pressure_total to confirm the escapes are actually firing, and confirm MAINTENANCE_ADMISSION_MAX_DEFER_AGE_MS / MAINTENANCE_ADMISSION_DRAIN_AGE_MS weren't raised. Sustained host_load_high suggests the box itself (not just this app) is overloaded -- host_load_high also blocks the drain escape specifically, see maintenance-admission.ts." + runbook: "Check loopover_jobs_maintenance_admission_deferred_by_reason_total for the dominant defer reason and loopover_jobs_maintenance_admission_granted_under_pressure_total to confirm the escapes are actually firing, and confirm MAINTENANCE_ADMISSION_MAX_DEFER_AGE_MS / MAINTENANCE_ADMISSION_DRAIN_AGE_MS weren't raised. Sustained host_load_high suggests the box itself (not just this app) is overloaded -- host_load_high also blocks the drain escape specifically, see maintenance-admission.ts." # ── GitHub API budget / queue admission pressure ────────────────────────── - - name: gittensory-github-rate-limits + - name: loopover-github-rate-limits rules: - - alert: GittensoryGitHubRateLimitResponses + - alert: LoopoverGitHubRateLimitResponses # Live 403/429 GitHub rate-limit responses mean admission/caching did not avoid # a depleted bucket. This should be rare; sustained values explain hard queue stalls. - expr: sum by (status, retry, key_scope) (rate(gittensory_github_rest_rate_limit_responses_total[5m])) > 0 + expr: sum by (status, retry, key_scope) (rate(loopover_github_rest_rate_limit_responses_total[5m])) > 0 for: 2m labels: severity: warning annotations: - summary: "gittensory is receiving GitHub REST rate-limit responses" - description: "GitHub REST rate-limit responses are occurring for over 2m. Check labels status/retry/key_scope on gittensory_github_rest_rate_limit_responses_total." + summary: "loopover is receiving GitHub REST rate-limit responses" + description: "GitHub REST rate-limit responses are occurring for over 2m. Check labels status/retry/key_scope on loopover_github_rest_rate_limit_responses_total." runbook: "Open the GitHub REST Rate Limits panel and compare with Queue Rate-Limit Deferrals. If responses are installation-scoped, lower concurrent GitHub work for that installation; if key_scope=none/other, identify the unaffiliated caller and route it through an admission key." - - alert: GittensoryQueueRateLimitDeferralsHigh + - alert: LoopoverQueueRateLimitDeferralsHigh # Jobs are being intentionally parked to preserve GitHub budget. That is better # than burning through the bucket, but a sustained storm means throughput is bound # by GitHub admission rather than worker capacity. expr: | - sum by (kind, key_scope, job_type) (rate(gittensory_jobs_rate_limit_admission_deferred_total[5m])) > 0.05 + sum by (kind, key_scope, job_type) (rate(loopover_jobs_rate_limit_admission_deferred_total[5m])) > 0.05 or - sum by (kind, key_scope, job_type) (rate(gittensory_jobs_rate_limit_budget_deferred_total[5m])) > 0.05 + sum by (kind, key_scope, job_type) (rate(loopover_jobs_rate_limit_budget_deferred_total[5m])) > 0.05 for: 10m labels: severity: warning annotations: - summary: "gittensory queue is rate-limit deferring jobs" + summary: "loopover queue is rate-limit deferring jobs" description: "GitHub rate-limit deferrals exceeded 0.05/s for 10m. The queue is protecting the bucket but throughput is GitHub-budget bound." - runbook: "Use gittensory_jobs_rate_limit_* labels (kind/key_scope/job_type) to identify the stuck class. Prefer reducing duplicate webhook/regate work or adding cache coverage before raising worker concurrency." + runbook: "Use loopover_jobs_rate_limit_* labels (kind/key_scope/job_type) to identify the stuck class. Prefer reducing duplicate webhook/regate work or adding cache coverage before raising worker concurrency." # ── Postgres database + backup freshness ─────────────────────────────────── - - name: gittensory-postgres + - name: loopover-postgres rules: - - alert: GittensoryPostgresConnectionPressure + - alert: LoopoverPostgresConnectionPressure # Postgres exporter is present only for Postgres-backed installs. Fire when the app DB # consumes more than 80% of max_connections for 10m; use PgBouncer or lower app concurrency. expr: | @@ -235,10 +235,10 @@ groups: severity: warning annotations: summary: "Postgres connection usage is above 80%" - description: "The gittensory database is using {{ $value | humanizePercentage }} of max_connections for over 10m." + description: "The loopover database is using {{ $value | humanizePercentage }} of max_connections for over 10m." runbook: "Enable the pgbouncer profile or reduce self-host worker/app concurrency. Check the Postgres Connections panel by state before increasing max_connections." - - alert: GittensoryPostgresLockWaits + - alert: LoopoverPostgresLockWaits # Wait-event labels are emitted by the postgres_exporter stat_activity collector. Any sustained # lock wait means a write path is blocked and queue processing can appear stuck. expr: sum(pg_stat_activity_count{datname="loopover", wait_event_type="Lock"}) > 0 @@ -247,10 +247,10 @@ groups: severity: warning annotations: summary: "Postgres sessions are waiting on locks" - description: "{{ $value | printf \"%.0f\" }} gittensory database session(s) have waited on Postgres locks for over 5m." + description: "{{ $value | printf \"%.0f\" }} loopover database session(s) have waited on Postgres locks for over 5m." runbook: "Inspect the Postgres Locks and Slow Transactions panels. Look for long transactions, migration jobs, or stuck writers before restarting workers." - - alert: GittensoryPostgresSlowTransaction + - alert: LoopoverPostgresSlowTransaction # Long active transactions are the self-host proxy for slow/problem queries without requiring # pg_stat_statements or superuser-only setup. expr: max(pg_stat_activity_max_tx_duration{datname="loopover"}) > 120 @@ -259,10 +259,10 @@ groups: severity: warning annotations: summary: "Postgres transaction has been active for over 2m" - description: "The longest active gittensory database transaction is {{ $value | printf \"%.0f\" }}s old." + description: "The longest active loopover database transaction is {{ $value | printf \"%.0f\" }}s old." runbook: "Check app logs around DB-heavy jobs and review whether queue workers are saturating Postgres. A single long transaction can block cleanup or migrations." - - alert: GittensoryPostgresDeadlocks + - alert: LoopoverPostgresDeadlocks # Deadlocks should be effectively zero; even a low rate points at competing write paths. expr: rate(pg_stat_database_deadlocks{datname="loopover"}[5m]) > 0 for: 2m @@ -270,10 +270,10 @@ groups: severity: warning annotations: summary: "Postgres deadlocks detected" - description: "Postgres deadlocks are occurring for the gittensory database." + description: "Postgres deadlocks are occurring for the loopover database." runbook: "Correlate with deploys and queue write paths. Repeated deadlocks usually mean a transaction ordering bug or too much parallel write pressure." - - alert: GittensoryPostgresDatabaseGrowingFast + - alert: LoopoverPostgresDatabaseGrowingFast # Growth over a 6h window smooths normal small-table churn while catching runaway queue/audit growth. # 262144 bytes/s is roughly 5.4 GiB over 6h. expr: deriv(pg_database_size_bytes{datname="loopover"}[6h]) > 262144 @@ -282,10 +282,10 @@ groups: severity: warning annotations: summary: "Postgres database is growing quickly" - description: "The gittensory database is growing at about {{ $value | humanize }} bytes/s over a 6h trend window." + description: "The loopover database is growing at about {{ $value | humanize }} bytes/s over a 6h trend window." runbook: "Open the Postgres Table Growth panel and inspect queue/audit/event tables first. Confirm retention and backup volume capacity before raising traffic." - - alert: GittensoryPostgresDeadTuplesHigh + - alert: LoopoverPostgresDeadTuplesHigh # Autovacuum pressure: high dead tuple ratio and enough absolute dead rows to avoid tiny-table noise. expr: | ( @@ -306,10 +306,10 @@ groups: description: "More than 20% of table tuples are dead and at least 10k dead tuples remain for over 30m." runbook: "Check the Dead Tuples / Autovacuum panel. If autovacuum counts are flat while dead tuples rise, reduce write pressure or tune autovacuum for the largest tables." - - alert: GittensoryBackupMissing + - alert: LoopoverBackupMissing # Fires only when the backup-exporter service is running (backup profile enabled) and no retained # DB backup exists yet. Qdrant backups are best-effort and not included in this DB backup alert. - expr: gittensory_backup_files{target=~"postgres|sqlite"} == 0 + expr: loopover_backup_files{target=~"postgres|sqlite"} == 0 for: 2h labels: severity: warning @@ -318,35 +318,35 @@ groups: description: "The backup profile is exposing metrics, but {{ $labels.target }} has no retained backup file after 2h." runbook: "Run `docker compose --profile backup run --rm backup sh /backup.sh` and inspect the loopover-backups volume. For Postgres, confirm DATABASE_URL is available to the backup service." - - alert: GittensoryBackupStale + - alert: LoopoverBackupStale # Default backup loop is daily. 26h allows one missed scrape/restart window before warning. expr: | - (time() - gittensory_backup_latest_timestamp_seconds{target=~"postgres|sqlite"} > 93600) + (time() - loopover_backup_latest_timestamp_seconds{target=~"postgres|sqlite"} > 93600) and - gittensory_backup_latest_timestamp_seconds{target=~"postgres|sqlite"} > 0 + loopover_backup_latest_timestamp_seconds{target=~"postgres|sqlite"} > 0 for: 30m labels: severity: warning annotations: - summary: "gittensory database backup is stale" + summary: "loopover database backup is stale" description: "The newest retained {{ $labels.target }} backup is older than 26h." runbook: "Check `docker compose --profile backup logs backup` and run the backup command manually. Pair this with the restore verification drill before deleting old backups." # ── Qdrant vector backend ────────────────────────────────────────────────── - - name: gittensory-qdrant + - name: loopover-qdrant rules: - - alert: GittensoryQdrantErrorRateHigh + - alert: LoopoverQdrantErrorRateHigh # Qdrant errors (across upsert/query/delete) relative to total Qdrant traffic. # `... or vector(0)` keeps the denominator non-empty so the ratio is defined even # when query/upsert counters haven't been created yet. > 0.05 = 5% error rate. expr: | ( - sum(rate(gittensory_qdrant_errors_total[10m])) + sum(rate(loopover_qdrant_errors_total[10m])) / ( - sum(rate(gittensory_qdrant_queries_total[10m])) - + sum(rate(gittensory_qdrant_upserts_total[10m])) - + sum(rate(gittensory_qdrant_errors_total[10m])) + sum(rate(loopover_qdrant_queries_total[10m])) + + sum(rate(loopover_qdrant_upserts_total[10m])) + + sum(rate(loopover_qdrant_errors_total[10m])) > 0 ) ) > 0.05 @@ -354,25 +354,25 @@ groups: labels: severity: warning annotations: - summary: "gittensory Qdrant error rate above 5%" + summary: "loopover Qdrant error rate above 5%" description: "{{ $value | humanizePercentage }} of Qdrant operations errored over the last 10m (sustained 10m). RAG retrieval/indexing is degraded." - runbook: "Check the qdrant container (`docker compose --profile qdrant ps/logs`) and gittensory_qdrant_errors_total{op=...} to see whether upsert, query, or delete is failing. A reachable-but-erroring Qdrant often means a schema/collection or disk problem." + runbook: "Check the qdrant container (`docker compose --profile qdrant ps/logs`) and loopover_qdrant_errors_total{op=...} to see whether upsert, query, or delete is failing. A reachable-but-erroring Qdrant often means a schema/collection or disk problem." # ── Orb usage-event export pipeline ──────────────────────────────────────── - - name: gittensory-orb + - name: loopover-orb rules: - - alert: GittensoryOrbExportErrorRateHigh + - alert: LoopoverOrbExportErrorRateHigh # Failed Orb event exports vs total export attempts (errors + successes). # The `> 0` denominator guard avoids 0/0. Export failures mean billed usage # events aren't reaching Orb — a revenue/billing-integrity issue, so it warns # even at a modest 5% rate. expr: | ( - sum(rate(gittensory_orb_export_errors_total[15m])) + sum(rate(loopover_orb_export_errors_total[15m])) / ( - sum(rate(gittensory_orb_events_exported_total[15m])) - + sum(rate(gittensory_orb_export_errors_total[15m])) + sum(rate(loopover_orb_events_exported_total[15m])) + + sum(rate(loopover_orb_export_errors_total[15m])) > 0 ) ) > 0.05 @@ -380,64 +380,64 @@ groups: labels: severity: warning annotations: - summary: "gittensory Orb export error rate above 5%" + summary: "loopover Orb export error rate above 5%" description: "{{ $value | humanizePercentage }} of Orb usage-event exports failed over the last 15m (sustained 15m). Billable events may not be reaching Orb." - runbook: "Verify Orb API credentials/connectivity and check level=error logs around orb export. Recorded-but-unexported events (gittensory_orb_events_recorded_total vs _exported_total diverging) confirm a stuck exporter." + runbook: "Verify Orb API credentials/connectivity and check level=error logs around orb export. Recorded-but-unexported events (loopover_orb_events_recorded_total vs _exported_total diverging) confirm a stuck exporter." - - alert: GittensoryWebhookEnqueueFailures + - alert: LoopoverWebhookEnqueueFailures # A webhook accepted by the receiver but not durably queued becomes GitHub # redelivery pressure and can look like queue jitter. Any sustained enqueue # failures deserve attention. - expr: sum(rate(gittensory_webhook_enqueue_total{result="enqueue_failed"}[5m])) > 0 + expr: sum(rate(loopover_webhook_enqueue_total{result="enqueue_failed"}[5m])) > 0 for: 2m labels: severity: warning annotations: - summary: "gittensory webhook enqueue failures detected" + summary: "loopover webhook enqueue failures detected" description: "Webhook enqueue failures are occurring for over 2m. Labels event/action show which GitHub event class is failing." - runbook: "Check WEBHOOKS queue availability and the durable queue backend. If this coincides with Orb relay drains, inspect gittensory_orb_webhook_total{result=\"enqueue_failed\"}." + runbook: "Check WEBHOOKS queue availability and the durable queue backend. If this coincides with Orb relay drains, inspect loopover_orb_webhook_total{result=\"enqueue_failed\"}." - - alert: GittensoryOrbRelayRegistrationStuck + - alert: LoopoverOrbRelayRegistrationStuck # #selfhost-runtime-drift follow-up: mirrors isOrbRelayRegistrationAlerting's own gate (3 consecutive # registration failures, OR the pull-mode drain loop gone quiet for 30m) so Prometheus escalates on # EXACTLY the same evidence the app itself uses to decide "actually stuck" vs "one hiccup, still # draining fine". A single transient registration timeout will not trip this on its own. - expr: gittensory_orb_relay_register_consecutive_failures >= 3 or gittensory_orb_relay_drain_seconds_since_last > 1800 + expr: loopover_orb_relay_register_consecutive_failures >= 3 or loopover_orb_relay_drain_seconds_since_last > 1800 for: 5m labels: severity: warning annotations: - summary: "gittensory orb relay registration looks actually stuck, not just a transient hiccup" + summary: "loopover orb relay registration looks actually stuck, not just a transient hiccup" description: "Either the consecutive registration-failure streak has reached {{ $value | printf \"%.0f\" }}, or the pull-mode drain loop hasn't completed in over 30m. A lone registration timeout alone would not trigger this." - runbook: "Check gittensory_orb_relay_register_total{result=\"failed\"} by mode for the failure pattern, and confirm ORB_BROKER_URL / ORB_ENROLLMENT_SECRET are still valid. If pull mode, verify the drain loop itself isn't crash-looping (selfhost_orb_relay_register_failed logs at level=error)." + runbook: "Check loopover_orb_relay_register_total{result=\"failed\"} by mode for the failure pattern, and confirm ORB_BROKER_URL / ORB_ENROLLMENT_SECRET are still valid. If pull mode, verify the drain loop itself isn't crash-looping (selfhost_orb_relay_register_failed logs at level=error)." # ── HTTP serving health (status label + duration histogram, both live in src/server.ts) ─ - # gittensory_http_requests_total carries a status="2xx|3xx|4xx|5xx" label (seeded at zero per - # class so every series exists from boot), and gittensory_http_request_duration_seconds is a + # loopover_http_requests_total carries a status="2xx|3xx|4xx|5xx" label (seeded at zero per + # class so every series exists from boot), and loopover_http_request_duration_seconds is a # histogram of request duration. Both rules below still tolerate an absent metric/label on an # older running build: the ratio uses a `> 0` denominator guard, and an absent series simply # yields no result (rule stays inactive) rather than erroring. - - name: gittensory-http + - name: loopover-http rules: - - alert: GittensoryHighHttp5xxRatio + - alert: LoopoverHighHttp5xxRatio # Share of responses that are 5xx. status="5xx" selects only server errors; # the denominator sums all statuses. If the status label is missing entirely # (old build), the numerator selector matches nothing → no firing. 0.05 = 5%. expr: | ( - sum(rate(gittensory_http_requests_total{status="5xx"}[5m])) + sum(rate(loopover_http_requests_total{status="5xx"}[5m])) / - sum(rate(gittensory_http_requests_total[5m])) > 0 + sum(rate(loopover_http_requests_total[5m])) > 0 ) > 0.05 for: 10m labels: severity: critical annotations: - summary: "gittensory HTTP 5xx ratio above 5%" + summary: "loopover HTTP 5xx ratio above 5%" description: "{{ $value | humanizePercentage }} of HTTP responses were 5xx over the last 5m (sustained 10m). The API is throwing server errors." - runbook: "Tail logs for level=error around the request path. Correlate with deploys, DB availability, and AI-provider outages. If 5xx coincides with a target restart, it may be a crash-loop — see GittensoryTargetDown." + runbook: "Tail logs for level=error around the request path. Correlate with deploys, DB availability, and AI-provider outages. If 5xx coincides with a target restart, it may be a crash-loop — see LoopoverTargetDown." - - alert: GittensoryRequestLatencySLOBreach + - alert: LoopoverRequestLatencySLOBreach # p95 request latency from the duration histogram. histogram_quantile over the # per-le bucket rate gives the 95th percentile end-to-end. 1s p95 is a reasonable # SLO for a review API; raise for heavier AI-bound endpoints. `by (le)` is @@ -445,125 +445,125 @@ groups: expr: | histogram_quantile( 0.95, - sum(rate(gittensory_http_request_duration_seconds_bucket[5m])) by (le) + sum(rate(loopover_http_request_duration_seconds_bucket[5m])) by (le) ) > 1 for: 10m labels: severity: warning annotations: - summary: "gittensory p95 request latency above 1s" + summary: "loopover p95 request latency above 1s" description: "p95 HTTP request latency is {{ $value | printf \"%.2f\" }}s over the last 5m (sustained 10m), breaching the 1s SLO." - runbook: "Check whether slowness is queue/DB/AI-bound: correlate with gittensory_queue_pending and Qdrant/AI latency. A rising p95 with flat error rate usually means a saturated dependency, not a bug." + runbook: "Check whether slowness is queue/DB/AI-bound: correlate with loopover_queue_pending and Qdrant/AI latency. A rising p95 with flat error rate usually means a saturated dependency, not a bug." # ── AI review reliability (dual-AI combiner + per-provider circuit breaker, #2540) ─ - - name: gittensory-ai-review + - name: loopover-ai-review rules: - - alert: GittensoryAiReviewInconclusiveSpike + - alert: LoopoverAiReviewInconclusiveSpike # `inconclusive` means the AI review pipeline could not produce a usable verdict (every reviewer # opinion missing/unparseable, or a required opinion never came back) -- the review still runs # deterministically, but dual-AI review is repeatedly failing to add value. Absolute-increase - # threshold (matching GittensoryDeadLetterJobsGrowing above): there's no clean matching-cardinality + # threshold (matching LoopoverDeadLetterJobsGrowing above): there's no clean matching-cardinality # denominator (total review attempts aren't broken out per-mode the same way), so a ratio query # would need an unrelated series. > 5 in 30m tolerates the occasional one-off degrade. - expr: increase(gittensory_ai_review_inconclusive_total[30m]) > 5 + expr: increase(loopover_ai_review_inconclusive_total[30m]) > 5 for: 10m labels: severity: warning annotations: - summary: "gittensory AI review is repeatedly inconclusive" + summary: "loopover AI review is repeatedly inconclusive" description: "{{ $value | printf \"%.0f\" }} AI review(s) came back inconclusive over the last 30m (sustained 10m). Dual-AI review is repeatedly failing to produce a usable verdict." - runbook: "Check provider health and circuit-breaker state (gittensory_ai_provider_failures_total / gittensory_ai_provider_circuit_open_total) and verify AI_PROVIDER credentials are still valid for every configured reviewer." + runbook: "Check provider health and circuit-breaker state (loopover_ai_provider_failures_total / loopover_ai_provider_circuit_open_total) and verify AI_PROVIDER credentials are still valid for every configured reviewer." - - alert: GittensoryAiReviewOnMergeFloorBypassAttempted + - alert: LoopoverAiReviewOnMergeFloorBypassAttempted # A repo's .gittensory.yml tried to loosen the operator's onMerge/combine/reviewer-count floor and # got clamped back to the operator's own configured minimum (#3901). A governance/config signal, not - # an operational outage, but same absolute-increase style as GittensoryAiProviderCircuitOpen above: + # an operational outage, but same absolute-increase style as LoopoverAiProviderCircuitOpen above: # any occurrence (> 0) over 1h is worth a look, sustained 5m so a single instantaneous blip doesn't # immediately page. - expr: increase(gittensory_ai_review_onmerge_clamped_total[1h]) > 0 + expr: increase(loopover_ai_review_onmerge_clamped_total[1h]) > 0 for: 5m labels: severity: warning annotations: summary: "a repo's review-floor clamp fired" description: "{{ $value | printf \"%.0f\" }} onMerge/combine/reviewer-count clamp event(s) over the last 1h — a repo config tried to loosen below the operator's configured floor." - runbook: "Check which repo's .gittensory.yml requested the loosened setting (gittensory_ai_review_onmerge_clamped_total is labeled by mode) and confirm whether that repo's floor should be adjusted or the request declined." + runbook: "Check which repo's .gittensory.yml requested the loosened setting (loopover_ai_review_onmerge_clamped_total is labeled by mode) and confirm whether that repo's floor should be adjusted or the request declined." - - alert: GittensoryAiProviderCircuitOpen + - alert: LoopoverAiProviderCircuitOpen # A provider's circuit breaker opens after AI_PROVIDER_FAILURE_THRESHOLD consecutive failures and # short-circuits further attempts for a cooldown window -- ANY circuit-open event in 15m means that # provider has been failing repeatedly and calls are being skipped fast rather than retried at full - # cost. Same absolute-increase style as GittensoryDeadLetterJobsGrowing: any occurrence is worth a look. - expr: increase(gittensory_ai_provider_circuit_open_total[15m]) > 0 + # cost. Same absolute-increase style as LoopoverDeadLetterJobsGrowing: any occurrence is worth a look. + expr: increase(loopover_ai_provider_circuit_open_total[15m]) > 0 for: 5m labels: severity: warning annotations: - summary: "gittensory AI provider {{ $labels.provider }} circuit breaker is open" + summary: "loopover AI provider {{ $labels.provider }} circuit breaker is open" description: "Provider {{ $labels.provider }} has failed repeatedly and its circuit breaker is skipping calls fast during its cooldown (sustained 5m)." - runbook: "Check that provider's credentials/reachability (CLI auth for claude-code/codex, or the configured API key/base URL for HTTP providers) via gittensory_ai_provider_failures_total{provider=\"...\"} and recent selfhost_ai_provider_failed logs." + runbook: "Check that provider's credentials/reachability (CLI auth for claude-code/codex, or the configured API key/base URL for HTTP providers) via loopover_ai_provider_failures_total{provider=\"...\"} and recent selfhost_ai_provider_failed logs." # ── Ops anomaly scan (review burst / review failure burst, #ops-anomaly-metric) ──── - - name: gittensory-ops-anomalies + - name: loopover-ops-anomalies rules: - - alert: GittensoryOpsAnomalyDetected - # runOpsAlerts (src/review/ops-wire.ts) scans gittensory's own outcome data hourly and increments this + - alert: LoopoverOpsAnomalyDetected + # runOpsAlerts (src/review/ops-wire.ts) scans loopover's own outcome data hourly and increments this # counter, labeled by kind, when it catches a review burst (a stuck-CI finalize loop or sweep retry # storm re-publishing the same PR far more than normal iteration ever does) or a review failure burst # (repeated inconclusive AI-review calls with zero successful publish -- the #3747 incident shape). - # Same absolute-increase style as GittensoryDeadLetterJobsGrowing: any occurrence over the scan's own + # Same absolute-increase style as LoopoverDeadLetterJobsGrowing: any occurrence over the scan's own # 2h detection window is worth a look, not a rate/ratio. - expr: increase(gittensory_ops_anomaly_total[2h]) > 0 + expr: increase(loopover_ops_anomaly_total[2h]) > 0 for: 5m labels: severity: warning annotations: - summary: "gittensory ops anomaly scan flagged a {{ $labels.kind }} on {{ $labels.repo }}" + summary: "loopover ops anomaly scan flagged a {{ $labels.kind }} on {{ $labels.repo }}" description: "{{ $value | printf \"%.0f\" }} {{ $labels.kind }} detection(s) over the last 2h for {{ $labels.repo }} (sustained 5m). Check the ops_anomaly structured log for the full detail line." runbook: "Tail logs for level=error event=ops_anomaly repo={{ $labels.repo }} for the human-readable anomaly text. A review_burst or review_failure_burst usually means a stuck-CI finalize loop or a sweep retry storm -- see #orb-ci-stuck-repeat / #review-burst-blind-spot." # ── Host clock sync (#3811) ─────────────────────────────────────────────── - - name: gittensory-system-health + - name: loopover-system-health rules: - - alert: GittensoryClockSkewWarning + - alert: LoopoverClockSkewWarning # GitHub App JWTs are signed with iat backdated 60s for skew tolerance and exp at now+540s # (createAppJwt, src/github/app.ts). A local clock running ahead erodes that backdate; once skew # approaches it, GitHub can reject the JWT as not-yet-valid ("Bad credentials"), breaking ALL # GitHub App auth fleet-wide -- exactly what happened when edge-us-01's sole NTP source died and # its clock drifted ~3 minutes unnoticed. 60s is well under that 3-minute drift, so this fires long # before auth actually breaks. - expr: abs(gittensory_clock_skew_seconds) > 60 + expr: abs(loopover_clock_skew_seconds) > 60 for: 5m labels: severity: warning annotations: - summary: "gittensory host clock has drifted {{ $value | printf \"%.0f\" }}s from GitHub's server time" + summary: "loopover host clock has drifted {{ $value | printf \"%.0f\" }}s from GitHub's server time" description: "Clock skew has been over 60s (sustained 5m). GitHub App JWT auth starts failing once skew approaches the 60s iat backdate margin." runbook: "Check `chronyc sources` / `chronyc tracking` on the host. Confirm at least one NTP source shows a nonzero Reach (Reach: 0 means that source has never successfully synced). Configure redundant NTP sources -- a single dead source is a silent single point of failure." - - alert: GittensoryClockSkewCritical + - alert: LoopoverClockSkewCritical # 120s is comfortably past the 60s JWT backdate margin -- GitHub App auth is very likely already # failing fleet-wide by this point. - expr: abs(gittensory_clock_skew_seconds) > 120 + expr: abs(loopover_clock_skew_seconds) > 120 for: 2m labels: severity: critical annotations: - summary: "gittensory host clock skew is CRITICAL ({{ $value | printf \"%.0f\" }}s) -- GitHub App auth is likely failing" + summary: "loopover host clock skew is CRITICAL ({{ $value | printf \"%.0f\" }}s) -- GitHub App auth is likely failing" description: "Clock skew has exceeded 120s (sustained 2m), well past the point GitHub App JWT auth (\"Bad credentials\") is expected to start failing fleet-wide." - runbook: "Same as GittensoryClockSkewWarning, but treat as urgent: fix NTP sync immediately (chronyc sources, chronyc makestep, restart chrony if every source stays at Reach: 0). Check for github_app_jwt_rejected logs to confirm auth impact." + runbook: "Same as LoopoverClockSkewWarning, but treat as urgent: fix NTP sync immediately (chronyc sources, chronyc makestep, restart chrony if every source stays at Reach: 0). Check for github_app_jwt_rejected logs to confirm auth impact." # ── Cloudflare D1 (central cloud) size + signal_snapshots dedup regression (#3810) ──── - # gittensory_d1_* metrics come from the OPT-IN Cloudflare Management API probe (src/selfhost/ + # loopover_d1_* metrics come from the OPT-IN Cloudflare Management API probe (src/selfhost/ # d1-size-probe.ts, CLOUDFLARE_D1_MONITOR_* env vars) -- absent/disabled reads -1 on every gauge below, # comfortably under every threshold here, so these rules never fire on an install that hasn't configured # the probe (most self-host installs run their own SQLite/Postgres backend and have nothing to monitor). - - name: gittensory-d1-storage + - name: loopover-d1-storage rules: - - alert: GittensoryD1DatabaseSizeWarning + - alert: LoopoverD1DatabaseSizeWarning # 7e9 bytes is ~70% of D1's known ~10GB per-database cap that was hit on 2026-07-06 (#3810), which # caused real D1 writes (including an Orb relay registration handshake) to start failing/timing out. - expr: gittensory_d1_database_size_bytes > 7000000000 + expr: loopover_d1_database_size_bytes > 7000000000 for: 30m labels: severity: warning @@ -572,10 +572,10 @@ groups: description: "The monitored D1 database is {{ $value | humanize }}B, over 70% of the ~10GB per-database cap (sustained 30m)." runbook: "Check the D1 panel's per-table row counts (Cloudflare D1 (Central Cloud) row) and confirm the daily signal_snapshots dedup job (dedupeSignalSnapshots, wired into prune-retention) is actually running. See #3810 for the 2026-07-06 incident this guards against." - - alert: GittensoryD1DatabaseSizeCritical + - alert: LoopoverD1DatabaseSizeCritical # 9e9 bytes is ~90% of the ~10GB cap -- D1 writes started failing fleet-wide the last time the # database actually hit the cap, so this is meant to fire well before that happens again. - expr: gittensory_d1_database_size_bytes > 9000000000 + expr: loopover_d1_database_size_bytes > 9000000000 for: 10m labels: severity: critical @@ -584,8 +584,8 @@ groups: description: "The monitored D1 database is {{ $value | humanize }}B, over 90% of the ~10GB per-database cap (sustained 10m). D1 writes failed fleet-wide the last time this cap was hit (2026-07-06)." runbook: "Immediate: verify the signal_snapshots dedup job is running (POST /v1/internal/retention/preview, or check audit_events for its record) and identify + trim/archive any other unbounded table from the row-count panel. Contact Cloudflare to raise the account storage limit if cleanup alone doesn't recover enough headroom." - - alert: GittensorySignalSnapshotsDedupRegression - # gittensory_signal_snapshots_rows_per_key is rows-per-distinct-key scoped ONLY to the four + - alert: LoopoverSignalSnapshotsDedupRegression + # loopover_signal_snapshots_rows_per_key is rows-per-distinct-key scoped ONLY to the four # latest-only-dedup signal types dedupeSignalSnapshots (src/db/retention.ts) actually converges to # ~1 row per key -- NOT the whole signal_snapshots table, which intentionally keeps bounded # multi-row history for other signal types (queue-health, contributor-decision-pack, ...). Healthy @@ -593,7 +593,7 @@ groups: # dedup run); 10 is a wide margin above that, while the actual 2026-07-06 incident ratio (342243 # rows / 2183 keys) was ~157 -- so a value anywhere near double digits means the dedup job has # stopped running, started erroring, or its allowlist regressed. - expr: gittensory_signal_snapshots_rows_per_key > 10 + expr: loopover_signal_snapshots_rows_per_key > 10 for: 30m labels: severity: warning @@ -602,12 +602,12 @@ groups: description: "signal_snapshots has {{ $value | printf \"%.1f\" }} rows per distinct dedup key (sustained 30m) -- the daily dedupeSignalSnapshots job may not be running." runbook: "Confirm the prune-retention cron (03:00 UTC daily) is completing (audit_events around that time) and that dedupeSignalSnapshots isn't throwing. See #3810 and src/db/retention.ts." - - alert: GittensoryD1ProbeFailing + - alert: LoopoverD1ProbeFailing # The probe itself (a Cloudflare Management API call) can fail independently of the database it # monitors -- an expired/rotated API token, a wrong account/database id, or a Cloudflare API outage. # Without this, an operator would see a flat/stale D1 panel and could mistake "the probe broke" for # "the database stopped growing". - expr: increase(gittensory_d1_probe_errors_total[1h]) > 3 + expr: increase(loopover_d1_probe_errors_total[1h]) > 3 for: 15m labels: severity: warning diff --git a/scripts/backup-metrics.sh b/scripts/backup-metrics.sh index 8d73fc2f18..1885a84d78 100644 --- a/scripts/backup-metrics.sh +++ b/scripts/backup-metrics.sh @@ -45,15 +45,15 @@ write_metrics() { mkdir -p "$OUT" tmp="$FILE.tmp" { - echo "# HELP gittensory_backup_latest_timestamp_seconds Unix timestamp of the newest retained self-host backup file by target." - echo "# TYPE gittensory_backup_latest_timestamp_seconds gauge" + echo "# HELP loopover_backup_latest_timestamp_seconds Unix timestamp of the newest retained self-host backup file by target." + echo "# TYPE loopover_backup_latest_timestamp_seconds gauge" for target in postgres sqlite qdrant; do - echo "gittensory_backup_latest_timestamp_seconds{target=\"$target\"} $(latest_timestamp "$ROOT/$target")" + echo "loopover_backup_latest_timestamp_seconds{target=\"$target\"} $(latest_timestamp "$ROOT/$target")" done - echo "# HELP gittensory_backup_files Retained self-host backup files by target." - echo "# TYPE gittensory_backup_files gauge" + echo "# HELP loopover_backup_files Retained self-host backup files by target." + echo "# TYPE loopover_backup_files gauge" for target in postgres sqlite qdrant; do - echo "gittensory_backup_files{target=\"$target\"} $(file_count "$ROOT/$target")" + echo "loopover_backup_files{target=\"$target\"} $(file_count "$ROOT/$target")" done } > "$tmp" mv "$tmp" "$FILE" diff --git a/scripts/smoke-observability-metrics.mjs b/scripts/smoke-observability-metrics.mjs index c63425c597..78bd82e7f4 100755 --- a/scripts/smoke-observability-metrics.mjs +++ b/scripts/smoke-observability-metrics.mjs @@ -1,12 +1,12 @@ #!/usr/bin/env node import { execFileSync } from "node:child_process"; -const composeService = process.env.SELFHOST_SERVICE ?? "gittensory"; +const composeService = process.env.SELFHOST_SERVICE ?? "loopover"; const timeoutMs = Number(process.env.OBSERVABILITY_SMOKE_TIMEOUT_MS ?? "30000"); const pollIntervalMs = Number( process.env.OBSERVABILITY_SMOKE_POLL_MS ?? "1000", ); -const metricName = `gittensory_selfhost_smoke_${Date.now()}_total`; +const metricName = `loopover_selfhost_smoke_${Date.now()}_total`; await main(); @@ -23,11 +23,11 @@ const body = { resourceMetrics: [{ resource: { attributes: [ - { key: "service.name", value: { stringValue: "gittensory-selfhost-smoke" } } + { key: "service.name", value: { stringValue: "loopover-selfhost-smoke" } } ] }, scopeMetrics: [{ - scope: { name: "gittensory-selfhost-smoke" }, + scope: { name: "loopover-selfhost-smoke" }, metrics: [{ name: metricName, sum: { @@ -63,8 +63,8 @@ while (Date.now() <= deadline) { const appRes = await fetch("http://localhost:8787/metrics"); if (!appRes.ok) throw new Error("app /metrics returned " + appRes.status); const appText = await appRes.text(); - if (!appText.includes("# HELP gittensory_uptime_seconds") || !appText.includes("# TYPE gittensory_uptime_seconds gauge")) { - throw new Error("app /metrics is missing expected HELP/TYPE shape for gittensory_uptime_seconds"); + if (!appText.includes("# HELP loopover_uptime_seconds") || !appText.includes("# TYPE loopover_uptime_seconds gauge")) { + throw new Error("app /metrics is missing expected HELP/TYPE shape for loopover_uptime_seconds"); } console.log(JSON.stringify({ ok: true, metricName })); process.exit(0); diff --git a/scripts/smoke-observability-traces.mjs b/scripts/smoke-observability-traces.mjs index 424a700071..173610661f 100755 --- a/scripts/smoke-observability-traces.mjs +++ b/scripts/smoke-observability-traces.mjs @@ -2,7 +2,7 @@ import { randomBytes } from "node:crypto"; import { execFileSync } from "node:child_process"; -const composeService = process.env.SELFHOST_SERVICE ?? "gittensory"; +const composeService = process.env.SELFHOST_SERVICE ?? "loopover"; const timeoutMs = Number(process.env.OBSERVABILITY_SMOKE_TIMEOUT_MS ?? "30000"); const pollIntervalMs = Number( process.env.OBSERVABILITY_SMOKE_POLL_MS ?? "1000", @@ -26,12 +26,12 @@ const body = { resourceSpans: [{ resource: { attributes: [ - { key: "service.name", value: { stringValue: "gittensory-selfhost-smoke" } }, + { key: "service.name", value: { stringValue: "loopover-selfhost-smoke" } }, { key: "deployment.environment.name", value: { stringValue: "selfhost-smoke" } } ] }, scopeSpans: [{ - scope: { name: "gittensory-selfhost-smoke" }, + scope: { name: "loopover-selfhost-smoke" }, spans: [{ traceId, spanId, diff --git a/scripts/smoke-selfhost.sh b/scripts/smoke-selfhost.sh index b1e80ded51..480ab16c42 100755 --- a/scripts/smoke-selfhost.sh +++ b/scripts/smoke-selfhost.sh @@ -177,7 +177,7 @@ fi echo "smoke-selfhost: checking /health, /ready, /metrics" curl -sf "http://127.0.0.1:${PORT}/health" | grep -q '"status":"ok"' curl -sf "http://127.0.0.1:${PORT}/ready" | grep -q '"ok":true' -curl -sf "http://127.0.0.1:${PORT}/metrics" | grep -q 'gittensory_uptime_seconds' +curl -sf "http://127.0.0.1:${PORT}/metrics" | grep -q 'loopover_uptime_seconds' if [ "$VISUAL_REVIEW" = "1" ]; then echo "smoke-selfhost: checking /gittensory/shot renders a real PNG through the self-host browser stub" diff --git a/src/github/backfill.ts b/src/github/backfill.ts index e170254221..3b5c32ffdf 100644 --- a/src/github/backfill.ts +++ b/src/github/backfill.ts @@ -350,24 +350,24 @@ const PR_DETAIL_BATCH_SIZE: Record = { light: 12, full: 40 // in a single job execution, draining the shared installation bucket before the once-per-segment rate check // runs again (#audit-rate-headroom). Any PR left un-hydrated this run stays a candidate on the next page/run. const MERGED_PR_FILE_HYDRATION_BATCH_SIZE: Record = { light: 10, full: 20, resume: 20 }; -const PULL_REQUEST_FILES_FETCH_METRIC = "gittensory_github_pull_request_files_fetch_total"; +const PULL_REQUEST_FILES_FETCH_METRIC = "loopover_github_pull_request_files_fetch_total"; // #selfhost-runtime-pressure: a bare 403 on the branch-protection probe (no admin:read on this installation/fork, // the common case) is a PERMISSION/config gap, not GitHub rate-limit exhaustion -- GitHubApiError.rateLimited // already makes that distinction (see isRateLimitedGitHubFailure below). Counted separately from the rate-limit // metrics so a dashboard can tell "GitHub is throttling us" apart from "this token can't read branch protection // (expected for most installations/forks)" instead of a permission gap inflating an apparent rate-limit signal. -const BRANCH_PROTECTION_PERMISSION_DENIED_METRIC = "gittensory_github_branch_protection_permission_denied_total"; +const BRANCH_PROTECTION_PERMISSION_DENIED_METRIC = "loopover_github_branch_protection_permission_denied_total"; type PullRequestFilesFetchCaller = "backfill_open_pr_details" | "backfill_merged_history" | "live_review"; // #2537: durable-cache counter for the bare PR-state read, mirroring PULL_REQUEST_FILES_FETCH_METRIC's bounded- // label style (no per-PR-number labels — cardinality-safe). -const PR_STATE_CACHE_METRIC = "gittensory_pr_state_cache_total"; +const PR_STATE_CACHE_METRIC = "loopover_pr_state_cache_total"; // Safety-net max age for a webhook-invalidated PR-state cache row (a dropped/missed webhook must not pin a stale // value forever). Short enough that a missed synchronize/closed/reopened event self-heals within one sweep tick. const PR_STATE_CACHE_MAX_AGE_MS = 5 * 60 * 1000; // #selfhost-ci-verification: durable-cache counter for the CI-state snapshot cache, sibling to PR_STATE_CACHE_METRIC. // Exported: the cache-check/hit/miss orchestration lives in queue/processors.ts (see writeThroughCiStateCache's // own doc comment for why), which needs this same metric name. -export const CI_STATE_CACHE_METRIC = "gittensory_ci_state_cache_total"; +export const CI_STATE_CACHE_METRIC = "loopover_ci_state_cache_total"; // Shorter than PR_STATE_CACHE_MAX_AGE_MS (5min): CI state changes faster and more consequentially than bare PR // state, and check_run/check_suite `completed` webhooks already invalidate this cache explicitly (see // invalidateCiStateCache below) -- this is purely the backstop for a delayed/missed webhook delivery. @@ -1299,7 +1299,7 @@ async function refreshStoredInstallation( // minted token's own installationId (parsed from the broker's response payload) against the row // being probed; a mismatch (or a missing/zero id from an older broker) must not be reported healthy. if (minted.installationId === 0 || minted.installationId !== installation.id) { - incr("gittensory_installation_health_broker_probe_total", { result: "mismatched_installation" }); + incr("loopover_installation_health_broker_probe_total", { result: "mismatched_installation" }); return { installation, errorSummary: `Token broker minted a token for installation ${minted.installationId || "unknown"}, not ${installation.id}.`, @@ -1313,10 +1313,10 @@ async function refreshStoredInstallation( if (refreshedInstallation !== installation) { await updateInstallationPermissions(env, installation.id, refreshedInstallation.permissions); } - incr("gittensory_installation_health_broker_probe_total", { result: "ok" }); + incr("loopover_installation_health_broker_probe_total", { result: "ok" }); return { installation: refreshedInstallation, authMode: "broker" }; } catch (error) { - incr("gittensory_installation_health_broker_probe_total", { result: "failed" }); + incr("loopover_installation_health_broker_probe_total", { result: "failed" }); return { installation, errorSummary: strippedErrorMessage(error, "Token broker did not mint an installation token."), diff --git a/src/github/client.ts b/src/github/client.ts index a0be770d50..7f76a416ef 100644 --- a/src/github/client.ts +++ b/src/github/client.ts @@ -18,9 +18,9 @@ import type { RepositorySettings } from "../types"; const GITHUB_FETCH_TIMEOUT_MS = 12_000; const GITHUB_API_PREFIX = "https://api.github.com"; -const GITHUB_RESPONSE_CACHE_METRIC = "gittensory_github_response_cache_total"; -const GITHUB_REST_RATE_LIMIT_OBSERVATION_METRIC = "gittensory_github_rest_rate_limit_observations_total"; -const GITHUB_REST_RATE_LIMIT_RESPONSE_METRIC = "gittensory_github_rest_rate_limit_responses_total"; +const GITHUB_RESPONSE_CACHE_METRIC = "loopover_github_response_cache_total"; +const GITHUB_REST_RATE_LIMIT_OBSERVATION_METRIC = "loopover_github_rest_rate_limit_observations_total"; +const GITHUB_REST_RATE_LIMIT_RESPONSE_METRIC = "loopover_github_rest_rate_limit_responses_total"; const DEFAULT_BRANCH_PROTECTION_TTL_SECONDS = 20 * 60; const DEFAULT_METADATA_TTL_SECONDS = 10 * 60; // A bare `/commits/{ref}` read resolves a ref to its HEAD commit — mutable (a branch moves), so cache it only @@ -268,7 +268,7 @@ export function latestGitHubRestRateLimitObservation(admissionKey: GitHubRateLim } /** gaugeVector sampler (see selfhost/metrics.ts + server.ts) for a genuine "remaining right now" GitHub REST - * rate-limit gauge -- the existing gittensory_github_rest_rate_limit_observations_total counter only supports a + * rate-limit gauge -- the existing loopover_github_rest_rate_limit_observations_total counter only supports a * bucketed `rate()` over a window, never the actual current value. Grouped by key_scope (installation / public / * global / unknown / other -- a small, fixed set, NOT per-installation, so cardinality stays bounded regardless * of how many installations a self-host deploy has), picking the NEWEST observation (by observedAtMs) among diff --git a/src/github/graphql-cache.ts b/src/github/graphql-cache.ts index 73feea6b05..4b7234a61e 100644 --- a/src/github/graphql-cache.ts +++ b/src/github/graphql-cache.ts @@ -10,7 +10,7 @@ import { import { incr } from "../selfhost/metrics"; const GITHUB_GRAPHQL_URL = "https://api.github.com/graphql"; -const GITHUB_GRAPHQL_CACHE_METRIC = "gittensory_github_graphql_cache_total"; +const GITHUB_GRAPHQL_CACHE_METRIC = "loopover_github_graphql_cache_total"; const DEFAULT_GRAPHQL_TTL_SECONDS = 10 * 60; export type GitHubGraphQlCacheClass = "repo_totals" | "contributor_activity"; diff --git a/src/github/webhook.ts b/src/github/webhook.ts index 2c124f148d..ca70301b72 100644 --- a/src/github/webhook.ts +++ b/src/github/webhook.ts @@ -73,7 +73,7 @@ function recordWebhookEnqueueMetric( action: unknown, result: EnqueueWebhookResult, ): void { - incr("gittensory_webhook_enqueue_total", { + incr("loopover_webhook_enqueue_total", { action: webhookMetricAction(action), event: webhookMetricEvent(eventName), result, diff --git a/src/orb/broker-client.ts b/src/orb/broker-client.ts index 9edd6d6371..09f4d87b9b 100644 --- a/src/orb/broker-client.ts +++ b/src/orb/broker-client.ts @@ -262,7 +262,7 @@ export async function drainOrbRelay( } // #zero-trace-webhook-loss: a batch entry missing/mistyping one of the three required fields was // previously discarded with no record anywhere — indistinguishable from the Orb never having relayed it. - incr("gittensory_orb_relay_malformed_events_total"); + incr("loopover_orb_relay_malformed_events_total"); console.error( JSON.stringify({ level: "error", diff --git a/src/queue/dlq.ts b/src/queue/dlq.ts index df049ffce8..bf0e7eff33 100644 --- a/src/queue/dlq.ts +++ b/src/queue/dlq.ts @@ -4,8 +4,8 @@ import { incr } from "../selfhost/metrics"; import { githubRateLimitAdmissionKeyForJob } from "../selfhost/queue-common"; import type { JobMessage, JsonValue } from "../types"; -const DLQ_DEAD_LETTERED_METRIC = "gittensory_dlq_dead_lettered_total"; -const DLQ_REDRIVEN_METRIC = "gittensory_dlq_redriven_total"; +const DLQ_DEAD_LETTERED_METRIC = "loopover_dlq_dead_lettered_total"; +const DLQ_REDRIVEN_METRIC = "loopover_dlq_redriven_total"; /** * DLQ consumer for both `gittensory-jobs-dlq` (maintenance lane) and `gittensory-webhooks-dlq` (the diff --git a/src/queue/processors.ts b/src/queue/processors.ts index c4c0338cd4..e52a10b7a9 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -1999,7 +1999,7 @@ export function precisionBreakerDowngradeDirections(planned: PlannedAgentAction[ return directions; } -/** PURE: the bounded `{actionClass, blockerClass}` label pair for the `gittensory_agent_disposition_total` +/** PURE: the bounded `{actionClass, blockerClass}` label pair for the `loopover_agent_disposition_total` * counter (#terminal-outcome-audit), derived from the FINAL post-breaker plan and the gate's own blocker/hold * codes -- never from free text. `actionClass` is "merge"/"close" when the final plan still contains that * action, else "hold" (guardrail, owner-exemption, migration-collision, not-yet-mergeable, breaker-downgraded, @@ -2853,7 +2853,7 @@ async function runAgentMaintenancePlanAndExecute( // whether/which direction the breaker engaged without recomputing it a second time (#selfhost-holdplan-audit). const precisionBreakerDirections = precisionBreakerDowngradeDirections(planned, breakerOnPlan); for (const direction of precisionBreakerDirections) { - incr("gittensory_precision_breaker_downgrades_total", { direction }); + incr("loopover_precision_breaker_downgrades_total", { direction }); } // Observability (#terminal-outcome-audit): the final per-pass disposition, ALWAYS recorded -- including the // "hold" bucket below (guardrail, owner-exemption, migration-collision, breaker-downgraded, or any other @@ -2866,7 +2866,7 @@ async function runAgentMaintenancePlanAndExecute( // reason from `gate.warnings` in that case, so a guardrail/size/manifest-blocked hold doesn't flatten to the // same "none" bucket as a merge-ready PR waiting on nothing more than pending CI. const disposition = agentDispositionLabels(breakerOnPlan, gate.blockers.map((blocker) => blocker.code), neutralHoldReasonCode(gate)); - incr("gittensory_agent_disposition_total", { + incr("loopover_agent_disposition_total", { repo: repoFullName, action_class: disposition.actionClass, blocker_class: disposition.blockerClass, @@ -6726,7 +6726,7 @@ export async function runLinkedIssueSatisfactionForAdvisory( let result: Awaited>; if (cached) { result = { status: "ok", result: cached.result, estimatedNeurons: cached.estimatedNeurons }; - incr("gittensory_linked_issue_satisfaction_cache_hit_total"); + incr("loopover_linked_issue_satisfaction_cache_hit_total"); await recordAuditEvent(env, { eventType: "github_app.linked_issue_satisfaction_cache_hit", actor: args.author, @@ -6737,7 +6737,7 @@ export async function runLinkedIssueSatisfactionForAdvisory( metadata: { repoFullName: args.repoFullName, headSha: args.advisory.headSha ?? null, linkedIssueNumber: primaryIssueNumber }, }).catch(() => undefined); } else { - incr("gittensory_linked_issue_satisfaction_cache_miss_total"); + incr("loopover_linked_issue_satisfaction_cache_miss_total"); await recordAuditEvent(env, { eventType: "github_app.linked_issue_satisfaction_cache_miss", actor: args.author, @@ -6770,7 +6770,7 @@ export async function runLinkedIssueSatisfactionForAdvisory( inputFingerprint, { status: result.status, result: result.result, estimatedNeurons: result.estimatedNeurons }, ).catch((error) => { - incr("gittensory_linked_issue_satisfaction_cache_write_error_total"); + incr("loopover_linked_issue_satisfaction_cache_write_error_total"); return recordAuditEvent(env, { eventType: "github_app.linked_issue_satisfaction_cache_write_error", actor: args.author, @@ -8613,7 +8613,7 @@ async function maybePublishPrPublicSurface( advisory.findings.push(...frozenReview.findings); aiReview = frozenReview; aiReviewWasReused = true; - incr("gittensory_ai_review_frozen_reuse_total"); + incr("loopover_ai_review_frozen_reuse_total"); await recordAuditEvent(env, { eventType: "github_app.ai_review_frozen_reuse", actor: author, @@ -8639,7 +8639,7 @@ async function maybePublishPrPublicSurface( advisory.findings.push(...pausedReview.findings); aiReview = pausedReview; aiReviewWasReused = true; - incr("gittensory_ai_review_paused_reuse_total"); + incr("loopover_ai_review_paused_reuse_total"); await recordAuditEvent(env, { eventType: "github_app.ai_review_paused_reuse", actor: author, @@ -8653,7 +8653,7 @@ async function maybePublishPrPublicSurface( advisory.findings.push(...oneShotPriorReview.findings); aiReview = oneShotPriorReview; aiReviewWasReused = true; - incr("gittensory_ai_review_one_shot_reuse_total"); + incr("loopover_ai_review_one_shot_reuse_total"); await recordAuditEvent(env, { eventType: "github_app.ai_review_one_shot_reuse", actor: author, @@ -8980,7 +8980,7 @@ async function maybePublishPrPublicSurface( advisory.findings.push(...cachedReview.findings); aiReview = cachedReview; aiReviewWasReused = true; - incr("gittensory_ai_review_cache_hit_total"); + incr("loopover_ai_review_cache_hit_total"); await recordAuditEvent(env, { eventType: "github_app.ai_review_cache_hit", actor: author, @@ -8997,14 +8997,14 @@ async function maybePublishPrPublicSurface( detail: "AI review already current for this head+fingerprint; skipped re-review", metadata: { deliveryId: webhook.deliveryId, repoFullName, /* v8 ignore next -- reached only inside aiReviewWillRun (which requires a truthy advisory.headSha) or the publish-skip guard's own `advisory.headSha &&` check; the `?? null` is a type-level fallback for an unreachable branch. */ headSha: advisory.headSha ?? null }, }).catch(() => undefined); - incr("gittensory_regate_ai_skipped_current_total"); + incr("loopover_regate_ai_skipped_current_total"); } else { // A forced bypass is NOT a cache miss — the cache may well have had a valid, reusable entry; the // caller explicitly asked to skip it. Counting it under the miss metric would make "the cache failed // to serve" indistinguishable from "a caller deliberately opted out," which muddies exactly the // incident-dashboard signal this whole fix exists to provide. if (webhook.forceAiReview === true) { - incr("gittensory_ai_review_force_bypass_total"); + incr("loopover_ai_review_force_bypass_total"); await recordAuditEvent(env, { eventType: "github_app.ai_review_force_bypass", actor: author, @@ -9014,7 +9014,7 @@ async function maybePublishPrPublicSurface( metadata: { deliveryId: webhook.deliveryId, repoFullName, /* v8 ignore next -- reached only inside aiReviewWillRun (which requires a truthy advisory.headSha) or the publish-skip guard's own `advisory.headSha &&` check; the `?? null` is a type-level fallback for an unreachable branch. */ headSha: advisory.headSha ?? null }, }).catch(() => undefined); } else { - incr("gittensory_ai_review_cache_miss_total"); + incr("loopover_ai_review_cache_miss_total"); await recordAuditEvent(env, { eventType: "github_app.ai_review_cache_miss", actor: author, @@ -9066,7 +9066,7 @@ async function maybePublishPrPublicSurface( // the review's own verdict (consensus defect / inconclusive → false). const cacheableForStorage = !dynamicReviewContextActive && aiReview.cacheable !== false; if (!cacheableForStorage) { - incr("gittensory_ai_review_non_cacheable_total"); + incr("loopover_ai_review_non_cacheable_total"); await recordAuditEvent(env, { eventType: "github_app.ai_review_non_cacheable", actor: author, @@ -9100,7 +9100,7 @@ async function maybePublishPrPublicSurface( ).catch((error) => { // #regate-churn (req 3/9): a swallowed write failure here is exactly how the cache goes silently // stale in production — make it observable instead of a bare no-op catch. - incr("gittensory_ai_review_cache_write_error_total"); + incr("loopover_ai_review_cache_write_error_total"); return recordAuditEvent(env, { eventType: "github_app.ai_review_cache_write_error", actor: author, @@ -9407,7 +9407,7 @@ async function maybePublishPrPublicSurface( } } if (canSkipCurrentSurface) { - incr("gittensory_public_surface_publish_skipped_current_total"); + incr("loopover_public_surface_publish_skipped_current_total"); await recordAuditEvent(env, { eventType: "github_app.public_surface_publish_skipped_current", actor: author, @@ -9855,7 +9855,7 @@ async function maybePublishPrPublicSurface( const commentGate = gateEvaluation; // Observability (#reviews-dashboard): record the would-be gate verdict so the Grafana panel shows the // merge/close/hold mix — the "are we rubber-stamping?" signal — even in advisory/dryRun (this is the rendered verdict). - incr("gittensory_gate_decisions_total", { + incr("loopover_gate_decisions_total", { repo: repoFullName, conclusion: commentGate.conclusion, }); @@ -10041,7 +10041,7 @@ async function maybePublishPrPublicSurface( ); if (suppressedCount > 0 || demotedCount > 0) { renderedGate = { ...commentGate, warnings: suppressedWarnings }; - incr("gittensory_review_memory_suppressed_total", { repo: repoFullName }); + incr("loopover_review_memory_suppressed_total", { repo: repoFullName }); console.log( JSON.stringify({ event: "review_memory_applied", @@ -10209,7 +10209,7 @@ async function maybePublishPrPublicSurface( ), ); publishedOutputs.push("comment"); - incr("gittensory_reviews_published_total", { repo: repoFullName }); + incr("loopover_reviews_published_total", { repo: repoFullName }); } catch (error) { const message = errorMessage(error); failedOutputs.push({ output: "comment", error: message, transient: isGitHubTransientPublishError(error) }); diff --git a/src/queue/slop-detection.ts b/src/queue/slop-detection.ts index 90fa863705..a145b42609 100644 --- a/src/queue/slop-detection.ts +++ b/src/queue/slop-detection.ts @@ -113,7 +113,7 @@ export async function runAiSlopForAdvisory( let result: Awaited>; if (cachedSlop) { result = { status: "ok", finding: cachedSlop.finding, band: cachedSlop.band as SlopBand | null, estimatedNeurons: cachedSlop.estimatedNeurons }; - incr("gittensory_ai_slop_cache_hit_total"); + incr("loopover_ai_slop_cache_hit_total"); await recordAuditEvent(env, { eventType: "github_app.ai_slop_cache_hit", actor: args.author, @@ -124,7 +124,7 @@ export async function runAiSlopForAdvisory( metadata: { repoFullName: args.repoFullName, headSha: args.advisory.headSha ?? null }, }).catch(() => undefined); } else { - incr("gittensory_ai_slop_cache_miss_total"); + incr("loopover_ai_slop_cache_miss_total"); await recordAuditEvent(env, { eventType: "github_app.ai_slop_cache_miss", actor: args.author, @@ -154,7 +154,7 @@ export async function runAiSlopForAdvisory( finding: result.finding, estimatedNeurons: result.estimatedNeurons, }).catch((error) => { - incr("gittensory_ai_slop_cache_write_error_total"); + incr("loopover_ai_slop_cache_write_error_total"); return recordAuditEvent(env, { eventType: "github_app.ai_slop_cache_write_error", actor: args.author, diff --git a/src/review/enrichment-wire.ts b/src/review/enrichment-wire.ts index 07a1006b8a..62de60c053 100644 --- a/src/review/enrichment-wire.ts +++ b/src/review/enrichment-wire.ts @@ -13,8 +13,8 @@ import { neutralizePromptInjection } from "./prompt-injection"; import { REES_ANALYZER_NAMES, REES_ANALYZER_NAME_SET, type ReesAnalyzerName } from "./enrichment-analyzer-names"; import type { PullRequestFileRecord } from "../types"; -const REES_ENRICH_REQUESTS_TOTAL = "gittensory_rees_enrich_requests_total"; -const REES_ENRICH_REQUEST_DURATION_SECONDS = "gittensory_rees_enrich_request_duration_seconds"; +const REES_ENRICH_REQUESTS_TOTAL = "loopover_rees_enrich_requests_total"; +const REES_ENRICH_REQUEST_DURATION_SECONDS = "loopover_rees_enrich_request_duration_seconds"; /** Records the client-observable outcome of one /v1/enrich attempt. `elapsedMs` is omitted for the * skipped-before-any-network-attempt case (the auth-rejected circuit breaker), since no call was timed. */ diff --git a/src/review/grounding-wire.ts b/src/review/grounding-wire.ts index e673bfdcc1..2bcc621416 100644 --- a/src/review/grounding-wire.ts +++ b/src/review/grounding-wire.ts @@ -151,7 +151,7 @@ export async function makeGithubFileFetcher(env: Env, repoFullName: string, inst if (cached !== null) { // #4448: mirrors repo-culture-profile's #4509 cache hit/miss instrumentation exactly -- one of the six // AI-touching capabilities that had no reuse-rate signal at all before this. - incr("gittensory_grounding_cache_hit_total"); + incr("loopover_grounding_cache_hit_total"); await recordAuditEvent(env, { eventType: "github_app.grounding_cache_hit", targetKey: repoFullName, @@ -161,7 +161,7 @@ export async function makeGithubFileFetcher(env: Env, repoFullName: string, inst }).catch(() => undefined); return cached; } - incr("gittensory_grounding_cache_miss_total"); + incr("loopover_grounding_cache_miss_total"); await recordAuditEvent(env, { eventType: "github_app.grounding_cache_miss", targetKey: repoFullName, diff --git a/src/review/impact-map.ts b/src/review/impact-map.ts index 7781c1fead..1322aa1341 100644 --- a/src/review/impact-map.ts +++ b/src/review/impact-map.ts @@ -162,7 +162,7 @@ export async function computeImpactMap( if (cached !== null) { // #4448: mirrors repo-culture-profile's #4509 cache hit/miss instrumentation exactly -- one of the six // AI-touching capabilities that had no reuse-rate signal at all before this. - incr("gittensory_impact_map_cache_hit_total"); + incr("loopover_impact_map_cache_hit_total"); await recordAuditEvent(env, { eventType: "github_app.impact_map_cache_hit", targetKey, @@ -172,7 +172,7 @@ export async function computeImpactMap( }).catch(() => undefined); result = cached; } else { - incr("gittensory_impact_map_cache_miss_total"); + incr("loopover_impact_map_cache_miss_total"); await recordAuditEvent(env, { eventType: "github_app.impact_map_cache_miss", targetKey, diff --git a/src/review/ops-wire.ts b/src/review/ops-wire.ts index 5416431afc..d4ba40837a 100644 --- a/src/review/ops-wire.ts +++ b/src/review/ops-wire.ts @@ -246,10 +246,10 @@ export async function runOpsAlerts(env: Env): Promise> // this module exists to catch fast (#orb-ci-stuck-repeat / #review-burst-blind-spot) -- rather than every // anomaly kind, so the counter stays a precise "stuck-CI/retry-storm" signal, not a catch-all. if (reviewBurst && reviewBurst.count >= REVIEW_BURST_THRESHOLD) { - incr("gittensory_ops_anomaly_total", { repo: repoFullName, kind: "review_burst" }); + incr("loopover_ops_anomaly_total", { repo: repoFullName, kind: "review_burst" }); } if (reviewFailureBurst && reviewFailureBurst.count >= REVIEW_FAILURE_BURST_THRESHOLD) { - incr("gittensory_ops_anomaly_total", { repo: repoFullName, kind: "review_failure_burst" }); + incr("loopover_ops_anomaly_total", { repo: repoFullName, kind: "review_failure_burst" }); } } catch (error) { console.error(JSON.stringify({ level: "error", event: "ops_anomaly_repo_error", repo: repoFullName, message: errorMessage(error).slice(0, 200) })); diff --git a/src/review/outcomes-wire.ts b/src/review/outcomes-wire.ts index c1a1c7f825..2549c6f625 100644 --- a/src/review/outcomes-wire.ts +++ b/src/review/outcomes-wire.ts @@ -326,7 +326,7 @@ export async function recordPrOutcome( const decision = merged ? "merged" : "closed"; // Observability (#reviews-dashboard): realized human outcome (merged vs closed) for the Grafana panel + as the // ground truth to compare against the engine's gate verdicts. - incr("gittensory_pr_outcomes_total", { outcome: decision }); + incr("loopover_pr_outcomes_total", { outcome: decision }); const targetId = reviewAuditTargetId(repoFullName, pr.number); await appendReviewAudit(env, { diff --git a/src/review/pr-reconciliation.ts b/src/review/pr-reconciliation.ts index d5a9c6b7e1..4e20f4d5a1 100644 --- a/src/review/pr-reconciliation.ts +++ b/src/review/pr-reconciliation.ts @@ -106,7 +106,7 @@ export async function runOpenPrReconciliation(env: Env): Promise undefined); return hit.signals; } - incr("gittensory_review_memory_cache_miss_total"); + incr("loopover_review_memory_cache_miss_total"); await recordAuditEvent(env, { eventType: "github_app.review_memory_cache_miss", targetKey: repoFullName, diff --git a/src/selfhost/ai.ts b/src/selfhost/ai.ts index 8aa076351b..fbf238ba95 100644 --- a/src/selfhost/ai.ts +++ b/src/selfhost/ai.ts @@ -703,11 +703,11 @@ function cliUsageFromStdout(provider: string, model: string, effort: string, std function recordCliUsageMetrics(provider: string, model: string, effort: string, stdout: string): AiUsage & { model: string } { const usage = cliUsageFromStdout(provider, model, effort, stdout); const labels = { provider, model: usage.model, effort }; - incr("gittensory_ai_requests_total", labels); - incr("gittensory_ai_cost_usd_total", { provider: labels.provider }, usage.costUsd ?? 0); - if (usage.inputTokens !== undefined) incr("gittensory_ai_input_tokens_total", { ...labels, kind: "review" }, usage.inputTokens); - if (usage.outputTokens !== undefined) incr("gittensory_ai_output_tokens_total", { ...labels, kind: "review" }, usage.outputTokens); - if (usage.totalTokens !== undefined) incr("gittensory_ai_total_tokens_total", labels, usage.totalTokens); + incr("loopover_ai_requests_total", labels); + incr("loopover_ai_cost_usd_total", { provider: labels.provider }, usage.costUsd ?? 0); + if (usage.inputTokens !== undefined) incr("loopover_ai_input_tokens_total", { ...labels, kind: "review" }, usage.inputTokens); + if (usage.outputTokens !== undefined) incr("loopover_ai_output_tokens_total", { ...labels, kind: "review" }, usage.outputTokens); + if (usage.totalTokens !== undefined) incr("loopover_ai_total_tokens_total", labels, usage.totalTokens); return usage; } @@ -1247,7 +1247,7 @@ async function runProviderWithOtel( ): Promise { const circuit = aiProviderCircuits.get(provider.name); if (circuit && circuit.cooldownUntil > Date.now()) { - incr("gittensory_ai_provider_circuit_open_total", { provider: provider.name }); + incr("loopover_ai_provider_circuit_open_total", { provider: provider.name }); throw new Error( `circuit_open: provider "${provider.name}" is in cooldown after ${AI_PROVIDER_FAILURE_THRESHOLD} consecutive failures — skipping this attempt`, ); @@ -1260,7 +1260,7 @@ async function runProviderWithOtel( { "ai.provider": provider.name, "ai.model": model || "default", "ai.request_kind": requestKindLabel }, () => provider.ai.run(model, options), ); - observe("gittensory_ai_provider_request_duration_seconds", (Date.now() - startedAtMs) / 1000, { + observe("loopover_ai_provider_request_duration_seconds", (Date.now() - startedAtMs) / 1000, { provider: provider.name, request_kind: requestKindLabel, }); @@ -1277,13 +1277,13 @@ async function runProviderWithOtel( } return result; } catch (error) { - observe("gittensory_ai_provider_request_duration_seconds", (Date.now() - startedAtMs) / 1000, { + observe("loopover_ai_provider_request_duration_seconds", (Date.now() - startedAtMs) / 1000, { provider: provider.name, request_kind: requestKindLabel, }); if (isExpectedEmbeddingRoutingError(options, error)) throw error; - incr("gittensory_ai_provider_failures_total", { provider: provider.name }); - incr("gittensory_ai_provider_request_errors_total", { provider: provider.name, request_kind: requestKindLabel }); + incr("loopover_ai_provider_failures_total", { provider: provider.name }); + incr("loopover_ai_provider_request_errors_total", { provider: provider.name, request_kind: requestKindLabel }); // Re-read the map here rather than reusing the `circuit` captured above: that read happened BEFORE the // `await` on the real provider call, so under concurrent same-provider calls it can be stale by the time // this catch runs, and computing `failures` from it would clobber a sibling call's write (lost-update race) diff --git a/src/selfhost/d1-size-probe.ts b/src/selfhost/d1-size-probe.ts index bed112b81a..68e9e06e5b 100644 --- a/src/selfhost/d1-size-probe.ts +++ b/src/selfhost/d1-size-probe.ts @@ -180,7 +180,7 @@ function redactD1ProbeSecret(message: string, apiToken: string): string { } function logD1ProbeError(config: D1SizeProbeConfig, part: "database_info" | "table_row_count", error: unknown, table?: string): void { - incr("gittensory_d1_probe_errors_total", { part }); + incr("loopover_d1_probe_errors_total", { part }); console.error( JSON.stringify({ level: "error", @@ -197,7 +197,7 @@ function logD1ProbeError(config: D1SizeProbeConfig, part: "database_info" | "tab * {@link resolveD1SizeProbeConfig} returns null (probe disabled/unconfigured). * * Size and each monitored table's row count are fetched independently and a failure in one never blanks the - * other: a failed fetch keeps its PREVIOUS reading (recorded via `gittensory_d1_probe_errors_total`) instead + * other: a failed fetch keeps its PREVIOUS reading (recorded via `loopover_d1_probe_errors_total`) instead * of resetting to -1 or dropping out of the row-count vector, so a transient Cloudflare API hiccup reads on * the dashboard as "stale" rather than a false "suddenly zero" or a gap. */ @@ -229,7 +229,7 @@ export async function runD1SizeProbe(env: D1SizeProbeEnv, fetchImpl: typeof fetc }; } -/** -1 sentinel (matching gittensory_host_load_avg1_per_core's convention): distinguishes "probe disabled or +/** -1 sentinel (matching loopover_host_load_avg1_per_core's convention): distinguishes "probe disabled or * has never completed a successful sample" from a genuine 0-byte reading. */ export function d1DatabaseSizeBytesSample(): number { return lastSample?.fileSizeBytes ?? -1; diff --git a/src/selfhost/dlq-recent.ts b/src/selfhost/dlq-recent.ts index b6ad954239..f4e34a2f78 100644 --- a/src/selfhost/dlq-recent.ts +++ b/src/selfhost/dlq-recent.ts @@ -1,8 +1,8 @@ import { countRecentDeadLetters } from "../db/repositories"; // Trailing window for the "is the DLQ dead-lettering right now?" gauge (#2083). Operators alert on the RATE of -// recent DLQ-consumer drops, which the cumulative `gittensory_dlq_dead_lettered_total` counter and the point-in-time -// `gittensory_queue_dead` depth gauge can't express on their own. +// recent DLQ-consumer drops, which the cumulative `loopover_dlq_dead_lettered_total` counter and the point-in-time +// `loopover_queue_dead` depth gauge can't express on their own. export const DLQ_RECENT_WINDOW_MS = 15 * 60 * 1000; // 15 minutes /** ISO-8601 timestamp `windowMs` before `now` (default: current time). Pure given `now`; the injectable clock keeps diff --git a/src/selfhost/maintenance-admission.ts b/src/selfhost/maintenance-admission.ts index a151730cea..d22ea8f1d8 100644 --- a/src/selfhost/maintenance-admission.ts +++ b/src/selfhost/maintenance-admission.ts @@ -77,7 +77,7 @@ export interface MaintenancePressureSignals { /** Foreground-priority rows in pending/processing regardless of run_after -- includes work deliberately * scheduled for later (e.g. agent-regate-pr's staggered/rate-deferred per-PR backlog, index.ts:24-29's * "normal, expected, can legitimately stay nonzero for long periods"). Retained ONLY for the - * gittensory_queue_live_pending observability gauge (server.ts) -- evaluateMaintenanceAdmission deliberately + * loopover_queue_live_pending observability gauge (server.ts) -- evaluateMaintenanceAdmission deliberately * does NOT gate on this (#selfhost-maintenance-admission-runnable-signal): a raw count would starve * maintenance on backlog that was never actually competing for a claim slot. Use liveRunnableNowCount for * any real pressure decision. */ diff --git a/src/selfhost/metrics.ts b/src/selfhost/metrics.ts index b5d48dd17d..9561838c1a 100644 --- a/src/selfhost/metrics.ts +++ b/src/selfhost/metrics.ts @@ -34,140 +34,140 @@ const gauges = new Map(); const gaugeVectors = new Map(); const histograms = new Map(); export const DEFAULT_METRIC_META: readonly (readonly [string, MetricMeta])[] = [ - ["gittensory_queue_pending", { help: "Current in-process queue depth.", type: "gauge" }], - ["gittensory_queue_dead", { help: "Current in-process dead queue depth.", type: "gauge" }], - ["gittensory_dlq_dead_lettered_recent", { help: "DLQ messages dead-lettered within the recent trailing window, sampled at scrape.", type: "gauge" }], - ["gittensory_queue_processing", { help: "Jobs currently claimed and mid-flight.", type: "gauge" }], - ["gittensory_queue_runnable_now", { help: "Pending jobs, any priority, currently due (run_after<=now).", type: "gauge" }], - ["gittensory_queue_live_pending", { help: "Current live-work queue depth.", type: "gauge" }], - ["gittensory_queue_live_runnable_now", { help: "Live (foreground) pending jobs currently due (run_after<=now).", type: "gauge" }], - ["gittensory_queue_maintenance_pending", { help: "Current maintenance-work queue depth.", type: "gauge" }], - ["gittensory_queue_oldest_live_pending_age_seconds", { help: "Age in seconds of the oldest live pending job.", type: "gauge" }], - ["gittensory_queue_oldest_live_runnable_age_seconds", { help: "Age in seconds of the oldest live pending job that is currently due.", type: "gauge" }], - ["gittensory_queue_oldest_maintenance_pending_age_seconds", { help: "Age in seconds of the oldest maintenance pending job.", type: "gauge" }], - ["gittensory_queue_backlog_convergence_pending", { help: "Pending+processing agent-regate-pr jobs tagged foreground_lane=backlog.", type: "gauge" }], - ["gittensory_queue_fresh_intake_pending", { help: "Pending+processing github-webhook jobs tagged foreground_lane=fresh.", type: "gauge" }], - ["gittensory_queue_backlog_by_repo", { help: "Top-N repos by backlog-convergence pending depth, this scrape.", type: "gauge" }], - ["gittensory_jobs_claimed_by_lane_total", { help: "Foreground jobs claimed via the backlog-vs-fresh-intake fairness lane.", type: "counter" }], - ["gittensory_github_rest_rate_limit_remaining", { help: "Newest observed GitHub REST rate-limit remaining count, by key scope.", type: "gauge" }], - ["gittensory_host_load_avg1_per_core", { help: "One-minute host load average normalized by CPU core count.", type: "gauge" }], - ["gittensory_clock_skew_seconds", { help: "Clock skew in seconds between this process and GitHub's server time (positive = ahead), sampled from GitHub App JWT-mint response Date headers.", type: "gauge" }], - ["gittensory_uptime_seconds", { help: "Self-host process uptime in seconds.", type: "gauge" }], - ["gittensory_backup_acknowledged", { help: "1 when SQLite backup is acknowledged or Postgres is in use; 0 when the boot backup advisory would fire.", type: "gauge" }], - ["gittensory_http_requests_total", { help: "HTTP app requests by response status class.", type: "counter" }], - ["gittensory_http_request_duration_seconds", { help: "HTTP app request duration in seconds.", type: "histogram" }], - ["gittensory_webhook_dedup_total", { help: "Webhook deliveries deduplicated before enqueue.", type: "counter" }], - ["gittensory_webhook_enqueue_total", { help: "Webhook enqueue outcomes by event and action.", type: "counter" }], - ["gittensory_jobs_enqueued_total", { help: "Durable queue jobs enqueued.", type: "counter" }], - ["gittensory_jobs_processed_total", { help: "Durable queue jobs processed successfully.", type: "counter" }], - ["gittensory_jobs_failed_total", { help: "Durable queue job processing failures.", type: "counter" }], - ["gittensory_jobs_dead_total", { help: "Durable queue jobs moved to dead status.", type: "counter" }], - ["gittensory_jobs_rate_limited_total", { help: "Durable queue jobs rate-limited before processing.", type: "counter" }], - ["gittensory_jobs_rate_limit_deferred_total", { help: "Durable queue jobs deferred by a rate-limit window.", type: "counter" }], - ["gittensory_jobs_coalesced_total", { help: "Durable queue jobs coalesced with an existing queued item.", type: "counter" }], - ["gittensory_jobs_recovered_total", { help: "Durable queue jobs recovered from stale in-flight state.", type: "counter" }], - ["gittensory_jobs_maintenance_admission_deferred_total", { help: "Maintenance jobs deferred by admission control.", type: "counter" }], - ["gittensory_jobs_enqueued_persisted_total", { help: "Persisted durable queue jobs enqueued.", type: "counter" }], - ["gittensory_jobs_processed_persisted_total", { help: "Persisted durable queue jobs processed successfully.", type: "counter" }], - ["gittensory_jobs_failed_persisted_total", { help: "Persisted durable queue job processing failures.", type: "counter" }], - ["gittensory_jobs_dead_persisted_total", { help: "Persisted durable queue jobs moved to dead status.", type: "counter" }], - ["gittensory_jobs_rate_limited_persisted_total", { help: "Persisted durable queue jobs rate-limited before processing.", type: "counter" }], - ["gittensory_jobs_rate_limit_deferred_persisted_total", { help: "Persisted durable queue jobs deferred by a rate-limit window.", type: "counter" }], - ["gittensory_jobs_coalesced_persisted_total", { help: "Persisted durable queue jobs coalesced with an existing queued item.", type: "counter" }], - ["gittensory_jobs_recovered_persisted_total", { help: "Persisted durable queue jobs recovered from stale in-flight state.", type: "counter" }], - ["gittensory_jobs_maintenance_admission_deferred_persisted_total", { help: "Persisted maintenance jobs deferred by admission control.", type: "counter" }], - ["gittensory_jobs_rate_limit_admission_deferred_total", { help: "Jobs deferred by rate-limit admission checks.", type: "counter" }], - ["gittensory_jobs_rate_limit_budget_deferred_total", { help: "Jobs deferred by rate-limit budget checks.", type: "counter" }], - ["gittensory_jobs_rate_limited_by_type_total", { help: "Jobs rate-limited by job type.", type: "counter" }], - ["gittensory_jobs_maintenance_admission_deferred_by_reason_total", { help: "Maintenance jobs deferred by reason.", type: "counter" }], - ["gittensory_jobs_installation_concurrency_deferred_total", { help: "Background jobs deferred by per-installation GitHub-fetch concurrency admission.", type: "counter" }], - ["gittensory_jobs_installation_concurrency_deferred_by_reason_total", { help: "Per-installation GitHub-fetch concurrency deferrals by reason and job type.", type: "counter" }], - ["gittensory_jobs_dead_letter_revived_total", { help: "Dead-letter jobs revived for retry.", type: "counter" }], - ["gittensory_jobs_foreground_liveness_released_total", { help: "Foreground-priority jobs force-released from a stale deferral by the liveness sweep.", type: "counter" }], - ["gittensory_jobs_foreground_liveness_released_by_reason_total", { help: "Foreground liveness releases by reason (age vs rate_limit_cleared).", type: "counter" }], - ["gittensory_dlq_dead_lettered_total", { help: "Messages moved to a dead-letter queue.", type: "counter" }], - ["gittensory_dlq_redriven_total", { help: "Dead-letter queue messages redriven into processing.", type: "counter" }], - ["gittensory_github_response_cache_total", { help: "GitHub response cache outcomes by response class.", type: "counter" }], - ["gittensory_github_graphql_cache_total", { help: "GitHub GraphQL cache outcomes by response class.", type: "counter" }], - ["gittensory_github_rest_rate_limit_observations_total", { help: "Observed GitHub REST rate-limit remaining buckets.", type: "counter" }], - ["gittensory_github_rest_rate_limit_responses_total", { help: "Observed GitHub REST rate-limit response statuses.", type: "counter" }], - ["gittensory_redis_gh_response_cache_total", { help: "Redis-backed GitHub response cache outcomes.", type: "counter" }], - ["gittensory_redis_gh_response_cache_hit_ratio", { help: "Redis GitHub response cache hit ratio (hits / (hits + misses)) at scrape time.", type: "gauge" }], - ["gittensory_redis_token_cache_total", { help: "Redis-backed GitHub token cache outcomes.", type: "counter" }], - ["gittensory_qdrant_queries_total", { help: "Qdrant vector query attempts.", type: "counter" }], - ["gittensory_qdrant_upserts_total", { help: "Qdrant vector upserted item count.", type: "counter" }], - ["gittensory_qdrant_errors_total", { help: "Qdrant vector operation errors.", type: "counter" }], - ["gittensory_rag_pipeline_errors_total", { help: "RAG index-population pipeline errors (repo/path indexing), by op.", type: "counter" }], - ["gittensory_orb_events_exported_total", { help: "Orb events exported from the self-host runtime.", type: "counter" }], - ["gittensory_orb_export_errors_total", { help: "Orb event export errors.", type: "counter" }], - ["gittensory_orb_relay_drains_total", { help: "Orb relay drain outcomes.", type: "counter" }], - ["gittensory_orb_relay_register_consecutive_failures", { help: "Current consecutive orb relay registration failure streak, reset to 0 on any success.", type: "gauge" }], - ["gittensory_orb_relay_drain_seconds_since_last", { help: "Seconds since the pull-mode orb relay drain loop last completed successfully, or -1 if never (or in push mode).", type: "gauge" }], - ["gittensory_orb_webhook_total", { help: "Orb webhook outcomes.", type: "counter" }], - ["gittensory_ai_requests_total", { help: "AI provider request outcomes.", type: "counter" }], - ["gittensory_ai_cost_usd_total", { help: "Estimated AI provider cost in USD.", type: "counter" }], - ["gittensory_ai_input_tokens_total", { help: "AI provider input tokens consumed.", type: "counter" }], - ["gittensory_ai_output_tokens_total", { help: "AI provider output tokens produced.", type: "counter" }], - ["gittensory_ai_total_tokens_total", { help: "AI provider total tokens observed.", type: "counter" }], - ["gittensory_ai_provider_circuit_open_total", { help: "AI provider circuit-open events.", type: "counter" }], - ["gittensory_ai_provider_failures_total", { help: "AI provider failures by provider.", type: "counter" }], - ["gittensory_ai_provider_request_duration_seconds", { help: "AI provider request duration in seconds, by provider and request kind.", type: "histogram" }], - ["gittensory_ai_provider_request_errors_total", { help: "AI provider request errors, by provider and request kind (excludes expected embedding-routing fallbacks).", type: "counter" }], - ["gittensory_ai_review_cache_hit_total", { help: "AI review cache hits.", type: "counter" }], - ["gittensory_ai_review_cache_miss_total", { help: "AI review cache misses.", type: "counter" }], - ["gittensory_ai_review_cache_write_error_total", { help: "AI review cache write errors.", type: "counter" }], - ["gittensory_ai_slop_cache_hit_total", { help: "AI slop advisory cache hits.", type: "counter" }], - ["gittensory_ai_slop_cache_miss_total", { help: "AI slop advisory cache misses.", type: "counter" }], - ["gittensory_ai_slop_cache_write_error_total", { help: "AI slop advisory cache write errors.", type: "counter" }], - ["gittensory_ai_review_non_cacheable_total", { help: "AI reviews skipped by cacheability rules.", type: "counter" }], - ["gittensory_ai_review_force_bypass_total", { help: "AI review cache force-bypass events.", type: "counter" }], - ["gittensory_ai_review_inconclusive_total", { help: "AI review inconclusive outcomes.", type: "counter" }], - ["gittensory_ai_review_onmerge_clamped_total", { help: "AI review on-merge mode clamp events.", type: "counter" }], - ["gittensory_ai_review_model_fallback_total", { help: "AI review model fallback attempts by primary and fallback model.", type: "counter" }], - ["gittensory_regate_ai_skipped_current_total", { help: "Regate requests skipped because AI state is current.", type: "counter" }], - ["gittensory_public_surface_publish_skipped_current_total", { help: "Public surface publishes skipped because state is current.", type: "counter" }], - ["gittensory_gate_decisions_total", { help: "Gate decisions by conclusion.", type: "counter" }], - ["gittensory_precision_breaker_downgrades_total", { help: "Would-merge/would-close actions downgraded to a human hold by an accuracy circuit-breaker, by breaker direction.", type: "counter" }], - ["gittensory_agent_disposition_total", { help: "Final agent disposition per PR pass (merge/close/hold), by repo, action class, blocker-code class, and autonomy level.", type: "counter" }], - ["gittensory_merge_train_deferred_total", { help: "Merge-train FIFO gate deferrals (an older still-viable sibling held a merge), by repo and mode (audit/enforce).", type: "counter" }], - ["gittensory_reviews_published_total", { help: "Published review comments.", type: "counter" }], - ["gittensory_github_branch_protection_permission_denied_total", { help: "GitHub branch-protection reads denied by permissions.", type: "counter" }], - ["gittensory_github_pr_files_fetch_total", { help: "GitHub pull-request file fetch attempts.", type: "counter" }], - ["gittensory_pr_state_cache_total", { help: "Pull-request state cache outcomes.", type: "counter" }], - ["gittensory_ci_state_cache_total", { help: "CI-state snapshot cache outcomes.", type: "counter" }], - ["gittensory_ops_anomaly_total", { help: "Ops anomaly scan detections (review burst / review failure burst), by repo and kind.", type: "counter" }], - ["gittensory_d1_database_size_bytes", { help: "Cloudflare D1 database file size in bytes, from the opt-in Management API size/row-count probe (#3810); -1 when the probe is disabled or has never completed a successful sample.", type: "gauge" }], - ["gittensory_d1_table_row_count", { help: "Row count for a monitored D1 table, from the same probe as gittensory_d1_database_size_bytes, labeled by table.", type: "gauge" }], - ["gittensory_signal_snapshots_rows_per_key", { help: "signal_snapshots row count divided by its distinct (signal_type, target_key) count, scoped to the latest-only-dedup signal types dedupeSignalSnapshots converges to ~1 row per key; -1 when the probe is disabled or has never completed a successful sample.", type: "gauge" }], - ["gittensory_d1_probe_errors_total", { help: "D1 size/row-count Management API probe failures, by part (database_info/table_row_count).", type: "counter" }], - ["gittensory_agent_action_permission_denied_total", { help: "Agent actions denied for missing a required GitHub App write permission, by action class.", type: "counter" }], - ["gittensory_agent_action_permission_denied_suppressed_total", { help: "Repeat permission denials suppressed within the cooldown window (still counted here, but not re-audited), by action class.", type: "counter" }], - ["gittensory_ai_review_frozen_reuse_total", { help: "AI review passes that reused a frozen (maintainer-gated) prior verdict instead of re-running.", type: "counter" }], - ["gittensory_ai_review_one_shot_reuse_total", { help: "AI review passes that reused a one-shot prior verdict instead of re-running.", type: "counter" }], - ["gittensory_ai_review_paused_reuse_total", { help: "AI review passes that reused a prior verdict because the repo is paused.", type: "counter" }], - ["gittensory_ai_review_tiebreak_order_unstable_total", { help: "Dual-reviewer tiebreak passes where reviewer order was not stable, by combine mode.", type: "counter" }], - ["gittensory_grounding_cache_hit_total", { help: "Review grounding-context cache hits.", type: "counter" }], - ["gittensory_grounding_cache_miss_total", { help: "Review grounding-context cache misses.", type: "counter" }], - ["gittensory_impact_map_cache_hit_total", { help: "Impact-map cache hits.", type: "counter" }], - ["gittensory_impact_map_cache_miss_total", { help: "Impact-map cache misses.", type: "counter" }], - ["gittensory_installation_health_broker_probe_total", { help: "Installation-health broker probes, by result (ok/failed/mismatched_installation).", type: "counter" }], - ["gittensory_jobs_maintenance_admission_granted_under_pressure_total", { help: "Maintenance jobs admitted despite backpressure via the trickle-admission allowance.", type: "counter" }], - ["gittensory_jobs_maintenance_trickle_admitted_by_type_total", { help: "Maintenance jobs admitted via trickle admission, by job type.", type: "counter" }], - ["gittensory_linked_issue_satisfaction_cache_hit_total", { help: "Linked-issue satisfaction assessment cache hits.", type: "counter" }], - ["gittensory_linked_issue_satisfaction_cache_miss_total", { help: "Linked-issue satisfaction assessment cache misses.", type: "counter" }], - ["gittensory_linked_issue_satisfaction_cache_write_error_total", { help: "Linked-issue satisfaction assessment cache write errors.", type: "counter" }], - ["gittensory_open_pr_reconciliation_missing_total", { help: "Open PRs found missing from local tracking during reconciliation, by repo.", type: "counter" }], - ["gittensory_orb_relay_malformed_events_total", { help: "Orb relay batch entries dropped for missing/mistyped required fields (deliveryId/eventName/rawBody).", type: "counter" }], - ["gittensory_orb_relay_register_total", { help: "Orb relay registration attempts, by mode and result (registered/recovered/failed).", type: "counter" }], - ["gittensory_pr_outcomes_total", { help: "Recorded PR gate outcomes, by decision.", type: "counter" }], - ["gittensory_public_origin_acknowledged", { help: "1 when the configured public origin is acknowledged as reachable; 0 otherwise.", type: "gauge" }], - ["gittensory_repo_culture_profile_cache_hit_total", { help: "Repo-culture-profile cache hits.", type: "counter" }], - ["gittensory_repo_culture_profile_cache_miss_total", { help: "Repo-culture-profile cache misses.", type: "counter" }], - ["gittensory_review_memory_cache_hit_total", { help: "Review-memory cache hits.", type: "counter" }], - ["gittensory_review_memory_cache_miss_total", { help: "Review-memory cache misses.", type: "counter" }], - ["gittensory_review_memory_suppressed_total", { help: "Review-memory entries suppressed from surfacing, by repo.", type: "counter" }], - ["gittensory_rees_enrich_requests_total", { help: "REES /v1/enrich call outcomes, by status (ok/empty/http_error/timeout/exception/skipped_auth_rejected).", type: "counter" }], - ["gittensory_rees_enrich_request_duration_seconds", { help: "REES /v1/enrich call duration in seconds, for calls that were actually attempted (excludes the auth-rejected circuit-breaker skip).", type: "histogram" }], + ["loopover_queue_pending", { help: "Current in-process queue depth.", type: "gauge" }], + ["loopover_queue_dead", { help: "Current in-process dead queue depth.", type: "gauge" }], + ["loopover_dlq_dead_lettered_recent", { help: "DLQ messages dead-lettered within the recent trailing window, sampled at scrape.", type: "gauge" }], + ["loopover_queue_processing", { help: "Jobs currently claimed and mid-flight.", type: "gauge" }], + ["loopover_queue_runnable_now", { help: "Pending jobs, any priority, currently due (run_after<=now).", type: "gauge" }], + ["loopover_queue_live_pending", { help: "Current live-work queue depth.", type: "gauge" }], + ["loopover_queue_live_runnable_now", { help: "Live (foreground) pending jobs currently due (run_after<=now).", type: "gauge" }], + ["loopover_queue_maintenance_pending", { help: "Current maintenance-work queue depth.", type: "gauge" }], + ["loopover_queue_oldest_live_pending_age_seconds", { help: "Age in seconds of the oldest live pending job.", type: "gauge" }], + ["loopover_queue_oldest_live_runnable_age_seconds", { help: "Age in seconds of the oldest live pending job that is currently due.", type: "gauge" }], + ["loopover_queue_oldest_maintenance_pending_age_seconds", { help: "Age in seconds of the oldest maintenance pending job.", type: "gauge" }], + ["loopover_queue_backlog_convergence_pending", { help: "Pending+processing agent-regate-pr jobs tagged foreground_lane=backlog.", type: "gauge" }], + ["loopover_queue_fresh_intake_pending", { help: "Pending+processing github-webhook jobs tagged foreground_lane=fresh.", type: "gauge" }], + ["loopover_queue_backlog_by_repo", { help: "Top-N repos by backlog-convergence pending depth, this scrape.", type: "gauge" }], + ["loopover_jobs_claimed_by_lane_total", { help: "Foreground jobs claimed via the backlog-vs-fresh-intake fairness lane.", type: "counter" }], + ["loopover_github_rest_rate_limit_remaining", { help: "Newest observed GitHub REST rate-limit remaining count, by key scope.", type: "gauge" }], + ["loopover_host_load_avg1_per_core", { help: "One-minute host load average normalized by CPU core count.", type: "gauge" }], + ["loopover_clock_skew_seconds", { help: "Clock skew in seconds between this process and GitHub's server time (positive = ahead), sampled from GitHub App JWT-mint response Date headers.", type: "gauge" }], + ["loopover_uptime_seconds", { help: "Self-host process uptime in seconds.", type: "gauge" }], + ["loopover_backup_acknowledged", { help: "1 when SQLite backup is acknowledged or Postgres is in use; 0 when the boot backup advisory would fire.", type: "gauge" }], + ["loopover_http_requests_total", { help: "HTTP app requests by response status class.", type: "counter" }], + ["loopover_http_request_duration_seconds", { help: "HTTP app request duration in seconds.", type: "histogram" }], + ["loopover_webhook_dedup_total", { help: "Webhook deliveries deduplicated before enqueue.", type: "counter" }], + ["loopover_webhook_enqueue_total", { help: "Webhook enqueue outcomes by event and action.", type: "counter" }], + ["loopover_jobs_enqueued_total", { help: "Durable queue jobs enqueued.", type: "counter" }], + ["loopover_jobs_processed_total", { help: "Durable queue jobs processed successfully.", type: "counter" }], + ["loopover_jobs_failed_total", { help: "Durable queue job processing failures.", type: "counter" }], + ["loopover_jobs_dead_total", { help: "Durable queue jobs moved to dead status.", type: "counter" }], + ["loopover_jobs_rate_limited_total", { help: "Durable queue jobs rate-limited before processing.", type: "counter" }], + ["loopover_jobs_rate_limit_deferred_total", { help: "Durable queue jobs deferred by a rate-limit window.", type: "counter" }], + ["loopover_jobs_coalesced_total", { help: "Durable queue jobs coalesced with an existing queued item.", type: "counter" }], + ["loopover_jobs_recovered_total", { help: "Durable queue jobs recovered from stale in-flight state.", type: "counter" }], + ["loopover_jobs_maintenance_admission_deferred_total", { help: "Maintenance jobs deferred by admission control.", type: "counter" }], + ["loopover_jobs_enqueued_persisted_total", { help: "Persisted durable queue jobs enqueued.", type: "counter" }], + ["loopover_jobs_processed_persisted_total", { help: "Persisted durable queue jobs processed successfully.", type: "counter" }], + ["loopover_jobs_failed_persisted_total", { help: "Persisted durable queue job processing failures.", type: "counter" }], + ["loopover_jobs_dead_persisted_total", { help: "Persisted durable queue jobs moved to dead status.", type: "counter" }], + ["loopover_jobs_rate_limited_persisted_total", { help: "Persisted durable queue jobs rate-limited before processing.", type: "counter" }], + ["loopover_jobs_rate_limit_deferred_persisted_total", { help: "Persisted durable queue jobs deferred by a rate-limit window.", type: "counter" }], + ["loopover_jobs_coalesced_persisted_total", { help: "Persisted durable queue jobs coalesced with an existing queued item.", type: "counter" }], + ["loopover_jobs_recovered_persisted_total", { help: "Persisted durable queue jobs recovered from stale in-flight state.", type: "counter" }], + ["loopover_jobs_maintenance_admission_deferred_persisted_total", { help: "Persisted maintenance jobs deferred by admission control.", type: "counter" }], + ["loopover_jobs_rate_limit_admission_deferred_total", { help: "Jobs deferred by rate-limit admission checks.", type: "counter" }], + ["loopover_jobs_rate_limit_budget_deferred_total", { help: "Jobs deferred by rate-limit budget checks.", type: "counter" }], + ["loopover_jobs_rate_limited_by_type_total", { help: "Jobs rate-limited by job type.", type: "counter" }], + ["loopover_jobs_maintenance_admission_deferred_by_reason_total", { help: "Maintenance jobs deferred by reason.", type: "counter" }], + ["loopover_jobs_installation_concurrency_deferred_total", { help: "Background jobs deferred by per-installation GitHub-fetch concurrency admission.", type: "counter" }], + ["loopover_jobs_installation_concurrency_deferred_by_reason_total", { help: "Per-installation GitHub-fetch concurrency deferrals by reason and job type.", type: "counter" }], + ["loopover_jobs_dead_letter_revived_total", { help: "Dead-letter jobs revived for retry.", type: "counter" }], + ["loopover_jobs_foreground_liveness_released_total", { help: "Foreground-priority jobs force-released from a stale deferral by the liveness sweep.", type: "counter" }], + ["loopover_jobs_foreground_liveness_released_by_reason_total", { help: "Foreground liveness releases by reason (age vs rate_limit_cleared).", type: "counter" }], + ["loopover_dlq_dead_lettered_total", { help: "Messages moved to a dead-letter queue.", type: "counter" }], + ["loopover_dlq_redriven_total", { help: "Dead-letter queue messages redriven into processing.", type: "counter" }], + ["loopover_github_response_cache_total", { help: "GitHub response cache outcomes by response class.", type: "counter" }], + ["loopover_github_graphql_cache_total", { help: "GitHub GraphQL cache outcomes by response class.", type: "counter" }], + ["loopover_github_rest_rate_limit_observations_total", { help: "Observed GitHub REST rate-limit remaining buckets.", type: "counter" }], + ["loopover_github_rest_rate_limit_responses_total", { help: "Observed GitHub REST rate-limit response statuses.", type: "counter" }], + ["loopover_redis_gh_response_cache_total", { help: "Redis-backed GitHub response cache outcomes.", type: "counter" }], + ["loopover_redis_gh_response_cache_hit_ratio", { help: "Redis GitHub response cache hit ratio (hits / (hits + misses)) at scrape time.", type: "gauge" }], + ["loopover_redis_token_cache_total", { help: "Redis-backed GitHub token cache outcomes.", type: "counter" }], + ["loopover_qdrant_queries_total", { help: "Qdrant vector query attempts.", type: "counter" }], + ["loopover_qdrant_upserts_total", { help: "Qdrant vector upserted item count.", type: "counter" }], + ["loopover_qdrant_errors_total", { help: "Qdrant vector operation errors.", type: "counter" }], + ["loopover_rag_pipeline_errors_total", { help: "RAG index-population pipeline errors (repo/path indexing), by op.", type: "counter" }], + ["loopover_orb_events_exported_total", { help: "Orb events exported from the self-host runtime.", type: "counter" }], + ["loopover_orb_export_errors_total", { help: "Orb event export errors.", type: "counter" }], + ["loopover_orb_relay_drains_total", { help: "Orb relay drain outcomes.", type: "counter" }], + ["loopover_orb_relay_register_consecutive_failures", { help: "Current consecutive orb relay registration failure streak, reset to 0 on any success.", type: "gauge" }], + ["loopover_orb_relay_drain_seconds_since_last", { help: "Seconds since the pull-mode orb relay drain loop last completed successfully, or -1 if never (or in push mode).", type: "gauge" }], + ["loopover_orb_webhook_total", { help: "Orb webhook outcomes.", type: "counter" }], + ["loopover_ai_requests_total", { help: "AI provider request outcomes.", type: "counter" }], + ["loopover_ai_cost_usd_total", { help: "Estimated AI provider cost in USD.", type: "counter" }], + ["loopover_ai_input_tokens_total", { help: "AI provider input tokens consumed.", type: "counter" }], + ["loopover_ai_output_tokens_total", { help: "AI provider output tokens produced.", type: "counter" }], + ["loopover_ai_total_tokens_total", { help: "AI provider total tokens observed.", type: "counter" }], + ["loopover_ai_provider_circuit_open_total", { help: "AI provider circuit-open events.", type: "counter" }], + ["loopover_ai_provider_failures_total", { help: "AI provider failures by provider.", type: "counter" }], + ["loopover_ai_provider_request_duration_seconds", { help: "AI provider request duration in seconds, by provider and request kind.", type: "histogram" }], + ["loopover_ai_provider_request_errors_total", { help: "AI provider request errors, by provider and request kind (excludes expected embedding-routing fallbacks).", type: "counter" }], + ["loopover_ai_review_cache_hit_total", { help: "AI review cache hits.", type: "counter" }], + ["loopover_ai_review_cache_miss_total", { help: "AI review cache misses.", type: "counter" }], + ["loopover_ai_review_cache_write_error_total", { help: "AI review cache write errors.", type: "counter" }], + ["loopover_ai_slop_cache_hit_total", { help: "AI slop advisory cache hits.", type: "counter" }], + ["loopover_ai_slop_cache_miss_total", { help: "AI slop advisory cache misses.", type: "counter" }], + ["loopover_ai_slop_cache_write_error_total", { help: "AI slop advisory cache write errors.", type: "counter" }], + ["loopover_ai_review_non_cacheable_total", { help: "AI reviews skipped by cacheability rules.", type: "counter" }], + ["loopover_ai_review_force_bypass_total", { help: "AI review cache force-bypass events.", type: "counter" }], + ["loopover_ai_review_inconclusive_total", { help: "AI review inconclusive outcomes.", type: "counter" }], + ["loopover_ai_review_onmerge_clamped_total", { help: "AI review on-merge mode clamp events.", type: "counter" }], + ["loopover_ai_review_model_fallback_total", { help: "AI review model fallback attempts by primary and fallback model.", type: "counter" }], + ["loopover_regate_ai_skipped_current_total", { help: "Regate requests skipped because AI state is current.", type: "counter" }], + ["loopover_public_surface_publish_skipped_current_total", { help: "Public surface publishes skipped because state is current.", type: "counter" }], + ["loopover_gate_decisions_total", { help: "Gate decisions by conclusion.", type: "counter" }], + ["loopover_precision_breaker_downgrades_total", { help: "Would-merge/would-close actions downgraded to a human hold by an accuracy circuit-breaker, by breaker direction.", type: "counter" }], + ["loopover_agent_disposition_total", { help: "Final agent disposition per PR pass (merge/close/hold), by repo, action class, blocker-code class, and autonomy level.", type: "counter" }], + ["loopover_merge_train_deferred_total", { help: "Merge-train FIFO gate deferrals (an older still-viable sibling held a merge), by repo and mode (audit/enforce).", type: "counter" }], + ["loopover_reviews_published_total", { help: "Published review comments.", type: "counter" }], + ["loopover_github_branch_protection_permission_denied_total", { help: "GitHub branch-protection reads denied by permissions.", type: "counter" }], + ["loopover_github_pull_request_files_fetch_total", { help: "GitHub pull-request file fetch attempts.", type: "counter" }], + ["loopover_pr_state_cache_total", { help: "Pull-request state cache outcomes.", type: "counter" }], + ["loopover_ci_state_cache_total", { help: "CI-state snapshot cache outcomes.", type: "counter" }], + ["loopover_ops_anomaly_total", { help: "Ops anomaly scan detections (review burst / review failure burst), by repo and kind.", type: "counter" }], + ["loopover_d1_database_size_bytes", { help: "Cloudflare D1 database file size in bytes, from the opt-in Management API size/row-count probe (#3810); -1 when the probe is disabled or has never completed a successful sample.", type: "gauge" }], + ["loopover_d1_table_row_count", { help: "Row count for a monitored D1 table, from the same probe as loopover_d1_database_size_bytes, labeled by table.", type: "gauge" }], + ["loopover_signal_snapshots_rows_per_key", { help: "signal_snapshots row count divided by its distinct (signal_type, target_key) count, scoped to the latest-only-dedup signal types dedupeSignalSnapshots converges to ~1 row per key; -1 when the probe is disabled or has never completed a successful sample.", type: "gauge" }], + ["loopover_d1_probe_errors_total", { help: "D1 size/row-count Management API probe failures, by part (database_info/table_row_count).", type: "counter" }], + ["loopover_agent_action_permission_denied_total", { help: "Agent actions denied for missing a required GitHub App write permission, by action class.", type: "counter" }], + ["loopover_agent_action_permission_denied_suppressed_total", { help: "Repeat permission denials suppressed within the cooldown window (still counted here, but not re-audited), by action class.", type: "counter" }], + ["loopover_ai_review_frozen_reuse_total", { help: "AI review passes that reused a frozen (maintainer-gated) prior verdict instead of re-running.", type: "counter" }], + ["loopover_ai_review_one_shot_reuse_total", { help: "AI review passes that reused a one-shot prior verdict instead of re-running.", type: "counter" }], + ["loopover_ai_review_paused_reuse_total", { help: "AI review passes that reused a prior verdict because the repo is paused.", type: "counter" }], + ["loopover_ai_review_tiebreak_order_unstable_total", { help: "Dual-reviewer tiebreak passes where reviewer order was not stable, by combine mode.", type: "counter" }], + ["loopover_grounding_cache_hit_total", { help: "Review grounding-context cache hits.", type: "counter" }], + ["loopover_grounding_cache_miss_total", { help: "Review grounding-context cache misses.", type: "counter" }], + ["loopover_impact_map_cache_hit_total", { help: "Impact-map cache hits.", type: "counter" }], + ["loopover_impact_map_cache_miss_total", { help: "Impact-map cache misses.", type: "counter" }], + ["loopover_installation_health_broker_probe_total", { help: "Installation-health broker probes, by result (ok/failed/mismatched_installation).", type: "counter" }], + ["loopover_jobs_maintenance_admission_granted_under_pressure_total", { help: "Maintenance jobs admitted despite backpressure via the trickle-admission allowance.", type: "counter" }], + ["loopover_jobs_maintenance_trickle_admitted_by_type_total", { help: "Maintenance jobs admitted via trickle admission, by job type.", type: "counter" }], + ["loopover_linked_issue_satisfaction_cache_hit_total", { help: "Linked-issue satisfaction assessment cache hits.", type: "counter" }], + ["loopover_linked_issue_satisfaction_cache_miss_total", { help: "Linked-issue satisfaction assessment cache misses.", type: "counter" }], + ["loopover_linked_issue_satisfaction_cache_write_error_total", { help: "Linked-issue satisfaction assessment cache write errors.", type: "counter" }], + ["loopover_open_pr_reconciliation_missing_total", { help: "Open PRs found missing from local tracking during reconciliation, by repo.", type: "counter" }], + ["loopover_orb_relay_malformed_events_total", { help: "Orb relay batch entries dropped for missing/mistyped required fields (deliveryId/eventName/rawBody).", type: "counter" }], + ["loopover_orb_relay_register_total", { help: "Orb relay registration attempts, by mode and result (registered/recovered/failed).", type: "counter" }], + ["loopover_pr_outcomes_total", { help: "Recorded PR gate outcomes, by decision.", type: "counter" }], + ["loopover_public_origin_acknowledged", { help: "1 when the configured public origin is acknowledged as reachable; 0 otherwise.", type: "gauge" }], + ["loopover_repo_culture_profile_cache_hit_total", { help: "Repo-culture-profile cache hits.", type: "counter" }], + ["loopover_repo_culture_profile_cache_miss_total", { help: "Repo-culture-profile cache misses.", type: "counter" }], + ["loopover_review_memory_cache_hit_total", { help: "Review-memory cache hits.", type: "counter" }], + ["loopover_review_memory_cache_miss_total", { help: "Review-memory cache misses.", type: "counter" }], + ["loopover_review_memory_suppressed_total", { help: "Review-memory entries suppressed from surfacing, by repo.", type: "counter" }], + ["loopover_rees_enrich_requests_total", { help: "REES /v1/enrich call outcomes, by status (ok/empty/http_error/timeout/exception/skipped_auth_rejected).", type: "counter" }], + ["loopover_rees_enrich_request_duration_seconds", { help: "REES /v1/enrich call duration in seconds, for calls that were actually attempted (excludes the auth-rejected circuit-breaker skip).", type: "histogram" }], ]; const metricMeta = new Map(DEFAULT_METRIC_META); @@ -184,14 +184,14 @@ export function setSelfHostedMetricsMode(isSelfHosted: boolean): void { } const PRIVATE_REPO_LABEL_METRICS = new Set([ - "gittensory_gate_decisions_total", - "gittensory_reviews_published_total", - "gittensory_ops_anomaly_total", + "loopover_gate_decisions_total", + "loopover_reviews_published_total", + "loopover_ops_anomaly_total", ]); const ALWAYS_REDACT_REPO_LABEL_METRICS = new Set([ - "gittensory_agent_disposition_total", - "gittensory_queue_backlog_by_repo", - "gittensory_merge_train_deferred_total", + "loopover_agent_disposition_total", + "loopover_queue_backlog_by_repo", + "loopover_merge_train_deferred_total", ]); const redactedRepoLabels = new Map(); diff --git a/src/selfhost/monitored-work.ts b/src/selfhost/monitored-work.ts index a26a87b540..9dae9174a1 100644 --- a/src/selfhost/monitored-work.ts +++ b/src/selfhost/monitored-work.ts @@ -82,7 +82,7 @@ export async function drainOrbRelayWithMonitor(args: { // BEFORE the per-event enqueue loop so a downstream enqueue failure still counts as drain progress // (the relay connection, not the local queue, is what registration-alerting cares about). args.state.lastDrainAtMs = args.nowMs ?? Date.now(); - incr("gittensory_orb_relay_drains_total", { + incr("loopover_orb_relay_drains_total", { result: events.length > 0 ? "events" : "empty", }); for (const ev of events) { @@ -100,7 +100,7 @@ export async function drainOrbRelayWithMonitor(args: { ev.rawBody, ); } catch (error) { - incr("gittensory_orb_webhook_total", { + incr("loopover_orb_webhook_total", { event: orbRelayMetricEvent(ev.eventName), result: "enqueue_failed", }); @@ -114,7 +114,7 @@ export async function drainOrbRelayWithMonitor(args: { ); continue; } - incr("gittensory_orb_webhook_total", { + incr("loopover_orb_webhook_total", { event: orbRelayMetricEvent(ev.eventName), result, }); @@ -185,18 +185,18 @@ export async function registerOrbRelayWithMonitor(args: { const mode = args.env.ORB_RELAY_MODE === "pull" ? "pull" : "push"; const log = args.log ?? console.log; if (result.status === "registered") { - incr("gittensory_orb_relay_register_total", { mode, result: "registered" }); + incr("loopover_orb_relay_register_total", { mode, result: "registered" }); // attempts === 1 means this succeeded on the very first try (parity with the original boot-only log); // a higher count means it recovered after one or more prior failures -- a distinct, more alertable event. if (args.state.attempts > 1) { - incr("gittensory_orb_relay_register_total", { mode, result: "recovered" }); + incr("loopover_orb_relay_register_total", { mode, result: "recovered" }); log(JSON.stringify({ event: "selfhost_orb_relay_register_recovered", mode, attempts: args.state.attempts })); } else { log(JSON.stringify({ event: "selfhost_orb_relay_register", mode, attempts: args.state.attempts })); } return; } - incr("gittensory_orb_relay_register_total", { mode, result: "failed" }); + incr("loopover_orb_relay_register_total", { mode, result: "failed" }); // A failed registration is fatal for PUSH mode (the Orb can't reach our public relay URL → the container // looks alive but reviews NOTHING → error). In PULL mode the outbound drain loop delivers events once a // later attempt succeeds, so a failed announce is only degraded telemetry -- UNLESS the streak/no-progress diff --git a/src/selfhost/orb-collector.ts b/src/selfhost/orb-collector.ts index 05c0a77363..c425ec6792 100644 --- a/src/selfhost/orb-collector.ts +++ b/src/selfhost/orb-collector.ts @@ -216,11 +216,11 @@ export async function exportOrbBatch(db: D1Database, batchSize = 200, fetchFn: t body, }); if (!res.ok) { - incr("gittensory_orb_export_errors_total"); + incr("loopover_orb_export_errors_total"); return 0; } } catch { - incr("gittensory_orb_export_errors_total"); + incr("loopover_orb_export_errors_total"); return 0; } @@ -233,6 +233,6 @@ export async function exportOrbBatch(db: D1Database, batchSize = 200, fetchFn: t .bind(instance, lastRow.event_at, lastRow.target_id, new Date().toISOString()) .run(); - incr("gittensory_orb_events_exported_total", {}, results.length); + incr("loopover_orb_events_exported_total", {}, results.length); return results.length; } diff --git a/src/selfhost/pg-queue.ts b/src/selfhost/pg-queue.ts index 84f513b69f..6476d8b78b 100644 --- a/src/selfhost/pg-queue.ts +++ b/src/selfhost/pg-queue.ts @@ -308,7 +308,7 @@ export function createPgQueue( ); const recovered = await recoverProcessingJobs(); if (recovered) { - await recordQueueMetric("gittensory_jobs_recovered_total", recovered); + await recordQueueMetric("loopover_jobs_recovered_total", recovered); console.log( JSON.stringify({ event: "selfhost_queue_recovered", count: recovered }), ); @@ -598,7 +598,7 @@ export function createPgQueue( async function reviveDeadLetterJobs(): Promise { const revived = await reviveEligibleDeadJobs(); if (revived) { - await recordQueueMetric("gittensory_jobs_dead_letter_revived_total", revived); + await recordQueueMetric("loopover_jobs_dead_letter_revived_total", revived); console.log(JSON.stringify({ event: "selfhost_queue_dead_letter_revived", count: revived })); kickAll(); } @@ -725,9 +725,9 @@ export function createPgQueue( else releasedByRateLimitClear += rowsChanged; } if (released) { - await recordQueueMetric("gittensory_jobs_foreground_liveness_released_total", released); - if (releasedByAge) incr("gittensory_jobs_foreground_liveness_released_by_reason_total", { reason: "age" }, releasedByAge); - if (releasedByRateLimitClear) incr("gittensory_jobs_foreground_liveness_released_by_reason_total", { reason: "rate_limit_cleared" }, releasedByRateLimitClear); + await recordQueueMetric("loopover_jobs_foreground_liveness_released_total", released); + if (releasedByAge) incr("loopover_jobs_foreground_liveness_released_by_reason_total", { reason: "age" }, releasedByAge); + if (releasedByRateLimitClear) incr("loopover_jobs_foreground_liveness_released_by_reason_total", { reason: "rate_limit_cleared" }, releasedByRateLimitClear); console.warn( JSON.stringify({ level: "warn", @@ -834,7 +834,7 @@ export function createPgQueue( ) ).rows[0] as { id: string } | undefined; if (existingFull) { - await recordQueueMetric("gittensory_jobs_coalesced_total"); + await recordQueueMetric("loopover_jobs_coalesced_total"); kickOne(); return; } @@ -872,7 +872,7 @@ export function createPgQueue( [mergedPayload, runAfter, now, priority, mergedKey, mergeCandidate.id, mergeCandidate.job_key, claimSortKey], ); if (merged.rowCount) { - await recordQueueMetric("gittensory_jobs_coalesced_total"); + await recordQueueMetric("loopover_jobs_coalesced_total"); kickOne(); return; } @@ -907,7 +907,7 @@ export function createPgQueue( WHERE status='pending' AND id<>$1 AND job_key IS NOT NULL AND left(job_key, $2)=$3`, [existing.id, supersededKeyPrefix.length, supersededKeyPrefix], ); - await recordQueueMetric("gittensory_jobs_coalesced_total"); + await recordQueueMetric("loopover_jobs_coalesced_total"); kickOne(); return; } @@ -935,7 +935,7 @@ export function createPgQueue( WHERE id=$5`, [payload, runAfter, priority, lane, existing.id, claimSortKey], ); - await recordQueueMetric("gittensory_jobs_coalesced_total"); + await recordQueueMetric("loopover_jobs_coalesced_total"); kickOne(); return; } @@ -944,7 +944,7 @@ export function createPgQueue( `INSERT INTO ${TABLE} (payload, status, attempts, run_after, created_at, priority, job_key, is_maintenance, foreground_lane, claim_sort_key) VALUES ($1,'pending',0,$2,$3,$4,$5,$6,$7,$8)`, [payload, runAfter, now, priority, key, isMaintenanceJobType(message.type) ? 1 : 0, lane, claimSortKey], ); - await recordQueueMetric("gittensory_jobs_enqueued_total"); + await recordQueueMetric("loopover_jobs_enqueued_total"); kickOne(); } @@ -1000,7 +1000,7 @@ export function createPgQueue( const lanePriorityFloor = unclassifiedPriority ?? FOREGROUND_QUEUE_PRIORITY_FLOOR; if (lane === "fresh") { const freshRow = await claimNextWhere(now, lanePriorityPredicate, { sql: "candidate.foreground_lane='fresh'", params: [] }, lanePriorityFloor); - if (freshRow) incr("gittensory_jobs_claimed_by_lane_total", { lane: "fresh" }); + if (freshRow) incr("loopover_jobs_claimed_by_lane_total", { lane: "fresh" }); return freshRow; } const backlogRes = await pool.query( @@ -1022,7 +1022,7 @@ export function createPgQueue( }, lanePriorityFloor); if (row) { await pool.query(`UPDATE ${FAIRNESS_TABLE} SET last_backlog_repo=$1 WHERE id='singleton'`, [repo]); - incr("gittensory_jobs_claimed_by_lane_total", { lane: "backlog" }); + incr("loopover_jobs_claimed_by_lane_total", { lane: "backlog" }); } return row; } @@ -1074,7 +1074,7 @@ export function createPgQueue( async function processOne(): Promise { const recovered = await reclaimExpiredProcessingJobs(); if (recovered) { - await recordQueueMetric("gittensory_jobs_recovered_total", recovered); + await recordQueueMetric("loopover_jobs_recovered_total", recovered); console.warn( JSON.stringify({ level: "warn", @@ -1103,7 +1103,7 @@ export function createPgQueue( `UPDATE ${TABLE} SET status='dead', attempts=attempts+1, last_error='unparseable payload', dead_at=$1 WHERE id=$2`, [Date.now(), job.id], ); - await recordQueueMetric("gittensory_jobs_dead_total"); + await recordQueueMetric("loopover_jobs_dead_total"); logAudit({ event: "job_dead", ts: Date.now(), @@ -1148,8 +1148,8 @@ export function createPgQueue( "selfhost_queue_pg_connection_lost_on_rate_limit_defer", ); if (update?.rowCount) { - await recordQueueMetric("gittensory_jobs_rate_limit_deferred_total"); - incr("gittensory_jobs_rate_limit_admission_deferred_total", rateLimitMetric.labels); + await recordQueueMetric("loopover_jobs_rate_limit_deferred_total"); + incr("loopover_jobs_rate_limit_admission_deferred_total", rateLimitMetric.labels); console.warn( JSON.stringify({ level: "warn", @@ -1191,8 +1191,8 @@ export function createPgQueue( "selfhost_queue_pg_connection_lost_on_maintenance_defer", ); if (update?.rowCount) { - await recordQueueMetric("gittensory_jobs_maintenance_admission_deferred_total"); - incr("gittensory_jobs_maintenance_admission_deferred_by_reason_total", { + await recordQueueMetric("loopover_jobs_maintenance_admission_deferred_total"); + incr("loopover_jobs_maintenance_admission_deferred_by_reason_total", { reason: decision.reason, job_type: message.type, }); @@ -1217,8 +1217,8 @@ export function createPgQueue( // counters distinguishes "load-shed maintenance is working as designed" from "maintenance is chronically // starved and only ever runs via the trickle floor" (the "truly stuck" signal operators need). if (decision.reason === "trickle_max_defer_age") { - await recordQueueMetric("gittensory_jobs_maintenance_trickle_admitted_total"); - incr("gittensory_jobs_maintenance_trickle_admitted_by_type_total", { job_type: message.type }); + await recordQueueMetric("loopover_jobs_maintenance_trickle_admitted_total"); + incr("loopover_jobs_maintenance_trickle_admitted_by_type_total", { job_type: message.type }); console.warn( JSON.stringify({ level: "warn", @@ -1233,7 +1233,7 @@ export function createPgQueue( // so an operator can trend "how often does pressure admission get overridden at all" without needing to // sum multiple per-reason metrics. if (isMaintenanceAdmissionGrantedUnderPressure(decision.reason)) { - incr("gittensory_jobs_maintenance_admission_granted_under_pressure_total", { + incr("loopover_jobs_maintenance_admission_granted_under_pressure_total", { reason: decision.reason, job_type: message.type, }); @@ -1272,8 +1272,8 @@ export function createPgQueue( "selfhost_queue_pg_connection_lost_on_installation_concurrency_defer", ); if (update?.rowCount) { - await recordQueueMetric("gittensory_jobs_installation_concurrency_deferred_total"); - incr("gittensory_jobs_installation_concurrency_deferred_by_reason_total", { + await recordQueueMetric("loopover_jobs_installation_concurrency_deferred_total"); + incr("loopover_jobs_installation_concurrency_deferred_by_reason_total", { reason: decision.reason, job_type: message.type, }); @@ -1322,7 +1322,7 @@ export function createPgQueue( } throw deleteErr; } - await recordQueueMetric("gittensory_jobs_processed_total"); + await recordQueueMetric("loopover_jobs_processed_total"); logAudit({ event: "job_complete", ts: Date.now(), @@ -1361,8 +1361,8 @@ export function createPgQueue( const deferred = target ? await deferPendingJobsForRateLimit(rateLimitDelayMs, now, target) : 0; const rateLimitMetric = githubRateLimitMetricContext(message, target); if (target !== null && deferred > 0) { - await recordQueueMetric("gittensory_jobs_rate_limit_deferred_total", deferred); - incr("gittensory_jobs_rate_limit_budget_deferred_total", rateLimitMetric.labels, deferred); + await recordQueueMetric("loopover_jobs_rate_limit_deferred_total", deferred); + incr("loopover_jobs_rate_limit_budget_deferred_total", rateLimitMetric.labels, deferred); console.warn( JSON.stringify({ level: "warn", @@ -1373,15 +1373,15 @@ export function createPgQueue( ); } if (job.job_key && (await mergeRescheduledJobIntoPending(job as JobRow & { job_key: string }, retryAfter, errMsg))) { - await recordQueueMetric("gittensory_jobs_coalesced_total"); + await recordQueueMetric("loopover_jobs_coalesced_total"); } else { await pool.query( `UPDATE ${TABLE} SET status='pending', run_after=$1, last_error=$2 WHERE id=$3`, [retryAfter, errMsg, job.id], ); } - await recordQueueMetric("gittensory_jobs_rate_limited_total"); - incr("gittensory_jobs_rate_limited_by_type_total", rateLimitMetric.labels); + await recordQueueMetric("loopover_jobs_rate_limited_total"); + incr("loopover_jobs_rate_limited_by_type_total", rateLimitMetric.labels); logAudit({ event: "job_rate_limited", ts: Date.now(), @@ -1395,13 +1395,13 @@ export function createPgQueue( }, jobTraceParent); return true; } - await recordQueueMetric("gittensory_jobs_failed_total"); + await recordQueueMetric("loopover_jobs_failed_total"); if (attempts >= maxRetries) { await pool.query( `UPDATE ${TABLE} SET status='dead', attempts=$1, last_error=$2, dead_at=$3 WHERE id=$4`, [attempts, errMsg, Date.now(), job.id], ); - await recordQueueMetric("gittensory_jobs_dead_total"); + await recordQueueMetric("loopover_jobs_dead_total"); console.error( JSON.stringify({ level: "error", diff --git a/src/selfhost/qdrant-vectorize.ts b/src/selfhost/qdrant-vectorize.ts index 7c3a8722d9..d1d93fa9c1 100644 --- a/src/selfhost/qdrant-vectorize.ts +++ b/src/selfhost/qdrant-vectorize.ts @@ -105,10 +105,10 @@ export function createQdrantVectorize(url: string, collection = DEFAULT_COLLECTI body: JSON.stringify({ points }), }); if (!res.ok) { - incr("gittensory_qdrant_errors_total", { op: "upsert" }); + incr("loopover_qdrant_errors_total", { op: "upsert" }); throw new Error(`Qdrant upsert failed: HTTP ${res.status}`); } - incr("gittensory_qdrant_upserts_total", {}, vectors.length); + incr("loopover_qdrant_upserts_total", {}, vectors.length); return { count: vectors.length, ids: vectors.map((v) => v.id) }; }, @@ -126,14 +126,14 @@ export function createQdrantVectorize(url: string, collection = DEFAULT_COLLECTI }); } catch { // Qdrant unreachable — degrade gracefully (RAG returns no context rather than crashing) - incr("gittensory_qdrant_errors_total", { op: "query" }); + incr("loopover_qdrant_errors_total", { op: "query" }); return { matches: [] }; } if (!res.ok) { - incr("gittensory_qdrant_errors_total", { op: "query" }); + incr("loopover_qdrant_errors_total", { op: "query" }); return { matches: [] }; } - incr("gittensory_qdrant_queries_total"); + incr("loopover_qdrant_queries_total"); const data = (await res.json()) as QdrantSearchResult; const matches: Match[] = data.result.map((r) => { const { _orig_id, namespace: _ns, ...rest } = r.payload; @@ -152,7 +152,7 @@ export function createQdrantVectorize(url: string, collection = DEFAULT_COLLECTI body: JSON.stringify({ points }), }); if (!res.ok) { - incr("gittensory_qdrant_errors_total", { op: "delete" }); + incr("loopover_qdrant_errors_total", { op: "delete" }); throw new Error(`Qdrant deleteByIds failed: HTTP ${res.status}`); } return { count: ids.length }; diff --git a/src/selfhost/redis-cache.ts b/src/selfhost/redis-cache.ts index 6040fba15b..50d692eb0f 100644 --- a/src/selfhost/redis-cache.ts +++ b/src/selfhost/redis-cache.ts @@ -13,13 +13,13 @@ export function webhookDeliveryCacheKey(deliveryId: string): string { } /** Returns true when this GitHub webhook delivery ID was already processed (Redis dedup hit). - * Increments `gittensory_webhook_dedup_total{backend="redis"}` on a hit. Does NOT mark the + * Increments `loopover_webhook_dedup_total{backend="redis"}` on a hit. Does NOT mark the * delivery — the caller marks only after a successful response (#2506 / #2572). */ export async function isWebhookDeliveryDuplicate(cache: RedisCache, deliveryId: string): Promise { try { const seen = await cache.get(webhookDeliveryCacheKey(deliveryId)); if (seen) { - incr("gittensory_webhook_dedup_total", { backend: "redis" }); + incr("loopover_webhook_dedup_total", { backend: "redis" }); return true; } return false; diff --git a/src/selfhost/redis-response-cache.ts b/src/selfhost/redis-response-cache.ts index fefb204b65..0b22e3ff05 100644 --- a/src/selfhost/redis-response-cache.ts +++ b/src/selfhost/redis-response-cache.ts @@ -11,11 +11,11 @@ import type { Redis } from "ioredis"; import type { CachedGitHubResponse, GitHubResponseCache } from "../github/client"; import { counterValue, gauge, hitRatio, incr } from "./metrics"; -const REDIS_GITHUB_RESPONSE_CACHE_METRIC = "gittensory_redis_gh_response_cache_total"; +const REDIS_GITHUB_RESPONSE_CACHE_METRIC = "loopover_redis_gh_response_cache_total"; const keyFor = (key: string): string => `gh:resp:${key}`; function registerRedisResponseCacheHitRatioGauge(): void { - gauge("gittensory_redis_gh_response_cache_hit_ratio", () => + gauge("loopover_redis_gh_response_cache_hit_ratio", () => hitRatio( counterValue(REDIS_GITHUB_RESPONSE_CACHE_METRIC, { result: "hit" }), counterValue(REDIS_GITHUB_RESPONSE_CACHE_METRIC, { result: "miss" }), diff --git a/src/selfhost/redis-token-cache.ts b/src/selfhost/redis-token-cache.ts index ae94f5e578..7246bfd6c4 100644 --- a/src/selfhost/redis-token-cache.ts +++ b/src/selfhost/redis-token-cache.ts @@ -8,7 +8,7 @@ import type { Redis } from "ioredis"; import type { InstallationTokenStore } from "../github/app"; import { incr } from "./metrics"; -const REDIS_TOKEN_CACHE_METRIC = "gittensory_redis_token_cache_total"; +const REDIS_TOKEN_CACHE_METRIC = "loopover_redis_token_cache_total"; const keyFor = (installationId: number): string => `gh:insttoken:${installationId}`; diff --git a/src/selfhost/sqlite-queue.ts b/src/selfhost/sqlite-queue.ts index 2bff63908b..2ff2e0bad7 100644 --- a/src/selfhost/sqlite-queue.ts +++ b/src/selfhost/sqlite-queue.ts @@ -253,7 +253,7 @@ export function createSqliteQueue( // Recover jobs a crashed previous run left mid-flight → make them claimable again. const recovered = recoverProcessingJobs(driver); if (recovered) { - recordQueueMetric(driver, "gittensory_jobs_recovered_total", recovered); + recordQueueMetric(driver, "loopover_jobs_recovered_total", recovered); console.log( JSON.stringify({ event: "selfhost_queue_recovered", count: recovered }), ); @@ -286,7 +286,7 @@ export function createSqliteQueue( async function reviveDeadLetterJobs(): Promise { const revived = reviveEligibleDeadJobs(driver, maxRetries); if (revived) { - recordQueueMetric(driver, "gittensory_jobs_dead_letter_revived_total", revived); + recordQueueMetric(driver, "loopover_jobs_dead_letter_revived_total", revived); console.log(JSON.stringify({ event: "selfhost_queue_dead_letter_revived", count: revived })); kickAll(); } @@ -408,9 +408,9 @@ export function createSqliteQueue( else releasedByRateLimitClear += changes; } if (released) { - recordQueueMetric(driver, "gittensory_jobs_foreground_liveness_released_total", released); - if (releasedByAge) incr("gittensory_jobs_foreground_liveness_released_by_reason_total", { reason: "age" }, releasedByAge); - if (releasedByRateLimitClear) incr("gittensory_jobs_foreground_liveness_released_by_reason_total", { reason: "rate_limit_cleared" }, releasedByRateLimitClear); + recordQueueMetric(driver, "loopover_jobs_foreground_liveness_released_total", released); + if (releasedByAge) incr("loopover_jobs_foreground_liveness_released_by_reason_total", { reason: "age" }, releasedByAge); + if (releasedByRateLimitClear) incr("loopover_jobs_foreground_liveness_released_by_reason_total", { reason: "rate_limit_cleared" }, releasedByRateLimitClear); console.warn( JSON.stringify({ level: "warn", @@ -492,7 +492,7 @@ export function createSqliteQueue( [absorbedByKey], ).rows[0] as { id: number } | undefined; if (existingFull) { - recordQueueMetric(driver, "gittensory_jobs_coalesced_total"); + recordQueueMetric(driver, "loopover_jobs_coalesced_total"); kickOne(); return; } @@ -525,7 +525,7 @@ export function createSqliteQueue( WHERE id=?`, [mergedPayload, runAfter, now, priority, mergedKey, claimSortKey, claimSortKey, mergeCandidate.id], ); - recordQueueMetric(driver, "gittensory_jobs_coalesced_total"); + recordQueueMetric(driver, "loopover_jobs_coalesced_total"); kickOne(); return; } @@ -559,7 +559,7 @@ export function createSqliteQueue( WHERE status='pending' AND id<>? AND job_key IS NOT NULL AND substr(job_key, 1, ?)=?`, [existing.id, prefixLength, supersededKeyPrefix], ); - recordQueueMetric(driver, "gittensory_jobs_coalesced_total"); + recordQueueMetric(driver, "loopover_jobs_coalesced_total"); kickOne(); return; } @@ -586,7 +586,7 @@ export function createSqliteQueue( WHERE id=?`, [payload, runAfter, priority, lane, claimSortKey, claimSortKey, existing.id], ); - recordQueueMetric(driver, "gittensory_jobs_coalesced_total"); + recordQueueMetric(driver, "loopover_jobs_coalesced_total"); kickOne(); return; } @@ -595,7 +595,7 @@ export function createSqliteQueue( `INSERT INTO ${TABLE} (payload, status, attempts, run_after, created_at, priority, job_key, is_maintenance, foreground_lane, claim_sort_key) VALUES (?, 'pending', 0, ?, ?, ?, ?, ?, ?, ?)`, [payload, runAfter, now, priority, key, isMaintenanceJobType(message.type) ? 1 : 0, lane, claimSortKey], ); - recordQueueMetric(driver, "gittensory_jobs_enqueued_total"); + recordQueueMetric(driver, "loopover_jobs_enqueued_total"); kickOne(); } @@ -647,7 +647,7 @@ export function createSqliteQueue( const lanePriorityFloor = unclassifiedPriority ?? FOREGROUND_QUEUE_PRIORITY_FLOOR; if (lane === "fresh") { const freshRow = claimNextWhere(now, lanePriorityPredicate, { sql: "candidate.foreground_lane='fresh'", params: [] }, lanePriorityFloor); - if (freshRow) incr("gittensory_jobs_claimed_by_lane_total", { lane: "fresh" }); + if (freshRow) incr("loopover_jobs_claimed_by_lane_total", { lane: "fresh" }); return freshRow; } const { rows: backlogRows } = driver.query( @@ -669,7 +669,7 @@ export function createSqliteQueue( }, lanePriorityFloor); if (row) { driver.query(`UPDATE ${FAIRNESS_TABLE} SET last_backlog_repo=? WHERE id='singleton'`, [repo]); - incr("gittensory_jobs_claimed_by_lane_total", { lane: "backlog" }); + incr("loopover_jobs_claimed_by_lane_total", { lane: "backlog" }); } return row; } @@ -812,7 +812,7 @@ export function createSqliteQueue( activeJobIds, ); if (recovered) { - recordQueueMetric(driver, "gittensory_jobs_recovered_total", recovered); + recordQueueMetric(driver, "loopover_jobs_recovered_total", recovered); console.warn( JSON.stringify({ level: "warn", @@ -841,7 +841,7 @@ export function createSqliteQueue( `UPDATE ${TABLE} SET status='dead', attempts=attempts+1, last_error='unparseable payload', dead_at=? WHERE id=?`, [Date.now(), job.id], ); - recordQueueMetric(driver, "gittensory_jobs_dead_total"); + recordQueueMetric(driver, "loopover_jobs_dead_total"); logAudit({ event: "job_dead", ts: Date.now(), @@ -881,8 +881,8 @@ export function createSqliteQueue( [retryAfter, lastError, job.id], ); if (changes) { - recordQueueMetric(driver, "gittensory_jobs_rate_limit_deferred_total"); - incr("gittensory_jobs_rate_limit_admission_deferred_total", rateLimitMetric.labels); + recordQueueMetric(driver, "loopover_jobs_rate_limit_deferred_total"); + incr("loopover_jobs_rate_limit_admission_deferred_total", rateLimitMetric.labels); console.warn( JSON.stringify({ level: "warn", @@ -919,8 +919,8 @@ export function createSqliteQueue( [retryAfter, `maintenance admission deferred: ${decision.reason}`, job.id], ); if (changes) { - recordQueueMetric(driver, "gittensory_jobs_maintenance_admission_deferred_total"); - incr("gittensory_jobs_maintenance_admission_deferred_by_reason_total", { + recordQueueMetric(driver, "loopover_jobs_maintenance_admission_deferred_total"); + incr("loopover_jobs_maintenance_admission_deferred_by_reason_total", { reason: decision.reason, job_type: message.type, }); @@ -945,8 +945,8 @@ export function createSqliteQueue( // counters distinguishes "load-shed maintenance is working as designed" from "maintenance is chronically // starved and only ever runs via the trickle floor" (the "truly stuck" signal operators need). if (decision.reason === "trickle_max_defer_age") { - recordQueueMetric(driver, "gittensory_jobs_maintenance_trickle_admitted_total"); - incr("gittensory_jobs_maintenance_trickle_admitted_by_type_total", { job_type: message.type }); + recordQueueMetric(driver, "loopover_jobs_maintenance_trickle_admitted_total"); + incr("loopover_jobs_maintenance_trickle_admitted_by_type_total", { job_type: message.type }); console.warn( JSON.stringify({ level: "warn", @@ -961,7 +961,7 @@ export function createSqliteQueue( // so an operator can trend "how often does pressure admission get overridden at all" without needing to // sum multiple per-reason metrics. if (isMaintenanceAdmissionGrantedUnderPressure(decision.reason)) { - incr("gittensory_jobs_maintenance_admission_granted_under_pressure_total", { + incr("loopover_jobs_maintenance_admission_granted_under_pressure_total", { reason: decision.reason, job_type: message.type, }); @@ -995,8 +995,8 @@ export function createSqliteQueue( [retryAfter, `installation concurrency admission deferred: ${decision.reason}`, job.id], ); if (changes) { - recordQueueMetric(driver, "gittensory_jobs_installation_concurrency_deferred_total"); - incr("gittensory_jobs_installation_concurrency_deferred_by_reason_total", { + recordQueueMetric(driver, "loopover_jobs_installation_concurrency_deferred_total"); + incr("loopover_jobs_installation_concurrency_deferred_by_reason_total", { reason: decision.reason, job_type: message.type, }); @@ -1026,7 +1026,7 @@ export function createSqliteQueue( { parentTraceParent: message.type === "github-webhook" ? message.traceParent : undefined }, ); driver.query(`DELETE FROM ${TABLE} WHERE id=?`, [job.id]); - recordQueueMetric(driver, "gittensory_jobs_processed_total"); + recordQueueMetric(driver, "loopover_jobs_processed_total"); logAudit({ event: "job_complete", ts: Date.now(), @@ -1047,8 +1047,8 @@ export function createSqliteQueue( const deferred = target ? deferPendingJobsForRateLimit(driver, rateLimitDelayMs, now, target) : 0; const rateLimitMetric = githubRateLimitMetricContext(message, target); if (target !== null && deferred > 0) { - recordQueueMetric(driver, "gittensory_jobs_rate_limit_deferred_total", deferred); - incr("gittensory_jobs_rate_limit_budget_deferred_total", rateLimitMetric.labels, deferred); + recordQueueMetric(driver, "loopover_jobs_rate_limit_deferred_total", deferred); + incr("loopover_jobs_rate_limit_budget_deferred_total", rateLimitMetric.labels, deferred); console.warn( JSON.stringify({ level: "warn", @@ -1059,15 +1059,15 @@ export function createSqliteQueue( ); } if (job.job_key && mergeRescheduledJobIntoPending(driver, job as JobRow & { job_key: string }, retryAfter, errMsg)) { - recordQueueMetric(driver, "gittensory_jobs_coalesced_total"); + recordQueueMetric(driver, "loopover_jobs_coalesced_total"); } else { driver.query( `UPDATE ${TABLE} SET status='pending', run_after=?, last_error=? WHERE id=?`, [retryAfter, errMsg, job.id], ); } - recordQueueMetric(driver, "gittensory_jobs_rate_limited_total"); - incr("gittensory_jobs_rate_limited_by_type_total", rateLimitMetric.labels); + recordQueueMetric(driver, "loopover_jobs_rate_limited_total"); + incr("loopover_jobs_rate_limited_by_type_total", rateLimitMetric.labels); logAudit({ event: "job_rate_limited", ts: Date.now(), @@ -1081,13 +1081,13 @@ export function createSqliteQueue( }, jobTraceParent); return true; } - recordQueueMetric(driver, "gittensory_jobs_failed_total"); + recordQueueMetric(driver, "loopover_jobs_failed_total"); if (attempts >= maxRetries) { driver.query( `UPDATE ${TABLE} SET status='dead', attempts=?, last_error=?, dead_at=? WHERE id=?`, [attempts, errMsg, Date.now(), job.id], ); - recordQueueMetric(driver, "gittensory_jobs_dead_total"); + recordQueueMetric(driver, "loopover_jobs_dead_total"); console.error( JSON.stringify({ level: "error", diff --git a/src/server.ts b/src/server.ts index cffa8d9215..9065e985ff 100644 --- a/src/server.ts +++ b/src/server.ts @@ -701,23 +701,23 @@ async function main(): Promise { }); } - gauge("gittensory_queue_pending", () => backend.queue.size()); - gauge("gittensory_queue_dead", () => backend.queue.deadCount()); - gauge("gittensory_dlq_dead_lettered_recent", () => sampleRecentDeadLetters(env)); - gauge("gittensory_queue_processing", () => backend.queue.processingCount()); + gauge("loopover_queue_pending", () => backend.queue.size()); + gauge("loopover_queue_dead", () => backend.queue.deadCount()); + gauge("loopover_dlq_dead_lettered_recent", () => sampleRecentDeadLetters(env)); + gauge("loopover_queue_processing", () => backend.queue.processingCount()); const durableJobMetric = async (name: string): Promise => Number((await backend.queue.stats())[name] ?? 0); for (const name of [ - "gittensory_jobs_enqueued_total", - "gittensory_jobs_processed_total", - "gittensory_jobs_failed_total", - "gittensory_jobs_dead_total", - "gittensory_jobs_rate_limited_total", - "gittensory_jobs_rate_limit_deferred_total", - "gittensory_jobs_coalesced_total", - "gittensory_jobs_recovered_total", - "gittensory_jobs_maintenance_admission_deferred_total", - "gittensory_jobs_maintenance_trickle_admitted_total", + "loopover_jobs_enqueued_total", + "loopover_jobs_processed_total", + "loopover_jobs_failed_total", + "loopover_jobs_dead_total", + "loopover_jobs_rate_limited_total", + "loopover_jobs_rate_limit_deferred_total", + "loopover_jobs_coalesced_total", + "loopover_jobs_recovered_total", + "loopover_jobs_maintenance_admission_deferred_total", + "loopover_jobs_maintenance_trickle_admitted_total", ]) { gauge(name.replace("_total", "_persisted_total"), () => durableJobMetric(name), @@ -729,79 +729,79 @@ async function main(): Promise { // (best-effort) host CPU pressure. Distinguishes "the app queue is backed up" from "CI/other host load is // starving the app" from "GitHub/AI latency", the ambiguity that made the original slowdown hard to diagnose. const maintenancePressure = () => backend.queue.pressureSignals(); - gauge("gittensory_queue_live_pending", async () => (await maintenancePressure()).livePendingCount); - gauge("gittensory_queue_maintenance_pending", async () => (await maintenancePressure()).maintenancePendingCount); - gauge("gittensory_queue_oldest_live_pending_age_seconds", async () => + gauge("loopover_queue_live_pending", async () => (await maintenancePressure()).livePendingCount); + gauge("loopover_queue_maintenance_pending", async () => (await maintenancePressure()).maintenancePendingCount); + gauge("loopover_queue_oldest_live_pending_age_seconds", async () => Math.floor(((await maintenancePressure()).oldestLivePendingAgeMs ?? 0) / 1000), ); - gauge("gittensory_queue_oldest_maintenance_pending_age_seconds", async () => + gauge("loopover_queue_oldest_maintenance_pending_age_seconds", async () => Math.floor(((await maintenancePressure()).oldestMaintenancePendingAgeMs ?? 0) / 1000), ); // #selfhost-queue-liveness: runnable-now is the "is anything actually due right now" signal the incident // this module fixes required manual SQL to answer (processing=0, runnable_now=0 with hundreds pending). - // gittensory_queue_runnable_now covers every priority; the live-scoped pair narrows to foreground work + // loopover_queue_runnable_now covers every priority; the live-scoped pair narrows to foreground work // specifically and adds the oldest-RUNNABLE age, distinct from oldest-PENDING age (which a job intentionally // scheduled far out can inflate without indicating anything is stuck). - gauge("gittensory_queue_runnable_now", async () => (await backend.queue.snapshot()).totals.due); - gauge("gittensory_queue_live_runnable_now", async () => (await maintenancePressure()).liveRunnableNowCount); - gauge("gittensory_queue_oldest_live_runnable_age_seconds", async () => + gauge("loopover_queue_runnable_now", async () => (await backend.queue.snapshot()).totals.due); + gauge("loopover_queue_live_runnable_now", async () => (await maintenancePressure()).liveRunnableNowCount); + gauge("loopover_queue_oldest_live_runnable_age_seconds", async () => Math.floor(((await maintenancePressure()).oldestLiveRunnableAgeMs ?? 0) / 1000), ); // -1 (not 0) when unavailable -- a genuine idle host reads 0, so a dashboard can tell "known idle" apart // from "no signal on this platform" (see host-pressure.ts). - gauge("gittensory_host_load_avg1_per_core", async () => (await maintenancePressure()).hostLoadAvg1PerCore ?? -1); - gauge("gittensory_clock_skew_seconds", () => clockSkewSecondsSample()); + gauge("loopover_host_load_avg1_per_core", async () => (await maintenancePressure()).hostLoadAvg1PerCore ?? -1); + gauge("loopover_clock_skew_seconds", () => clockSkewSecondsSample()); // D1 size/row-count observability probe (#3810): opt-in Cloudflare Management API poll for the shared // cloud D1's file size and monitored-table row counts. Always registered (byte-identical -1/empty samples // when the probe is disabled or has never completed) so the metric names/HELP/TYPE lines are present on // the very first scrape, matching the seeded-counter convention below. - gauge("gittensory_d1_database_size_bytes", () => d1DatabaseSizeBytesSample()); - gaugeVector("gittensory_d1_table_row_count", () => d1TableRowCountSamples()); - gauge("gittensory_signal_snapshots_rows_per_key", () => d1SignalSnapshotsRowsPerKeySample()); + gauge("loopover_d1_database_size_bytes", () => d1DatabaseSizeBytesSample()); + gaugeVector("loopover_d1_table_row_count", () => d1TableRowCountSamples()); + gauge("loopover_signal_snapshots_rows_per_key", () => d1SignalSnapshotsRowsPerKeySample()); // Backlog-vs-fresh-intake fairness lanes (#selfhost-lane-observability, see queue-fairness.ts): the SAME // `foreground_lane` classification the claim-time fairness mechanism itself consults, so an operator can see // whether a stuck-looking queue is actually a real, unresolved PR-review backlog (high backlog-convergence // pending) or a burst of brand-new webhook traffic (high fresh-intake pending) -- two very different causes // that both otherwise just show up as "live pending is high." - gauge("gittensory_queue_backlog_convergence_pending", async () => (await maintenancePressure()).backlogConvergencePendingCount); - gauge("gittensory_queue_fresh_intake_pending", async () => (await maintenancePressure()).freshIntakePendingCount); + gauge("loopover_queue_backlog_convergence_pending", async () => (await maintenancePressure()).backlogConvergencePendingCount); + gauge("loopover_queue_fresh_intake_pending", async () => (await maintenancePressure()).freshIntakePendingCount); // Top-10 repos by backlog-convergence depth, recomputed fresh every scrape (gaugeVector -- see metrics.ts) so // a repo that drains out of the top-10 stops appearing on its own, with no stale per-repo series lingering. // Bounded to 10 regardless of how many repos a self-host install has registered. - gaugeVector("gittensory_queue_backlog_by_repo", async () => + gaugeVector("loopover_queue_backlog_by_repo", async () => (await backend.queue.topBacklogRepos(10)).map((r) => ({ labels: { repo: r.repo }, value: r.count })), ); - // A genuine "remaining right now" gauge, by key_scope -- gittensory_github_rest_rate_limit_observations_total + // A genuine "remaining right now" gauge, by key_scope -- loopover_github_rest_rate_limit_observations_total // only supports a bucketed rate() over a window, never the actual current value (#selfhost-lane-observability). - gaugeVector("gittensory_github_rest_rate_limit_remaining", () => githubRestRateLimitRemainingSamples()); - gauge("gittensory_uptime_seconds", () => + gaugeVector("loopover_github_rest_rate_limit_remaining", () => githubRestRateLimitRemainingSamples()); + gauge("loopover_uptime_seconds", () => Math.floor((Date.now() - startedAt) / 1000), ); - gauge("gittensory_backup_acknowledged", () => backupAcknowledgedGaugeValue(sqliteBackupOpts)); - gauge("gittensory_public_origin_acknowledged", () => publicOriginAcknowledgedGaugeValue(publicOriginOpts)); + gauge("loopover_backup_acknowledged", () => backupAcknowledgedGaugeValue(sqliteBackupOpts)); + gauge("loopover_public_origin_acknowledged", () => publicOriginAcknowledgedGaugeValue(publicOriginOpts)); // Pre-initialize job counters to 0 so they appear in the first Prometheus scrape (lazy counters // created on first use would otherwise cause "No data" in Grafana until the first job event). for (const c of [ - "gittensory_jobs_enqueued_total", - "gittensory_jobs_processed_total", - "gittensory_jobs_failed_total", - "gittensory_jobs_dead_total", - "gittensory_jobs_rate_limit_deferred_total", - "gittensory_jobs_recovered_total", - "gittensory_webhook_dedup_total", - "gittensory_qdrant_queries_total", - "gittensory_qdrant_upserts_total", - "gittensory_orb_events_exported_total", - "gittensory_orb_export_errors_total", + "loopover_jobs_enqueued_total", + "loopover_jobs_processed_total", + "loopover_jobs_failed_total", + "loopover_jobs_dead_total", + "loopover_jobs_rate_limit_deferred_total", + "loopover_jobs_recovered_total", + "loopover_webhook_dedup_total", + "loopover_qdrant_queries_total", + "loopover_qdrant_upserts_total", + "loopover_orb_events_exported_total", + "loopover_orb_export_errors_total", ]) - incr(c, c === "gittensory_webhook_dedup_total" ? { backend: "redis" } : undefined, 0); - // Seed gittensory_http_requests_total per status class so the breakdown panel has every series from the + incr(c, c === "loopover_webhook_dedup_total" ? { backend: "redis" } : undefined, 0); + // Seed loopover_http_requests_total per status class so the breakdown panel has every series from the // first scrape (keeping the metric consistently labeled — never mix labeled and unlabeled samples). for (const status of ["2xx", "3xx", "4xx", "5xx"]) - incr("gittensory_http_requests_total", { status }, 0); + incr("loopover_http_requests_total", { status }, 0); // Same seeding for the D1 probe's error counter (#3810) -- byte-identical to 0 whether or not the probe is // even enabled, so its stat panel reads "0" rather than "No data" before any failure has ever occurred. - for (const part of ["database_info", "table_row_count"]) incr("gittensory_d1_probe_errors_total", { part }, 0); + for (const part of ["database_info", "table_row_count"]) incr("loopover_d1_probe_errors_total", { part }, 0); const ctx = { waitUntil: (p: Promise) => @@ -957,11 +957,11 @@ async function main(): Promise { // /health /ready /metrics and the setup wizard already returned above and are not counted.) const startedReq = Date.now(); const finish = (response: Response): Response => { - incr("gittensory_http_requests_total", { + incr("loopover_http_requests_total", { status: `${Math.floor(response.status / 100)}xx`, }); observe( - "gittensory_http_request_duration_seconds", + "loopover_http_request_duration_seconds", (Date.now() - startedReq) / 1000, ); setCurrentOtelSpanAttributes(selfHostHttpResponseAttributes(response.status)); @@ -978,7 +978,7 @@ async function main(): Promise { : null; if (deliveryId) { // Redis dedup hit — return 204 before enqueue (#1216). - // Metric: gittensory_webhook_dedup_total{backend="redis"} (#2075). + // Metric: loopover_webhook_dedup_total{backend="redis"} (#2075). if (await isWebhookDeliveryDuplicate(webhookCache!, deliveryId)) { return finish(new Response(null, { status: 204 })); } @@ -1082,8 +1082,8 @@ async function main(): Promise { // Dashboard-visible counterparts to the streak/no-progress alert gate in isOrbRelayRegistrationAlerting: // an operator staring at the registration-failures counter alone can't tell "one hiccup" from "actually // stuck" -- these two gauges are the SAME two signals that gate, sampled live at scrape time. - gauge("gittensory_orb_relay_register_consecutive_failures", () => orbRelayRegistrationState.consecutiveFailures); - gauge("gittensory_orb_relay_drain_seconds_since_last", () => + gauge("loopover_orb_relay_register_consecutive_failures", () => orbRelayRegistrationState.consecutiveFailures); + gauge("loopover_orb_relay_drain_seconds_since_last", () => relayDrainState?.lastDrainAtMs == null ? -1 : Math.floor((Date.now() - relayDrainState.lastDrainAtMs) / 1000), ); /* v8 ignore stop */ diff --git a/src/services/agent-action-executor.ts b/src/services/agent-action-executor.ts index 3ed53a2bb6..2358a42cdb 100644 --- a/src/services/agent-action-executor.ts +++ b/src/services/agent-action-executor.ts @@ -308,12 +308,12 @@ export async function executeAgentMaintenanceActions(env: Env, ctx: AgentActionE // before the freshness/live-CI GitHub calls below so a known-denied action never spends that API budget on // an outcome that cannot change until the maintainer re-consents (#selfhost-runtime-drift). if (PR_WRITE_CLASSES.has(action.actionClass) && resolveAgentPermissionReadiness({ autonomy: ctx.autonomy, installationPermissions: ctx.installationPermissions, actionClass: action.actionClass }) !== "ready") { - incr("gittensory_agent_action_permission_denied_total", { actionClass: action.actionClass }); + incr("loopover_agent_action_permission_denied_total", { actionClass: action.actionClass }); const cooldownKey = writePermissionDenialKey(ctx.installationId, ctx.repoFullName, ctx.pullNumber, action.actionClass); if (shouldSuppressWritePermissionDenial(cooldownKey, Date.now())) { // Already denied + audited for this exact installation/repo/action-class within the cooldown window -- // count it (the denial stays visible in metrics) without re-writing an identical audit record every pass. - incr("gittensory_agent_action_permission_denied_suppressed_total", { actionClass: action.actionClass }); + incr("loopover_agent_action_permission_denied_suppressed_total", { actionClass: action.actionClass }); outcomes.push({ actionClass: action.actionClass, outcome: "denied", @@ -509,7 +509,7 @@ export async function executeAgentMaintenanceActions(env: Env, ctx: AgentActionE nowMs: Date.now(), }); if (decision.wait) { - incr("gittensory_merge_train_deferred_total", { repo: ctx.repoFullName, mode: ctx.mergeTrainMode }); + incr("loopover_merge_train_deferred_total", { repo: ctx.repoFullName, mode: ctx.mergeTrainMode }); if (ctx.mergeTrainMode === "enforce") { await audit("denied", `merge train: waiting for older mergeable sibling #${decision.blockingPr} — action not executed`); continue; diff --git a/src/services/ai-review.ts b/src/services/ai-review.ts index 16f4b6034d..33b124179e 100644 --- a/src/services/ai-review.ts +++ b/src/services/ai-review.ts @@ -1060,7 +1060,7 @@ async function runWorkersOpinion( const models = fallback && fallback !== primary ? [primary, fallback] : [primary]; for (const [modelIndex, model] of models.entries()) { if (modelIndex > 0) { - incr("gittensory_ai_review_model_fallback_total", { primary, fallback: model }); + incr("loopover_ai_review_model_fallback_total", { primary, fallback: model }); } for (let attempt = 0; attempt < 3; attempt += 1) { try { @@ -1804,7 +1804,7 @@ async function runDualAiTieBreakJudgeCall( const models = fallback && fallback !== model ? [model, fallback] : [model]; for (const [modelIndex, activeModel] of models.entries()) { if (modelIndex > 0) { - incr("gittensory_ai_review_model_fallback_total", { primary: model, fallback: activeModel }); + incr("loopover_ai_review_model_fallback_total", { primary: model, fallback: activeModel }); } for (let attempt = 0; attempt < 3; attempt += 1) { try { @@ -2075,7 +2075,7 @@ export async function runGittensoryAiReview( // switching to `combine: "single"` either (a floor of "either ONE of two reviewers can flag it" is just as // bypassed by dropping to one reviewer as by flipping onMerge itself). resolveEffectiveAiReviewPlan enforces // the clamp across all three fields together; a fired clamp increments a metric so it is surfaced, not - // silently ignored (mirrors the gittensory_ai_review_inconclusive_total pattern below). + // silently ignored (mirrors the loopover_ai_review_inconclusive_total pattern below). const plan = env.AI_REVIEW_PLAN; const planResolution = resolveEffectiveAiReviewPlan( { combine: input.combine, onMerge: input.onMerge, reviewers: input.reviewers }, @@ -2100,7 +2100,7 @@ export async function runGittensoryAiReview( const combine: CombineStrategy = planResolution.combine ?? "consensus"; const onMerge = planResolution.onMerge; if (planResolution.clamped) { - incr("gittensory_ai_review_onmerge_clamped_total", { mode: input.mode }); + incr("loopover_ai_review_onmerge_clamped_total", { mode: input.mode }); } const dual = combine !== "single" && (!configured || configured.length > 1); const freeAiCalls = @@ -2278,7 +2278,7 @@ export async function runGittensoryAiReview( correlation: aiRunCorrelation, }); if (tieBreak.orderUnstable) { - incr("gittensory_ai_review_tiebreak_order_unstable_total", { mode: input.mode }); + incr("loopover_ai_review_tiebreak_order_unstable_total", { mode: input.mode }); console.warn( JSON.stringify({ level: "warn", @@ -2334,7 +2334,7 @@ export async function runGittensoryAiReview( // review call -- increment exactly once here, never at the downstream consumers in queue/processors.ts that // push an `ai_review_inconclusive` advisory finding off this same already-computed result (incrementing there // too would double/triple-count one review). - if (inconclusive) incr("gittensory_ai_review_inconclusive_total", { mode: input.mode }); + if (inconclusive) incr("loopover_ai_review_inconclusive_total", { mode: input.mode }); const advisoryNotes = reviewsForNotes.length > 0 ? (composeAdvisoryNotes(reviewsForNotes) ?? composeFallbackAdvisoryNotes(fallbackNotes)) diff --git a/test/unit/adapters.test.ts b/test/unit/adapters.test.ts index 0d7b512f89..11523d6fec 100644 --- a/test/unit/adapters.test.ts +++ b/test/unit/adapters.test.ts @@ -206,7 +206,7 @@ describe("small adapters and normalizers", () => { // (invisible via structured logs, only found through this metric). await fetchPublicContributorProfile("dev", { GITHUB_PUBLIC_TOKEN: "public-token" }); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_github_rest_rate_limit_responses_total{key_scope="public"'); - expect(metrics).not.toContain('gittensory_github_rest_rate_limit_responses_total{key_scope="unknown"'); + expect(metrics).toContain('loopover_github_rest_rate_limit_responses_total{key_scope="public"'); + expect(metrics).not.toContain('loopover_github_rest_rate_limit_responses_total{key_scope="unknown"'); }); }); diff --git a/test/unit/agent-action-executor.test.ts b/test/unit/agent-action-executor.test.ts index 9cbe46fe6d..7253ff79d3 100644 --- a/test/unit/agent-action-executor.test.ts +++ b/test/unit/agent-action-executor.test.ts @@ -1110,8 +1110,8 @@ describe("executeAgentMaintenanceActions (#778 gate stack)", () => { expect(mergePullRequest).not.toHaveBeenCalled(); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_agent_action_permission_denied_total{actionClass="merge"} 2'); - expect(metrics).toContain('gittensory_agent_action_permission_denied_suppressed_total{actionClass="merge"} 1'); + expect(metrics).toContain('loopover_agent_action_permission_denied_total{actionClass="merge"} 2'); + expect(metrics).toContain('loopover_agent_action_permission_denied_suppressed_total{actionClass="merge"} 1'); }); it("resumes loud auditing once the cooldown window elapses", async () => { diff --git a/test/unit/ai-review.test.ts b/test/unit/ai-review.test.ts index 98e97f98f5..b2fab3a471 100644 --- a/test/unit/ai-review.test.ts +++ b/test/unit/ai-review.test.ts @@ -754,7 +754,7 @@ describe("runGittensoryAiReview block mode (consensus)", () => { expect(result.inconclusive).toBe(true); // FAIL-CLOSED: a missing second opinion holds the PR, never passes it expect(result.advisoryNotes).not.toBeNull(); // notes still come from the one parseable opinion // Observability (#2540): the single canonical increment fires once for this inconclusive review. - expect(await renderMetrics()).toContain('gittensory_ai_review_inconclusive_total{mode="block"} 1'); + expect(await renderMetrics()).toContain('loopover_ai_review_inconclusive_total{mode="block"} 1'); }); it("a clean dual review is NOT inconclusive (both models parsed, neither blocks → passes)", async () => { @@ -768,7 +768,7 @@ describe("runGittensoryAiReview block mode (consensus)", () => { expect(result.status === "ok" && result.consensusDefect).toBeNull(); expect(result.status === "ok" && result.inconclusive).toBe(false); // A non-inconclusive review must NOT increment the inconclusive counter. - expect(await renderMetrics()).not.toContain("gittensory_ai_review_inconclusive_total"); + expect(await renderMetrics()).not.toContain("loopover_ai_review_inconclusive_total"); }); it("block mode with BYOK: provider writes the advisory, the free Workers-AI pair drives consensus", async () => { @@ -1586,7 +1586,7 @@ describe("runGittensoryAiReview self-host dual-AI plan (#dual-ai-combiner)", () expect(result.consensusDefect?.title).toContain("Race condition"); expect(seen).toEqual(["codex", "codex", "codex", "claude-code"]); expect(await renderMetrics()).toContain( - 'gittensory_ai_review_model_fallback_total{fallback="claude-code",primary="codex"} 1', + 'loopover_ai_review_model_fallback_total{fallback="claude-code",primary="codex"} 1', ); }); @@ -1752,7 +1752,7 @@ describe("runGittensoryAiReview self-host dual-AI plan (#dual-ai-combiner)", () }); if (result.status !== "ok") throw new Error("expected ok"); expect(result.consensusDefect?.title).toContain("Lone blocker"); // "either" honored unchanged - expect(await renderMetrics()).not.toContain("gittensory_ai_review_onmerge_clamped_total"); // no clamp fired + expect(await renderMetrics()).not.toContain("loopover_ai_review_onmerge_clamped_total"); // no clamp fired }); it("a repo tightening either -> either against an either floor is a no-op, not a clamp", async () => { @@ -1771,7 +1771,7 @@ describe("runGittensoryAiReview self-host dual-AI plan (#dual-ai-combiner)", () }); if (result.status !== "ok") throw new Error("expected ok"); expect(result.consensusDefect?.title).toContain("Lone blocker"); - expect(await renderMetrics()).not.toContain("gittensory_ai_review_onmerge_clamped_total"); // not a clamp + expect(await renderMetrics()).not.toContain("loopover_ai_review_onmerge_clamped_total"); // not a clamp }); it("a repo attempting to LOOSEN either -> both against an either floor is clamped back to either, and it is metered (not silently ignored)", async () => { @@ -1797,7 +1797,7 @@ describe("runGittensoryAiReview self-host dual-AI plan (#dual-ai-combiner)", () expect(result.consensusDefect?.title).toContain("Lone blocker"); expect([...seen].sort()).toEqual(["claude-code", "codex"]); // Surfaced via a metric, not silently dropped. - expect(await renderMetrics()).toContain('gittensory_ai_review_onmerge_clamped_total{mode="block"} 1'); + expect(await renderMetrics()).toContain('loopover_ai_review_onmerge_clamped_total{mode="block"} 1'); }); it("a repo picking both against a both (or unset) operator floor is honored unclamped", async () => { @@ -1817,7 +1817,7 @@ describe("runGittensoryAiReview self-host dual-AI plan (#dual-ai-combiner)", () if (result.status !== "ok") throw new Error("expected ok"); // Under "both", a single reviewer's blocker does NOT decide the outcome on its own. expect(result.consensusDefect).toBeNull(); - expect(await renderMetrics()).not.toContain("gittensory_ai_review_onmerge_clamped_total"); + expect(await renderMetrics()).not.toContain("loopover_ai_review_onmerge_clamped_total"); }); it("a synthesis operator plan with no onMerge still clamps repo both against the implicit either floor", async () => { @@ -1836,7 +1836,7 @@ describe("runGittensoryAiReview self-host dual-AI plan (#dual-ai-combiner)", () }); if (result.status !== "ok") throw new Error("expected ok"); expect(result.consensusDefect?.title).toContain("Lone blocker"); - expect(await renderMetrics()).toContain('gittensory_ai_review_onmerge_clamped_total{mode="block"} 1'); + expect(await renderMetrics()).toContain('loopover_ai_review_onmerge_clamped_total{mode="block"} 1'); }); }); }); @@ -2319,7 +2319,7 @@ describe("pure helpers", () => { expect(primaryAttempts).toBe(3); expect(run).toHaveBeenCalledTimes(4); expect(await renderMetrics()).toContain( - 'gittensory_ai_review_model_fallback_total{fallback="fallback-model",primary="primary-model"} 1', + 'loopover_ai_review_model_fallback_total{fallback="fallback-model",primary="primary-model"} 1', ); expect(diagnostics.some((d) => d.status === "unparseable_output")).toBe(true); expect(diagnostics.some((d) => d.status === "parsed")).toBe(true); @@ -2558,7 +2558,7 @@ describe("pure helpers", () => { expect(result.split).toBe(true); expect(result.consensusDefect).toBeNull(); expect(await renderMetrics()).toContain( - 'gittensory_ai_review_tiebreak_order_unstable_total{mode="block"} 1', + 'loopover_ai_review_tiebreak_order_unstable_total{mode="block"} 1', ); expect(run).toHaveBeenCalledTimes(4); }); @@ -2594,7 +2594,7 @@ describe("pure helpers", () => { expect(result.split).toBe(false); expect(result.consensusDefect?.title).toContain("Null deref"); expect(await renderMetrics()).not.toContain( - "gittensory_ai_review_tiebreak_order_unstable_total", + "loopover_ai_review_tiebreak_order_unstable_total", ); expect(judgeCalls).toBe(2); }); @@ -2624,7 +2624,7 @@ describe("pure helpers", () => { if (result.status !== "ok") throw new Error("expected ok"); expect(result.split).toBe(true); expect(await renderMetrics()).not.toContain( - "gittensory_ai_review_tiebreak_order_unstable_total", + "loopover_ai_review_tiebreak_order_unstable_total", ); }); @@ -2764,7 +2764,7 @@ describe("pure helpers", () => { expect(result.split).toBe(true); expect(judgeCalls).toBe(2); expect(await renderMetrics()).not.toContain( - "gittensory_ai_review_tiebreak_order_unstable_total", + "loopover_ai_review_tiebreak_order_unstable_total", ); }); diff --git a/test/unit/alerts-job-failure-ratio-formula.test.ts b/test/unit/alerts-job-failure-ratio-formula.test.ts index 7b09b2c769..f41b17c849 100644 --- a/test/unit/alerts-job-failure-ratio-formula.test.ts +++ b/test/unit/alerts-job-failure-ratio-formula.test.ts @@ -2,8 +2,8 @@ import { readFileSync } from "node:fs"; import { parse as parseYaml } from "yaml"; import { describe, expect, it } from "vitest"; -// Regression test (#3892): GittensoryHighJobFailureRatio's expr used to divide failed-job rate by -// gittensory_jobs_processed_total alone. That metric only increments on SUCCESS (src/selfhost/pg-queue.ts, +// Regression test (#3892): LoopoverHighJobFailureRatio's expr used to divide failed-job rate by +// loopover_jobs_processed_total alone. That metric only increments on SUCCESS (src/selfhost/pg-queue.ts, // src/selfhost/sqlite-queue.ts), so the old expr computed failed:success, not a true failure percentage -- // at a genuine 50% failure rate it evaluated to 100%. This pins the corrected failed/(failed+processed) // shape (matching the Grafana "Job Failure Rate" panel's formula) so the bug can't silently return. @@ -30,13 +30,13 @@ function findAlert(name: string): AlertRule { throw new Error(`alert ${name} not found in prometheus/rules/alerts.yml`); } -describe("GittensoryHighJobFailureRatio alert formula (#3892)", () => { - const expr = findAlert("GittensoryHighJobFailureRatio").expr; +describe("LoopoverHighJobFailureRatio alert formula (#3892)", () => { + const expr = findAlert("LoopoverHighJobFailureRatio").expr; const flat = expr.replace(/\s+/g, " ").trim(); it("divides failed by (failed + processed), not by processed alone", () => { expect(flat).toMatch( - /sum\(rate\(gittensory_jobs_failed_total\[10m\]\)\) \/ \( sum\(rate\(gittensory_jobs_failed_total\[10m\]\)\) \+ sum\(rate\(gittensory_jobs_processed_total\[10m\]\)\) \) > 0/, + /sum\(rate\(loopover_jobs_failed_total\[10m\]\)\) \/ \( sum\(rate\(loopover_jobs_failed_total\[10m\]\)\) \+ sum\(rate\(loopover_jobs_processed_total\[10m\]\)\) \) > 0/, ); }); diff --git a/test/unit/ams-ledgers-datasource.test.ts b/test/unit/ams-ledgers-datasource.test.ts index f7380e84e5..d2441cd4d3 100644 --- a/test/unit/ams-ledgers-datasource.test.ts +++ b/test/unit/ams-ledgers-datasource.test.ts @@ -54,6 +54,6 @@ describe("AMS ledger Grafana datasources (#5184)", () => { expect(existingUids.has(ds.uid)).toBe(false); } // the maintainer LoopoverDB datasource still exists — this change added a file, it did not edit one - expect(existingUids.has("gittensory-db")).toBe(true); + expect(existingUids.has("loopover-db")).toBe(true); }); }); diff --git a/test/unit/backfill-2.test.ts b/test/unit/backfill-2.test.ts index e315d59ed4..28a17a9314 100644 --- a/test/unit/backfill-2.test.ts +++ b/test/unit/backfill-2.test.ts @@ -2206,7 +2206,7 @@ describe("GitHub backfill", () => { const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); vi.stubGlobal("fetch", async () => new Response("forbidden", { status: 403 })); expect(await fetchRequiredStatusContexts(env, "JSONbored/gittensory", "main", "public-token")).toBeNull(); - expect(await renderMetrics()).toContain("gittensory_github_branch_protection_permission_denied_total 1"); + expect(await renderMetrics()).toContain("loopover_github_branch_protection_permission_denied_total 1"); }); it("does not count a 404 (no branch protection configured) as permission-denied", async () => { @@ -2214,7 +2214,7 @@ describe("GitHub backfill", () => { const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); vi.stubGlobal("fetch", async () => new Response("not found", { status: 404 })); expect(await fetchRequiredStatusContexts(env, "JSONbored/gittensory", "main", "public-token")).toBeNull(); - expect(await renderMetrics()).not.toContain("gittensory_github_branch_protection_permission_denied_total"); + expect(await renderMetrics()).not.toContain("loopover_github_branch_protection_permission_denied_total"); }); it("does not count a genuinely rate-limited 403 (x-ratelimit-remaining: 0) as permission-denied", async () => { @@ -2229,7 +2229,7 @@ describe("GitHub backfill", () => { }), ); expect(await fetchRequiredStatusContexts(env, "JSONbored/gittensory", "main", "public-token")).toBeNull(); - expect(await renderMetrics()).not.toContain("gittensory_github_branch_protection_permission_denied_total"); + expect(await renderMetrics()).not.toContain("loopover_github_branch_protection_permission_denied_total"); }, 15_000); }); diff --git a/test/unit/backfill-file-hydration-scoping.test.ts b/test/unit/backfill-file-hydration-scoping.test.ts index 6924b49320..f5dff72695 100644 --- a/test/unit/backfill-file-hydration-scoping.test.ts +++ b/test/unit/backfill-file-hydration-scoping.test.ts @@ -451,11 +451,11 @@ describe("GitHub PR file hydration scoping (#audit-rate-headroom)", () => { await backfillRepositorySegment(env, { repoFullName: "JSONbored/gittensory", segment: "recent_merged_pull_requests", mode: "light" }); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_github_pull_request_files_fetch_total{caller="backfill_open_pr_details"} 1'); - expect(metrics).toContain('gittensory_github_pull_request_files_fetch_total{caller="live_review"} 1'); - expect(metrics).toContain('gittensory_github_pull_request_files_fetch_total{caller="backfill_merged_history"} 1'); + expect(metrics).toContain('loopover_github_pull_request_files_fetch_total{caller="backfill_open_pr_details"} 1'); + expect(metrics).toContain('loopover_github_pull_request_files_fetch_total{caller="live_review"} 1'); + expect(metrics).toContain('loopover_github_pull_request_files_fetch_total{caller="backfill_merged_history"} 1'); // Bounded: only the 3 known caller values appear, never a per-PR-number label. - const callerLines = metrics.split("\n").filter((line) => line.startsWith("gittensory_github_pull_request_files_fetch_total{")); + const callerLines = metrics.split("\n").filter((line) => line.startsWith("loopover_github_pull_request_files_fetch_total{")); expect(callerLines).toHaveLength(3); }); diff --git a/test/unit/backfill.test.ts b/test/unit/backfill.test.ts index 4743cff473..b819c52a34 100644 --- a/test/unit/backfill.test.ts +++ b/test/unit/backfill.test.ts @@ -700,7 +700,7 @@ describe("GitHub backfill", () => { }); // Never takes the local App-JWT path (which would 404 here and throw "credentials not configured"). expect(calls.some((url) => url.includes("/app/installations/"))).toBe(false); - expect(await renderMetrics()).toContain('gittensory_installation_health_broker_probe_total{result="ok"} 1'); + expect(await renderMetrics()).toContain('loopover_installation_health_broker_probe_total{result="ok"} 1'); // The persisted authMode round-trips as "broker" through the repository read path (getInstallationHealth), // not just the in-memory refresh result — mirrors the "local" round-trip check above. expect(await getInstallationHealth(env, 900)).toMatchObject({ authMode: "broker" }); @@ -762,7 +762,7 @@ describe("GitHub backfill", () => { expect(result.installations[0]?.status).toBe("needs_attention"); expect(result.installations[0]?.authMode).toBe("broker"); expect(result.installations[0]?.errorSummary).toMatch(/910/); - expect(await renderMetrics()).toContain('gittensory_installation_health_broker_probe_total{result="mismatched_installation"} 1'); + expect(await renderMetrics()).toContain('loopover_installation_health_broker_probe_total{result="mismatched_installation"} 1'); }); it("REGRESSION (gate finding): a broker-mode refresh preserves the previously-persisted missingPermissions/missingEvents instead of fabricating a clean []", async () => { @@ -819,7 +819,7 @@ describe("GitHub backfill", () => { expect(result.installations[0]?.authMode).toBe("broker"); expect(result.installations[0]?.errorSummary).toMatch(/token/i); expect(result.installations[0]?.errorSummary).not.toMatch(/GitHub App credentials are not configured/); - expect(await renderMetrics()).toContain('gittensory_installation_health_broker_probe_total{result="failed"} 1'); + expect(await renderMetrics()).toContain('loopover_installation_health_broker_probe_total{result="failed"} 1'); }); it("enrichInstallationHealth's broker branch reports introspection-unavailable remediation, not fabricated grants or gaps", () => { diff --git a/test/unit/dlq.test.ts b/test/unit/dlq.test.ts index f4d404b111..3d643908eb 100644 --- a/test/unit/dlq.test.ts +++ b/test/unit/dlq.test.ts @@ -87,7 +87,7 @@ describe("DLQ consumer (processDlqBatch)", () => { await expect(processDlqBatch(batch as unknown as MessageBatch, env)).resolves.toBeUndefined(); expect(batch.acked).toEqual(["msg-6"]); - expect(await renderMetrics()).toContain('gittensory_dlq_dead_lettered_total{jobType="unknown",redriven="false"} 1'); + expect(await renderMetrics()).toContain('loopover_dlq_dead_lettered_total{jobType="unknown",redriven="false"} 1'); }); it("is fail-safe when recordAuditEvent throws — the catch body runs and ack is not blocked", async () => { @@ -98,7 +98,7 @@ describe("DLQ consumer (processDlqBatch)", () => { await expect(processDlqBatch(batch as unknown as MessageBatch, brokenEnv)).resolves.toBeUndefined(); expect(batch.acked).toEqual(["msg-7"]); - expect(await renderMetrics()).toContain('gittensory_dlq_dead_lettered_total{jobType="github-webhook",redriven="false"} 1'); + expect(await renderMetrics()).toContain('loopover_dlq_dead_lettered_total{jobType="github-webhook",redriven="false"} 1'); }); describe("webhook self-heal re-drive (#1276)", () => { @@ -120,8 +120,8 @@ describe("DLQ consumer (processDlqBatch)", () => { expect(sent[0]).toMatchObject({ type: "github-webhook", deliveryId: "fresh-1", eventName: "pull_request", redriven: true }); expect(batch.acked).toEqual(["wh-1"]); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_dlq_dead_lettered_total{jobType="github-webhook",redriven="false"} 1'); - expect(metrics).toContain('gittensory_dlq_redriven_total{eventName="pull_request"} 1'); + expect(metrics).toContain('loopover_dlq_dead_lettered_total{jobType="github-webhook",redriven="false"} 1'); + expect(metrics).toContain('loopover_dlq_redriven_total{eventName="pull_request"} 1'); }); it("REGRESSION (idempotency): does NOT re-drive a webhook whose event row is already 'processed'", async () => { @@ -134,7 +134,7 @@ describe("DLQ consumer (processDlqBatch)", () => { expect(sent).toEqual([]); // already processed → no duplicate side effects expect(batch.acked).toEqual(["wh-2"]); - expect(await renderMetrics()).not.toContain("gittensory_dlq_redriven_total"); + expect(await renderMetrics()).not.toContain("loopover_dlq_redriven_total"); }); it("REGRESSION (metrics): failed webhook re-drive sends do not increment the redriven counter", async () => { @@ -150,8 +150,8 @@ describe("DLQ consumer (processDlqBatch)", () => { expect(batch.acked).toEqual(["wh-send-fail"]); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_dlq_dead_lettered_total{jobType="github-webhook",redriven="false"} 1'); - expect(metrics).not.toContain("gittensory_dlq_redriven_total"); + expect(metrics).toContain('loopover_dlq_dead_lettered_total{jobType="github-webhook",redriven="false"} 1'); + expect(metrics).not.toContain("loopover_dlq_redriven_total"); }); it("REGRESSION (metrics): missing webhook queue binding does not increment the redriven counter", async () => { @@ -163,8 +163,8 @@ describe("DLQ consumer (processDlqBatch)", () => { expect(batch.acked).toEqual(["wh-no-queue"]); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_dlq_dead_lettered_total{jobType="github-webhook",redriven="false"} 1'); - expect(metrics).not.toContain("gittensory_dlq_redriven_total"); + expect(metrics).toContain('loopover_dlq_dead_lettered_total{jobType="github-webhook",redriven="false"} 1'); + expect(metrics).not.toContain("loopover_dlq_redriven_total"); }); it("REGRESSION (no DLQ loop): does NOT re-drive a webhook that was already re-driven once", async () => { @@ -177,8 +177,8 @@ describe("DLQ consumer (processDlqBatch)", () => { expect(sent).toEqual([]); // bounded to a single re-drive expect(batch.acked).toEqual(["wh-3"]); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_dlq_dead_lettered_total{jobType="github-webhook",redriven="true"} 1'); - expect(metrics).not.toContain("gittensory_dlq_redriven_total"); + expect(metrics).toContain('loopover_dlq_dead_lettered_total{jobType="github-webhook",redriven="true"} 1'); + expect(metrics).not.toContain("loopover_dlq_redriven_total"); }); it("REGRESSION (no re-drive of maintenance jobs): a backfill job is audited and dropped, never re-driven", async () => { @@ -191,8 +191,8 @@ describe("DLQ consumer (processDlqBatch)", () => { expect(sent).toEqual([]); // cron self-heals maintenance jobs expect(batch.acked).toEqual(["mn-1"]); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_dlq_dead_lettered_total{jobType="backfill-repo-segment",redriven="false"} 1'); - expect(metrics).not.toContain("gittensory_dlq_redriven_total"); + expect(metrics).toContain('loopover_dlq_dead_lettered_total{jobType="backfill-repo-segment",redriven="false"} 1'); + expect(metrics).not.toContain("loopover_dlq_redriven_total"); }); it("does not re-drive webhook jobs when webhook redrive is disabled for broker-only Cloudflare", async () => { diff --git a/test/unit/docs-selfhost-troubleshooting-metric-names.test.ts b/test/unit/docs-selfhost-troubleshooting-metric-names.test.ts index 15ec7034e5..037b1c367a 100644 --- a/test/unit/docs-selfhost-troubleshooting-metric-names.test.ts +++ b/test/unit/docs-selfhost-troubleshooting-metric-names.test.ts @@ -9,7 +9,7 @@ import { describe, expect, it } from "vitest"; const DOC_PATH = "apps/gittensory-ui/src/routes/docs.self-hosting-troubleshooting.tsx"; const doc = readFileSync(DOC_PATH, "utf8"); -// The exact source files that emit every gittensory_*_total metric referenced in the runbooks, per an +// The exact source files that emit every loopover_*_total metric referenced in the runbooks, per an // audit against the real incr()/gauge()/observe() call sites (src/selfhost/metrics.ts's API). const METRIC_SOURCE_FILES = [ "src/github/client.ts", @@ -26,15 +26,15 @@ const metricSource = METRIC_SOURCE_FILES.map((path) => readFileSync(path, "utf8" const alertsSource = readFileSync("prometheus/rules/alerts.yml", "utf8"); describe("self-hosting-troubleshooting doc: metric/alert names match source (#1943)", () => { - it("every gittensory_..._total metric name referenced in the doc is actually emitted by the code", () => { - const names = [...new Set([...doc.matchAll(/gittensory_[a-z0-9_]+_total/g)].map((m) => m[0]))]; + it("every loopover_..._total metric name referenced in the doc is actually emitted by the code", () => { + const names = [...new Set([...doc.matchAll(/loopover_[a-z0-9_]+_total/g)].map((m) => m[0]))]; expect(names.length).toBeGreaterThan(5); // sanity: the extraction found the runbooks' real content const missing = names.filter((name) => !metricSource.includes(name)); expect(missing).toEqual([]); }); - it("every GittensoryXxx alert name referenced in the doc exists in prometheus/rules/alerts.yml", () => { - const names = [...new Set([...doc.matchAll(/Gittensory[A-Za-z]+/g)].map((m) => m[0]))]; + it("every LoopoverXxx alert name referenced in the doc exists in prometheus/rules/alerts.yml", () => { + const names = [...new Set([...doc.matchAll(/Loopover[A-Za-z]+/g)].map((m) => m[0]))]; expect(names.length).toBeGreaterThan(2); const missing = names.filter((name) => !alertsSource.includes(`alert: ${name}`)); expect(missing).toEqual([]); diff --git a/test/unit/enrichment-wire.test.ts b/test/unit/enrichment-wire.test.ts index 87772a8170..ae17a56315 100644 --- a/test/unit/enrichment-wire.test.ts +++ b/test/unit/enrichment-wire.test.ts @@ -683,8 +683,8 @@ describe("buildReviewEnrichment metrics recording (#5367)", () => { ) as unknown as typeof fetch; await buildReviewEnrichment(env({ REES_URL: "https://r" }), input); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_rees_enrich_requests_total{status="ok"} 1'); - expect(metrics).toContain("gittensory_rees_enrich_request_duration_seconds_count 1"); + expect(metrics).toContain('loopover_rees_enrich_requests_total{status="ok"} 1'); + expect(metrics).toContain("loopover_rees_enrich_request_duration_seconds_count 1"); }); it('records status="empty" when the response is 2xx but the brief has no usable promptSection', async () => { @@ -693,8 +693,8 @@ describe("buildReviewEnrichment metrics recording (#5367)", () => { ) as unknown as typeof fetch; await buildReviewEnrichment(env({ REES_URL: "https://r" }), input); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_rees_enrich_requests_total{status="empty"} 1'); - expect(metrics).toContain("gittensory_rees_enrich_request_duration_seconds_count 1"); + expect(metrics).toContain('loopover_rees_enrich_requests_total{status="empty"} 1'); + expect(metrics).toContain("loopover_rees_enrich_request_duration_seconds_count 1"); }); it('records status="http_error" on a non-2xx response', async () => { @@ -704,8 +704,8 @@ describe("buildReviewEnrichment metrics recording (#5367)", () => { ) as unknown as typeof fetch; await buildReviewEnrichment(env({ REES_URL: "https://r" }), input); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_rees_enrich_requests_total{status="http_error"} 1'); - expect(metrics).toContain("gittensory_rees_enrich_request_duration_seconds_count 1"); + expect(metrics).toContain('loopover_rees_enrich_requests_total{status="http_error"} 1'); + expect(metrics).toContain("loopover_rees_enrich_request_duration_seconds_count 1"); errSpy.mockRestore(); }); @@ -716,7 +716,7 @@ describe("buildReviewEnrichment metrics recording (#5367)", () => { }) as unknown as typeof fetch; await buildReviewEnrichment(env({ REES_URL: "https://r" }), input); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_rees_enrich_requests_total{status="timeout"} 1'); + expect(metrics).toContain('loopover_rees_enrich_requests_total{status="timeout"} 1'); expect(metrics).not.toContain('status="exception"'); errSpy.mockRestore(); }); @@ -728,7 +728,7 @@ describe("buildReviewEnrichment metrics recording (#5367)", () => { }) as unknown as typeof fetch; await buildReviewEnrichment(env({ REES_URL: "https://r" }), input); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_rees_enrich_requests_total{status="exception"} 1'); + expect(metrics).toContain('loopover_rees_enrich_requests_total{status="exception"} 1'); expect(metrics).not.toContain('status="timeout"'); errSpy.mockRestore(); }); @@ -743,9 +743,9 @@ describe("buildReviewEnrichment metrics recording (#5367)", () => { await buildReviewEnrichment(env({ REES_URL: "https://rees.example", REES_SHARED_SECRET: "s3cret" }), input); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_rees_enrich_requests_total{status="skipped_auth_rejected"} 1'); + expect(metrics).toContain('loopover_rees_enrich_requests_total{status="skipped_auth_rejected"} 1'); // No network attempt was made, so no duration sample -- the histogram must not appear at all. - expect(metrics).not.toContain("gittensory_rees_enrich_request_duration_seconds"); + expect(metrics).not.toContain("loopover_rees_enrich_request_duration_seconds"); warnSpy.mockRestore(); errSpy.mockRestore(); }); @@ -753,8 +753,8 @@ describe("buildReviewEnrichment metrics recording (#5367)", () => { it("never records any outcome when REES_URL is unset (not a real attempt)", async () => { await buildReviewEnrichment(env({}), input); const metrics = await renderMetrics(); - expect(metrics).not.toContain("gittensory_rees_enrich_requests_total"); - expect(metrics).not.toContain("gittensory_rees_enrich_request_duration_seconds"); + expect(metrics).not.toContain("loopover_rees_enrich_requests_total"); + expect(metrics).not.toContain("loopover_rees_enrich_request_duration_seconds"); }); }); diff --git a/test/unit/github-client.test.ts b/test/unit/github-client.test.ts index 816e5eb1e4..3f79dd8b7b 100644 --- a/test/unit/github-client.test.ts +++ b/test/unit/github-client.test.ts @@ -263,7 +263,7 @@ describe("timeoutFetch", () => { resetAt: "2026-06-24T12:10:00.000Z", observedAtMs: now, }); - expect(await renderMetrics()).toContain('gittensory_github_rest_rate_limit_observations_total{key_scope="installation",remaining_bucket="1-75"} 1'); + expect(await renderMetrics()).toContain('loopover_github_rest_rate_limit_observations_total{key_scope="installation",remaining_bucket="1-75"} 1'); expect(latestGitHubRestRateLimitObservation(otherKey)).toBeNull(); headers = new Headers({ @@ -333,10 +333,10 @@ describe("timeoutFetch", () => { }); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_github_rest_rate_limit_observations_total{key_scope="installation",remaining_bucket="0"} 1'); - expect(metrics).toContain('gittensory_github_rest_rate_limit_observations_total{key_scope="installation",remaining_bucket="1-75"} 1'); - expect(metrics).toContain('gittensory_github_rest_rate_limit_observations_total{key_scope="other",remaining_bucket="76-150"} 1'); - expect(metrics).toContain('gittensory_github_rest_rate_limit_observations_total{key_scope="other",remaining_bucket="151+"} 1'); + expect(metrics).toContain('loopover_github_rest_rate_limit_observations_total{key_scope="installation",remaining_bucket="0"} 1'); + expect(metrics).toContain('loopover_github_rest_rate_limit_observations_total{key_scope="installation",remaining_bucket="1-75"} 1'); + expect(metrics).toContain('loopover_github_rest_rate_limit_observations_total{key_scope="other",remaining_bucket="76-150"} 1'); + expect(metrics).toContain('loopover_github_rest_rate_limit_observations_total{key_scope="other",remaining_bucket="151+"} 1'); }); it("labels public-token REST observations separately from installation and unknown buckets", async () => { @@ -359,7 +359,7 @@ describe("timeoutFetch", () => { }); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_github_rest_rate_limit_observations_total{key_scope="public",remaining_bucket="1-75"} 1'); + expect(metrics).toContain('loopover_github_rest_rate_limit_observations_total{key_scope="public",remaining_bucket="1-75"} 1'); expect(latestGitHubRestRateLimitObservation(githubRateLimitAdmissionKeyForPublicToken())).toMatchObject({ remaining: 22, resetAt: "2026-06-24T12:10:00.000Z", @@ -417,9 +417,9 @@ describe("timeoutFetch", () => { expect(getFetches).toBe(1); expect([...store.keys()].some((url) => url.endsWith("/repos/o/r"))).toBe(true); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_github_response_cache_total{class="metadata",result="miss"} 1'); - expect(metrics).toContain('gittensory_github_response_cache_total{class="metadata",result="hit"} 1'); - expect(metrics).toContain('gittensory_github_response_cache_total{class="metadata",result="set"} 1'); + expect(metrics).toContain('loopover_github_response_cache_total{class="metadata",result="miss"} 1'); + expect(metrics).toContain('loopover_github_response_cache_total{class="metadata",result="hit"} 1'); + expect(metrics).toContain('loopover_github_response_cache_total{class="metadata",result="set"} 1'); }); it("single-flights concurrent cacheable Octokit GET misses before Redis is warm", async () => { @@ -461,7 +461,7 @@ describe("timeoutFetch", () => { expect.objectContaining({ data: { contexts: ["ci"] } }), ]); expect(getFetches).toBe(1); - expect(await renderMetrics()).toContain('gittensory_github_response_cache_total{class="branch_protection",result="coalesced"} 1'); + expect(await renderMetrics()).toContain('loopover_github_response_cache_total{class="branch_protection",result="coalesced"} 1'); }); it("keys safe GitHub GETs by auth identity and response-shaping headers without storing the token", async () => { @@ -697,8 +697,8 @@ describe("timeoutFetch", () => { expect(getFetches).toBe(4); expect(set).not.toHaveBeenCalled(); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_github_rest_rate_limit_responses_total{key_scope="unknown",retry="scheduled",status="403"} 3'); - expect(metrics).toContain('gittensory_github_rest_rate_limit_responses_total{key_scope="unknown",retry="exhausted",status="403"} 1'); + expect(metrics).toContain('loopover_github_rest_rate_limit_responses_total{key_scope="unknown",retry="scheduled",status="403"} 3'); + expect(metrics).toContain('loopover_github_rest_rate_limit_responses_total{key_scope="unknown",retry="exhausted",status="403"} 1'); }); it("does not negative-cache stable metadata denials outside branch protection", async () => { @@ -823,7 +823,7 @@ describe("timeoutFetch", () => { } expect(getFetches).toBe(decisionCases.length * 2); - expect(await renderMetrics()).toContain(`gittensory_github_response_cache_total{class="sensitive",result="bypassed"} ${decisionCases.length * 2}`); + expect(await renderMetrics()).toContain(`loopover_github_response_cache_total{class="sensitive",result="bypassed"} ${decisionCases.length * 2}`); }); it("bypasses mutable PR and issue subresources instead of replaying stale coordination state", async () => { @@ -860,7 +860,7 @@ describe("timeoutFetch", () => { } expect(getFetches).toBe(mutableCases.length * 2); - expect(await renderMetrics()).toContain(`gittensory_github_response_cache_total{class="sensitive",result="bypassed"} ${mutableCases.length * 2}`); + expect(await renderMetrics()).toContain(`loopover_github_response_cache_total{class="sensitive",result="bypassed"} ${mutableCases.length * 2}`); }); it("single-flights concurrent mutable GitHub GETs without persisting them in Redis", async () => { @@ -907,8 +907,8 @@ describe("timeoutFetch", () => { expect(cacheGet).not.toHaveBeenCalled(); expect(cacheSet).not.toHaveBeenCalled(); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_github_response_cache_total{class="sensitive",result="bypassed"} 2'); - expect(metrics).toContain('gittensory_github_response_cache_total{class="sensitive",result="coalesced"} 1'); + expect(metrics).toContain('loopover_github_response_cache_total{class="sensitive",result="bypassed"} 2'); + expect(metrics).toContain('loopover_github_response_cache_total{class="sensitive",result="coalesced"} 1'); }); it("single-flights concurrent mutable GitHub GETs even when Redis is disabled", async () => { @@ -1131,7 +1131,7 @@ describe("timeoutFetch", () => { await expect(second).rejects.toThrow("caller aborted"); releaseFetch(); await expect(first.then((response) => response.json())).resolves.toEqual({ state: "live" }); - expect(await renderMetrics()).toContain('gittensory_github_response_cache_total{class="sensitive",result="coalesced"} 2'); + expect(await renderMetrics()).toContain('loopover_github_response_cache_total{class="sensitive",result="coalesced"} 2'); }); it("bypasses conditional GitHub GETs so validator headers keep shaping the live response", async () => { @@ -1149,7 +1149,7 @@ describe("timeoutFetch", () => { expect(await first.json()).toEqual({ fetches: 1 }); expect(await second.json()).toEqual({ fetches: 2 }); expect(store.size).toBe(0); - expect(await renderMetrics()).toContain('gittensory_github_response_cache_total{class="conditional",result="bypassed"} 2'); + expect(await renderMetrics()).toContain('loopover_github_response_cache_total{class="conditional",result="bypassed"} 2'); }); it("normalizes Request inputs for GitHub cache detection and auth-aware keys", async () => { @@ -1302,7 +1302,7 @@ describe("timeoutFetch", () => { expect(cacheReads).toBe(1); expect(cacheWrites).toBe(1); expect(getFetches).toBe(1); - expect(await renderMetrics()).toContain('gittensory_github_response_cache_total{class="metadata",result="error"} 2'); + expect(await renderMetrics()).toContain('loopover_github_response_cache_total{class="metadata",result="error"} 2'); }); it("counts bypassed non-GET, non-GitHub, and sensitive GitHub requests", async () => { @@ -1317,9 +1317,9 @@ describe("timeoutFetch", () => { await timeoutFetch("https://api.github.com/repos/o/r/collaborators/alice/permission"); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_github_response_cache_total{class="non_get",result="bypassed"} 1'); - expect(metrics).toContain('gittensory_github_response_cache_total{class="non_github",result="bypassed"} 1'); - expect(metrics).toContain('gittensory_github_response_cache_total{class="sensitive",result="bypassed"} 1'); + expect(metrics).toContain('loopover_github_response_cache_total{class="non_get",result="bypassed"} 1'); + expect(metrics).toContain('loopover_github_response_cache_total{class="non_github",result="bypassed"} 1'); + expect(metrics).toContain('loopover_github_response_cache_total{class="sensitive",result="bypassed"} 1'); }); }); diff --git a/test/unit/github-graphql-cache.test.ts b/test/unit/github-graphql-cache.test.ts index ab26e66d8b..415a1c2f65 100644 --- a/test/unit/github-graphql-cache.test.ts +++ b/test/unit/github-graphql-cache.test.ts @@ -232,7 +232,7 @@ describe("fetchCachedGitHubGraphQl", () => { await fetchCachedGitHubGraphQl(MUTABLE_QUERY, "token-a"); expect(fetches).toBe(2); - expect(await renderMetrics()).toContain('gittensory_github_graphql_cache_total{class="sensitive",result="bypassed"}'); + expect(await renderMetrics()).toContain('loopover_github_graphql_cache_total{class="sensitive",result="bypassed"}'); }); it("does not cache non-200 GraphQL responses", async () => { @@ -256,7 +256,7 @@ describe("fetchCachedGitHubGraphQl", () => { expect(fetches).toBe(2); expect(store.size).toBe(0); - expect(await renderMetrics()).not.toContain('gittensory_github_graphql_cache_total{class="repo_totals",result="set"}'); + expect(await renderMetrics()).not.toContain('loopover_github_graphql_cache_total{class="repo_totals",result="set"}'); }); it("treats cached GraphQL error envelopes as a miss on replay", async () => { @@ -295,7 +295,7 @@ describe("fetchCachedGitHubGraphQl", () => { const response = await fetchCachedGitHubGraphQl(TOTALS_QUERY, "token-a"); expect(response.ok).toBe(true); expect(fetches).toBe(1); - expect(await renderMetrics()).toContain('gittensory_github_graphql_cache_total{class="repo_totals",result="error"}'); + expect(await renderMetrics()).toContain('loopover_github_graphql_cache_total{class="repo_totals",result="error"}'); }); it("fail-opens on cache read errors and still fetches upstream", async () => { @@ -314,7 +314,7 @@ describe("fetchCachedGitHubGraphQl", () => { const response = await fetchCachedGitHubGraphQl(TOTALS_QUERY, "token-a"); expect(response.ok).toBe(true); expect(fetches).toBe(1); - expect(await renderMetrics()).toContain('gittensory_github_graphql_cache_total{class="repo_totals",result="error"}'); + expect(await renderMetrics()).toContain('loopover_github_graphql_cache_total{class="repo_totals",result="error"}'); }); it("treats malformed cached payloads as a miss", async () => { @@ -374,7 +374,7 @@ describe("fetchCachedGitHubGraphQl", () => { expect(second.ok).toBe(true); expect(fetches).toBe(2); expect(setCalls).toBeGreaterThanOrEqual(2); - expect(await renderMetrics()).toContain('gittensory_github_graphql_cache_total{class="repo_totals",result="coalesced"}'); + expect(await renderMetrics()).toContain('loopover_github_graphql_cache_total{class="repo_totals",result="coalesced"}'); }); it("surfaces upstream fetch failures from the cache path", async () => { @@ -410,7 +410,7 @@ describe("fetchCachedGitHubGraphQl", () => { await fetchCachedGitHubGraphQl(TOTALS_QUERY, "token-a"); expect(fetches).toBe(2); - expect(await renderMetrics()).toContain('gittensory_github_graphql_cache_total{class="repo_totals",result="bypassed"}'); + expect(await renderMetrics()).toContain('loopover_github_graphql_cache_total{class="repo_totals",result="bypassed"}'); }); }); diff --git a/test/unit/grounding-wiring.test.ts b/test/unit/grounding-wiring.test.ts index 65ef71d320..2daae32b56 100644 --- a/test/unit/grounding-wiring.test.ts +++ b/test/unit/grounding-wiring.test.ts @@ -428,8 +428,8 @@ describe("makeGithubFileFetcher (GitHub Contents-API-backed FileFetcher)", () => expect(second).toBe("export const v = 1;"); const rendered = await renderMetrics(); - expect(rendered).toContain("gittensory_grounding_cache_hit_total 1"); - expect(rendered).not.toContain("gittensory_grounding_cache_miss_total"); + expect(rendered).toContain("loopover_grounding_cache_hit_total 1"); + expect(rendered).not.toContain("loopover_grounding_cache_miss_total"); const hitEvent = await auditEvent(env, "github_app.grounding_cache_hit", "acme/telemetry"); expect(hitEvent?.outcome).toBe("completed"); expect(await auditEvent(env, "github_app.grounding_cache_miss", "acme/telemetry")).toBeUndefined(); @@ -444,8 +444,8 @@ describe("makeGithubFileFetcher (GitHub Contents-API-backed FileFetcher)", () => expect(first).toBe("export const v = 1;"); const rendered = await renderMetrics(); - expect(rendered).toContain("gittensory_grounding_cache_miss_total 1"); - expect(rendered).not.toContain("gittensory_grounding_cache_hit_total"); + expect(rendered).toContain("loopover_grounding_cache_miss_total 1"); + expect(rendered).not.toContain("loopover_grounding_cache_hit_total"); const missEvent = await auditEvent(env, "github_app.grounding_cache_miss", "acme/telemetry"); expect(missEvent?.outcome).toBe("completed"); expect(await auditEvent(env, "github_app.grounding_cache_hit", "acme/telemetry")).toBeUndefined(); diff --git a/test/unit/impact-map.test.ts b/test/unit/impact-map.test.ts index 17055ae02f..00b01ea7dd 100644 --- a/test/unit/impact-map.test.ts +++ b/test/unit/impact-map.test.ts @@ -456,8 +456,8 @@ describe("computeImpactMap", () => { await computeImpactMap(env, symbols, { infra, project: "acme", repo: "telemetry-widgets" }); // same query -- a hit const rendered = await renderMetrics(); - expect(rendered).toContain("gittensory_impact_map_cache_hit_total 1"); - expect(rendered).not.toContain("gittensory_impact_map_cache_miss_total"); + expect(rendered).toContain("loopover_impact_map_cache_hit_total 1"); + expect(rendered).not.toContain("loopover_impact_map_cache_miss_total"); const hitEvent = await auditEvent(env, "github_app.impact_map_cache_hit", "acme/telemetry-widgets"); expect(hitEvent?.outcome).toBe("completed"); expect(await auditEvent(env, "github_app.impact_map_cache_miss", "acme/telemetry-widgets")).toBeUndefined(); @@ -472,8 +472,8 @@ describe("computeImpactMap", () => { await computeImpactMap(env, symbols, { infra, project: "acme", repo: "telemetry-widgets-2" }); const rendered = await renderMetrics(); - expect(rendered).toContain("gittensory_impact_map_cache_miss_total 1"); - expect(rendered).not.toContain("gittensory_impact_map_cache_hit_total"); + expect(rendered).toContain("loopover_impact_map_cache_miss_total 1"); + expect(rendered).not.toContain("loopover_impact_map_cache_hit_total"); const missEvent = await auditEvent(env, "github_app.impact_map_cache_miss", "acme/telemetry-widgets-2"); expect(missEvent?.outcome).toBe("completed"); expect(await auditEvent(env, "github_app.impact_map_cache_hit", "acme/telemetry-widgets-2")).toBeUndefined(); diff --git a/test/unit/ops-wire.test.ts b/test/unit/ops-wire.test.ts index 086d468302..ba20bb6b0d 100644 --- a/test/unit/ops-wire.test.ts +++ b/test/unit/ops-wire.test.ts @@ -313,7 +313,7 @@ describe("runOpsAlerts — cron path over gittensory's outcome data", () => { expect(row?.anomalies.some((a) => /review burst/.test(a))).toBe(true); // #ops-anomaly-metric: the Prometheus counterpart to the log line, labeled by kind (self-host mode preserves // the repo label so the assertion can target the exact series without relying on cloud-worker redaction). - expect(counterValue("gittensory_ops_anomaly_total", { repo: "owner/repo", kind: "review_burst" })).toBe(1); + expect(counterValue("loopover_ops_anomaly_total", { repo: "owner/repo", kind: "review_burst" })).toBe(1); }); it("detects and reports a review FAILURE burst end-to-end -- reproduces the #3747 incident shape (repeated inconclusive calls, zero publishes) (#review-burst-blind-spot)", async () => { diff --git a/test/unit/orb-broker-client.test.ts b/test/unit/orb-broker-client.test.ts index 15207e03c6..2f4cda2945 100644 --- a/test/unit/orb-broker-client.test.ts +++ b/test/unit/orb-broker-client.test.ts @@ -383,7 +383,7 @@ describe("drainOrbRelay (pull-mode drain)", () => { expect(calls[0]?.url).toBe("https://gittensory-api.aethereal.dev/v1/orb/relay/pull"); expect((calls[0]?.init?.headers as Record).authorization).toBe("Bearer s"); expect(JSON.parse(String(calls[0]?.init?.body))).toEqual({ ack: ["prev-1"] }); - expect(counterValue("gittensory_orb_relay_malformed_events_total")).toBe(1); + expect(counterValue("loopover_orb_relay_malformed_events_total")).toBe(1); const logged = errors.mock.calls.map((c) => String(c[0])).find((line) => line.includes("orb_relay_malformed_event_dropped")); expect(logged).toBeDefined(); expect(JSON.parse(logged!)).toMatchObject({ level: "error", event: "orb_relay_malformed_event_dropped", hasDeliveryId: true, hasEventName: true, hasRawBody: false }); diff --git a/test/unit/pr-detail-durable-cache.test.ts b/test/unit/pr-detail-durable-cache.test.ts index 43585d3510..fc179a55f9 100644 --- a/test/unit/pr-detail-durable-cache.test.ts +++ b/test/unit/pr-detail-durable-cache.test.ts @@ -530,9 +530,9 @@ describe("durable PR-state cache (#2537)", () => { await cachedFetchLivePullRequestMergeState(env, "owner/repo", 70, "tok"); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_pr_state_cache_total{field="mergeable_state",result="miss"} 1'); - expect(metrics).toContain('gittensory_pr_state_cache_total{field="mergeable_state",result="hit"} 1'); - expect(metrics).toContain('gittensory_pr_state_cache_total{field="write",result="set"} 1'); + expect(metrics).toContain('loopover_pr_state_cache_total{field="mergeable_state",result="miss"} 1'); + expect(metrics).toContain('loopover_pr_state_cache_total{field="mergeable_state",result="hit"} 1'); + expect(metrics).toContain('loopover_pr_state_cache_total{field="write",result="set"} 1'); }); }); }); @@ -760,7 +760,7 @@ describe("durable CI-state cache (#selfhost-ci-verification)", () => { resetMetrics(); const env = createTestEnv(); await writeThroughCiStateCache(env, "owner/repo", 84, null, "sha1", "", sampleAggregate); - expect(await renderMetrics()).toContain('gittensory_ci_state_cache_total{field="write",result="set"} 1'); + expect(await renderMetrics()).toContain('loopover_ci_state_cache_total{field="write",result="set"} 1'); }); }); diff --git a/test/unit/pr-reconciliation.test.ts b/test/unit/pr-reconciliation.test.ts index ce19bb1637..d96df9915f 100644 --- a/test/unit/pr-reconciliation.test.ts +++ b/test/unit/pr-reconciliation.test.ts @@ -36,7 +36,7 @@ describe("runOpenPrReconciliation (#audit-open-pr-reconciliation)", () => { const found = await runOpenPrReconciliation(env); expect(found).toEqual([{ repoFullName: "owner/lost-repo", remoteOpenCount: 1, localOpenCount: 0, missingNumbers: [7] }]); - expect(counterValue("gittensory_open_pr_reconciliation_missing_total", { repo: "owner/lost-repo" })).toBe(1); + expect(counterValue("loopover_open_pr_reconciliation_missing_total", { repo: "owner/lost-repo" })).toBe(1); const logged = errors.mock.calls.map((c) => String(c[0])).find((line) => line.includes("open_pr_reconciliation_divergence")); expect(logged).toBeDefined(); expect(JSON.parse(logged!)).toMatchObject({ level: "error", event: "open_pr_reconciliation_divergence", repository: "owner/lost-repo", missingNumbers: [7] }); diff --git a/test/unit/queue-lifecycle-guards.test.ts b/test/unit/queue-lifecycle-guards.test.ts index ea38a1945a..5695aeb605 100644 --- a/test/unit/queue-lifecycle-guards.test.ts +++ b/test/unit/queue-lifecycle-guards.test.ts @@ -4232,7 +4232,7 @@ describe("auto-action convergence: end-to-end plan+execute for the general heuri // #terminal-outcome-audit: the disposition counter's "close" action_class, with the actual gate-blocker // code (missing_linked_issue, from the default linkedIssueGateMode:block + no-linked-issue body) as the // bounded blocker_class -- proof this reaches the real gate.blockers, not just a hardcoded label. - expect(await renderMetrics()).toContain('gittensory_agent_disposition_total{action_class="close",autonomy_level="auto",blocker_class="missing_linked_issue",repo="redacted-1"} 1'); + expect(await renderMetrics()).toContain('loopover_agent_disposition_total{action_class="close",autonomy_level="auto",blocker_class="missing_linked_issue",repo="redacted-1"} 1'); const nativeDecision = await env.DB.prepare("select decision, summary, source from review_audit where event_type = 'gate_decision' and target_id = ?").bind(`${REPO}#60`).first<{ decision: string; summary: string; source: string }>(); expect(nativeDecision).toMatchObject({ decision: "close", summary: "missing_linked_issue", source: "gittensory-native" }); }); @@ -4275,7 +4275,7 @@ describe("auto-action convergence: end-to-end plan+execute for the general heuri expect(seen.merged).toBe(false); expect(seen.closed).toBe(false); - expect(await renderMetrics()).toContain('gittensory_agent_disposition_total{action_class="hold",autonomy_level="auto",blocker_class="guardrail_hold",repo="redacted-1"} 1'); + expect(await renderMetrics()).toContain('loopover_agent_disposition_total{action_class="hold",autonomy_level="auto",blocker_class="guardrail_hold",repo="redacted-1"} 1'); const holdAudit = await env.DB.prepare("select metadata_json from audit_events where event_type = 'agent.action.hold' order by created_at desc limit 1").first<{ metadata_json: string }>(); expect(JSON.parse(holdAudit?.metadata_json ?? "{}")).toMatchObject({ repoFullName: REPO, @@ -4403,12 +4403,12 @@ describe("auto-action convergence: end-to-end plan+execute for the general heuri const mergeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.merge'").first<{ n: number }>(); expect(mergeAudit?.n).toBeGreaterThanOrEqual(1); // #terminal-outcome-audit: the disposition counter's "merge" action_class, on the actual live call site. - expect(await renderMetrics()).toContain('gittensory_agent_disposition_total{action_class="merge",autonomy_level="auto",blocker_class="none",repo="redacted-1"} 1'); + expect(await renderMetrics()).toContain('loopover_agent_disposition_total{action_class="merge",autonomy_level="auto",blocker_class="none",repo="redacted-1"} 1'); }); // #terminal-outcome-audit: end-to-end proof that the LIVE runAgentMaintenancePlanAndExecute call site (not just // the extracted pure precisionBreakerDowngradeDirections/applyPrecisionBreakers unit tests) actually increments - // gittensory_precision_breaker_downgrades_total when an engaged accuracy circuit-breaker rewrites a real plan. + // loopover_precision_breaker_downgrades_total when an engaged accuracy circuit-breaker rewrites a real plan. it("REGRESSION (#terminal-outcome-audit): an engaged holdonly breaker withholds a real would-merge AND increments the downgrade counter", async () => { // Mirrors the "#selfhost-backlog-convergence" chain test above (same two-step CI-pending-then-green shape, // the proven way this suite reaches a REAL merge attempt): a plain "opened" webhook with CI already green @@ -4472,12 +4472,12 @@ describe("auto-action convergence: end-to-end plan+execute for the general heuri expect(seen.merged).toBe(false); const mergeAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = 'agent.action.merge'").first<{ n: number }>(); expect(mergeAudit?.n).toBe(0); - expect(await renderMetrics()).toContain('gittensory_precision_breaker_downgrades_total{direction="merge"} 1'); + expect(await renderMetrics()).toContain('loopover_precision_breaker_downgrades_total{direction="merge"} 1'); // #terminal-outcome-audit: the ALWAYS-recorded disposition counter, placed before the "nothing was planned" // early return -- this is the exact "hold, but no audit_events row at all" shape (the breaker downgrade // leaves no merge/close action) that previously had zero aggregate signal. close autonomy is unset in this // repo's settings (only merge/approve are configured), so it resolves to the default "observe". - expect(await renderMetrics()).toContain('gittensory_agent_disposition_total{action_class="hold",autonomy_level="observe",blocker_class="none",repo="redacted-1"} 1'); + expect(await renderMetrics()).toContain('loopover_agent_disposition_total{action_class="hold",autonomy_level="observe",blocker_class="none",repo="redacted-1"} 1'); const holdAudit = await env.DB.prepare("select detail, metadata_json from audit_events where event_type = 'agent.action.hold' order by created_at desc limit 1").first<{ detail: string; metadata_json: string }>(); expect(holdAudit?.detail).toBe("auto-action held by precision circuit breaker"); expect(JSON.parse(holdAudit?.metadata_json ?? "{}")).toMatchObject({ diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index 4c9475b52d..f1c415ca34 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -2551,7 +2551,7 @@ describe("queue processors", () => { ).resolves.toBeUndefined(); expect(liveCiSpy).toHaveBeenCalled(); // No "hit" recorded -- the corrupted row was NOT trusted; the live-fetched aggregate overwrote it. - expect(await renderMetrics()).not.toContain('gittensory_ci_state_cache_total{field="aggregate",result="hit"}'); + expect(await renderMetrics()).not.toContain('loopover_ci_state_cache_total{field="aggregate",result="hit"}'); expect(await getPullRequestDetailSyncState(env, "owner/agent-repo", 7)).toMatchObject({ ciState: "failed", ciFailingDetailsJson: "[]" }); } finally { liveCiSpy.mockRestore(); @@ -2588,8 +2588,8 @@ describe("queue processors", () => { await processJob(env, { type: "agent-regate-pr", deliveryId: "cross-job-pass-1", repoFullName: "owner/agent-repo", prNumber: 7, installationId: 9001 }); const callsAfterPass1 = liveCiSpy.mock.calls.length; expect(callsAfterPass1).toBeGreaterThan(0); - expect(await renderMetrics()).toContain('gittensory_ci_state_cache_total{field="aggregate",result="miss"} 1'); - expect(await renderMetrics()).toContain('gittensory_ci_state_cache_total{field="aggregate",result="forced"} 1'); + expect(await renderMetrics()).toContain('loopover_ci_state_cache_total{field="aggregate",result="miss"} 1'); + expect(await renderMetrics()).toContain('loopover_ci_state_cache_total{field="aggregate",result="forced"} 1'); // Pass 2: SAME PR, SAME head_sha, no invalidating webhook in between. Readiness's cachedLiveCiAggregate // now HITS the row pass 1's disposition planner wrote through -- one fewer live call than pass 1, even @@ -2597,9 +2597,9 @@ describe("queue processors", () => { await processJob(env, { type: "agent-regate-pr", deliveryId: "cross-job-pass-2", repoFullName: "owner/agent-repo", prNumber: 7, installationId: 9001 }); const callsDuringPass2 = liveCiSpy.mock.calls.length - callsAfterPass1; expect(callsDuringPass2).toBeLessThan(callsAfterPass1); - expect(await renderMetrics()).toContain('gittensory_ci_state_cache_total{field="aggregate",result="hit"} 1'); + expect(await renderMetrics()).toContain('loopover_ci_state_cache_total{field="aggregate",result="hit"} 1'); // The disposition planner's forced refresh fired again on pass 2 too (now 2 total across both passes). - expect(await renderMetrics()).toContain('gittensory_ci_state_cache_total{field="aggregate",result="forced"} 2'); + expect(await renderMetrics()).toContain('loopover_ci_state_cache_total{field="aggregate",result="forced"} 2'); } finally { liveCiSpy.mockRestore(); requiredContextsSpy.mockRestore(); @@ -2631,7 +2631,7 @@ describe("queue processors", () => { try { resetMetrics(); await processJob(env, { type: "agent-regate-pr", deliveryId: "invalidate-pass-1", repoFullName: "owner/agent-repo", prNumber: 7, installationId: 9001 }); - expect(await renderMetrics()).toContain('gittensory_ci_state_cache_total{field="aggregate",result="miss"} 1'); + expect(await renderMetrics()).toContain('loopover_ci_state_cache_total{field="aggregate",result="miss"} 1'); // The durable row now has a fresh ciState, well within the 60s TTL. expect(await getPullRequestDetailSyncState(env, "owner/agent-repo", 7)).toMatchObject({ ciState: "passed" }); @@ -2658,7 +2658,7 @@ describe("queue processors", () => { // A subsequent readiness check misses again -- proving invalidation, not just a coincidental TTL expiry. resetMetrics(); await processJob(env, { type: "agent-regate-pr", deliveryId: "invalidate-pass-2", repoFullName: "owner/agent-repo", prNumber: 7, installationId: 9001 }); - expect(await renderMetrics()).toContain('gittensory_ci_state_cache_total{field="aggregate",result="miss"} 1'); + expect(await renderMetrics()).toContain('loopover_ci_state_cache_total{field="aggregate",result="miss"} 1'); } finally { liveCiSpy.mockRestore(); requiredContextsSpy.mockRestore(); @@ -2760,7 +2760,7 @@ describe("queue processors", () => { resetMetrics(); await processJob(env, { type: "agent-regate-pr", deliveryId: "required-contexts-lookup-recovers", repoFullName: "owner/agent-repo", prNumber: 7, installationId: 9001 }); expect(liveCiSpy.mock.calls.length).toBeGreaterThan(liveReadsAfterFailedLookup); - expect(await renderMetrics()).toContain('gittensory_ci_state_cache_total{field="aggregate",result="miss"} 1'); + expect(await renderMetrics()).toContain('loopover_ci_state_cache_total{field="aggregate",result="miss"} 1'); expect(await getPullRequestDetailSyncState(env, "owner/agent-repo", 7)).toMatchObject({ ciState: "passed", ciRequiredContextsKey: JSON.stringify(["trusted-required-ci"]) }); } finally { liveCiSpy.mockRestore(); diff --git a/test/unit/rag-index.test.ts b/test/unit/rag-index.test.ts index cb4b1e7894..54d919bb77 100644 --- a/test/unit/rag-index.test.ts +++ b/test/unit/rag-index.test.ts @@ -336,7 +336,7 @@ describe("indexRepo: full repo index (tree → chunk → embed → upsert)", () await expect(indexRepo(throwingEnv as typeof env, PROJECT, REPO)).resolves.toEqual({ indexed: 0, files: 0, capped: false }); const parsed = errSpy.mock.calls.map((c) => JSON.parse(c[0] as string)); expect(parsed.some((p) => p.level === "error" && p.event === "rag_index_repo_error" && p.ev === "rag_index_repo_error")).toBe(true); - expect(await renderMetrics()).toContain('gittensory_rag_pipeline_errors_total{op="index_repo"}'); + expect(await renderMetrics()).toContain('loopover_rag_pipeline_errors_total{op="index_repo"}'); errSpy.mockRestore(); }); }); @@ -810,7 +810,7 @@ describe("reindexChangedPaths: delete + re-upsert only the changed paths", () => await expect(reindexChangedPaths(throwingEnv as typeof env, PROJECT, REPO, ["src/a.ts"])).resolves.toEqual({ indexed: 0, files: 0, capped: false }); const parsed = errSpy.mock.calls.map((c) => JSON.parse(c[0] as string)); expect(parsed.some((p) => p.level === "error" && p.event === "rag_reindex_paths_error" && p.ev === "rag_reindex_paths_error")).toBe(true); - expect(await renderMetrics()).toContain('gittensory_rag_pipeline_errors_total{op="reindex_paths"}'); + expect(await renderMetrics()).toContain('loopover_rag_pipeline_errors_total{op="reindex_paths"}'); errSpy.mockRestore(); }); }); diff --git a/test/unit/repo-culture-profile.test.ts b/test/unit/repo-culture-profile.test.ts index 393762a287..6eca81226b 100644 --- a/test/unit/repo-culture-profile.test.ts +++ b/test/unit/repo-culture-profile.test.ts @@ -461,8 +461,8 @@ describe("extractRepoCultureProfile: cache hit/miss telemetry (#4509)", () => { expect(second.present).toBe(true); const rendered = await renderMetrics(); - expect(rendered).toContain("gittensory_repo_culture_profile_cache_hit_total 1"); - expect(rendered).not.toContain("gittensory_repo_culture_profile_cache_miss_total"); + expect(rendered).toContain("loopover_repo_culture_profile_cache_hit_total 1"); + expect(rendered).not.toContain("loopover_repo_culture_profile_cache_miss_total"); const hitEvent = await auditEvent(env, "github_app.repo_culture_profile_cache_hit"); expect(hitEvent?.outcome).toBe("completed"); expect(await auditEvent(env, "github_app.repo_culture_profile_cache_miss")).toBeUndefined(); @@ -492,8 +492,8 @@ describe("extractRepoCultureProfile: cache hit/miss telemetry (#4509)", () => { expect(second.present).toBe(true); const rendered = await renderMetrics(); - expect(rendered).toContain("gittensory_repo_culture_profile_cache_miss_total 1"); - expect(rendered).not.toContain("gittensory_repo_culture_profile_cache_hit_total"); + expect(rendered).toContain("loopover_repo_culture_profile_cache_miss_total 1"); + expect(rendered).not.toContain("loopover_repo_culture_profile_cache_hit_total"); const missEvent = await auditEvent(env, "github_app.repo_culture_profile_cache_miss"); expect(missEvent?.outcome).toBe("completed"); expect(await auditEvent(env, "github_app.repo_culture_profile_cache_hit")).toBeUndefined(); @@ -517,8 +517,8 @@ describe("extractRepoCultureProfile: cache hit/miss telemetry (#4509)", () => { expect(refreshed.pullRequestNorms.sampleSize).toBe(MIN_SAMPLE_PULL_REQUESTS + 1); // confirms the drift path actually re-derived const rendered = await renderMetrics(); - expect(rendered).toContain("gittensory_repo_culture_profile_cache_miss_total 1"); - expect(rendered).not.toContain("gittensory_repo_culture_profile_cache_hit_total"); + expect(rendered).toContain("loopover_repo_culture_profile_cache_miss_total 1"); + expect(rendered).not.toContain("loopover_repo_culture_profile_cache_hit_total"); expect((await auditEvent(env, "github_app.repo_culture_profile_cache_miss"))?.outcome).toBe("completed"); }); }); diff --git a/test/unit/review-memory-store.test.ts b/test/unit/review-memory-store.test.ts index 3b3b292f17..b76b0f1191 100644 --- a/test/unit/review-memory-store.test.ts +++ b/test/unit/review-memory-store.test.ts @@ -289,8 +289,8 @@ describe("getCachedReviewSuppressions: cache hit/miss telemetry (#4448)", () => expect(second).toEqual([]); const rendered = await renderMetrics(); - expect(rendered).toContain("gittensory_review_memory_cache_hit_total 1"); - expect(rendered).not.toContain("gittensory_review_memory_cache_miss_total"); + expect(rendered).toContain("loopover_review_memory_cache_hit_total 1"); + expect(rendered).not.toContain("loopover_review_memory_cache_miss_total"); const hitEvent = await auditEvent(env, "github_app.review_memory_cache_hit", "owner/telemetry-repo"); expect(hitEvent?.outcome).toBe("completed"); expect(await auditEvent(env, "github_app.review_memory_cache_miss", "owner/telemetry-repo")).toBeUndefined(); @@ -303,8 +303,8 @@ describe("getCachedReviewSuppressions: cache hit/miss telemetry (#4448)", () => expect(first).toEqual([]); const rendered = await renderMetrics(); - expect(rendered).toContain("gittensory_review_memory_cache_miss_total 1"); - expect(rendered).not.toContain("gittensory_review_memory_cache_hit_total"); + expect(rendered).toContain("loopover_review_memory_cache_miss_total 1"); + expect(rendered).not.toContain("loopover_review_memory_cache_hit_total"); const missEvent = await auditEvent(env, "github_app.review_memory_cache_miss", "owner/telemetry-repo-2"); expect(missEvent?.outcome).toBe("completed"); expect(await auditEvent(env, "github_app.review_memory_cache_hit", "owner/telemetry-repo-2")).toBeUndefined(); @@ -321,8 +321,8 @@ describe("getCachedReviewSuppressions: cache hit/miss telemetry (#4448)", () => await getCachedReviewSuppressions(env, "owner/telemetry-repo-3", t0 + 60_001); // past the 60s TTL const rendered = await renderMetrics(); - expect(rendered).toContain("gittensory_review_memory_cache_miss_total 1"); - expect(rendered).not.toContain("gittensory_review_memory_cache_hit_total"); + expect(rendered).toContain("loopover_review_memory_cache_miss_total 1"); + expect(rendered).not.toContain("loopover_review_memory_cache_hit_total"); }); it("swallows a failing cache-hit audit-event write without throwing, still returning the cached suppression list", async () => { diff --git a/test/unit/selfhost-ai.test.ts b/test/unit/selfhost-ai.test.ts index 2d325c85d5..fb03f10658 100644 --- a/test/unit/selfhost-ai.test.ts +++ b/test/unit/selfhost-ai.test.ts @@ -568,34 +568,34 @@ describe("AI provider request duration/error metrics (#4367)", () => { const provider = { name: "gpu-metrics-ok-provider", ai: { run: async () => ({ response: "ok" }) } }; await createChainAi([provider]).run("m", { prompt: "review this" }); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_ai_provider_request_duration_seconds_count{provider="gpu-metrics-ok-provider",request_kind="review"} 1'); - expect(metrics).not.toContain('gittensory_ai_provider_request_errors_total{provider="gpu-metrics-ok-provider"'); + expect(metrics).toContain('loopover_ai_provider_request_duration_seconds_count{provider="gpu-metrics-ok-provider",request_kind="review"} 1'); + expect(metrics).not.toContain('loopover_ai_provider_request_errors_total{provider="gpu-metrics-ok-provider"'); }); it("labels an embedding call's duration by request_kind=embedding", async () => { const provider = { name: "gpu-metrics-embed-provider", ai: { run: async () => ({ response: "ok" }) } }; await createChainAi([provider]).run("m", { text: ["chunk one", "chunk two"] }); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_ai_provider_request_duration_seconds_count{provider="gpu-metrics-embed-provider",request_kind="embedding"} 1'); + expect(metrics).toContain('loopover_ai_provider_request_duration_seconds_count{provider="gpu-metrics-embed-provider",request_kind="embedding"} 1'); }); it("records duration AND increments the error counter on a real failure", async () => { const provider = { name: "gpu-metrics-fail-provider", ai: { run: async () => { throw new Error("boom"); } } }; await expect(createChainAi([provider]).run("m", { prompt: "review this" })).rejects.toThrow(/boom/); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_ai_provider_request_duration_seconds_count{provider="gpu-metrics-fail-provider",request_kind="review"} 1'); - expect(metrics).toContain('gittensory_ai_provider_request_errors_total{provider="gpu-metrics-fail-provider",request_kind="review"} 1'); + expect(metrics).toContain('loopover_ai_provider_request_duration_seconds_count{provider="gpu-metrics-fail-provider",request_kind="review"} 1'); + expect(metrics).toContain('loopover_ai_provider_request_errors_total{provider="gpu-metrics-fail-provider",request_kind="review"} 1'); }); - it("records duration but NOT the error counter for an expected embedding-routing fallback (matches gittensory_ai_provider_failures_total's exemption)", async () => { + it("records duration but NOT the error counter for an expected embedding-routing fallback (matches loopover_ai_provider_failures_total's exemption)", async () => { const provider = { name: "gpu-metrics-routing-provider", ai: { run: async () => { throw new Error("claude_code_no_embed"); } }, }; await expect(createChainAi([provider]).run("m", { text: ["chunk"] })).rejects.toThrow(); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_ai_provider_request_duration_seconds_count{provider="gpu-metrics-routing-provider",request_kind="embedding"} 1'); - expect(metrics).not.toContain('gittensory_ai_provider_request_errors_total{provider="gpu-metrics-routing-provider"'); + expect(metrics).toContain('loopover_ai_provider_request_duration_seconds_count{provider="gpu-metrics-routing-provider",request_kind="embedding"} 1'); + expect(metrics).not.toContain('loopover_ai_provider_request_errors_total{provider="gpu-metrics-routing-provider"'); }); }); @@ -618,8 +618,8 @@ describe("per-provider circuit breaker (#2540 — skip fast during a sustained o await expect(createChainAi([flaky]).run("m", { prompt: "x" })).rejects.toThrow(/circuit_open: provider "flaky-provider"/); expect(calls).toHaveBeenCalledTimes(3); // unchanged — the real provider was never reached const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_ai_provider_circuit_open_total{provider="flaky-provider"} 1'); - expect(metrics).toContain('gittensory_ai_provider_failures_total{provider="flaky-provider"} 3'); + expect(metrics).toContain('loopover_ai_provider_circuit_open_total{provider="flaky-provider"} 1'); + expect(metrics).toContain('loopover_ai_provider_failures_total{provider="flaky-provider"} 3'); }); it("REGRESSION (gate finding): concurrent same-provider failures accumulate correctly (no lost-update race)", async () => { @@ -722,8 +722,8 @@ describe("per-provider circuit breaker (#2540 — skip fast during a sustained o await expect(route.run("claude-code", { prompt: "review this" })).resolves.toEqual({ response: "review ok" }); expect(chatOnlyCalls).toHaveBeenCalledTimes(4); const metrics = await renderMetrics(); - expect(metrics).not.toContain('gittensory_ai_provider_failures_total{provider="claude-code"}'); - expect(metrics).not.toContain('gittensory_ai_provider_circuit_open_total{provider="claude-code"}'); + expect(metrics).not.toContain('loopover_ai_provider_failures_total{provider="claude-code"}'); + expect(metrics).not.toContain('loopover_ai_provider_circuit_open_total{provider="claude-code"}'); }); it("does not affect isAiProviderHealthy / aiConsecutiveFailures — independent whole-chain streak", async () => { @@ -1698,7 +1698,7 @@ describe("subscription CLI helpers + fail-safe", () => { const empty: StubSpawn = async () => ({ stdout: "", code: 0 }); await expect(createClaudeCodeAi({ CLAUDE_CODE_OAUTH_TOKEN: "t" }, empty).run("m", { prompt: "x" })).rejects.toThrow(/claude_code_empty_output/); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_ai_requests_total{effort="medium",model="m",provider="claude-code"} 2'); + expect(metrics).toContain('loopover_ai_requests_total{effort="medium",model="m",provider="claude-code"} 2'); }); it("Codex throws on empty output", async () => { @@ -1707,7 +1707,7 @@ describe("subscription CLI helpers + fail-safe", () => { createCodexAi({ GITTENSORY_ENABLE_UNSAFE_CODEX_REVIEWER: "1" }, empty, noAuthCheck).run("gpt-5", { prompt: "x" }), ).rejects.toThrow(/codex_empty_output/); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_ai_requests_total{effort="medium",model="gpt-5",provider="codex"} 1'); + expect(metrics).toContain('loopover_ai_requests_total{effort="medium",model="gpt-5",provider="codex"} 1'); }); it("Claude Code throws subscription_cli_timeout when the CLI is killed for exceeding its deadline", async () => { @@ -1868,7 +1868,7 @@ describe("subscription CLI helpers + fail-safe", () => { /codex_credential_isolation_required/, ); const metrics = await renderMetrics(); - expect(metrics).not.toContain("gittensory_ai_requests_total"); + expect(metrics).not.toContain("loopover_ai_requests_total"); }); it("resolveCodexAuthPath: CODEX_HOME wins, else HOME/.codex, else ~/.codex", () => { @@ -1947,7 +1947,7 @@ describe("subscription CLI helpers + fail-safe", () => { /codex_exit_1: stream error: rate limit reached/, ); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_ai_requests_total{effort="medium",model="m",provider="codex"} 1'); + expect(metrics).toContain('loopover_ai_requests_total{effort="medium",model="m",provider="codex"} 1'); }); it("redacts the OAuth token and key-shaped tokens from claude stderr before they reach the error (#1605 sec)", async () => { @@ -2026,10 +2026,10 @@ describe("subscription CLI helpers + fail-safe", () => { const result = await createCodexAi({ GITTENSORY_ENABLE_UNSAFE_CODEX_REVIEWER: "1", CODEX_AI_EFFORT: "medium" }, ok, noAuthCheck).run("", { prompt: "x" }); expect(result.usage).toMatchObject({ provider: "codex", model: "gpt-5-codex", effort: "medium", inputTokens: 20, outputTokens: 7, totalTokens: 27 }); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_ai_requests_total{effort="medium",model="gpt-5-codex",provider="codex"} 1'); - expect(metrics).toContain('gittensory_ai_input_tokens_total{effort="medium",kind="review",model="gpt-5-codex",provider="codex"} 20'); - expect(metrics).toContain('gittensory_ai_output_tokens_total{effort="medium",kind="review",model="gpt-5-codex",provider="codex"} 7'); - expect(metrics).toContain('gittensory_ai_total_tokens_total{effort="medium",model="gpt-5-codex",provider="codex"} 27'); + expect(metrics).toContain('loopover_ai_requests_total{effort="medium",model="gpt-5-codex",provider="codex"} 1'); + expect(metrics).toContain('loopover_ai_input_tokens_total{effort="medium",kind="review",model="gpt-5-codex",provider="codex"} 20'); + expect(metrics).toContain('loopover_ai_output_tokens_total{effort="medium",kind="review",model="gpt-5-codex",provider="codex"} 7'); + expect(metrics).toContain('loopover_ai_total_tokens_total{effort="medium",model="gpt-5-codex",provider="codex"} 27'); }); }); diff --git a/test/unit/selfhost-backup-metrics-script.test.ts b/test/unit/selfhost-backup-metrics-script.test.ts index 794375e2be..e1c5964535 100644 --- a/test/unit/selfhost-backup-metrics-script.test.ts +++ b/test/unit/selfhost-backup-metrics-script.test.ts @@ -50,12 +50,12 @@ describe("backup-metrics.sh", () => { const metrics = runExporterOnce(root); - expect(metrics).toContain(`gittensory_backup_latest_timestamp_seconds{target="postgres"} ${newestPostgres}`); - expect(metrics).toContain(`gittensory_backup_latest_timestamp_seconds{target="sqlite"} ${sqlite}`); - expect(metrics).toContain(`gittensory_backup_latest_timestamp_seconds{target="qdrant"} ${qdrant}`); - expect(metrics).toContain('gittensory_backup_files{target="postgres"} 2'); - expect(metrics).toContain('gittensory_backup_files{target="sqlite"} 1'); - expect(metrics).toContain('gittensory_backup_files{target="qdrant"} 1'); + expect(metrics).toContain(`loopover_backup_latest_timestamp_seconds{target="postgres"} ${newestPostgres}`); + expect(metrics).toContain(`loopover_backup_latest_timestamp_seconds{target="sqlite"} ${sqlite}`); + expect(metrics).toContain(`loopover_backup_latest_timestamp_seconds{target="qdrant"} ${qdrant}`); + expect(metrics).toContain('loopover_backup_files{target="postgres"} 2'); + expect(metrics).toContain('loopover_backup_files{target="sqlite"} 1'); + expect(metrics).toContain('loopover_backup_files{target="qdrant"} 1'); }); it("exports zeroes for missing backup directories instead of failing the scrape", () => { @@ -63,11 +63,11 @@ describe("backup-metrics.sh", () => { const metrics = runExporterOnce(root); - expect(metrics).toContain('gittensory_backup_latest_timestamp_seconds{target="postgres"} 0'); - expect(metrics).toContain('gittensory_backup_latest_timestamp_seconds{target="sqlite"} 0'); - expect(metrics).toContain('gittensory_backup_latest_timestamp_seconds{target="qdrant"} 0'); - expect(metrics).toContain('gittensory_backup_files{target="postgres"} 0'); - expect(metrics).toContain('gittensory_backup_files{target="sqlite"} 0'); - expect(metrics).toContain('gittensory_backup_files{target="qdrant"} 0'); + expect(metrics).toContain('loopover_backup_latest_timestamp_seconds{target="postgres"} 0'); + expect(metrics).toContain('loopover_backup_latest_timestamp_seconds{target="sqlite"} 0'); + expect(metrics).toContain('loopover_backup_latest_timestamp_seconds{target="qdrant"} 0'); + expect(metrics).toContain('loopover_backup_files{target="postgres"} 0'); + expect(metrics).toContain('loopover_backup_files{target="sqlite"} 0'); + expect(metrics).toContain('loopover_backup_files{target="qdrant"} 0'); }); }); diff --git a/test/unit/selfhost-d1-size-probe.test.ts b/test/unit/selfhost-d1-size-probe.test.ts index d53a3fc226..a978b0f928 100644 --- a/test/unit/selfhost-d1-size-probe.test.ts +++ b/test/unit/selfhost-d1-size-probe.test.ts @@ -230,8 +230,8 @@ describe("runD1SizeProbe", () => { expect(d1DatabaseSizeBytesSample()).toBe(1_000_000); // stale, not reset to -1 expect(d1TableRowCountSamples().length).toBeGreaterThan(0); // stale rows kept, not blanked - expect(counterValue("gittensory_d1_probe_errors_total", { part: "database_info" })).toBeGreaterThan(0); - expect(counterValue("gittensory_d1_probe_errors_total", { part: "table_row_count" })).toBeGreaterThan(0); + expect(counterValue("loopover_d1_probe_errors_total", { part: "database_info" })).toBeGreaterThan(0); + expect(counterValue("loopover_d1_probe_errors_total", { part: "table_row_count" })).toBeGreaterThan(0); }); it("reads -1 on the very first tick when everything fails (no previous sample to fall back to)", async () => { @@ -285,7 +285,7 @@ describe("runD1SizeProbe", () => { expect(samples).toContainEqual({ labels: { table: "audit_events" }, value: 1 }); // stale, kept from tick 1 expect(samples).toContainEqual({ labels: { table: "signal_snapshots" }, value: 100 }); // freshly updated expect(d1SignalSnapshotsRowsPerKeySample()).toBeCloseTo(20 / 5, 6); // ratio reflects the FRESH sample - expect(counterValue("gittensory_d1_probe_errors_total", { part: "table_row_count" })).toBeGreaterThan(0); + expect(counterValue("loopover_d1_probe_errors_total", { part: "table_row_count" })).toBeGreaterThan(0); }); it("keeps size at its previous value when only the database-info fetch fails but tables succeed", async () => { @@ -331,9 +331,9 @@ describe("d1SignalSnapshotsRowsPerKeySample", () => { describe("D1 metrics end-to-end via renderMetrics()", () => { it("renders gauges and vector series with the registered HELP/TYPE metadata after a successful probe", async () => { - gauge("gittensory_d1_database_size_bytes", () => d1DatabaseSizeBytesSample()); - gaugeVector("gittensory_d1_table_row_count", () => d1TableRowCountSamples()); - gauge("gittensory_signal_snapshots_rows_per_key", () => d1SignalSnapshotsRowsPerKeySample()); + gauge("loopover_d1_database_size_bytes", () => d1DatabaseSizeBytesSample()); + gaugeVector("loopover_d1_table_row_count", () => d1TableRowCountSamples()); + gauge("loopover_signal_snapshots_rows_per_key", () => d1SignalSnapshotsRowsPerKeySample()); await runD1SizeProbe( FULL_ENV, @@ -344,23 +344,23 @@ describe("D1 metrics end-to-end via renderMetrics()", () => { ); const out = await renderMetrics(); - expect(out).toContain("# TYPE gittensory_d1_database_size_bytes gauge"); - expect(out).toContain("gittensory_d1_database_size_bytes 3890057216"); - expect(out).toContain("# TYPE gittensory_d1_table_row_count gauge"); - expect(out).toContain('gittensory_d1_table_row_count{table="signal_snapshots"} 107'); - expect(out).toContain("# TYPE gittensory_signal_snapshots_rows_per_key gauge"); - expect(out).toMatch(/gittensory_signal_snapshots_rows_per_key 2\.9\d+/); + expect(out).toContain("# TYPE loopover_d1_database_size_bytes gauge"); + expect(out).toContain("loopover_d1_database_size_bytes 3890057216"); + expect(out).toContain("# TYPE loopover_d1_table_row_count gauge"); + expect(out).toContain('loopover_d1_table_row_count{table="signal_snapshots"} 107'); + expect(out).toContain("# TYPE loopover_signal_snapshots_rows_per_key gauge"); + expect(out).toMatch(/loopover_signal_snapshots_rows_per_key 2\.9\d+/); }); it("renders -1 sentinels and an empty vector before the probe ever runs", async () => { - gauge("gittensory_d1_database_size_bytes", () => d1DatabaseSizeBytesSample()); - gaugeVector("gittensory_d1_table_row_count", () => d1TableRowCountSamples()); - gauge("gittensory_signal_snapshots_rows_per_key", () => d1SignalSnapshotsRowsPerKeySample()); + gauge("loopover_d1_database_size_bytes", () => d1DatabaseSizeBytesSample()); + gaugeVector("loopover_d1_table_row_count", () => d1TableRowCountSamples()); + gauge("loopover_signal_snapshots_rows_per_key", () => d1SignalSnapshotsRowsPerKeySample()); const out = await renderMetrics(); - expect(out).toContain("gittensory_d1_database_size_bytes -1"); - expect(out).toContain("gittensory_signal_snapshots_rows_per_key -1"); - expect(out).toContain("# TYPE gittensory_d1_table_row_count gauge"); - expect(out).not.toContain('gittensory_d1_table_row_count{table='); + expect(out).toContain("loopover_d1_database_size_bytes -1"); + expect(out).toContain("loopover_signal_snapshots_rows_per_key -1"); + expect(out).toContain("# TYPE loopover_d1_table_row_count gauge"); + expect(out).not.toContain('loopover_d1_table_row_count{table='); }); }); diff --git a/test/unit/selfhost-grafana-ai-usage-dashboard.test.ts b/test/unit/selfhost-grafana-ai-usage-dashboard.test.ts index 97dbe708ab..157f734c6e 100644 --- a/test/unit/selfhost-grafana-ai-usage-dashboard.test.ts +++ b/test/unit/selfhost-grafana-ai-usage-dashboard.test.ts @@ -106,7 +106,7 @@ afterEach(() => { for (const dir of tmpRoots.splice(0)) rmSync(dir, { force: true, recursive: true }); }); -describe("Gittensory - AI usage dashboard (Phase B2 consolidation)", () => { +describe("Loopover - AI usage dashboard (Phase B2 consolidation)", () => { it("replaces the 3 old AI-usage dashboards, not just adds a 4th", () => { const files = readdirSync(dashboardsDir); expect(files).toContain("ai-usage.json"); @@ -179,12 +179,12 @@ describe("Gittensory - AI usage dashboard (Phase B2 consolidation)", () => { it("carries over the exact Prometheus expressions from the removed dashboards, byte-for-byte (no copy-paste drift)", () => { const targets = readDashboard().panels.flatMap((panel) => panel.targets ?? []); // From gittensory.json's removed "AI Usage & Cost" row. - expect(targets.some((t) => t.expr === "sum by (provider) (gittensory_ai_cost_usd_total) or vector(0)")).toBe(true); - expect(targets.some((t) => t.expr === "sum by (provider) ((rate(gittensory_ai_input_tokens_total[5m]) + rate(gittensory_ai_output_tokens_total[5m])) * 60)")).toBe(true); - expect(targets.some((t) => t.expr === "sum by (model, effort) (increase(gittensory_ai_requests_total[1h]))")).toBe(true); - expect(targets.some((t) => t.expr === "sum by (primary, fallback) (increase(gittensory_ai_review_model_fallback_total[1h]))")).toBe(true); + expect(targets.some((t) => t.expr === "sum by (provider) (loopover_ai_cost_usd_total) or vector(0)")).toBe(true); + expect(targets.some((t) => t.expr === "sum by (provider) ((rate(loopover_ai_input_tokens_total[5m]) + rate(loopover_ai_output_tokens_total[5m])) * 60)")).toBe(true); + expect(targets.some((t) => t.expr === "sum by (model, effort) (increase(loopover_ai_requests_total[1h]))")).toBe(true); + expect(targets.some((t) => t.expr === "sum by (primary, fallback) (increase(loopover_ai_review_model_fallback_total[1h]))")).toBe(true); // From codex-usage.json. - expect(targets.some((t) => t.expr === "sum by (model, effort) (increase(gittensory_ai_requests_total{provider=\"codex\"}[$__rate_interval]))")).toBe(true); + expect(targets.some((t) => t.expr === "sum by (model, effort) (increase(loopover_ai_requests_total{provider=\"codex\"}[$__rate_interval]))")).toBe(true); // From claude-usage.json's OTEL section (uses $claudeModel, not $model, to stay independent of the durable-log filters). expect(targets.some((t) => t.expr === "sum(last_over_time(claude_code_cost_usage_USD_total{model=~\"$claudeModel\"}[$__range]))")).toBe(true); }); diff --git a/test/unit/selfhost-grafana-browserless-dashboard.test.ts b/test/unit/selfhost-grafana-browserless-dashboard.test.ts index aaa865b9c4..78b6e5aff4 100644 --- a/test/unit/selfhost-grafana-browserless-dashboard.test.ts +++ b/test/unit/selfhost-grafana-browserless-dashboard.test.ts @@ -28,12 +28,12 @@ function allTargets(dashboard = readDashboard()): DashboardTarget[] { return dashboard.panels.flatMap((panel) => panel.targets ?? []); } -describe("Gittensory — Browserless (visual review) dashboard (#5368)", () => { +describe("LoopOver — Browserless (visual review) dashboard (#5368)", () => { it("declares the expected uid/title/tags", () => { const dashboard = readDashboard(); - expect(dashboard.uid).toBe("gittensory-browserless"); - expect(dashboard.title).toBe("Gittensory — Browserless (visual review)"); - expect(dashboard.tags).toEqual(["gittensory", "browserless", "observability"]); + expect(dashboard.uid).toBe("loopover-browserless"); + expect(dashboard.title).toBe("LoopOver — Browserless (visual review)"); + expect(dashboard.tags).toEqual(["loopover", "browserless", "observability"]); }); it("every panel target uses the Prometheus datasource variable, never a hardcoded uid", () => { diff --git a/test/unit/selfhost-grafana-dashboard.test.ts b/test/unit/selfhost-grafana-dashboard.test.ts index f643017f9a..bf1c892d38 100644 --- a/test/unit/selfhost-grafana-dashboard.test.ts +++ b/test/unit/selfhost-grafana-dashboard.test.ts @@ -88,19 +88,19 @@ afterEach(() => { for (const dir of tmpRoots.splice(0)) rmSync(dir, { force: true, recursive: true }); }); -describe("Gittensory Self-Host Grafana dashboard", () => { +describe("Loopover Self-Host Grafana dashboard", () => { it("surfaces the GitHub response cache Prometheus counters", () => { const dashboard = readDashboard(selfhostDashboardPath); const targets = dashboard.panels.flatMap((panel) => panel.targets ?? []); - expect(targets.some((target) => target.expr === "sum by (result) (rate(gittensory_github_response_cache_total[5m]))")).toBe(true); - expect(targets.some((target) => target.expr === "sum by (class, result) (gittensory_github_response_cache_total)")).toBe(true); + expect(targets.some((target) => target.expr === "sum by (result) (rate(loopover_github_response_cache_total[5m]))")).toBe(true); + expect(targets.some((target) => target.expr === "sum by (class, result) (loopover_github_response_cache_total)")).toBe(true); expect(targets.some((target) => target.legendFormat === "{{class}} {{result}}")).toBe(true); - expect(targets.some((target) => target.expr === "sum by (remaining_bucket, key_scope) (rate(gittensory_github_rest_rate_limit_observations_total[5m])) or vector(0)")).toBe(true); - expect(targets.some((target) => target.expr === "sum by (status, retry, key_scope) (rate(gittensory_github_rest_rate_limit_responses_total[5m])) or vector(0)")).toBe(true); - expect(targets.some((target) => target.expr === "sum by (kind, key_scope, job_type) (rate(gittensory_jobs_rate_limit_admission_deferred_total[5m])) or vector(0)")).toBe(true); - expect(targets.some((target) => target.expr === "sum by (kind, key_scope, job_type) (rate(gittensory_jobs_rate_limit_budget_deferred_total[5m])) or vector(0)")).toBe(true); - expect(targets.some((target) => target.expr === "sum by (kind, key_scope, job_type) (rate(gittensory_jobs_rate_limited_by_type_total[5m])) or vector(0)")).toBe(true); + expect(targets.some((target) => target.expr === "sum by (remaining_bucket, key_scope) (rate(loopover_github_rest_rate_limit_observations_total[5m])) or vector(0)")).toBe(true); + expect(targets.some((target) => target.expr === "sum by (status, retry, key_scope) (rate(loopover_github_rest_rate_limit_responses_total[5m])) or vector(0)")).toBe(true); + expect(targets.some((target) => target.expr === "sum by (kind, key_scope, job_type) (rate(loopover_jobs_rate_limit_admission_deferred_total[5m])) or vector(0)")).toBe(true); + expect(targets.some((target) => target.expr === "sum by (kind, key_scope, job_type) (rate(loopover_jobs_rate_limit_budget_deferred_total[5m])) or vector(0)")).toBe(true); + expect(targets.some((target) => target.expr === "sum by (kind, key_scope, job_type) (rate(loopover_jobs_rate_limited_by_type_total[5m])) or vector(0)")).toBe(true); // The AI request/fallback + cost/token panels moved to the consolidated grafana/dashboards/ai-usage.json // (Phase B2, 2026-07) — see test/unit/selfhost-grafana-ai-usage-dashboard.test.ts for their coverage there. }); @@ -122,19 +122,19 @@ describe("Gittensory Self-Host Grafana dashboard", () => { const dashboard = readDashboard(selfhostDashboardPath); const targets = dashboard.panels.flatMap((panel) => panel.targets ?? []); - expect(targets.some((target) => target.expr === "gittensory_orb_events_exported_total or vector(0)")).toBe(true); - expect(targets.some((target) => target.expr === "gittensory_orb_export_errors_total or vector(0)")).toBe(true); - expect(targets.some((target) => target.expr === "sum by (result) (rate(gittensory_orb_webhook_total[5m])) or vector(0)")).toBe(true); + expect(targets.some((target) => target.expr === "loopover_orb_events_exported_total or vector(0)")).toBe(true); + expect(targets.some((target) => target.expr === "loopover_orb_export_errors_total or vector(0)")).toBe(true); + expect(targets.some((target) => target.expr === "sum by (result) (rate(loopover_orb_webhook_total[5m])) or vector(0)")).toBe(true); }); - it("no longer references gittensory_orb_events_recorded_total / gittensory_orb_installs_total, retired with the per-instance Orb App in #1256 but never cleaned out of the dashboard (2026-07 fix)", () => { + it("no longer references loopover_orb_events_recorded_total / loopover_orb_installs_total, retired with the per-instance Orb App in #1256 but never cleaned out of the dashboard (2026-07 fix)", () => { const dashboard = readDashboard(selfhostDashboardPath); const targets = dashboard.panels.flatMap((panel) => panel.targets ?? []); const titles = dashboard.panels.map((panel) => panel.title); for (const target of targets) { - expect(target.expr ?? "").not.toContain("gittensory_orb_events_recorded_total"); - expect(target.expr ?? "").not.toContain("gittensory_orb_installs_total"); + expect(target.expr ?? "").not.toContain("loopover_orb_events_recorded_total"); + expect(target.expr ?? "").not.toContain("loopover_orb_installs_total"); } expect(titles).not.toContain("Orb Events Recorded"); expect(titles).not.toContain("Orb Installations"); @@ -154,17 +154,17 @@ describe("Gittensory Self-Host Grafana dashboard", () => { const targets = dashboard.panels.flatMap((panel) => panel.targets ?? []); const alerts = readFileSync(selfhostAlertsPath, "utf8"); - expect(targets.some((target) => target.expr === "gittensory_ai_review_onmerge_clamped_total or vector(0)")).toBe(true); - expect(alerts).toContain("alert: GittensoryAiReviewOnMergeFloorBypassAttempted"); - expect(alerts).toContain("expr: increase(gittensory_ai_review_onmerge_clamped_total[1h]) > 0"); + expect(targets.some((target) => target.expr === "loopover_ai_review_onmerge_clamped_total or vector(0)")).toBe(true); + expect(alerts).toContain("alert: LoopoverAiReviewOnMergeFloorBypassAttempted"); + expect(alerts).toContain("expr: increase(loopover_ai_review_onmerge_clamped_total[1h]) > 0"); }); it("keeps rate-limit alerts grouped by the dashboard label dimensions", () => { const alerts = readFileSync(selfhostAlertsPath, "utf8"); - expect(alerts).toContain("sum by (status, retry, key_scope) (rate(gittensory_github_rest_rate_limit_responses_total[5m])) > 0"); - expect(alerts).toContain("sum by (kind, key_scope, job_type) (rate(gittensory_jobs_rate_limit_admission_deferred_total[5m])) > 0.05"); - expect(alerts).toContain("sum by (kind, key_scope, job_type) (rate(gittensory_jobs_rate_limit_budget_deferred_total[5m])) > 0.05"); + expect(alerts).toContain("sum by (status, retry, key_scope) (rate(loopover_github_rest_rate_limit_responses_total[5m])) > 0"); + expect(alerts).toContain("sum by (kind, key_scope, job_type) (rate(loopover_jobs_rate_limit_admission_deferred_total[5m])) > 0.05"); + expect(alerts).toContain("sum by (kind, key_scope, job_type) (rate(loopover_jobs_rate_limit_budget_deferred_total[5m])) > 0.05"); }); it("surfaces Postgres internals and backup freshness panels", () => { @@ -182,28 +182,28 @@ describe("Gittensory Self-Host Grafana dashboard", () => { expect(targets.some((target) => target.expr === 'topk(10, pg_stat_user_tables_n_live_tup{datname="loopover"}) or vector(0)')).toBe(true); expect(targets.some((target) => target.expr === 'topk(10, pg_stat_user_tables_n_dead_tup{datname="loopover"}) or vector(0)')).toBe(true); expect(targets.some((target) => target.expr === 'sum by (relname) (increase(pg_stat_user_tables_autovacuum_count{datname="loopover"}[1h])) or vector(0)')).toBe(true); - expect(targets.some((target) => target.expr === 'gittensory_backup_files{target=~"postgres|sqlite|qdrant"} or vector(0)')).toBe(true); + expect(targets.some((target) => target.expr === 'loopover_backup_files{target=~"postgres|sqlite|qdrant"} or vector(0)')).toBe(true); }); it("ships Postgres and backup alerts for the same dashboarded failure modes", () => { const alerts = readFileSync(selfhostAlertsPath, "utf8"); - expect(alerts).toContain("alert: GittensoryPostgresConnectionPressure"); + expect(alerts).toContain("alert: LoopoverPostgresConnectionPressure"); expect(alerts).toContain('sum(pg_stat_activity_count{datname="loopover"})'); - expect(alerts).toContain("alert: GittensoryPostgresLockWaits"); + expect(alerts).toContain("alert: LoopoverPostgresLockWaits"); expect(alerts).toContain('pg_stat_activity_count{datname="loopover", wait_event_type="Lock"}'); - expect(alerts).toContain("alert: GittensoryPostgresSlowTransaction"); + expect(alerts).toContain("alert: LoopoverPostgresSlowTransaction"); expect(alerts).toContain('pg_stat_activity_max_tx_duration{datname="loopover"}'); - expect(alerts).toContain("alert: GittensoryPostgresDeadlocks"); + expect(alerts).toContain("alert: LoopoverPostgresDeadlocks"); expect(alerts).toContain('pg_stat_database_deadlocks{datname="loopover"}'); - expect(alerts).toContain("alert: GittensoryPostgresDatabaseGrowingFast"); + expect(alerts).toContain("alert: LoopoverPostgresDatabaseGrowingFast"); expect(alerts).toContain('deriv(pg_database_size_bytes{datname="loopover"}[6h]) > 262144'); - expect(alerts).toContain("alert: GittensoryPostgresDeadTuplesHigh"); + expect(alerts).toContain("alert: LoopoverPostgresDeadTuplesHigh"); expect(alerts).toContain('pg_stat_user_tables_n_dead_tup{datname="loopover"}'); - expect(alerts).toContain("alert: GittensoryBackupMissing"); - expect(alerts).toContain('gittensory_backup_files{target=~"postgres|sqlite"} == 0'); - expect(alerts).toContain("alert: GittensoryBackupStale"); - expect(alerts).toContain('time() - gittensory_backup_latest_timestamp_seconds{target=~"postgres|sqlite"} > 93600'); + expect(alerts).toContain("alert: LoopoverBackupMissing"); + expect(alerts).toContain('loopover_backup_files{target=~"postgres|sqlite"} == 0'); + expect(alerts).toContain("alert: LoopoverBackupStale"); + expect(alerts).toContain('time() - loopover_backup_latest_timestamp_seconds{target=~"postgres|sqlite"} > 93600'); }); it("surfaces a Maintenance Admission Deferrals (total) panel alongside the by-reason breakdown", () => { @@ -218,8 +218,8 @@ describe("Gittensory Self-Host Grafana dashboard", () => { "Maintenance Admission Deferrals (total)", ]), ); - expect(targets.some((target) => target.expr === "sum by (reason, job_type) (rate(gittensory_jobs_maintenance_admission_deferred_by_reason_total[5m])) or vector(0)")).toBe(true); - expect(targets.some((target) => target.expr === "sum(rate(gittensory_jobs_maintenance_admission_deferred_total[5m])) or vector(0)")).toBe(true); + expect(targets.some((target) => target.expr === "sum by (reason, job_type) (rate(loopover_jobs_maintenance_admission_deferred_by_reason_total[5m])) or vector(0)")).toBe(true); + expect(targets.some((target) => target.expr === "sum(rate(loopover_jobs_maintenance_admission_deferred_total[5m])) or vector(0)")).toBe(true); }); it("surfaces self-host runtime-drift signal panels, every counter query fleet-aggregated", () => { @@ -241,25 +241,25 @@ describe("Gittensory Self-Host Grafana dashboard", () => { ); // Every stat-panel counter is sum()-wrapped, matching its siblings -- a multi-instance self-host scrape // must render one fleet-level value per stat, not one value per target (gate finding, #chore-runtime-drift). - expect(targets.some((target) => target.expr === "sum(gittensory_jobs_maintenance_trickle_admitted_persisted_total) or vector(0)")).toBe(true); - expect(targets.some((target) => target.expr === 'sum(gittensory_orb_relay_register_total{result="failed"}) or vector(0)')).toBe(true); - expect(targets.some((target) => target.expr === 'sum(gittensory_installation_health_broker_probe_total{result="failed"}) or vector(0)')).toBe(true); - expect(targets.some((target) => target.expr === "sum(gittensory_agent_action_permission_denied_total) or vector(0)")).toBe(true); + expect(targets.some((target) => target.expr === "sum(loopover_jobs_maintenance_trickle_admitted_persisted_total) or vector(0)")).toBe(true); + expect(targets.some((target) => target.expr === 'sum(loopover_orb_relay_register_total{result="failed"}) or vector(0)')).toBe(true); + expect(targets.some((target) => target.expr === 'sum(loopover_installation_health_broker_probe_total{result="failed"}) or vector(0)')).toBe(true); + expect(targets.some((target) => target.expr === "sum(loopover_agent_action_permission_denied_total) or vector(0)")).toBe(true); // Grouped (sum-by) queries must NOT have "or vector(0)": Prometheus's `or` unions result sets, and // vector(0) is a single unlabeled series that can't match the actionClass/mode,result label set -- // that renders a bogus extra unlabeled zero-series alongside the real labeled series (gate finding). - expect(targets.some((target) => target.expr === "sum by (actionClass) (rate(gittensory_agent_action_permission_denied_total[5m]))")).toBe(true); - expect(targets.some((target) => target.expr === "sum by (actionClass) (rate(gittensory_agent_action_permission_denied_suppressed_total[5m]))")).toBe(true); - expect(targets.some((target) => target.expr === "sum by (mode, result) (rate(gittensory_orb_relay_register_total[5m]))")).toBe(true); + expect(targets.some((target) => target.expr === "sum by (actionClass) (rate(loopover_agent_action_permission_denied_total[5m]))")).toBe(true); + expect(targets.some((target) => target.expr === "sum by (actionClass) (rate(loopover_agent_action_permission_denied_suppressed_total[5m]))")).toBe(true); + expect(targets.some((target) => target.expr === "sum by (mode, result) (rate(loopover_orb_relay_register_total[5m]))")).toBe(true); // #selfhost-runtime-drift follow-up: the streak-vs-drain-progress panel is the dashboard-visible // counterpart to isOrbRelayRegistrationAlerting's gate -- a lone registration timeout must not read as // a dashboard error on its own as long as the drain loop is still making progress. - expect(targets.some((target) => target.expr === "gittensory_orb_relay_register_consecutive_failures or vector(0)")).toBe(true); - expect(targets.some((target) => target.expr === "gittensory_orb_relay_drain_seconds_since_last or vector(0)")).toBe(true); + expect(targets.some((target) => target.expr === "loopover_orb_relay_register_consecutive_failures or vector(0)")).toBe(true); + expect(targets.some((target) => target.expr === "loopover_orb_relay_drain_seconds_since_last or vector(0)")).toBe(true); const alerts = readFileSync(selfhostAlertsPath, "utf8"); - expect(alerts).toContain("alert: GittensoryOrbRelayRegistrationStuck"); - expect(alerts).toContain("gittensory_orb_relay_register_consecutive_failures >= 3 or gittensory_orb_relay_drain_seconds_since_last > 1800"); + expect(alerts).toContain("alert: LoopoverOrbRelayRegistrationStuck"); + expect(alerts).toContain("loopover_orb_relay_register_consecutive_failures >= 3 or loopover_orb_relay_drain_seconds_since_last > 1800"); }); it("surfaces the backlog-vs-fresh-intake lane fairness panels (#selfhost-lane-observability)", () => { @@ -277,12 +277,12 @@ describe("Gittensory Self-Host Grafana dashboard", () => { "Top Repos by Backlog Depth", ]), ); - expect(targets.some((target) => target.expr === "gittensory_queue_backlog_convergence_pending")).toBe(true); - expect(targets.some((target) => target.expr === "gittensory_queue_fresh_intake_pending")).toBe(true); - expect(targets.some((target) => target.expr === "gittensory_github_rest_rate_limit_remaining")).toBe(true); - expect(targets.some((target) => target.legendFormat === "{{key_scope}}" && target.expr === "gittensory_github_rest_rate_limit_remaining")).toBe(true); - expect(targets.some((target) => target.expr === "sum by (lane) (rate(gittensory_jobs_claimed_by_lane_total[5m])) or vector(0)")).toBe(true); - expect(targets.some((target) => target.expr === "gittensory_queue_backlog_by_repo" && target.format === "table" && target.instant === true)).toBe(true); + expect(targets.some((target) => target.expr === "loopover_queue_backlog_convergence_pending")).toBe(true); + expect(targets.some((target) => target.expr === "loopover_queue_fresh_intake_pending")).toBe(true); + expect(targets.some((target) => target.expr === "loopover_github_rest_rate_limit_remaining")).toBe(true); + expect(targets.some((target) => target.legendFormat === "{{key_scope}}" && target.expr === "loopover_github_rest_rate_limit_remaining")).toBe(true); + expect(targets.some((target) => target.expr === "sum by (lane) (rate(loopover_jobs_claimed_by_lane_total[5m])) or vector(0)")).toBe(true); + expect(targets.some((target) => target.expr === "loopover_queue_backlog_by_repo" && target.format === "table" && target.instant === true)).toBe(true); }); }); diff --git a/test/unit/selfhost-grafana-infra-health-dashboard.test.ts b/test/unit/selfhost-grafana-infra-health-dashboard.test.ts index 87a9bf86cf..49fa8d2663 100644 --- a/test/unit/selfhost-grafana-infra-health-dashboard.test.ts +++ b/test/unit/selfhost-grafana-infra-health-dashboard.test.ts @@ -28,12 +28,12 @@ function allTargets(dashboard = readDashboard()): DashboardTarget[] { return dashboard.panels.flatMap((panel) => panel.targets ?? []); } -describe("Gittensory — Infra Health dashboard (#5366)", () => { +describe("LoopOver — Infra Health dashboard (#5366)", () => { it("declares the expected uid/title/tags", () => { const dashboard = readDashboard(); - expect(dashboard.uid).toBe("gittensory-infra-health"); - expect(dashboard.title).toBe("Gittensory — Infra Health (Host/Container/Redis/Qdrant)"); - expect(dashboard.tags).toEqual(["gittensory", "infra", "observability"]); + expect(dashboard.uid).toBe("loopover-infra-health"); + expect(dashboard.title).toBe("LoopOver — Infra Health (Host/Container/Redis/Qdrant)"); + expect(dashboard.tags).toEqual(["loopover", "infra", "observability"]); }); it("every panel target uses the Prometheus datasource variable, never a hardcoded uid", () => { diff --git a/test/unit/selfhost-grafana-rees-dashboard.test.ts b/test/unit/selfhost-grafana-rees-dashboard.test.ts index 530022e5bc..787d6be78d 100644 --- a/test/unit/selfhost-grafana-rees-dashboard.test.ts +++ b/test/unit/selfhost-grafana-rees-dashboard.test.ts @@ -28,12 +28,12 @@ function allTargets(dashboard = readDashboard()): DashboardTarget[] { return dashboard.panels.flatMap((panel) => panel.targets ?? []); } -describe("Gittensory — REES (review-enrichment) dashboard (#5367)", () => { +describe("LoopOver — REES (review-enrichment) dashboard (#5367)", () => { it("declares the expected uid/title/tags", () => { const dashboard = readDashboard(); - expect(dashboard.uid).toBe("gittensory-rees"); - expect(dashboard.title).toBe("Gittensory — REES (review-enrichment)"); - expect(dashboard.tags).toEqual(["gittensory", "rees", "observability"]); + expect(dashboard.uid).toBe("loopover-rees"); + expect(dashboard.title).toBe("LoopOver — REES (review-enrichment)"); + expect(dashboard.tags).toEqual(["loopover", "rees", "observability"]); }); it("every panel target uses the Prometheus datasource variable, never a hardcoded uid", () => { diff --git a/test/unit/selfhost-grafana-sentry-datasource.test.ts b/test/unit/selfhost-grafana-sentry-datasource.test.ts index 17ba7db64a..299519ba6e 100644 --- a/test/unit/selfhost-grafana-sentry-datasource.test.ts +++ b/test/unit/selfhost-grafana-sentry-datasource.test.ts @@ -102,9 +102,9 @@ describe("Grafana Sentry data source (#5369)", () => { it("declares the expected uid/title/tags and a $DS_SENTRY datasource-type template variable", () => { const dashboard = readDashboard(); - expect(dashboard.uid).toBe("gittensory-sentry"); - expect(dashboard.title).toBe("Gittensory — Sentry issues"); - expect(dashboard.tags).toEqual(["gittensory", "sentry", "observability"]); + expect(dashboard.uid).toBe("loopover-sentry"); + expect(dashboard.title).toBe("LoopOver — Sentry issues"); + expect(dashboard.tags).toEqual(["loopover", "sentry", "observability"]); }); it("every panel uses the grafana-sentry-datasource type and the ${DS_SENTRY} variable, never a hardcoded uid", () => { @@ -133,6 +133,6 @@ describe("Grafana Sentry data source (#5369)", () => { }; expect(hub.links.some((l) => l.title === "Sentry — errors")).toBe(true); const markdown = hub.panels.map((p) => p.options?.content ?? "").join("\n"); - expect(markdown).toContain("/d/gittensory-sentry"); + expect(markdown).toContain("/d/loopover-sentry"); }); }); diff --git a/test/unit/selfhost-metrics.test.ts b/test/unit/selfhost-metrics.test.ts index 2cb60e70ca..0c0fddddc3 100644 --- a/test/unit/selfhost-metrics.test.ts +++ b/test/unit/selfhost-metrics.test.ts @@ -62,20 +62,20 @@ describe("metrics registry (#982)", () => { it("resetMetrics preserves seeded metadata for built-in metrics", async () => { resetMetrics(); - incr("gittensory_jobs_processed_total"); + incr("loopover_jobs_processed_total"); expect(await renderMetrics()).toBe( - "# HELP gittensory_jobs_processed_total Durable queue jobs processed successfully.\n# TYPE gittensory_jobs_processed_total counter\ngittensory_jobs_processed_total 1\n", + "# HELP loopover_jobs_processed_total Durable queue jobs processed successfully.\n# TYPE loopover_jobs_processed_total counter\nloopover_jobs_processed_total 1\n", ); }); - it("renders gittensory_backup_acknowledged with seeded metadata (#2089)", async () => { - gauge("gittensory_backup_acknowledged", () => + it("renders loopover_backup_acknowledged with seeded metadata (#2089)", async () => { + gauge("loopover_backup_acknowledged", () => backupAcknowledgedGaugeValue({ usingSqlite: true, backupAcknowledged: false }), ); expect(await renderMetrics()).toBe( - "# HELP gittensory_backup_acknowledged 1 when SQLite backup is acknowledged or Postgres is in use; 0 when the boot backup advisory would fire.\n# TYPE gittensory_backup_acknowledged gauge\ngittensory_backup_acknowledged 0\n", + "# HELP loopover_backup_acknowledged 1 when SQLite backup is acknowledged or Postgres is in use; 0 when the boot backup advisory would fire.\n# TYPE loopover_backup_acknowledged gauge\nloopover_backup_acknowledged 0\n", ); }); @@ -96,37 +96,37 @@ describe("metrics registry (#982)", () => { }); it("redacts private repository labels from public review counters", async () => { - incr("gittensory_reviews_published_total", { repo: "private-owner/secret-repo" }); + incr("loopover_reviews_published_total", { repo: "private-owner/secret-repo" }); const out = await renderMetrics(); - expect(out).toContain("gittensory_reviews_published_total 1"); + expect(out).toContain("loopover_reviews_published_total 1"); expect(out).not.toContain("private-owner/secret-repo"); expect(out).not.toContain('repo="'); }); it("keeps non-sensitive gate labels after redacting the repository", async () => { - incr("gittensory_gate_decisions_total", { + incr("loopover_gate_decisions_total", { repo: "private-owner/secret-repo", conclusion: "success", }); const out = await renderMetrics(); - expect(out).toContain('gittensory_gate_decisions_total{conclusion="success"} 1'); + expect(out).toContain('loopover_gate_decisions_total{conclusion="success"} 1'); expect(out).not.toContain("private-owner/secret-repo"); expect(out).not.toContain('repo="'); }); it("keeps sensitive metric labels when no repository label is present", async () => { - incr("gittensory_gate_decisions_total", { conclusion: "hold" }); + incr("loopover_gate_decisions_total", { conclusion: "hold" }); - expect(await renderMetrics()).toContain('gittensory_gate_decisions_total{conclusion="hold"} 1'); + expect(await renderMetrics()).toContain('loopover_gate_decisions_total{conclusion="hold"} 1'); }); it("redacts the repository label from the ops anomaly counter but keeps the kind label (#ops-anomaly-metric)", async () => { - incr("gittensory_ops_anomaly_total", { repo: "private-owner/secret-repo", kind: "review_burst" }); + incr("loopover_ops_anomaly_total", { repo: "private-owner/secret-repo", kind: "review_burst" }); const out = await renderMetrics(); - expect(out).toContain('gittensory_ops_anomaly_total{kind="review_burst"} 1'); + expect(out).toContain('loopover_ops_anomaly_total{kind="review_burst"} 1'); expect(out).not.toContain("private-owner/secret-repo"); expect(out).not.toContain('repo="'); }); @@ -141,31 +141,31 @@ describe("metrics registry (#982)", () => { // redacting `repo` from these counters so an operator can actually slice their OWN dashboards by repo. it("setSelfHostedMetricsMode(true) stops redacting the repo label on the cloud-private counters", async () => { setSelfHostedMetricsMode(true); - incr("gittensory_gate_decisions_total", { repo: "owner/repo", conclusion: "success" }); - incr("gittensory_reviews_published_total", { repo: "owner/repo" }); - incr("gittensory_ops_anomaly_total", { repo: "owner/repo", kind: "review_burst" }); + incr("loopover_gate_decisions_total", { repo: "owner/repo", conclusion: "success" }); + incr("loopover_reviews_published_total", { repo: "owner/repo" }); + incr("loopover_ops_anomaly_total", { repo: "owner/repo", kind: "review_burst" }); const out = await renderMetrics(); - expect(out).toContain('gittensory_gate_decisions_total{conclusion="success",repo="owner/repo"} 1'); - expect(out).toContain('gittensory_reviews_published_total{repo="owner/repo"} 1'); - expect(out).toContain('gittensory_ops_anomaly_total{kind="review_burst",repo="owner/repo"} 1'); + expect(out).toContain('loopover_gate_decisions_total{conclusion="success",repo="owner/repo"} 1'); + expect(out).toContain('loopover_reviews_published_total{repo="owner/repo"} 1'); + expect(out).toContain('loopover_ops_anomaly_total{kind="review_burst",repo="owner/repo"} 1'); expect(out).toContain('repo="owner/repo"'); }); it("setSelfHostedMetricsMode(false) (the default) still redacts — byte-identical to the cloud worker", async () => { setSelfHostedMetricsMode(false); - incr("gittensory_agent_disposition_total", { repo: "owner/repo", action_class: "hold", blocker_class: "none", autonomy_level: "auto" }); + incr("loopover_agent_disposition_total", { repo: "owner/repo", action_class: "hold", blocker_class: "none", autonomy_level: "auto" }); const out = await renderMetrics(); expect(out).not.toContain("owner/repo"); expect(out).toContain( - 'gittensory_agent_disposition_total{action_class="hold",autonomy_level="auto",blocker_class="none",repo="redacted-1"} 1', + 'loopover_agent_disposition_total{action_class="hold",autonomy_level="auto",blocker_class="none",repo="redacted-1"} 1', ); }); it("keeps agent disposition repository labels redacted in self-hosted metrics mode", async () => { setSelfHostedMetricsMode(true); - incr("gittensory_agent_disposition_total", { + incr("loopover_agent_disposition_total", { repo: "private-owner/secret-repo", action_class: "hold", blocker_class: "manifest_blocked", @@ -174,7 +174,7 @@ describe("metrics registry (#982)", () => { const out = await renderMetrics(); expect(out).toContain( - 'gittensory_agent_disposition_total{action_class="hold",autonomy_level="auto",blocker_class="manifest_blocked",repo="redacted-1"} 1', + 'loopover_agent_disposition_total{action_class="hold",autonomy_level="auto",blocker_class="manifest_blocked",repo="redacted-1"} 1', ); expect(out).not.toContain("private-owner/secret-repo"); }); @@ -212,43 +212,43 @@ describe("gaugeVector (#selfhost-lane-observability)", () => { }); it("redacts repository labels from the public backlog-by-repo gauge vector", async () => { - gaugeVector("gittensory_queue_backlog_by_repo", () => [ + gaugeVector("loopover_queue_backlog_by_repo", () => [ { labels: { rank: "1", repo: "private-owner/secret-repo" }, value: 3 }, { labels: { rank: "2", repo: "other-org/confidential" }, value: 1 }, ]); const out = await renderMetrics(); - expect(out).toContain('gittensory_queue_backlog_by_repo{rank="1",repo="redacted-1"} 3'); - expect(out).toContain('gittensory_queue_backlog_by_repo{rank="2",repo="redacted-2"} 1'); + expect(out).toContain('loopover_queue_backlog_by_repo{rank="1",repo="redacted-1"} 3'); + expect(out).toContain('loopover_queue_backlog_by_repo{rank="2",repo="redacted-2"} 1'); resetMetrics(); - gaugeVector("gittensory_queue_backlog_by_repo", () => [ + gaugeVector("loopover_queue_backlog_by_repo", () => [ { labels: { repo: "private-owner/secret-repo" }, value: 4 }, ]); - expect(await renderMetrics()).toContain('gittensory_queue_backlog_by_repo{repo="redacted-1"} 4'); + expect(await renderMetrics()).toContain('loopover_queue_backlog_by_repo{repo="redacted-1"} 4'); expect(out).not.toContain("private-owner/secret-repo"); expect(out).not.toContain("other-org/confidential"); }); it("keeps backlog-by-repo redacted even in self-hosted metrics mode", async () => { setSelfHostedMetricsMode(true); - gaugeVector("gittensory_queue_backlog_by_repo", () => [ + gaugeVector("loopover_queue_backlog_by_repo", () => [ { labels: { rank: "1", repo: "owner/repo" }, value: 2 }, ]); const out = await renderMetrics(); - expect(out).toContain('gittensory_queue_backlog_by_repo{rank="1",repo="redacted-1"} 2'); + expect(out).toContain('loopover_queue_backlog_by_repo{rank="1",repo="redacted-1"} 2'); expect(out).not.toContain("owner/repo"); }); it("reuses the same redacted label for a repo across repeated scrapes, without resetting", async () => { - gaugeVector("gittensory_queue_backlog_by_repo", () => [ + gaugeVector("loopover_queue_backlog_by_repo", () => [ { labels: { rank: "1", repo: "owner/repo" }, value: 2 }, ]); const first = await renderMetrics(); const second = await renderMetrics(); - expect(first).toContain('gittensory_queue_backlog_by_repo{rank="1",repo="redacted-1"} 2'); - expect(second).toContain('gittensory_queue_backlog_by_repo{rank="1",repo="redacted-1"} 2'); + expect(first).toContain('loopover_queue_backlog_by_repo{rank="1",repo="redacted-1"} 2'); + expect(second).toContain('loopover_queue_backlog_by_repo{rank="1",repo="redacted-1"} 2'); }); it("supports an async sampler", async () => { @@ -349,11 +349,11 @@ describe("hitRatio (#2090)", () => { }); it("counterValue reads labeled counter totals and defaults missing series to 0", () => { - incr("gittensory_redis_gh_response_cache_total", { result: "hit" }, 4); - incr("gittensory_redis_gh_response_cache_total", { result: "miss" }, 1); - expect(counterValue("gittensory_redis_gh_response_cache_total", { result: "hit" })).toBe(4); - expect(counterValue("gittensory_redis_gh_response_cache_total", { result: "miss" })).toBe(1); - expect(counterValue("gittensory_redis_gh_response_cache_total", { result: "set" })).toBe(0); + incr("loopover_redis_gh_response_cache_total", { result: "hit" }, 4); + incr("loopover_redis_gh_response_cache_total", { result: "miss" }, 1); + expect(counterValue("loopover_redis_gh_response_cache_total", { result: "hit" })).toBe(4); + expect(counterValue("loopover_redis_gh_response_cache_total", { result: "miss" })).toBe(1); + expect(counterValue("loopover_redis_gh_response_cache_total", { result: "set" })).toBe(0); }); }); diff --git a/test/unit/selfhost-monitored-work.test.ts b/test/unit/selfhost-monitored-work.test.ts index adccf01535..0d1df65032 100644 --- a/test/unit/selfhost-monitored-work.test.ts +++ b/test/unit/selfhost-monitored-work.test.ts @@ -142,10 +142,10 @@ describe("self-host monitored recurring work", () => { JSON.stringify({ event: "orb_relay_drained", count: 3 }), ); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_orb_relay_drains_total{result="events"} 1'); - expect(metrics).toContain('gittensory_orb_webhook_total{event="pull_request",result="queued"} 1'); - expect(metrics).toContain('gittensory_orb_webhook_total{event="other",result="enqueue_failed"} 1'); - expect(metrics).toContain('gittensory_orb_webhook_total{event="check_suite",result="duplicate"} 1'); + expect(metrics).toContain('loopover_orb_relay_drains_total{result="events"} 1'); + expect(metrics).toContain('loopover_orb_webhook_total{event="pull_request",result="queued"} 1'); + expect(metrics).toContain('loopover_orb_webhook_total{event="other",result="enqueue_failed"} 1'); + expect(metrics).toContain('loopover_orb_webhook_total{event="check_suite",result="duplicate"} 1'); }); it("REGRESSION (#audit-orb-relay-enqueue-isolation): an enqueue that throws for one event does not abort the rest of the batch", async () => { @@ -179,9 +179,9 @@ describe("self-host monitored recurring work", () => { expect(logged).toBeDefined(); expect(JSON.parse(logged!)).toMatchObject({ level: "error", event: "orb_relay_enqueue_threw", eventName: "issues", error: "D1 write error" }); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_orb_webhook_total{event="pull_request",result="queued"} 1'); - expect(metrics).toContain('gittensory_orb_webhook_total{event="issues",result="enqueue_failed"} 1'); - expect(metrics).toContain('gittensory_orb_webhook_total{event="check_suite",result="queued"} 1'); + expect(metrics).toContain('loopover_orb_webhook_total{event="pull_request",result="queued"} 1'); + expect(metrics).toContain('loopover_orb_webhook_total{event="issues",result="enqueue_failed"} 1'); + expect(metrics).toContain('loopover_orb_webhook_total{event="check_suite",result="queued"} 1'); errors.mockRestore(); }); @@ -217,7 +217,7 @@ describe("self-host monitored recurring work", () => { expect(state.pendingAck).toEqual([]); expect(enqueue).not.toHaveBeenCalled(); expect(log).not.toHaveBeenCalled(); - expect(await renderMetrics()).toContain('gittensory_orb_relay_drains_total{result="empty"} 1'); + expect(await renderMetrics()).toContain('loopover_orb_relay_drains_total{result="empty"} 1'); // An empty poll still proves the broker round-trip succeeded -- stamped even with zero events. expect(state.lastDrainAtMs).toBe(5_000); }); @@ -321,7 +321,7 @@ describe("self-host monitored recurring work", () => { expect(log).toHaveBeenCalledWith( JSON.stringify({ event: "selfhost_orb_relay_register", mode: "push", attempts: 1 }), ); - expect(await renderMetrics()).toContain('gittensory_orb_relay_register_total{mode="push",result="registered"} 1'); + expect(await renderMetrics()).toContain('loopover_orb_relay_register_total{mode="push",result="registered"} 1'); // A first-try success is not a recovery -- no recovered series at all. expect(await renderMetrics()).not.toContain('result="recovered"'); }); @@ -337,7 +337,7 @@ describe("self-host monitored recurring work", () => { expect(log).toHaveBeenCalledWith( JSON.stringify({ event: "selfhost_orb_relay_register_recovered", mode: "pull", attempts: 3 }), ); - expect(await renderMetrics()).toContain('gittensory_orb_relay_register_total{mode="pull",result="recovered"} 1'); + expect(await renderMetrics()).toContain('loopover_orb_relay_register_total{mode="pull",result="recovered"} 1'); }); it("warns (not errors) on a single pull-mode failure below the streak threshold with no drain-progress evidence yet", async () => { @@ -355,7 +355,7 @@ describe("self-host monitored recurring work", () => { JSON.stringify({ level: "warn", event: "selfhost_orb_relay_register_failed", mode: "pull", error: "http_500", attempts: 1, consecutiveFailures: 1 }), ); expect(errorSpy).not.toHaveBeenCalled(); - expect(await renderMetrics()).toContain('gittensory_orb_relay_register_total{mode="pull",result="failed"} 1'); + expect(await renderMetrics()).toContain('loopover_orb_relay_register_total{mode="pull",result="failed"} 1'); } finally { errorSpy.mockRestore(); warnSpy.mockRestore(); @@ -466,7 +466,7 @@ describe("self-host monitored recurring work", () => { await registerOrbRelayWithMonitor({ env: {}, state: freshState(), register, log }); } expect(log).not.toHaveBeenCalled(); - expect(await renderMetrics()).not.toContain("gittensory_orb_relay_register_total"); + expect(await renderMetrics()).not.toContain("loopover_orb_relay_register_total"); }); it("uses console.log as the default logger", async () => { diff --git a/test/unit/selfhost-observability-config.test.ts b/test/unit/selfhost-observability-config.test.ts index 4b793dc0a1..4ad7bd1354 100644 --- a/test/unit/selfhost-observability-config.test.ts +++ b/test/unit/selfhost-observability-config.test.ts @@ -85,7 +85,7 @@ describe("self-host observability trace config", () => { expect(script).toContain("http://otel-collector:4318/v1/traces"); expect(script).toContain("http://tempo:3200/api/traces/"); - expect(script).toContain("gittensory-selfhost-smoke"); + expect(script).toContain("loopover-selfhost-smoke"); expect(script).toContain("selfhost.observability.smoke"); }); @@ -98,8 +98,8 @@ describe("self-host observability trace config", () => { expect(script).toContain("http://otel-collector:4318/v1/metrics"); expect(script).toContain("http://otel-collector:8889/metrics"); expect(script).toContain("http://localhost:8787/metrics"); - expect(script).toContain("gittensory-selfhost-smoke"); - expect(script).toContain("# HELP gittensory_uptime_seconds"); + expect(script).toContain("loopover-selfhost-smoke"); + expect(script).toContain("# HELP loopover_uptime_seconds"); const packageJson = JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf8")); expect(packageJson.scripts["test:smoke:observability:metrics"]).toBe( @@ -138,7 +138,7 @@ describe("self-host observability trace config", () => { ]); expect(backupExporter.healthcheck?.test).toEqual([ "CMD-SHELL", - "wget -qO- http://127.0.0.1:9101/metrics | grep -q '^gittensory_backup_latest_timestamp_seconds'", + "wget -qO- http://127.0.0.1:9101/metrics | grep -q '^loopover_backup_latest_timestamp_seconds'", ]); expect(scrapeConfigs).toEqual( diff --git a/test/unit/selfhost-orb-collector.test.ts b/test/unit/selfhost-orb-collector.test.ts index 1e452b1766..7c2ec285f5 100644 --- a/test/unit/selfhost-orb-collector.test.ts +++ b/test/unit/selfhost-orb-collector.test.ts @@ -186,7 +186,7 @@ describe("exportOrbBatch() — always-on; reads review_audit, ships anonymized r await audit(db, "o/r", 1, "gate_decision", "merge", "2026-01-01T00:00:00Z"); await audit(db, "o/r", 1, "pr_outcome", "merged", "2026-01-01T01:00:00Z"); expect(await exportOrbBatch(db, 200, async () => new Response(null, { status: 503 }))).toBe(0); - expect(await renderMetrics()).toContain("gittensory_orb_export_errors_total"); + expect(await renderMetrics()).toContain("loopover_orb_export_errors_total"); }); it("returns 0 + increments error counter when the collector is unreachable", async () => { @@ -194,7 +194,7 @@ describe("exportOrbBatch() — always-on; reads review_audit, ships anonymized r await audit(db, "o/r", 1, "gate_decision", "merge", "2026-01-01T00:00:00Z"); await audit(db, "o/r", 1, "pr_outcome", "merged", "2026-01-01T01:00:00Z"); expect(await exportOrbBatch(db, 200, async () => { throw new Error("ECONNREFUSED"); })).toBe(0); - expect(await renderMetrics()).toContain("gittensory_orb_export_errors_total"); + expect(await renderMetrics()).toContain("loopover_orb_export_errors_total"); }); it("signs the batch and respects batchSize", async () => { diff --git a/test/unit/selfhost-pg-queue.test.ts b/test/unit/selfhost-pg-queue.test.ts index 90d405064a..1c4d33bb6d 100644 --- a/test/unit/selfhost-pg-queue.test.ts +++ b/test/unit/selfhost-pg-queue.test.ts @@ -1189,7 +1189,7 @@ describe("createPgQueue (durable #977)", () => { expect(claimSql[0]).toContain("foreground_lane='backlog'"); expect(sequenceAllocations).toBeGreaterThan(0); expect(repoRecorded).toBe("owner/repo"); - expect(await renderMetrics()).toContain('gittensory_jobs_claimed_by_lane_total{lane="backlog"} 1'); + expect(await renderMetrics()).toContain('loopover_jobs_claimed_by_lane_total{lane="backlog"} 1'); }); it("falls through to the plain unscoped foreground claim when the backlog lane has no pending candidates", async () => { @@ -1229,7 +1229,7 @@ describe("createPgQueue (durable #977)", () => { expect(claimSql[0]).not.toContain("foreground_lane"); // The unscoped fallback claim is not lane-scoped, so it must never record a lane-claim increment // (#selfhost-lane-observability). - expect(await renderMetrics()).not.toContain("gittensory_jobs_claimed_by_lane_total"); + expect(await renderMetrics()).not.toContain("loopover_jobs_claimed_by_lane_total"); }); it("does not let a lower-priority classified lane starve a higher-priority manual regate", async () => { @@ -1280,7 +1280,7 @@ describe("createPgQueue (durable #977)", () => { expect(claimSql[0]).toContain("foreground_lane='backlog'"); expect(claimSql[0]).toContain("candidate.priority > $2"); expect(claimSql[1]).not.toContain("foreground_lane"); - expect(await renderMetrics()).not.toContain("gittensory_jobs_claimed_by_lane_total"); + expect(await renderMetrics()).not.toContain("loopover_jobs_claimed_by_lane_total"); }); it("records the fresh-intake lane-claim counter on a successful fresh-lane claim (#selfhost-lane-observability)", async () => { @@ -1307,7 +1307,7 @@ describe("createPgQueue (durable #977)", () => { await q.drain(); expect(seen).toEqual(["github-webhook"]); - expect(await renderMetrics()).toContain('gittensory_jobs_claimed_by_lane_total{lane="fresh"} 1'); + expect(await renderMetrics()).toContain('loopover_jobs_claimed_by_lane_total{lane="fresh"} 1'); }); it("allocates the claim sequence atomically via UPDATE ... RETURNING, not a separate SELECT-then-UPDATE (#selfhost-backlog-convergence review)", async () => { @@ -1729,9 +1729,9 @@ describe("createPgQueue (durable #977)", () => { ); expect(m.pool.query).toHaveBeenCalledWith( expect.stringContaining("INSERT INTO _selfhost_job_stats"), - ["gittensory_jobs_rate_limit_deferred_total", 1], + ["loopover_jobs_rate_limit_deferred_total", 1], ); - expect(await renderMetrics()).toContain('gittensory_jobs_rate_limit_admission_deferred_total{job_type="agent-regate-pr",key_scope="installation",kind="background"} 1'); + expect(await renderMetrics()).toContain('loopover_jobs_rate_limit_admission_deferred_total{job_type="agent-regate-pr",key_scope="installation",kind="background"} 1'); } finally { if (oldJitter === undefined) delete process.env.QUEUE_RATE_LIMIT_JITTER_MS; else process.env.QUEUE_RATE_LIMIT_JITTER_MS = oldJitter; @@ -1801,7 +1801,7 @@ describe("createPgQueue (durable #977)", () => { expect.stringContaining("SET status='pending', run_after=GREATEST"), [Date.parse("2026-06-24T12:10:15.000Z"), "github rate-limit background admission", "background"], ); - expect(await renderMetrics()).toContain('gittensory_jobs_rate_limit_admission_deferred_total{job_type="agent-regate-sweep",key_scope="public",kind="background"} 1'); + expect(await renderMetrics()).toContain('loopover_jobs_rate_limit_admission_deferred_total{job_type="agent-regate-sweep",key_scope="public",kind="background"} 1'); } finally { if (oldJitter === undefined) delete process.env.QUEUE_RATE_LIMIT_JITTER_MS; else process.env.QUEUE_RATE_LIMIT_JITTER_MS = oldJitter; @@ -1858,9 +1858,9 @@ describe("createPgQueue (durable #977)", () => { ); expect(m.pool.query).toHaveBeenCalledWith( expect.stringContaining("INSERT INTO _selfhost_job_stats"), - ["gittensory_jobs_rate_limit_deferred_total", 1], + ["loopover_jobs_rate_limit_deferred_total", 1], ); - expect(await renderMetrics()).toContain('gittensory_jobs_rate_limit_admission_deferred_total{job_type="github-webhook",key_scope="installation",kind="webhook"} 1'); + expect(await renderMetrics()).toContain('loopover_jobs_rate_limit_admission_deferred_total{job_type="github-webhook",key_scope="installation",kind="webhook"} 1'); } finally { if (oldJitter === undefined) delete process.env.QUEUE_RATE_LIMIT_JITTER_MS; else process.env.QUEUE_RATE_LIMIT_JITTER_MS = oldJitter; @@ -2039,7 +2039,7 @@ describe("createPgQueue (durable #977)", () => { expect(warned).not.toHaveBeenCalled(); expect(m.pool.query).not.toHaveBeenCalledWith( expect.stringContaining("INSERT INTO _selfhost_job_stats"), - ["gittensory_jobs_rate_limit_deferred_total", 1], + ["loopover_jobs_rate_limit_deferred_total", 1], ); } finally { if (oldJitter === undefined) delete process.env.QUEUE_RATE_LIMIT_JITTER_MS; @@ -2106,7 +2106,7 @@ describe("createPgQueue (durable #977)", () => { expect.stringContaining("SET status='pending', run_after=$1, last_error=NULL"), expect.arrayContaining(["2"]), ); - expect(await renderMetrics()).toContain("gittensory_jobs_dead_letter_revived_total 2"); + expect(await renderMetrics()).toContain("loopover_jobs_dead_letter_revived_total 2"); }); it("is a no-op (and records nothing) when no dead job is under the ceiling", async () => { @@ -2117,7 +2117,7 @@ describe("createPgQueue (durable #977)", () => { const revived = await q.reviveDeadLetterJobs(); expect(revived).toBe(0); - expect(await renderMetrics()).not.toContain("gittensory_jobs_dead_letter_revived_total"); + expect(await renderMetrics()).not.toContain("loopover_jobs_dead_letter_revived_total"); }); // REGRESSION (#2581 review defect): the SELECT is a stale snapshot. Without an "AND status='dead'" re-check on @@ -2155,7 +2155,7 @@ describe("createPgQueue (durable #977)", () => { ), expect.arrayContaining(["2"]), ); - expect(await renderMetrics()).toContain("gittensory_jobs_dead_letter_revived_total 1"); + expect(await renderMetrics()).toContain("loopover_jobs_dead_letter_revived_total 1"); }); // REGRESSION (#2581 review defect): the revive interval had no error handler of its own, so a thrown @@ -2268,7 +2268,7 @@ describe("createPgQueue (durable #977)", () => { expect.stringContaining("SET run_after=$1 WHERE id=$2 AND status='pending' AND run_after>$1"), expect.arrayContaining(["fg-1"]), ); - expect(await renderMetrics()).toContain("gittensory_jobs_foreground_liveness_released_total 1"); + expect(await renderMetrics()).toContain("loopover_jobs_foreground_liveness_released_total 1"); }); // Isolates the AGE condition from the OR'd rate-limit-clear condition: the default candidate payload @@ -2304,7 +2304,7 @@ describe("createPgQueue (durable #977)", () => { expect.stringContaining("SET run_after=$1 WHERE id=$2 AND status='pending' AND run_after>$1"), expect.arrayContaining(["fg-fresh"]), ); - expect(await renderMetrics()).not.toContain("gittensory_jobs_foreground_liveness_released_total"); + expect(await renderMetrics()).not.toContain("loopover_jobs_foreground_liveness_released_total"); }); it("caches foreground-liveness admission reads for candidates sharing the same rate-limit target", async () => { @@ -2360,7 +2360,7 @@ describe("createPgQueue (durable #977)", () => { const released = await q.releaseStaleForegroundDeferrals(); expect(released).toBe(1); - expect(await renderMetrics()).toContain("gittensory_jobs_foreground_liveness_released_total 1"); + expect(await renderMetrics()).toContain("loopover_jobs_foreground_liveness_released_total 1"); }); // The payload is unparseable -- isRateLimitAdmissionNowClear's own catch(){ return false } branch -- so ONLY @@ -2411,7 +2411,7 @@ describe("createPgQueue (durable #977)", () => { expect.stringContaining("SELECT id, payload, created_at FROM _selfhost_jobs WHERE status='pending' AND priority>=$1 AND run_after>$2"), expect.anything(), ); - expect(await renderMetrics()).not.toContain("gittensory_jobs_foreground_liveness_released_total"); + expect(await renderMetrics()).not.toContain("loopover_jobs_foreground_liveness_released_total"); }); // REGRESSION (#selfhost-queue-liveness): the production incident this module exists to make structurally @@ -2444,7 +2444,7 @@ describe("createPgQueue (durable #977)", () => { ); } // Exactly one aggregate increment of 3, not three separate increments of 1. - expect(await renderMetrics()).toContain("gittensory_jobs_foreground_liveness_released_total 3"); + expect(await renderMetrics()).toContain("loopover_jobs_foreground_liveness_released_total 3"); }); // Ramp-up cap (#selfhost-queue-liveness): a large inherited backlog (the production incident had ~190 @@ -2493,7 +2493,7 @@ describe("createPgQueue (durable #977)", () => { expect.arrayContaining([id]), ); } - expect(await renderMetrics()).toContain("gittensory_jobs_foreground_liveness_released_total 2"); + expect(await renderMetrics()).toContain("loopover_jobs_foreground_liveness_released_total 2"); delete process.env.FOREGROUND_LIVENESS_MAX_RELEASE_PER_SWEEP; }); @@ -2639,7 +2639,7 @@ describe("createPgQueue (durable #977)", () => { const released = await q.releaseStaleForegroundDeferrals(); expect(released).toBe(0); // null ?? 0 -- no metric recorded, no crash - expect(await renderMetrics()).not.toContain("gittensory_jobs_foreground_liveness_released_total"); + expect(await renderMetrics()).not.toContain("loopover_jobs_foreground_liveness_released_total"); }); }); @@ -2707,7 +2707,7 @@ describe("createPgQueue (durable #977)", () => { [2, "openai api rate limit exceeded", expect.any(Number), "1"], ); expect(m.pool.query).not.toHaveBeenCalledWith( - expect.stringContaining("gittensory_jobs_rate_limited_total"), + expect.stringContaining("loopover_jobs_rate_limited_total"), expect.anything(), ); }); @@ -2742,7 +2742,7 @@ describe("createPgQueue (durable #977)", () => { expect.stringContaining("DELETE FROM _selfhost_jobs WHERE id=$1"), ["2"], ); - expect(await renderMetrics()).toContain('gittensory_jobs_rate_limited_by_type_total{job_type="refresh-registry",key_scope="unknown",kind="unknown"} 1'); + expect(await renderMetrics()).toContain('loopover_jobs_rate_limited_by_type_total{job_type="refresh-registry",key_scope="unknown",kind="unknown"} 1'); }); it("defers matching GitHub-budget jobs and coalesces a keyed rate-limit retry into the pending duplicate", async () => { @@ -2843,8 +2843,8 @@ describe("createPgQueue (durable #977)", () => { expect.arrayContaining([expect.stringContaining('"deliveryId":"after-rate-limit"'), expect.any(Number)]), ); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_jobs_rate_limit_budget_deferred_total{job_type="github-webhook",key_scope="installation",kind="webhook"} 1'); - expect(metrics).toContain('gittensory_jobs_rate_limited_by_type_total{job_type="github-webhook",key_scope="installation",kind="webhook"} 1'); + expect(metrics).toContain('loopover_jobs_rate_limit_budget_deferred_total{job_type="github-webhook",key_scope="installation",kind="webhook"} 1'); + expect(metrics).toContain('loopover_jobs_rate_limited_by_type_total{job_type="github-webhook",key_scope="installation",kind="webhook"} 1'); } finally { if (oldJitter === undefined) delete process.env.QUEUE_STARTUP_JITTER_MS; else process.env.QUEUE_STARTUP_JITTER_MS = oldJitter; @@ -3161,14 +3161,14 @@ describe("createPgQueue (durable #977)", () => { await q.init(); m.fn.mockResolvedValueOnce({ rows: [ - { name: "gittensory_jobs_processed_total", value: "42" }, - { name: "gittensory_jobs_dead_total", value: null }, + { name: "loopover_jobs_processed_total", value: "42" }, + { name: "loopover_jobs_dead_total", value: null }, ], rowCount: 2, }); await expect(q.stats()).resolves.toEqual({ - gittensory_jobs_processed_total: 42, - gittensory_jobs_dead_total: 0, + loopover_jobs_processed_total: 42, + loopover_jobs_dead_total: 0, }); }); @@ -3230,7 +3230,7 @@ describe("createPgQueue (durable #977)", () => { expect.arrayContaining([expect.stringContaining("live_pending_high")]), ); expect(await renderMetrics()).toContain( - 'gittensory_jobs_maintenance_admission_deferred_by_reason_total{job_type="build-contributor-evidence",reason="live_pending_high"} 1', + 'loopover_jobs_maintenance_admission_deferred_by_reason_total{job_type="build-contributor-evidence",reason="live_pending_high"} 1', ); }); @@ -3299,7 +3299,7 @@ describe("createPgQueue (durable #977)", () => { await q.drain(); expect(started).not.toContain("build-contributor-evidence"); expect(await renderMetrics()).toContain( - 'gittensory_jobs_maintenance_admission_deferred_by_reason_total{job_type="build-contributor-evidence",reason="backlog_convergence_high"} 1', + 'loopover_jobs_maintenance_admission_deferred_by_reason_total{job_type="build-contributor-evidence",reason="backlog_convergence_high"} 1', ); }); @@ -3332,7 +3332,7 @@ describe("createPgQueue (durable #977)", () => { await q.drain(); expect(started).toEqual(["build-contributor-evidence"]); expect(await renderMetrics()).toContain( - 'gittensory_jobs_maintenance_admission_granted_under_pressure_total{job_type="build-contributor-evidence",reason="maintenance_pending_high_drain"} 1', + 'loopover_jobs_maintenance_admission_granted_under_pressure_total{job_type="build-contributor-evidence",reason="maintenance_pending_high_drain"} 1', ); } finally { if (oldEnv === undefined) delete process.env.MAINTENANCE_ADMISSION_DRAIN_AGE_MS; @@ -3390,8 +3390,8 @@ describe("createPgQueue (durable #977)", () => { await q.drain(); expect(started).toEqual(["build-contributor-evidence"]); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_jobs_maintenance_trickle_admitted_by_type_total{job_type="build-contributor-evidence"} 1'); - expect(metrics).toContain('gittensory_jobs_maintenance_admission_granted_under_pressure_total{job_type="build-contributor-evidence",reason="trickle_max_defer_age"} 1'); + expect(metrics).toContain('loopover_jobs_maintenance_trickle_admitted_by_type_total{job_type="build-contributor-evidence"} 1'); + expect(metrics).toContain('loopover_jobs_maintenance_admission_granted_under_pressure_total{job_type="build-contributor-evidence",reason="trickle_max_defer_age"} 1'); } finally { if (oldEnv === undefined) delete process.env.MAINTENANCE_ADMISSION_MAX_DEFER_AGE_MS; else process.env.MAINTENANCE_ADMISSION_MAX_DEFER_AGE_MS = oldEnv; @@ -3406,7 +3406,7 @@ describe("createPgQueue (durable #977)", () => { const q = createPgQueue(m.pool, async (j) => void started.push(typeOf(j))); await q.drain(); expect(started).toEqual(["build-contributor-evidence"]); - expect(await renderMetrics()).not.toContain("gittensory_jobs_maintenance_admission_granted_under_pressure_total"); + expect(await renderMetrics()).not.toContain("loopover_jobs_maintenance_admission_granted_under_pressure_total"); }); it("pressureSignals() surfaces the live, maintenance, backlog-convergence, and fresh-intake aggregate reads", async () => { @@ -3506,7 +3506,7 @@ describe("createPgQueue (durable #977)", () => { expect.arrayContaining([expect.stringContaining("installation concurrency admission deferred: concurrency_high")]), ); expect(await renderMetrics()).toContain( - 'gittensory_jobs_installation_concurrency_deferred_by_reason_total{job_type="backfill-repo-segment",reason="concurrency_high"} 1', + 'loopover_jobs_installation_concurrency_deferred_by_reason_total{job_type="backfill-repo-segment",reason="concurrency_high"} 1', ); } finally { release(); @@ -3560,7 +3560,7 @@ describe("createPgQueue (durable #977)", () => { expect.arrayContaining([expect.stringContaining("installation concurrency admission deferred: concurrency_high")]), ); expect(await renderMetrics()).toContain( - 'gittensory_jobs_installation_concurrency_deferred_by_reason_total{job_type="agent-regate-sweep",reason="concurrency_high"} 1', + 'loopover_jobs_installation_concurrency_deferred_by_reason_total{job_type="agent-regate-sweep",reason="concurrency_high"} 1', ); } finally { release(); diff --git a/test/unit/selfhost-qdrant-vectorize.test.ts b/test/unit/selfhost-qdrant-vectorize.test.ts index ce4ed259fb..ce8fe6d51c 100644 --- a/test/unit/selfhost-qdrant-vectorize.test.ts +++ b/test/unit/selfhost-qdrant-vectorize.test.ts @@ -162,15 +162,15 @@ describe("createQdrantVectorize (#1217 Qdrant adapter)", () => { vi.stubGlobal("fetch", mockFetch(503)); const v = createQdrantVectorize(BASE); await expect(v.upsert([{ id: "x", values: [1] }])).rejects.toThrow(/HTTP 503/); - expect(await renderMetrics()).toContain('gittensory_qdrant_errors_total{op="upsert"}'); + expect(await renderMetrics()).toContain('loopover_qdrant_errors_total{op="upsert"}'); }); - it("successful upsert increments gittensory_qdrant_upserts_total by vector count", async () => { + it("successful upsert increments loopover_qdrant_upserts_total by vector count", async () => { vi.stubGlobal("fetch", mockFetch(200)); const v = createQdrantVectorize(BASE); await v.upsert([{ id: "a", values: [1] }, { id: "b", values: [0] }]); const metrics = await renderMetrics(); - expect(metrics).toMatch(/gittensory_qdrant_upserts_total 2/); + expect(metrics).toMatch(/loopover_qdrant_upserts_total 2/); }); it("same string ID always produces the same UUID (deterministic mapping)", async () => { @@ -224,7 +224,7 @@ describe("createQdrantVectorize (#1217 Qdrant adapter)", () => { const v = createQdrantVectorize(BASE); const { matches } = await v.query([1, 0], { topK: 5 }); expect(matches).toEqual([]); - expect(await renderMetrics()).toContain('gittensory_qdrant_errors_total{op="query"}'); + expect(await renderMetrics()).toContain('loopover_qdrant_errors_total{op="query"}'); }); it("query returns empty matches on non-OK HTTP response (graceful degrade) and tracks error", async () => { @@ -232,15 +232,15 @@ describe("createQdrantVectorize (#1217 Qdrant adapter)", () => { const v = createQdrantVectorize(BASE); const { matches } = await v.query([1, 0], { topK: 5 }); expect(matches).toEqual([]); - expect(await renderMetrics()).toContain('gittensory_qdrant_errors_total{op="query"}'); + expect(await renderMetrics()).toContain('loopover_qdrant_errors_total{op="query"}'); }); - it("successful query increments gittensory_qdrant_queries_total", async () => { + it("successful query increments loopover_qdrant_queries_total", async () => { vi.stubGlobal("fetch", mockFetch(200, { result: [] })); const v = createQdrantVectorize(BASE); await v.query([1], {}); await v.query([0], {}); - expect(await renderMetrics()).toMatch(/gittensory_qdrant_queries_total 2/); + expect(await renderMetrics()).toMatch(/loopover_qdrant_queries_total 2/); }); it("query returns match without metadata when payload has no extra fields", async () => { @@ -290,7 +290,7 @@ describe("createQdrantVectorize (#1217 Qdrant adapter)", () => { vi.stubGlobal("fetch", mockFetch(400)); const v = createQdrantVectorize(BASE); await expect(v.deleteByIds(["id"])).rejects.toThrow(/HTTP 400/); - expect(await renderMetrics()).toContain('gittensory_qdrant_errors_total{op="delete"}'); + expect(await renderMetrics()).toContain('loopover_qdrant_errors_total{op="delete"}'); }); it("trailing slash in URL is stripped", async () => { diff --git a/test/unit/selfhost-redis-cache.test.ts b/test/unit/selfhost-redis-cache.test.ts index f15fa1c520..b4b56fa785 100644 --- a/test/unit/selfhost-redis-cache.test.ts +++ b/test/unit/selfhost-redis-cache.test.ts @@ -111,21 +111,21 @@ describe("isWebhookDeliveryDuplicate (#2075)", () => { it("returns false and does not increment on a first-time delivery", async () => { const cache = createRedisCache(fakeRedis()); await expect(isWebhookDeliveryDuplicate(cache, "delivery-1")).resolves.toBe(false); - expect(await renderMetrics()).not.toContain('gittensory_webhook_dedup_total{backend="redis"}'); + expect(await renderMetrics()).not.toContain('loopover_webhook_dedup_total{backend="redis"}'); }); - it("returns true and increments gittensory_webhook_dedup_total{backend=\"redis\"} when already seen", async () => { + it("returns true and increments loopover_webhook_dedup_total{backend=\"redis\"} when already seen", async () => { const cache = createRedisCache(fakeRedis()); await cache.set(webhookDeliveryCacheKey("delivery-2"), "1", 300); await expect(isWebhookDeliveryDuplicate(cache, "delivery-2")).resolves.toBe(true); - expect(await renderMetrics()).toContain('gittensory_webhook_dedup_total{backend="redis"} 1'); + expect(await renderMetrics()).toContain('loopover_webhook_dedup_total{backend="redis"} 1'); }); it("returns false without incrementing when Redis get throws", async () => { const brokenRedis = { async get() { throw new Error("connection refused"); } } as unknown as Redis; const cache = createRedisCache(brokenRedis); await expect(isWebhookDeliveryDuplicate(cache, "delivery-3")).resolves.toBe(false); - expect(await renderMetrics()).not.toContain('gittensory_webhook_dedup_total{backend="redis"}'); + expect(await renderMetrics()).not.toContain('loopover_webhook_dedup_total{backend="redis"}'); }); it("rememberWebhookDelivery stores the delivery key for later dedup", async () => { diff --git a/test/unit/selfhost-redis-response-cache.test.ts b/test/unit/selfhost-redis-response-cache.test.ts index dbff3d2329..c7406b8b5b 100644 --- a/test/unit/selfhost-redis-response-cache.test.ts +++ b/test/unit/selfhost-redis-response-cache.test.ts @@ -33,7 +33,7 @@ describe("createRedisResponseCache (#perf GitHub GET cache)", () => { await createRedisResponseCache(fakeRedis().redis).get(URL_A), ).toBeNull(); expect(await renderMetrics()).toContain( - 'gittensory_redis_gh_response_cache_total{result="miss"} 1', + 'loopover_redis_gh_response_cache_total{result="miss"} 1', ); }); @@ -63,10 +63,10 @@ describe("createRedisResponseCache (#perf GitHub GET cache)", () => { }); const metrics = await renderMetrics(); expect(metrics).toContain( - 'gittensory_redis_gh_response_cache_total{result="set"} 1', + 'loopover_redis_gh_response_cache_total{result="set"} 1', ); expect(metrics).toContain( - 'gittensory_redis_gh_response_cache_total{result="hit"} 1', + 'loopover_redis_gh_response_cache_total{result="hit"} 1', ); }); @@ -127,7 +127,7 @@ describe("createRedisResponseCache (#perf GitHub GET cache)", () => { f.store.set("gh:resp:" + URL_A, "{nope"); expect(await createRedisResponseCache(f.redis).get(URL_A)).toBeNull(); expect(await renderMetrics()).toContain( - 'gittensory_redis_gh_response_cache_total{result="miss"} 1', + 'loopover_redis_gh_response_cache_total{result="miss"} 1', ); }); @@ -136,7 +136,7 @@ describe("createRedisResponseCache (#perf GitHub GET cache)", () => { f.store.set("gh:resp:" + URL_A, JSON.stringify({ status: "200", body: 1 })); expect(await createRedisResponseCache(f.redis).get(URL_A)).toBeNull(); expect(await renderMetrics()).toContain( - 'gittensory_redis_gh_response_cache_total{result="miss"} 1', + 'loopover_redis_gh_response_cache_total{result="miss"} 1', ); }); @@ -206,7 +206,7 @@ describe("createRedisResponseCache (#perf GitHub GET cache)", () => { contentType: "application/json", }); expect(await renderMetrics()).toContain( - 'gittensory_redis_gh_response_cache_total{result="hit"} 1', + 'loopover_redis_gh_response_cache_total{result="hit"} 1', ); }); @@ -221,7 +221,7 @@ describe("createRedisResponseCache (#perf GitHub GET cache)", () => { "redis read failed", ); expect(await renderMetrics()).toContain( - 'gittensory_redis_gh_response_cache_total{result="error"} 1', + 'loopover_redis_gh_response_cache_total{result="error"} 1', ); }); @@ -244,14 +244,14 @@ describe("createRedisResponseCache (#perf GitHub GET cache)", () => { ), ).rejects.toThrow("redis write failed"); expect(await renderMetrics()).toContain( - 'gittensory_redis_gh_response_cache_total{result="error"} 1', + 'loopover_redis_gh_response_cache_total{result="error"} 1', ); }); it("registers a scrape-time hit-ratio gauge when the cache is constructed (#2090)", async () => { - incr("gittensory_redis_gh_response_cache_total", { result: "hit" }, 3); - incr("gittensory_redis_gh_response_cache_total", { result: "miss" }, 1); + incr("loopover_redis_gh_response_cache_total", { result: "hit" }, 3); + incr("loopover_redis_gh_response_cache_total", { result: "miss" }, 1); createRedisResponseCache(fakeRedis().redis); - expect(await renderMetrics()).toContain("gittensory_redis_gh_response_cache_hit_ratio 0.75"); + expect(await renderMetrics()).toContain("loopover_redis_gh_response_cache_hit_ratio 0.75"); }); }); diff --git a/test/unit/selfhost-redis-token-cache.test.ts b/test/unit/selfhost-redis-token-cache.test.ts index 967ad371fb..42013b6d10 100644 --- a/test/unit/selfhost-redis-token-cache.test.ts +++ b/test/unit/selfhost-redis-token-cache.test.ts @@ -32,7 +32,7 @@ describe("createRedisTokenCache (#perf installation-token persistence)", () => { expect(await createRedisTokenCache(redis).get(42)).toBeNull(); expect(await renderMetrics()).toContain( - 'gittensory_redis_token_cache_total{result="miss"} 1', + 'loopover_redis_token_cache_total{result="miss"} 1', ); }); @@ -47,7 +47,7 @@ describe("createRedisTokenCache (#perf installation-token persistence)", () => { const metrics = await renderMetrics(); expect(metrics).toContain( - 'gittensory_redis_token_cache_total{result="hit"} 1', + 'loopover_redis_token_cache_total{result="hit"} 1', ); expect(metrics).not.toContain("sensitive-value"); }); @@ -67,7 +67,7 @@ describe("createRedisTokenCache (#perf installation-token persistence)", () => { expect(await createRedisTokenCache(f.redis).get(9)).toBeNull(); expect(await renderMetrics()).toContain( - 'gittensory_redis_token_cache_total{result="miss"} 1', + 'loopover_redis_token_cache_total{result="miss"} 1', ); }); @@ -80,7 +80,7 @@ describe("createRedisTokenCache (#perf installation-token persistence)", () => { expect(await createRedisTokenCache(f.redis).get(9)).toBeNull(); expect(await renderMetrics()).toContain( - 'gittensory_redis_token_cache_total{result="miss"} 1', + 'loopover_redis_token_cache_total{result="miss"} 1', ); }); @@ -93,7 +93,7 @@ describe("createRedisTokenCache (#perf installation-token persistence)", () => { expect(await createRedisTokenCache(f.redis).get(9)).toBeNull(); expect(await renderMetrics()).toContain( - 'gittensory_redis_token_cache_total{result="miss"} 1', + 'loopover_redis_token_cache_total{result="miss"} 1', ); }); }); diff --git a/test/unit/selfhost-sqlite-queue.test.ts b/test/unit/selfhost-sqlite-queue.test.ts index 4e3d52a49a..68112288b7 100644 --- a/test/unit/selfhost-sqlite-queue.test.ts +++ b/test/unit/selfhost-sqlite-queue.test.ts @@ -232,10 +232,10 @@ describe("createSqliteQueue (durable #980)", () => { ["github rate-limit background admission"], ).rows[0] as { c: number }; expect(pendingBackground.c).toBe(2); - expect(await q.stats()).toMatchObject({ gittensory_jobs_rate_limit_deferred_total: 2 }); + expect(await q.stats()).toMatchObject({ loopover_jobs_rate_limit_deferred_total: 2 }); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_jobs_rate_limit_admission_deferred_total{job_type="agent-regate-pr",key_scope="installation",kind="background"} 1'); - expect(metrics).toContain('gittensory_jobs_rate_limit_admission_deferred_total{job_type="rag-index-repo",key_scope="public",kind="background"} 1'); + expect(metrics).toContain('loopover_jobs_rate_limit_admission_deferred_total{job_type="agent-regate-pr",key_scope="installation",kind="background"} 1'); + expect(metrics).toContain('loopover_jobs_rate_limit_admission_deferred_total{job_type="rag-index-repo",key_scope="public",kind="background"} 1'); } finally { if (oldJitter === undefined) delete process.env.QUEUE_RATE_LIMIT_JITTER_MS; else process.env.QUEUE_RATE_LIMIT_JITTER_MS = oldJitter; @@ -389,8 +389,8 @@ describe("createSqliteQueue (durable #980)", () => { run_after: Date.parse("2026-06-24T12:10:15.000Z"), last_error: "github rate-limit webhook admission", }); - expect(await q.stats()).toMatchObject({ gittensory_jobs_rate_limit_deferred_total: 1 }); - expect(await renderMetrics()).toContain('gittensory_jobs_rate_limit_admission_deferred_total{job_type="github-webhook",key_scope="installation",kind="webhook"} 1'); + expect(await q.stats()).toMatchObject({ loopover_jobs_rate_limit_deferred_total: 1 }); + expect(await renderMetrics()).toContain('loopover_jobs_rate_limit_admission_deferred_total{job_type="github-webhook",key_scope="installation",kind="webhook"} 1'); } finally { if (oldJitter === undefined) delete process.env.QUEUE_RATE_LIMIT_JITTER_MS; else process.env.QUEUE_RATE_LIMIT_JITTER_MS = oldJitter; @@ -441,8 +441,8 @@ describe("createSqliteQueue (durable #980)", () => { await q.binding.send(installedWebhook("fresh", 123)); await q.drain(); - expect(await q.stats()).not.toHaveProperty("gittensory_jobs_rate_limit_deferred_total"); - expect(await renderMetrics()).not.toContain("gittensory_jobs_rate_limit_admission_deferred_total"); + expect(await q.stats()).not.toHaveProperty("loopover_jobs_rate_limit_deferred_total"); + expect(await renderMetrics()).not.toContain("loopover_jobs_rate_limit_admission_deferred_total"); } finally { if (oldJitter === undefined) delete process.env.QUEUE_RATE_LIMIT_JITTER_MS; else process.env.QUEUE_RATE_LIMIT_JITTER_MS = oldJitter; @@ -543,7 +543,7 @@ describe("createSqliteQueue (durable #980)", () => { await q.drain(); expect(seen).toEqual(["github-webhook"]); - expect(await q.stats()).not.toHaveProperty("gittensory_jobs_rate_limit_deferred_total"); + expect(await q.stats()).not.toHaveProperty("loopover_jobs_rate_limit_deferred_total"); } finally { if (oldJitter === undefined) delete process.env.QUEUE_RATE_LIMIT_JITTER_MS; else process.env.QUEUE_RATE_LIMIT_JITTER_MS = oldJitter; @@ -647,7 +647,7 @@ describe("createSqliteQueue (durable #980)", () => { await q.drain(); expect(seen).toEqual(["github-webhook"]); - expect(await q.stats()).not.toHaveProperty("gittensory_jobs_rate_limit_deferred_total"); + expect(await q.stats()).not.toHaveProperty("loopover_jobs_rate_limit_deferred_total"); }); it("does not keep webhook admission closed from stale legacy rows after a newer healthy exact observation", async () => { @@ -686,7 +686,7 @@ describe("createSqliteQueue (durable #980)", () => { await q.drain(); expect(seen).toEqual(["github-webhook"]); - expect(await q.stats()).not.toHaveProperty("gittensory_jobs_rate_limit_deferred_total"); + expect(await q.stats()).not.toHaveProperty("loopover_jobs_rate_limit_deferred_total"); }); it("does not pre-yield webhook jobs for another installation's persisted REST exhaustion", async () => { @@ -720,7 +720,7 @@ describe("createSqliteQueue (durable #980)", () => { await q.drain(); expect(seen).toEqual(["github-webhook"]); - expect(await q.stats()).not.toHaveProperty("gittensory_jobs_rate_limit_deferred_total"); + expect(await q.stats()).not.toHaveProperty("loopover_jobs_rate_limit_deferred_total"); }); it("skips the background-admission metric when the defer update changes no rows", async () => { @@ -770,7 +770,7 @@ describe("createSqliteQueue (durable #980)", () => { expect(seen).toEqual([]); expect(warned).not.toHaveBeenCalled(); - expect(await q.stats()).not.toHaveProperty("gittensory_jobs_rate_limit_deferred_total"); + expect(await q.stats()).not.toHaveProperty("loopover_jobs_rate_limit_deferred_total"); } finally { if (oldJitter === undefined) delete process.env.QUEUE_RATE_LIMIT_JITTER_MS; else process.env.QUEUE_RATE_LIMIT_JITTER_MS = oldJitter; @@ -866,8 +866,8 @@ describe("createSqliteQueue (durable #980)", () => { ]); expect(rows.map((row) => JSON.parse(row.payload).deliveryId).filter(Boolean).sort()).toEqual(["ci-2", "pr-2"]); expect(await q.stats()).toMatchObject({ - gittensory_jobs_enqueued_total: 3, - gittensory_jobs_coalesced_total: 3, + loopover_jobs_enqueued_total: 3, + loopover_jobs_coalesced_total: 3, }); }); @@ -986,8 +986,8 @@ describe("createSqliteQueue (durable #980)", () => { repoFullName: "JSONbored/gittensory", }); expect(await q.stats()).toMatchObject({ - gittensory_jobs_enqueued_total: 1, - gittensory_jobs_coalesced_total: 1, + loopover_jobs_enqueued_total: 1, + loopover_jobs_coalesced_total: 1, }); }); @@ -1026,8 +1026,8 @@ describe("createSqliteQueue (durable #980)", () => { // The two incrementals now MERGE into one row before the full job supersedes it (#selfhost-maintenance-self-pin): // 1 insert (the first incremental) + 2 coalesces (the merge, then the supersede), not 2 inserts + 1 coalesce. expect(await q.stats()).toMatchObject({ - gittensory_jobs_enqueued_total: 1, - gittensory_jobs_coalesced_total: 2, + loopover_jobs_enqueued_total: 1, + loopover_jobs_coalesced_total: 2, }); }); @@ -1063,8 +1063,8 @@ describe("createSqliteQueue (durable #980)", () => { }); expect(rows[0]?.job_key).toBe(jobCoalesceKey(rows[0]!.payload)); expect(await q.stats()).toMatchObject({ - gittensory_jobs_enqueued_total: 1, - gittensory_jobs_coalesced_total: 1, + loopover_jobs_enqueued_total: 1, + loopover_jobs_coalesced_total: 1, }); }); @@ -1263,8 +1263,8 @@ describe("createSqliteQueue (durable #980)", () => { "schedule", ]); expect(await q.stats()).toMatchObject({ - gittensory_jobs_enqueued_total: 5, - gittensory_jobs_coalesced_total: 4, + loopover_jobs_enqueued_total: 5, + loopover_jobs_coalesced_total: 4, }); }); @@ -1298,7 +1298,7 @@ describe("createSqliteQueue (durable #980)", () => { expect(rows[0]?.id).toBe(first.id); expect(rows[0]?.created_at).toBe(first.created_at); // NOT reset to the re-enqueue time expect(rows[0]?.run_after).toBeGreaterThan(first.run_after); // still advances with the new request - expect(await q.stats()).toMatchObject({ gittensory_jobs_coalesced_total: 1 }); + expect(await q.stats()).toMatchObject({ loopover_jobs_coalesced_total: 1 }); } finally { vi.useRealTimers(); } @@ -1706,7 +1706,7 @@ describe("createSqliteQueue (durable #980)", () => { const q = createSqliteQueue(driver, async () => undefined, { concurrency: 1 }); await q.binding.send(backlogJob("owner/repo", 1)); await q.drain(); - expect(await renderMetrics()).toContain('gittensory_jobs_claimed_by_lane_total{lane="backlog"} 1'); + expect(await renderMetrics()).toContain('loopover_jobs_claimed_by_lane_total{lane="backlog"} 1'); expect(await renderMetrics()).not.toContain('lane="fresh"'); }); @@ -1721,8 +1721,8 @@ describe("createSqliteQueue (durable #980)", () => { await q.binding.send(backlogJob("owner/repo", 3)); await q.binding.send(prWebhook("fresh-1")); await q.drain(); - expect(await renderMetrics()).toContain('gittensory_jobs_claimed_by_lane_total{lane="fresh"} 1'); - expect(await renderMetrics()).toContain('gittensory_jobs_claimed_by_lane_total{lane="backlog"} 3'); + expect(await renderMetrics()).toContain('loopover_jobs_claimed_by_lane_total{lane="fresh"} 1'); + expect(await renderMetrics()).toContain('loopover_jobs_claimed_by_lane_total{lane="backlog"} 3'); }); it("does NOT increment the lane-claim counter when the preferred lane has nothing pending (falls through unscoped)", async () => { @@ -1733,7 +1733,7 @@ describe("createSqliteQueue (durable #980)", () => { // claimNextForegroundLane's "fresh" branch -- so NEITHER lane value is recorded for this claim. await q.binding.send(prWebhook("fresh-only")); await q.drain(); - expect(await renderMetrics()).not.toContain("gittensory_jobs_claimed_by_lane_total"); + expect(await renderMetrics()).not.toContain("loopover_jobs_claimed_by_lane_total"); }); it("does NOT increment the lane-claim counter when the picked repo's candidate row can't actually be claimed (defensive)", async () => { @@ -1744,7 +1744,7 @@ describe("createSqliteQueue (durable #980)", () => { [JSON.stringify(backlogJob("owner/repo", 1)), "agent-regate-pr:owner/repo#1"], ); await q.drain(); - expect(await renderMetrics()).not.toContain("gittensory_jobs_claimed_by_lane_total"); + expect(await renderMetrics()).not.toContain("loopover_jobs_claimed_by_lane_total"); }); }); @@ -2286,7 +2286,7 @@ describe("createSqliteQueue (durable #980)", () => { expect(released).toBe(1); const row = driver.query("SELECT run_after FROM _selfhost_jobs", []).rows[0] as { run_after: number }; expect(row.run_after).toBeLessThanOrEqual(Date.now()); - expect(await renderMetrics()).toContain("gittensory_jobs_foreground_liveness_released_total 1"); + expect(await renderMetrics()).toContain("loopover_jobs_foreground_liveness_released_total 1"); }); // Isolates the AGE condition from the OR'd rate-limit-clear condition: uses a github-webhook row (which IS @@ -2317,7 +2317,7 @@ describe("createSqliteQueue (durable #980)", () => { expect(released).toBe(0); const row = driver.query("SELECT run_after FROM _selfhost_jobs", []).rows[0] as { run_after: number }; expect(row.run_after).toBe(futureRunAfter); - expect(await renderMetrics()).not.toContain("gittensory_jobs_foreground_liveness_released_total"); + expect(await renderMetrics()).not.toContain("loopover_jobs_foreground_liveness_released_total"); }); it("caches foreground-liveness admission reads for candidates sharing the same rate-limit target", async () => { @@ -2373,7 +2373,7 @@ describe("createSqliteQueue (durable #980)", () => { const released = await q.releaseStaleForegroundDeferrals(); expect(released).toBe(1); - expect(await renderMetrics()).toContain("gittensory_jobs_foreground_liveness_released_total 1"); + expect(await renderMetrics()).toContain("loopover_jobs_foreground_liveness_released_total 1"); }); // The payload is unparseable -- isRateLimitAdmissionNowClear's own catch(){ return false } branch -- so ONLY @@ -2433,7 +2433,7 @@ describe("createSqliteQueue (durable #980)", () => { expect(released).toBe(0); const row = driver.query("SELECT run_after FROM _selfhost_jobs", []).rows[0] as { run_after: number }; expect(row.run_after).toBeGreaterThan(Date.now()); // still deferred -- the escape hatch never touched it - expect(await renderMetrics()).not.toContain("gittensory_jobs_foreground_liveness_released_total"); + expect(await renderMetrics()).not.toContain("loopover_jobs_foreground_liveness_released_total"); }); // REGRESSION (#selfhost-queue-liveness): the production incident this module exists to make structurally @@ -2460,7 +2460,7 @@ describe("createSqliteQueue (durable #980)", () => { const released = await q.releaseStaleForegroundDeferrals(); expect(released).toBe(3); - expect(await renderMetrics()).toContain("gittensory_jobs_foreground_liveness_released_total 3"); + expect(await renderMetrics()).toContain("loopover_jobs_foreground_liveness_released_total 3"); // kickAll() (called internally once released > 0) means pump activity picks these up without waiting for // the next poll tick -- drain() confirms all three are now genuinely runnable. await q.drain(); @@ -2488,7 +2488,7 @@ describe("createSqliteQueue (durable #980)", () => { const released = await q.releaseStaleForegroundDeferrals(); expect(released).toBe(2); - expect(await renderMetrics()).toContain("gittensory_jobs_foreground_liveness_released_total 2"); + expect(await renderMetrics()).toContain("loopover_jobs_foreground_liveness_released_total 2"); const remainingFuture = driver.query( `SELECT COUNT(*) AS c FROM _selfhost_jobs WHERE status='pending' AND run_after>?`, [now], @@ -2704,10 +2704,10 @@ describe("createSqliteQueue (durable #980)", () => { last_error: "openai api rate limit exceeded", }); expect(await q.stats()).toMatchObject({ - gittensory_jobs_failed_total: 2, - gittensory_jobs_dead_total: 1, + loopover_jobs_failed_total: 2, + loopover_jobs_dead_total: 1, }); - expect(await q.stats()).not.toHaveProperty("gittensory_jobs_rate_limited_total"); + expect(await q.stats()).not.toHaveProperty("loopover_jobs_rate_limited_total"); }); it("does not defer GitHub work when a non-GitHub job throws a GitHub-looking rate limit", async () => { @@ -2745,9 +2745,9 @@ describe("createSqliteQueue (durable #980)", () => { expect(pending).toHaveLength(1); expect(JSON.parse(pending[0]!.payload)).toMatchObject({ type: "refresh-registry" }); expect(pending[0]!.last_error).toBe("API rate limit exceeded for installation ID 123"); - expect(await q.stats()).toMatchObject({ gittensory_jobs_rate_limited_total: 1 }); - expect(await q.stats()).not.toHaveProperty("gittensory_jobs_rate_limit_deferred_total"); - expect(await renderMetrics()).toContain('gittensory_jobs_rate_limited_by_type_total{job_type="refresh-registry",key_scope="unknown",kind="unknown"} 1'); + expect(await q.stats()).toMatchObject({ loopover_jobs_rate_limited_total: 1 }); + expect(await q.stats()).not.toHaveProperty("loopover_jobs_rate_limit_deferred_total"); + expect(await renderMetrics()).toContain('loopover_jobs_rate_limited_by_type_total{job_type="refresh-registry",key_scope="unknown",kind="unknown"} 1'); }); it("defers only the depleted keyed GitHub budget while unrelated work keeps draining", async () => { @@ -2824,13 +2824,13 @@ describe("createSqliteQueue (durable #980)", () => { expect(byType.get("agent-regate-pr:9")?.last_error).toBe("github rate-limit budget deferred"); expect(byType.has("agent-regate-pr:10")).toBe(false); expect(await q.stats()).toMatchObject({ - gittensory_jobs_processed_total: 3, - gittensory_jobs_rate_limited_total: 1, - gittensory_jobs_rate_limit_deferred_total: 1, + loopover_jobs_processed_total: 3, + loopover_jobs_rate_limited_total: 1, + loopover_jobs_rate_limit_deferred_total: 1, }); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_jobs_rate_limit_budget_deferred_total{job_type="github-webhook",key_scope="installation",kind="webhook"} 1'); - expect(metrics).toContain('gittensory_jobs_rate_limited_by_type_total{job_type="github-webhook",key_scope="installation",kind="webhook"} 1'); + expect(metrics).toContain('loopover_jobs_rate_limit_budget_deferred_total{job_type="github-webhook",key_scope="installation",kind="webhook"} 1'); + expect(metrics).toContain('loopover_jobs_rate_limited_by_type_total{job_type="github-webhook",key_scope="installation",kind="webhook"} 1'); }); it("coalesces a rate-limited active job into an existing pending duplicate without consuming attempts", async () => { @@ -2868,7 +2868,7 @@ describe("createSqliteQueue (durable #980)", () => { expect(JSON.parse(rows[0]!.payload).deliveryId).toBe("ci-existing"); expect(rows[0]!.attempts).toBe(0); expect(rows[0]!.last_error).toContain("secondary rate limit"); - expect(await q.stats()).toMatchObject({ gittensory_jobs_coalesced_total: 1 }); + expect(await q.stats()).toMatchObject({ loopover_jobs_coalesced_total: 1 }); }); it("reschedules a keyed rate-limited job when no pending duplicate exists", async () => { @@ -2908,7 +2908,7 @@ describe("createSqliteQueue (durable #980)", () => { expect(row.attempts).toBe(0); expect(row.run_after).toBeGreaterThan(Date.now()); expect(row.last_error).toContain("secondary rate limit"); - expect(await q.stats()).toMatchObject({ gittensory_jobs_rate_limited_total: 1 }); + expect(await q.stats()).toMatchObject({ loopover_jobs_rate_limited_total: 1 }); }); it("consumes retryable incomplete review attempts and dead-letters after maxRetries", async () => { @@ -2995,7 +2995,7 @@ describe("createSqliteQueue (durable #980)", () => { expect(JSON.parse(rows[1]!.payload).deliveryId).toBe("ci-existing"); expect(rows[1]!.attempts).toBe(0); expect(rows[1]!.last_error).toBeNull(); - expect((await q.stats()).gittensory_jobs_coalesced_total ?? 0).toBe(0); + expect((await q.stats()).loopover_jobs_coalesced_total ?? 0).toBe(0); }); it("SURVIVES A RESTART: a fresh queue over the same DB processes a persisted pending job", async () => { @@ -3022,7 +3022,7 @@ describe("createSqliteQueue (durable #980)", () => { await q.drain(); expect(driver.query("SELECT status FROM _selfhost_jobs", []).rows[0]).toMatchObject({ status: "processing" }); - expect((await q.stats()).gittensory_jobs_recovered_total ?? 0).toBe(0); + expect((await q.stats()).loopover_jobs_recovered_total ?? 0).toBe(0); } finally { if (old === undefined) delete process.env.QUEUE_PROCESSING_TIMEOUT_MS; else process.env.QUEUE_PROCESSING_TIMEOUT_MS = old; @@ -3156,8 +3156,8 @@ describe("createSqliteQueue (durable #980)", () => { expect(seen).toEqual(["lease-expired"]); expect(await q.stats()).toMatchObject({ - gittensory_jobs_recovered_total: 1, - gittensory_jobs_processed_total: 1, + loopover_jobs_recovered_total: 1, + loopover_jobs_processed_total: 1, }); } finally { if (oldTimeout === undefined) delete process.env.QUEUE_PROCESSING_TIMEOUT_MS; @@ -3201,7 +3201,7 @@ describe("createSqliteQueue (durable #980)", () => { await new Promise((r) => setTimeout(r, 10)); expect(seen.filter((type) => type === "slow")).toHaveLength(1); - expect((await queue.stats()).gittensory_jobs_recovered_total ?? 0).toBe(0); + expect((await queue.stats()).loopover_jobs_recovered_total ?? 0).toBe(0); } finally { for (const release of releases) release(); if (q) await q.stop(); @@ -3411,9 +3411,9 @@ describe("createSqliteQueue (durable #980)", () => { expect(row.status).toBe("pending"); expect(row.run_after).toBeGreaterThan(before); expect(row.last_error).toContain("live_pending_high"); - expect(await q.stats()).toMatchObject({ gittensory_jobs_maintenance_admission_deferred_total: 1 }); + expect(await q.stats()).toMatchObject({ loopover_jobs_maintenance_admission_deferred_total: 1 }); expect(await renderMetrics()).toContain( - 'gittensory_jobs_maintenance_admission_deferred_by_reason_total{job_type="build-contributor-evidence",reason="live_pending_high"} 1', + 'loopover_jobs_maintenance_admission_deferred_by_reason_total{job_type="build-contributor-evidence",reason="live_pending_high"} 1', ); }); @@ -3529,7 +3529,7 @@ describe("createSqliteQueue (durable #980)", () => { ).rows[0] as { last_error: string }; expect(freshRow.last_error).toContain("maintenance_pending_high"); expect(await renderMetrics()).toContain( - 'gittensory_jobs_maintenance_admission_granted_under_pressure_total{job_type="build-contributor-evidence",reason="maintenance_pending_high_drain"} 1', + 'loopover_jobs_maintenance_admission_granted_under_pressure_total{job_type="build-contributor-evidence",reason="maintenance_pending_high_drain"} 1', ); }); @@ -3601,10 +3601,10 @@ describe("createSqliteQueue (durable #980)", () => { ); await q.drain(); expect(started).toEqual(["build-contributor-evidence"]); - expect(await q.stats()).toMatchObject({ gittensory_jobs_maintenance_trickle_admitted_total: 1 }); + expect(await q.stats()).toMatchObject({ loopover_jobs_maintenance_trickle_admitted_total: 1 }); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_jobs_maintenance_trickle_admitted_by_type_total{job_type="build-contributor-evidence"} 1'); - expect(metrics).toContain('gittensory_jobs_maintenance_admission_granted_under_pressure_total{job_type="build-contributor-evidence",reason="trickle_max_defer_age"} 1'); + expect(metrics).toContain('loopover_jobs_maintenance_trickle_admitted_by_type_total{job_type="build-contributor-evidence"} 1'); + expect(metrics).toContain('loopover_jobs_maintenance_admission_granted_under_pressure_total{job_type="build-contributor-evidence",reason="trickle_max_defer_age"} 1'); }); it("does not record a trickle-admitted metric on a normal clear-pressure admission", async () => { @@ -3614,8 +3614,8 @@ describe("createSqliteQueue (durable #980)", () => { await q.binding.send(msg("build-contributor-evidence")); await q.drain(); expect(started).toEqual(["build-contributor-evidence"]); - expect(await q.stats()).not.toHaveProperty("gittensory_jobs_maintenance_trickle_admitted_total"); - expect(await renderMetrics()).not.toContain("gittensory_jobs_maintenance_trickle_admitted"); + expect(await q.stats()).not.toHaveProperty("loopover_jobs_maintenance_trickle_admitted_total"); + expect(await renderMetrics()).not.toContain("loopover_jobs_maintenance_trickle_admitted"); }); it("does not record the granted-under-pressure metric for an ordinary pressure_clear admission", async () => { @@ -3625,7 +3625,7 @@ describe("createSqliteQueue (durable #980)", () => { await q.binding.send(msg("build-contributor-evidence")); await q.drain(); expect(started).toEqual(["build-contributor-evidence"]); - expect(await renderMetrics()).not.toContain("gittensory_jobs_maintenance_admission_granted_under_pressure_total"); + expect(await renderMetrics()).not.toContain("loopover_jobs_maintenance_admission_granted_under_pressure_total"); }); it("pressureSignals() reports live/maintenance pending counts and oldest ages", async () => { @@ -3837,8 +3837,8 @@ describe("createSqliteQueue (durable #980)", () => { await q.binding.send(msg("build-contributor-evidence")); await q.drain(); expect(started).not.toContain("build-contributor-evidence"); - expect(await q.stats()).not.toHaveProperty("gittensory_jobs_maintenance_admission_deferred_total"); - expect(await renderMetrics()).not.toContain("gittensory_jobs_maintenance_admission_deferred_by_reason_total"); + expect(await q.stats()).not.toHaveProperty("loopover_jobs_maintenance_admission_deferred_total"); + expect(await renderMetrics()).not.toContain("loopover_jobs_maintenance_admission_deferred_by_reason_total"); }); }); @@ -3893,7 +3893,7 @@ describe("createSqliteQueue (durable #980)", () => { ).rows[0] as { last_error: string } | undefined; expect(row?.last_error).toContain("installation concurrency admission deferred: concurrency_high"); expect(await renderMetrics()).toContain( - 'gittensory_jobs_installation_concurrency_deferred_by_reason_total{job_type="backfill-repo-segment",reason="concurrency_high"} 1', + 'loopover_jobs_installation_concurrency_deferred_by_reason_total{job_type="backfill-repo-segment",reason="concurrency_high"} 1', ); } finally { release(); @@ -3936,7 +3936,7 @@ describe("createSqliteQueue (durable #980)", () => { ).rows[0] as { last_error: string } | undefined; expect(row?.last_error).toContain("installation concurrency admission deferred: concurrency_high"); expect(await renderMetrics()).toContain( - 'gittensory_jobs_installation_concurrency_deferred_by_reason_total{job_type="agent-regate-sweep",reason="concurrency_high"} 1', + 'loopover_jobs_installation_concurrency_deferred_by_reason_total{job_type="agent-regate-sweep",reason="concurrency_high"} 1', ); } finally { release(); @@ -4055,8 +4055,8 @@ describe("createSqliteQueue (durable #980)", () => { for (let i = 0; i < 20 && started < 1; i += 1) await new Promise((r) => setTimeout(r, 10)); await new Promise((r) => setTimeout(r, 30)); expect(started).toBe(1); - expect(await renderMetrics()).not.toContain("gittensory_jobs_installation_concurrency_deferred_total"); - expect(await renderMetrics()).not.toContain("gittensory_jobs_installation_concurrency_deferred_by_reason_total"); + expect(await renderMetrics()).not.toContain("loopover_jobs_installation_concurrency_deferred_total"); + expect(await renderMetrics()).not.toContain("loopover_jobs_installation_concurrency_deferred_by_reason_total"); } finally { release(); await q.stop(); diff --git a/test/unit/server-persisted-job-metrics.test.ts b/test/unit/server-persisted-job-metrics.test.ts index 6293148a16..c4fded37c4 100644 --- a/test/unit/server-persisted-job-metrics.test.ts +++ b/test/unit/server-persisted-job-metrics.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest"; const read = (path: string) => readFileSync(path, "utf8"); -// Regression for #2508: gittensory_jobs_deferred_total was registered as a persisted gauge in server.ts +// Regression for #2508: loopover_jobs_deferred_total was registered as a persisted gauge in server.ts // but no queue driver ever called recordQueueMetric with that name -- dead instrumentation that always // reported 0. Pin the invariant the fix establishes: every metric name in server.ts's persisted-gauge list // must have a real recordQueueMetric call site in BOTH queue drivers, so a future dead entry can't sneak @@ -28,6 +28,6 @@ describe("server.ts persisted job-queue metrics (#2508)", () => { expect(pgQueue.includes(`recordQueueMetric("${name}"`), `pg-queue.ts never calls recordQueueMetric for "${name}"`).toBe(true); } - expect(registered).not.toContain("gittensory_jobs_deferred_total"); + expect(registered).not.toContain("loopover_jobs_deferred_total"); }); }); diff --git a/test/unit/smoke-selfhost-script.test.ts b/test/unit/smoke-selfhost-script.test.ts index c6ff828884..ef47196891 100644 --- a/test/unit/smoke-selfhost-script.test.ts +++ b/test/unit/smoke-selfhost-script.test.ts @@ -32,7 +32,7 @@ printf '\\n' >> "${calls}" case "$*" in *'/health'*) echo '{"status":"ok"}' ;; *'/ready'*) echo '{"ok":true}' ;; - *'/metrics'*) echo 'gittensory_uptime_seconds 1' ;; + *'/metrics'*) echo 'loopover_uptime_seconds 1' ;; esac `, ); diff --git a/test/unit/webhook.test.ts b/test/unit/webhook.test.ts index 1444751107..8ef4bf9b34 100644 --- a/test/unit/webhook.test.ts +++ b/test/unit/webhook.test.ts @@ -79,7 +79,7 @@ describe("github webhook enqueue failure (#786)", () => { await expect(response.json()).resolves.toMatchObject({ error: "enqueue_failed" }); const event = await getWebhookEvent(env, "enqueue-missing-binding-1"); expect(event?.status).toBe("error"); - expect(await renderMetrics()).toContain('gittensory_webhook_enqueue_total{action="opened",event="pull_request",result="enqueue_failed"} 1'); + expect(await renderMetrics()).toContain('loopover_webhook_enqueue_total{action="opened",event="pull_request",result="enqueue_failed"} 1'); // ERROR level so the central Sentry forwarder captures a missing-binding webhook ingest failure (#1824) -- // previously this only moved a Prometheus counter, invisible without comparing dashboards. expect( @@ -130,7 +130,7 @@ describe("github webhook enqueue failure (#786)", () => { // Flagged "error" so the dedup guard lets GitHub redeliver instead of suppressing it. const event = await getWebhookEvent(env, "enqueue-fail-1"); expect(event?.status).toBe("error"); - expect(await renderMetrics()).toContain('gittensory_webhook_enqueue_total{action="opened",event="pull_request",result="enqueue_failed"} 1'); + expect(await renderMetrics()).toContain('loopover_webhook_enqueue_total{action="opened",event="pull_request",result="enqueue_failed"} 1'); // ERROR level so the central Sentry forwarder captures a failing webhook enqueue (#1824). Never logs rawBody, // parsed payload, or repository metadata -- only the event kind + the thrown error's message. expect( @@ -257,7 +257,7 @@ describe("github webhook dedup (#789)", () => { expect(response.status).toBe(202); await expect(response.json()).resolves.toMatchObject({ status: "duplicate" }); expect(sendCount).toBe(0); // not re-enqueued - expect(await renderMetrics()).toContain('gittensory_webhook_enqueue_total{action="opened",event="pull_request",result="duplicate"} 1'); + expect(await renderMetrics()).toContain('loopover_webhook_enqueue_total{action="opened",event="pull_request",result="duplicate"} 1'); }); }); @@ -281,8 +281,8 @@ describe("github webhook queue isolation (#audit-webhook-queue)", () => { ).resolves.toBe("queued"); const metrics = await renderMetrics(); - expect(metrics).toContain('gittensory_webhook_enqueue_total{action="none",event="pull_request",result="invalid_json"} 1'); - expect(metrics).toContain('gittensory_webhook_enqueue_total{action="other",event="other",result="queued"} 1'); + expect(metrics).toContain('loopover_webhook_enqueue_total{action="none",event="pull_request",result="invalid_json"} 1'); + expect(metrics).toContain('loopover_webhook_enqueue_total{action="other",event="other",result="queued"} 1'); }); it("REGRESSION (#zero-trace-webhook-loss): an unparseable delivery still gets a durable webhook_events row instead of vanishing with no trace", async () => { @@ -327,7 +327,7 @@ describe("github webhook queue isolation (#audit-webhook-queue)", () => { expect(response.status).toBe(410); await expect(response.json()).resolves.toMatchObject({ error: "selfhost_review_runtime_required" }); expect(webhookSends).toBe(0); - expect(await renderMetrics()).toContain('gittensory_webhook_enqueue_total{action="none",event="pull_request",result="review_unavailable"} 1'); + expect(await renderMetrics()).toContain('loopover_webhook_enqueue_total{action="none",event="pull_request",result="review_unavailable"} 1'); }); it("INVARIANT: a valid webhook is enqueued onto the dedicated WEBHOOKS lane, never the shared JOBS queue", async () => { @@ -362,7 +362,7 @@ describe("github webhook queue isolation (#audit-webhook-queue)", () => { await expect(response.json()).resolves.toMatchObject({ status: "queued" }); expect(webhookSends).toBe(1); // routed to the dedicated webhook lane expect(jobsSends).toBe(0); // never the shared maintenance queue - expect(await renderMetrics()).toContain('gittensory_webhook_enqueue_total{action="opened",event="pull_request",result="queued"} 1'); + expect(await renderMetrics()).toContain('loopover_webhook_enqueue_total{action="opened",event="pull_request",result="queued"} 1'); }); it("copies the internal self-host traceparent onto queued webhook jobs", async () => { @@ -438,7 +438,7 @@ describe("github webhook queue isolation (#audit-webhook-queue)", () => { expect(webhookSends).toBe(0); const event = await getWebhookEvent(env, "self-comment-ignore-1"); expect(event?.status).toBe("processed"); - expect(await renderMetrics()).toContain('gittensory_webhook_enqueue_total{action="edited",event="issue_comment",result="ignored"} 1'); + expect(await renderMetrics()).toContain('loopover_webhook_enqueue_total{action="edited",event="issue_comment",result="ignored"} 1'); }); it("drops self-authored app CI completion webhooks before they add queue pressure", async () => {