docs: measurements + measurement-jobs resources (chat#1796 contract)#242
Conversation
…dpoints
The contract for chat#1796 — consolidate the /research play-count surface into
two REST resources.
- GET /research/tracks/{id}/measurements + /albums/{id}/measurements (reads;
granularity/aggregate=run_rate/latest projections)
- POST /research/measurement-jobs (+ GET /{id}) — ingest, source=current|historical
- mark track/historic-stats, track/playcount-deltas, playcounts, snapshots
deprecated with migration notes
- 4 frontmatter-only MDX pages + Catalog nav entries
mintlify openapi-check: valid. All $refs resolve.
📝 WalkthroughWalkthroughThe Research API OpenAPI spec replaces legacy playcount/snapshot/historic-stats endpoints with three new measurement store endpoints ( ChangesResearch API Measurement Store Migration
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
| "/api/research/track/historic-stats": { | ||
| "get": { | ||
| "description": "Get historic per-track, per-source stats by ISRC or track id over a date range — a daily time-series of cumulative counters (e.g. `streams_total`). Diff the window endpoints for streams gained over a period.", | ||
| "description": "**Deprecated (2026-06).** Use `GET /api/research/tracks/{id}/measurements?granularity=daily` instead.\n\nGet historic per-track, per-source stats by ISRC or track id over a date range — a daily time-series of cumulative counters (e.g. `streams_total`). Diff the window endpoints for streams gained over a period.", |
There was a problem hiding this comment.
YAGNI - delete deprecated docs / code.
There was a problem hiding this comment.
Done — deleted track/historic-stats (endpoint, MDX page, nav entry, orphaned schemas). Not deprecated, removed outright per YAGNI.
| "/api/research/playcounts": { | ||
| "get": { | ||
| "description": "Latest platform-displayed play counts for all tracks on an album, served from the measurement store (populated by snapshots). Counts are scraped from public pages — not royalty-bearing stream counts.", | ||
| "description": "**Deprecated (2026-06).** Use `GET /api/research/albums/{id}/measurements?latest=true` instead.\n\nLatest platform-displayed play counts for all tracks on an album, served from the measurement store (populated by snapshots). Counts are scraped from public pages — not royalty-bearing stream counts.", |
There was a problem hiding this comment.
YAGNI - delete deprecated docs / code.
There was a problem hiding this comment.
Done — deleted playcounts outright (page + nav + orphaned schemas).
| "/api/research/snapshots": { | ||
| "post": { | ||
| "description": "Capture platform-displayed play counts for a whole catalog, album list, or ISRC list in one server-side snapshot. Executes asynchronously; one album call captures all of its tracks. The cost estimate is returned before any scraper spend. Two snapshots over time yield per-track run-rates via [GET /api/research/track/playcount-deltas](/api-reference/research/track-playcount-deltas).", | ||
| "description": "**Deprecated (2026-06).** Use `POST /api/research/measurement-jobs` with `source:\"current\"` instead.\n\nCapture platform-displayed play counts for a whole catalog, album list, or ISRC list in one server-side snapshot. Executes asynchronously; one album call captures all of its tracks. The cost estimate is returned before any scraper spend. Two snapshots over time yield per-track run-rates via [GET /api/research/track/playcount-deltas](/api-reference/research/track-playcount-deltas).", |
There was a problem hiding this comment.
YAGNI - delete deprecated docs / code.
There was a problem hiding this comment.
Done — deleted snapshots outright (page + nav + ResearchSnapshotRequest/Response).
| "/api/research/track/playcount-deltas": { | ||
| "get": { | ||
| "description": "Play-count change between [snapshots](/api-reference/research/snapshots) for one recording, with an annualized run-rate. Requires at least two captures in the window; returns an empty `deltas` array (not an error) when history is insufficient — create captures with [POST /api/research/snapshots](/api-reference/research/snapshots).", | ||
| "description": "**Deprecated (2026-06).** Use `GET /api/research/tracks/{id}/measurements?aggregate=run_rate` instead.\n\nPlay-count change between [snapshots](/api-reference/research/snapshots) for one recording, with an annualized run-rate. Requires at least two captures in the window; returns an empty `deltas` array (not an error) when history is insufficient — create captures with [POST /api/research/snapshots](/api-reference/research/snapshots).", |
There was a problem hiding this comment.
YAGNI - delete deprecated docs / code.
There was a problem hiding this comment.
Done — deleted track/playcount-deltas outright (page + nav + orphaned schemas).
| "/api/research/tracks/{id}/measurements": { | ||
| "get": { | ||
| "summary": "Track measurements", | ||
| "description": "Time-series of a track's measured counts from the measurement store, projected by query params. Replaces `track/historic-stats` (use `granularity=daily`) and `track/playcount-deltas` (use `aggregate=run_rate`). `{id}` is a provider-neutral identifier (ISRC or Spotify track id). Spotify history is served from the store (snapshot captures + Songstats backfill); no vendor quota is spent in the request path.", |
There was a problem hiding this comment.
KISS
| "description": "Time-series of a track's measured counts from the measurement store, projected by query params. Replaces `track/historic-stats` (use `granularity=daily`) and `track/playcount-deltas` (use `aggregate=run_rate`). `{id}` is a provider-neutral identifier (ISRC or Spotify track id). Spotify history is served from the store (snapshot captures + Songstats backfill); no vendor quota is spent in the request path.", | |
| "description": "Time-series of a track's measured counts.", |
| "/api/research/albums/{id}/measurements": { | ||
| "get": { | ||
| "summary": "Album measurements", | ||
| "description": "Latest measured count per track on an album, from the measurement store. Replaces `GET /api/research/playcounts` (`?latest=true`). `{id}` is a Spotify album id.", |
There was a problem hiding this comment.
KISS
| "description": "Latest measured count per track on an album, from the measurement store. Replaces `GET /api/research/playcounts` (`?latest=true`). `{id}` is a Spotify album id.", | |
| "description": "Latest measured count per track on an album, from the measurement store. `{id}` is a Spotify album id.", |
| "/api/research/measurement-jobs": { | ||
| "post": { | ||
| "summary": "Create a measurement job", | ||
| "description": "One async ingest resource. `source:\"current\"` captures present counts via the snapshot pipeline (replaces `POST /api/research/snapshots`). `source:\"historical\"` enqueues each resolved recording for Songstats deep backfill ranked by all-time streams (idempotent — songs already carrying `songstats` history are skipped; no track is fetched twice). Provide exactly one of `catalog_id` / `album_ids` / `isrcs` in `scope`.", |
There was a problem hiding this comment.
KISS
| "description": "One async ingest resource. `source:\"current\"` captures present counts via the snapshot pipeline (replaces `POST /api/research/snapshots`). `source:\"historical\"` enqueues each resolved recording for Songstats deep backfill ranked by all-time streams (idempotent — songs already carrying `songstats` history are skipped; no track is fetched twice). Provide exactly one of `catalog_id` / `album_ids` / `isrcs` in `scope`.", | |
| "description": "One async ingest resource. `source:\"current\"` captures present counts via the snapshot pipeline. `source:\"historical\"` enqueues each resolved recording for Songstats deep backfill ranked by all-time streams (idempotent — songs already carrying `songstats` history are skipped; no track is fetched twice). Provide exactly one of `catalog_id` / `album_ids` / `isrcs` in `scope`.", |
| } | ||
| } | ||
| }, | ||
| "/api/research/measurement-jobs/{id}": { |
There was a problem hiding this comment.
DRY - how is this endpoint different from https://developers.recoupable.com/api-reference/tasks/runs ?
There was a problem hiding this comment.
Good catch — it duplicates GET /api/tasks/runs (the generic run-status endpoint). The old snapshots flow never had a status endpoint either (you read results from the data endpoints). Removed GET /research/measurement-jobs/{id} + its schema. POST still returns the job id; if unified run-status is wanted, tasks/runs is the place — no per-resource duplicate. Note: this means dropping getMeasurementJob/its route from api#668 to match — I'll apply that there.
…, drop bespoke job-status
- YAGNI: delete the 4 legacy endpoints outright (historic-stats, playcount-deltas,
playcounts, snapshots) instead of deprecating them — plus their MDX pages, nav
entries, and 8 now-orphaned schemas.
- DRY: remove GET /research/measurement-jobs/{id}; run status is covered by the
generic GET /api/tasks/runs, and the old snapshot flow never had a status endpoint.
- KISS: shorten the measurements / album-measurements / measurement-jobs descriptions
per the suggested edits.
mintlify openapi-check: valid; all $refs resolve.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
api-reference/openapi/research.json (1)
2870-2911: 💤 Low valueConsider adding a 400 response for validation errors.
The sibling
/tracks/{id}/measurementsendpoint includes a 400 response for validation errors (bad identifier or query param), but this endpoint does not. Both accept similar query parameters (platform,metric) that could fail validation. For consistent client error handling, consider adding:"400": { "description": "Validation error — bad identifier or query param.", "content": { "application/json": { "schema": { "$ref": "`#/components/schemas/ResearchErrorResponse`" } } } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@api-reference/openapi/research.json` around lines 2870 - 2911, The responses object for the album measurements endpoint is missing a 400 response to handle validation errors for bad identifiers or query parameters, while the sibling /tracks/{id}/measurements endpoint includes one. Add a 400 response to the responses object that provides a clear description of validation errors and references the ResearchErrorResponse schema, placing it logically with the other error responses (401, 402, 404) to ensure consistent client error handling across both endpoints that accept similar query parameters.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@api-reference/openapi/research.json`:
- Around line 2870-2911: The responses object for the album measurements
endpoint is missing a 400 response to handle validation errors for bad
identifiers or query parameters, while the sibling /tracks/{id}/measurements
endpoint includes one. Add a 400 response to the responses object that provides
a clear description of validation errors and references the
ResearchErrorResponse schema, placing it logically with the other error
responses (401, 402, 404) to ensure consistent client error handling across both
endpoints that accept similar query parameters.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9149ed15-c00f-480d-b159-86ece9c86076
📒 Files selected for processing (6)
api-reference/openapi/research.jsonapi-reference/research/album-measurements.mdxapi-reference/research/measurement-jobs.mdxapi-reference/research/measurements.mdxapi-reference/research/track-playcount-deltas.mdxdocs.json
💤 Files with no reviewable changes (1)
- api-reference/research/track-playcount-deltas.mdx
…drain (chat#1796) (#668) * feat(research): measurement-jobs write resource + retryable backfill drain Implements the chat#1796 api consolidation (write side) against the docs contract (recoupable/docs#242), TDD throughout. measurement-jobs (the ingest resource that replaces snapshots + the never-built backfill verb, and unblocks the catalog-value-estimator seed in skills#43): - POST /api/research/measurement-jobs {scope, source} - source=current -> reuses the snapshot pipeline (maps snapshot_id -> id) - source=historical -> resolves scope to ISRCs, enqueues Songstats backfill ranked by latest count, skips songs already carrying songstats history - GET /api/research/measurement-jobs/{id} -> poll a current job (uncharged) retryable backfill drain (fixes the starvation root cause's robustness gap): - backfillTrackStep: 404 -> done (terminal no-data); 429/5xx/timeout -> failed (transient, reclaimable) instead of permanently stranding tracks - reclaimStaleBackfillRows: resets failed + orphaned in_progress rows to pending - playcount-maintenance cron reclaims before each drain (auto-recovers the 2 stranded rows) 29 new/updated unit tests; full research+workflows suite green (291); tsc clean on touched files; lint clean. * refactor(research): drop bespoke measurement-job status endpoint (docs#242 review) Per review on docs#242 (DRY): GET /research/measurement-jobs/{id} duplicated the generic GET /api/tasks/runs, and the old snapshot flow had no status endpoint. Remove the GET status route/handler/data fn + test. POST still returns the job id. * chore: prettier format playcountMaintenanceHandler test (fix CI format check) * refactor(research): address api#668 review — card-on-file gate, terminal/retryable backfill, KISS reclaim Review feedback on chat#1796 (sweetmantech + bots): - **Card on file (Songstats gate):** `historical` measurement-jobs now require a payment method (they spend the capped Songstats quota). New ensureSongstatsPaymentMethod: 402 + free-tier Stripe Checkout link when no card. `current` (Apify) is exempt. - **backfillTrackStep:** only 408/429/5xx are retryable (`failed`); 404 + other permanent 4xx are terminal (`done`) — prevents reclaim from recycling perma-fails. - **KISS:** moved the reclaim into updateSongstatsBackfillQueue.ts as reclaimStaleSongstatsBackfillRows; it now throws on DB error instead of masking it as 0; stronger test asserts the and() grouping. - **enqueueHistoricalBackfill:** bounded-concurrency batches (25) instead of N serial upserts. DRY/idempotency unchanged (already enforced: skip songstats-having songs + upsert dedup + drain skips done). 39 unit tests; research+workflows suite 313 green; tsc/lint/format clean. * refactor(backfillTrackStep): flatten status classification into named vars (KISS, api#668 review)
The contract (docs-first) for chat#1796 — consolidate the
/researchplay-count surface into two REST resources. Head of the docs → api → skill merge order.New endpoints
GET /research/tracks/{id}/measurements— the measurement series;granularity=daily(series),aggregate=run_rate&window=365d(derived projection). Replacestrack/historic-stats+track/playcount-deltas.GET /research/albums/{id}/measurements?latest=true— latest per-track counts. Replacesplaycounts.POST /research/measurement-jobs{ scope, source: "current" | "historical" }→ 202 + pollableGET /research/measurement-jobs/{id}.currentreplacessnapshots;historicalis the backfill seed (replaces the never-builtbackfillverb).Deprecations
track/historic-stats,track/playcount-deltas,playcounts,snapshotsmarkeddeprecated: truewith a migration note pointing at the new resource.Changes
api-reference/openapi/research.json— 4 path operations + 8 schemas (additive diff; the 7 "deletions" are descriptions re-prefixed with the deprecation note).docs.jsonCatalog nav entries.Validation
mintlify openapi-check→ valid.$refs resolve; 4 new paths present; 4 legacy ops flagged deprecated.Summary by cubic
Consolidates the research play-count APIs into two REST resources and a single ingest job. Removes the old RPC endpoints and aligns with chat#1796.
New Features
GET /api/research/tracks/{id}/measurements— daily series; supportsaggregate=run_rate&window=365d.{id}accepts ISRC or Spotify track id.GET /api/research/albums/{id}/measurements?latest=true— latest per-track counts for an album.POST /api/research/measurement-jobs(source: "current" | "historical") — create ingest jobs; poll status viaGET /api/tasks/runs.Migration
GET /api/research/track/historic-statswithGET /api/research/tracks/{id}/measurements?granularity=daily.GET /api/research/track/playcount-deltaswithGET /api/research/tracks/{id}/measurements?aggregate=run_rate.GET /api/research/playcountswithGET /api/research/albums/{id}/measurements?latest=true.POST /api/research/snapshotswithPOST /api/research/measurement-jobs(source:"current"). Usesource:"historical"for deep backfill.Written for commit d2efd84. Summary will update on new commits.
Summary by CodeRabbit
New Features
Documentation