Skip to content

feat(feeds): per-watchlist subscribe feed — GET /api/v1/feeds/watch?ids= - #8471

Merged
JSONbored merged 3 commits into
mainfrom
feat/8376-watch-feed
Jul 28, 2026
Merged

feat(feeds): per-watchlist subscribe feed — GET /api/v1/feeds/watch?ids=#8471
JSONbored merged 3 commits into
mainfrom
feat/8376-watch-feed

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Closes #8376 — with a documented scope note posted first (#8376 (comment)): a real data-model gap this issue's own text didn't anticipate, plus a deliberate Path split.

The gap

There is no change-class or incident data source for validators or accounts anywhere in this codebase's feeds infrastructure — registryItems/incidentItems are both keyed on subnet netuid only (the changelog tracks subnet/artifact/coverage deltas; incidents track per-surface, i.e. per-subnet, downtime). WatchlistExport has carried validator/account kinds since #8248, but nothing downstream has ever produced a feed-shaped event for either.

Handled honestly, not silently: validator/account ids in ?ids= are still accepted and counted toward the 50-id cap (so a URL built from a real mixed watchlist never silently exceeds it), but produce zero items — and the feed's own description field says plainly how many ids produced nothing, rather than leaving a gap between what was asked for and what came back.

What shipped

Stateless by construction (requirement #1): GET /api/v1/feeds/watch.{rss,atom,json}?ids=<compact set> — the watchlist travels in the URL, never server-side. ?ids= is kind-prefixed (s<netuid>/v<hotkey>/a<ss58>), comma-joined, capped at 50 — a malformed token is a 400, over 50 is a 413.

Content (requirement #2): reuses registryItems/incidentItems per unique watched netuid — literally the same functions the existing per-subnet feed (/api/v1/feeds/subnets/{netuid}) already calls, so there's exactly one registry+incident item-building path in this codebase, not a second one for watch.

Cache posture (requirement #4): a real bug I caught while wiring this — workers/api.ts's edge-cache key composition builds a canonical query string from tag/since/until/limit, and ids wasn't in it. Without the fix, two different watchlists would have shared one cached response. Fixed, with a dedicated regression test proving different ?ids= → different cache entries, and identical ?ids= → the same entry reused.

Privacy line (requirement #5): "anyone with this URL sees which entities it tracks" appears both in the new docs section (apps/ui/content/docs/feeds.mdx, a <Callout type="warn">) and baked into the feed's own description field, so it reaches a feed reader that never visits the docs page at all.

Not in this PR (Path split, noted on the issue)

Requirement #3's "Subscribe to this watchlist" UI affordance is a apps/ui/ Path C change (its own component, its own screenshot-table requirement) — shipping as a focused follow-up once this endpoint exists to point it at, matching how #8364#8365#8366 split backend-then-UI on the chain-firehose work this session.

Validation

npx vitest run tests/feeds.test.ts   # 128 passed (23 new)
npm run lint                          # clean
npx tsc --noEmit -p .                 # clean
npm run validate                      # 129 subnets, 3444 surfaces
npm run test:coverage                 # 502 files / 12380 tests, repo-wide

src/feeds.ts patch coverage: 98.87%/95.45% (stmts/branches) — the one gap (line 777) is pre-existing, unrelated defensive code (readData's catch block). Docs page verified rendering in a live dev server (screenshot below via get_page_text — table + Callout render correctly).

No OpenAPI/schema surface touched — /api/v1/feeds/* sits outside the JSON contract already (matches every existing feed route). Not a frontend visual PR — Path B (route + tests) + a docs content addition, no apps/ui/src component/route changes.

Stateless by construction (requirement #1): the watchlist travels IN the
URL, not server-side. `?ids=` is a compact, kind-prefixed comma list
(s<netuid>/v<hotkey>/a<ss58>), capped at 50 entities -- a malformed token
is a 400, more than 50 is a 413. The cap is deliberately its own constant
(WATCH_MAX_IDS) rather than reusing FEED_MAX_ITEMS: "how many entities a
URL may name" and "how many items a feed page may return" are
independently-tuned numbers that only happen to both be 50 today.

A real gap this issue's own text didn't anticipate, documented on the
issue before implementing: there is no change-class or incident data
source for validators or accounts anywhere in this codebase's feeds
infrastructure (registryItems/incidentItems are both keyed on subnet
netuid only). validator/account ids are still accepted and counted toward
the 50-id cap -- so a URL built from a mixed real watchlist never
silently exceeds it -- but produce zero items, and the feed's own
`description` says plainly how many ids produced nothing rather than
staying silent about the gap.

Reuses registryItems/incidentItems per unique watched netuid (requirement
#2's "same lane discipline" -- literally the same functions the existing
per-subnet feed already calls), so there's exactly one registry+incident
item-building path in this codebase, not a second one for watch.

workers/api.ts: `ids` added to the edge-cache key composition alongside
the existing tag/since/until/limit params -- omitting it would have let
two different watchlists share one cached response, since the cache key
is the composed query string, not the raw request URL. Covered by a
dedicated regression test (different ?ids= -> different cache entries;
same ?ids= again -> the existing entry, not a third).

apps/ui/content/docs/feeds.mdx: a "Watchlist feed" section -- the ?ids=
encoding table, the validator/account limitation stated plainly, and the
issue's own required privacy line ("anyone with this URL sees which
entities it tracks") both in the docs prose and baked into the feed's
own `description` field, so it reaches a feed reader, not just this page.

23 new tests (17 in the parseWatchIds/watchFeedItems/handleFeedRequest
suites, 1 dedicated cache-key regression, plus assertions folded into
existing blocks) -- 502 test files / 12380 tests passing repo-wide.
feeds.ts patch coverage 98.87%/95.45% (stmts/branches); the one gap
(line 777) is pre-existing, unrelated defensive code.

Path split (noted on the issue): this PR is the backend route only.
Requirement #3's "Subscribe to this watchlist" UI affordance is a
apps/ui/ Path C change with its own screenshot-table requirement --
follow-up PR once this endpoint exists to point it at.

Closes #8376
@loopover-orb

loopover-orb Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Warning

⏸️ LoopOver review result - manual review recommended

Review updated: 2026-07-28 04:05:34 UTC

4 files · 1 AI reviewer · no blockers · CI green · clean

⏸️ Suggested Action - Manual Review

  • Touches a guarded path — held for manual review: This PR changes guardrail-protected path(s): apps/ui/content/docs/feeds.mdx (matched apps/ui/**).

Review summary
This is a code/schema PR (not a data PR) that adds a stateless per-watchlist feed at `/api/v1/feeds/watch?ids=` by reusing the existing `registryItems`/`incidentItems` builders keyed on subnet netuid, with validator/account ids honestly counted but unresolved. The implementation is correct and well-tested: `parseWatchIds` validates/caps at 50 tokens before any work, `watchFeedItems` correctly deduplicates subnet ids via a Set while still counting unsupported kinds from the raw list, and `workers/api.ts` correctly adds `ids` to the edge-cache key composition to prevent cross-watchlist cache bleed — a real bug the author caught and fixed themselves. This closes #8376 with a documented, honest scope reduction (validator/account resolution punted due to a genuine data-model gap) rather than silently faking coverage.

Nits — 7 non-blocking
  • The `Workers Builds: metagraphed-registry-sync-api` and `test` CI checks failed with no detail provided in this run; cause could not be verified from what's given here.
  • src/feeds.ts:426/882/889 use bare literals (400/413) for status codes inline with the `fail(...)` calls — minor, matches existing sibling patterns (invalid_since/invalid_until/invalid_limit) already in the file, so not a real deviation.
  • The doc claim 'Five GET feeds' in apps/ui/content/docs/feeds.mdx should be double-checked against the actual route table for consistency now that `watch` is added alongside registry/incidents/gaps/subnets.
  • Consider whether `unsupportedCount` derivation via `ids.filter(w => w.kind !== 'subnet').length` (src/feeds.ts, `watchFeedItems`) could instead be computed once alongside the netuid Set construction to avoid a second full array pass — purely a style/perf nit given the 50-id cap bounds this trivially.
  • The `WATCH_ID_PREFIX` lookup and validation logic in `parseWatchIds` is clear but could share more structure with the existing `?since=`/`?until=` strict-parsing pattern documented in the same file, for consistency — not required.
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.
  • Touches a guarded path — held for manual review — A maintainer must review and merge this change.

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ⚠️ Gate result — Not blocking (Advisory; not blocking this PR.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #8376
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 14 registered-repo PR(s), 13 merged, 329 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 14 PR(s), 329 issue(s).
Linked issue satisfaction

Partially addressed
The PR delivers a solid, well-tested stateless watch feed (endpoint, cap/413, cache-key fix, contract-ish tests, and a clear privacy line in docs) and honestly documents a real data-model gap for validator/account items, but it explicitly omits the UI 'Subscribe to this watchlist' affordance (requirement #3) that the issue lists as a deliverable, and doesn't mention the computed-routes allowlist e

Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: Python, TypeScript, Ruby, Go, MDX, Shell, Solidity, JavaScript
  • Official Gittensor activity: 14 PR(s), 329 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask &lt;question&gt; answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat &lt;question&gt; answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

Decision record
  • action: hold · clause: guardrail_hold
  • config: 8076e139b685afeeccb8aa65cd3cdd0a3f261d5f64b5202812947d5294651461 · pack: oss-anti-slop · ci: failed
  • record: e7043739be3482029dc9e2216a353e9198c1c3eb0c7bfd86ab6a47ab4991e6a4 (schema v5, head eefcc27)
Visual preview
Route Viewport Before (production) After (this PR's preview) Diff
/ desktop before /
before /
after /
after /
/ mobile before / (mobile)
before / (mobile)
after / (mobile)
after / (mobile)

Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 28, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
metagraphed-registry-sync-api b0e6b0e Jul 28 2026, 03:50 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 28, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
metagraphed-data-api b0e6b0e Jul 28 2026, 03:49 AM

@superagent-security

Copy link
Copy Markdown

Superagent didn't find any vulnerabilities or security issues in this PR.

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.82%. Comparing base (e022c8e) to head (b0e6b0e).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #8471   +/-   ##
=======================================
  Coverage   97.82%   97.82%           
=======================================
  Files         419      419           
  Lines       29126    29171   +45     
  Branches    10944    10956   +12     
=======================================
+ Hits        28492    28537   +45     
  Misses        143      143           
  Partials      491      491           
Files with missing lines Coverage Δ
src/feeds.ts 95.82% <100.00%> (+0.61%) ⬆️
workers/api.ts 91.58% <100.00%> (+0.01%) ⬆️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
metagraphed-ui 20874fd Commit Preview URL

Branch Preview URL
Jul 28 2026, 03:25 AM

@loopover-orb loopover-orb Bot added the manual-review Gittensor contributor context label Jul 28, 2026
token is always non-empty (filtered by .filter(Boolean) beforehand),
so token[0] can never be undefined -- the fallback was dead code and
the uncovered branch codecov/patch was flagging.
@JSONbored
JSONbored merged commit 6b0a594 into main Jul 28, 2026
12 checks passed
@JSONbored
JSONbored deleted the feat/8376-watch-feed branch July 28, 2026 05:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

manual-review Gittensor contributor context

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(feeds): per-watchlist subscribe feeds — RSS/Atom/JSON for your starred set

1 participant