Feat analytics - #19
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an analytics aggregation flow to the Cloudflare Worker to track daily G$ deposits/streaming and Base “AI credits used” from Antseed Channels, persisted in KV and exposed via versioned API endpoints. This fits the repo’s Worker responsibilities (credit accounting + chain event ingestion) and extends it with reporting/observability data.
Changes:
- Add
backend/src/analytics.tsKV-backed aggregation querying Blockscout (Celo/Base) + Superfluid subgraph, with daily + global windows. - Expose analytics via
GET /v1/analyticsand a recompute endpointPOST /v1/analytics/refresh; also run aggregation from the scheduled cron. - Add env/wrangler config for Blockscout URLs + Channels contract address, update docs, and add analytics-focused tests.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| backend/wrangler.toml | Adds Blockscout + Channels env vars; changes cron to run every 6 hours. |
| backend/test/worker.test.ts | Adds tests around analytics aggregation logic and KV persistence behavior. |
| backend/src/worker.ts | Wires new /v1/analytics routes and runs analytics aggregation in the scheduled handler. |
| backend/src/logging.ts | Adjusts log emission behavior (notably affects logInfo). |
| backend/src/env.ts | Adds env bindings for Blockscout URLs + Channels address to the Worker config surface. |
| backend/src/analytics.ts | New module implementing aggregation, KV storage, and explorer/subgraph fetching utilities. |
| backend/README.md | Documents analytics endpoints and required config. |
Comments suppressed due to low confidence (3)
backend/src/analytics.ts:138
- These
logInfocalls use free-text event names (with punctuation/ellipses), which is inconsistent with the rest of the codebase’s structured dot-separated events and makes log querying harder. Consider using stable event IDs for start/end of base metrics collection.
logInfo("getting base metrics....");
const baseMetrics = await collectBaseDayMetrics(cfg, dayWindow, aggregate, knownBuyers);
logInfo("got base metrics....");
backend/src/analytics.ts:141
- This log event string has typos (
dialy reocrd) and uses a free-text name. Prefer a stable, dot-separated event ID (and include details as structured data).
logInfo("building dialy reocrd....");
backend/src/analytics.ts:368
- This log call uses an unstructured, punctuation-heavy event name (
"base range:"). For consistency with the rest of the repo, use a stable dot-separated event name and pass the range as structured data.
logInfo("base range:", range);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
blueogin
left a comment
There was a problem hiding this comment.
Buyer registry only grows from Celo GdDeposited buyers in collectCeloDayMetrics, then Base events are filtered by knownBuyers in collectBaseDayMetrics. Stream-only users may miss Base credit attribution until they deposit.
Is this intentional?
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (4)
backend/src/worker.ts:224
- The first-run analytics backfill runs a potentially unbounded day-by-day loop inside a single scheduled invocation. This can exceed Cloudflare cron execution limits and/or hit Blockscout/Superfluid rate limits, leaving analytics partially backfilled with no clear recovery path. At minimum, add a TODO noting the need to persist a backfill cursor and process a bounded number of days per cron tick (or move the backfill to a manual/admin operation).
logInfo("cron.analytics.first-run");
const startDate = new Date("2026-07-02T00:00:00Z");
// TODO: Persist a backfill cursor and process a bounded number of days per cron tick;
// this first-run catch-up loop can exceed scheduled execution limits as the date range grows.
while (startDate < new Date()) {
backend/src/analytics.ts:276
analyticsConfigFromEnv()defaultsSUPERFLUID_SUBGRAPH_URLtohttps://celo-mainnet.subgraph.x.superfluid.dev/, but the rest of the Worker useshttps://subgraph-endpoints.superfluid.dev/celo-mainnet/protocol-v1. IfSUPERFLUID_SUBGRAPH_URLis not set in the environment, analytics stream collection is likely to query the wrong endpoint and fail.
superfluidSubgraphUrl: env.SUPERFLUID_SUBGRAPH_URL ?? "https://celo-mainnet.subgraph.x.superfluid.dev/"
backend/src/analytics.ts:143
- Log message has a spelling typo ("dialy reocrd"), which makes searching/alerting on logs harder and looks unpolished in production output.
logInfo("building dialy reocrd....");
backend/README.md:49
- Docs say known buyers are learned only from Celo vault
buyerfields, but the implementation also learns buyers from Superfluid streamuserData(seedecodeBuyerFromUserData(...)usage). Update this line so the filtering/learning rules match the code.
- Base usage (`aiCreditsUsedWei`, `uniqueCreditUsers`) is filtered to known buyers only; known buyers are learned from Celo vault `buyer` fields.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (7)
backend/src/analytics.ts:396
- This log call uses an inconsistent event name (contains spaces/colon) compared to the rest of the codebase’s structured log events; it also makes it harder to filter by prefix.
logInfo("base range:", range);
backend/README.md:51
- Docs say refresh always recomputes the current day, but
runAnalyticsAggregation()may backfill the next non-finalized day instead (viaresolveRunDate). Update this line so consumers don’t assume refresh only affects “today”.
`POST /v1/analytics/refresh` recomputes the current UTC day from midnight to now, overwrites the day snapshot, finalizes any closed days into persisted globals, and returns a summary.
backend/src/worker.ts:8
KVAnalyticsStoreis imported but never used in this file, which can cause lint/typecheck failures and adds noise. Remove it from the import list.
import { getAnalyticsWindow, runAnalyticsAggregation, KVAnalyticsStore } from "./analytics.js";
backend/src/analytics.ts:145
- These log event names contain typos ("dialy reocrd") and don't follow the structured
namespace.actionpattern used elsewhere, making logs harder to query/debug.
This issue also appears on line 396 of the same file.
logInfo("getting base metrics....");
const baseMetrics = await collectBaseDayMetrics(cfg, dayWindow, aggregate, knownBuyers);
logInfo("got base metrics....");
logInfo("building dialy reocrd....");
backend/src/analytics.ts:314
- Minor typo in comment: "dont" → "don't" (helps keep the codebase’s comments polished/searchable).
//dont finalize if a day is missing
backend/README.md:46
- Docs say the current day is always recomputed on each refresh, but the aggregator can also run for past (backfill) days depending on
finalizedThroughDate. Clarify that the overwritten snapshot is for the day being aggregated.
This issue also appears on line 51 of the same file.
- `daily` — one record per UTC day, newest last; the current day is always recomputed from midnight to now on each refresh and its snapshot is overwritten
backend/README.md:49
- Known buyers aren’t only learned from Celo vault
buyerfields: the implementation also discovers buyers from Superfluid streamuserDataand adds them to the registry. Update the docs so the Base filtering behavior matches the code.
- Base usage (`aiCreditsUsedWei`, `uniqueCreditUsers`) is filtered to known buyers only; known buyers are learned from Celo vault `buyer` fields.
| export function resolveRunDate(state: AnalyticsState, requestedDate: Date, runDay: string): string { | ||
| const requested = dayFromDate(requestedDate); | ||
| if (state.finalizedThroughDate) { | ||
| const next = nextDate(state.finalizedThroughDate); | ||
| if (next && next <= runDay) { | ||
| return next; | ||
| } | ||
| } | ||
|
|
||
| return requested <= runDay ? requested : runDay; | ||
| } |
There was a problem hiding this comment.
On a fresh KV, finalizedThroughDate is absent, so this returns today instead of the intended backfill start date. Finalization then encounters yesterday as missing and leaves the cursor undefined. Because cron sees that currentDate === today, it exits, and every later invocation repeats today forever. The original July 2 backfill seed has effectively been removed. Please initialize/persist an explicit backfill cursor when no analytics state exists, and add a fresh-KV integration test.
There was a problem hiding this comment.
@copilot on first run use 30 days backfill
There was a problem hiding this comment.
Implemented in da8eef2: scheduled analytics now seeds and persists a 30-day backfill cursor (analytics:cron:backfill-cursor) on fresh KV and advances it across cron ticks. Added a fresh-KV scheduled integration test to verify the first run backfills and persists cursor progression.
| flowUpdatedEvents(orderBy: timestamp, orderDirection: desc, first: 1) { | ||
| userData | ||
| oldFlowRate |
There was a problem hiding this comment.
This uses the current stream entity and only its latest update to reconstruct an arbitrary historical day.
That is insufficient if the stream changed more than once during the day or its latest update happened after the backfilled day. For example, rates 2 -> 4 at 06:00 and 4 -> 8 at 10:00 are calculated as 4 for the entire pre-10:00 period. Historical aggregation needs all relevant FlowUpdated events for the target window and the rate active at midnight.
|
@copilot review latest changes |
Reviewed latest changes and fixed the failing analytics/worker tests in |
| if (!cursorDate) { | ||
| maxRunsPerTick = ANALYTICS_CRON_BACKFILL_DAYS; | ||
| cursorDate = dateDaysAgo(runAt, ANALYTICS_CRON_BACKFILL_DAYS); | ||
| await env.ANTSEED_KV.put(ANALYTICS_CRON_BACKFILL_CURSOR_KEY, cursorDate); |
There was a problem hiding this comment.
Fresh KV sets maxRunsPerTick = 30, so the first scheduled invocation can issue dozens of Blockscout + Superfluid calls and hit Worker time/subrequest limits. Prefer a small bound per tick (e.g. 2) and rely on the persisted cursor across crons
the cursor already supports that.
There was a problem hiding this comment.
should be ok and changed in the futures
| const json = (await response.json()) as { | ||
| data?: { | ||
| streamPeriods?: Array<{ | ||
| sender: { id: string }; | ||
| flowRate: string; | ||
| startedAtTimestamp: string; | ||
| stoppedAtTimestamp: string | null; | ||
| userData: string; | ||
| }>; | ||
| }; | ||
| }; | ||
|
|
||
| const batch = json.data?.streamPeriods ?? []; |
There was a problem hiding this comment.
GraphQL { errors: [...] } with no data is treated as empty success. That can write a zero daily snapshot and later finalize it.
Please fail the aggregation on subgraph/explorer logical errors and skip replace/finalize.
|
@sirpy Hey -- the Also a heads-up: the CI run triggered by the merge (#106) is failing on the Backend job -- might be worth checking before deploying. Let me know if there's anything blocking the deploy or if I can help. |
No description provided.