Skip to content

docs: measurements + measurement-jobs resources (chat#1796 contract)#242

Merged
sweetmantech merged 2 commits into
mainfrom
feat/measurements-measurement-jobs-contract
Jun 16, 2026
Merged

docs: measurements + measurement-jobs resources (chat#1796 contract)#242
sweetmantech merged 2 commits into
mainfrom
feat/measurements-measurement-jobs-contract

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

The contract (docs-first) for chat#1796 — consolidate the /research play-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). Replaces track/historic-stats + track/playcount-deltas.
  • GET /research/albums/{id}/measurements?latest=true — latest per-track counts. Replaces playcounts.
  • POST /research/measurement-jobs { scope, source: "current" | "historical" } → 202 + pollable GET /research/measurement-jobs/{id}. current replaces snapshots; historical is the backfill seed (replaces the never-built backfill verb).

Deprecations

track/historic-stats, track/playcount-deltas, playcounts, snapshots marked deprecated: true with 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).
  • 4 frontmatter-only MDX pages + docs.json Catalog nav entries.

Validation

  • mintlify openapi-checkvalid.
  • All 98 $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; supports aggregate=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 via GET /api/tasks/runs.
  • Migration

    • Replace GET /api/research/track/historic-stats with GET /api/research/tracks/{id}/measurements?granularity=daily.
    • Replace GET /api/research/track/playcount-deltas with GET /api/research/tracks/{id}/measurements?aggregate=run_rate.
    • Replace GET /api/research/playcounts with GET /api/research/albums/{id}/measurements?latest=true.
    • Replace POST /api/research/snapshots with POST /api/research/measurement-jobs (source:"current"). Use source:"historical" for deep backfill.

Written for commit d2efd84. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added track measurement endpoints providing time-series data with optional aggregation options
    • Added album measurement endpoints for latest per-track measured counts
    • Added ability to enqueue asynchronous measurement jobs for current and historical data capture
  • Documentation

    • Updated API reference documentation to reflect new measurement endpoints
    • Reorganized Research API navigation structure

…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.
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The Research API OpenAPI spec replaces legacy playcount/snapshot/historic-stats endpoints with three new measurement store endpoints (GET /tracks/{id}/measurements, GET /albums/{id}/measurements, POST /measurement-jobs). Corresponding component schemas are replaced, MDX page frontmatter is updated to reference new routes, and three nav entries are added to docs.json.

Changes

Research API Measurement Store Migration

Layer / File(s) Summary
New measurement schemas replacing legacy playcount schemas
api-reference/openapi/research.json
Removes ResearchTrackHistoricStatsResponse, ResearchPlaycount*, ResearchSnapshot*, ResearchPlaycountDelta* schemas and introduces ResearchMeasurementPoint, ResearchMeasurementsResponse, ResearchAlbumMeasurementItem, ResearchAlbumMeasurementsResponse, ResearchMeasurementJobScope/Request/Response. Updates ResearchInsufficientCreditsResponse with new required fields (error enum, remaining_credits, required_credits, checkoutUrl).
GET /api/research/tracks/{id}/measurements operation
api-reference/openapi/research.json
Adds the track measurement time-series endpoint with platform, metric, from/to, granularity, and optional aggregate=run_rate + window parameters. Updates existing historic-stats 402/404 error descriptions to reference measurement jobs for backfilling.
GET album measurements + POST measurement-jobs operations
api-reference/openapi/research.json
Adds GET /api/research/albums/{id}/measurements with Spotify album id, platform, metric, and latest parameters. Adds POST /api/research/measurement-jobs with 202/400/402/404 responses, scope validation (catalog_id/album_ids/isrcs), and `source: current
MDX pages and navigation wiring
api-reference/research/measurements.mdx, api-reference/research/album-measurements.mdx, api-reference/research/measurement-jobs.mdx, api-reference/research/track-playcount-deltas.mdx, docs.json
Updates frontmatter in three MDX pages to reference new endpoint paths/titles. Clears track-playcount-deltas.mdx of its frontmatter. Adds measurements, album-measurements, and measurement-jobs page routes to the Research → Catalog nav section.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • recoupable/docs#239: Both PRs modify the Research API's HTTP 402 insufficient credits documentation and the ResearchInsufficientCreditsResponse schema in api-reference/openapi/research.json.

Poem

🐇 Hoppity-hop through the measurement store,
Old playcounts and snapshots? We keep them no more!
New endpoints for tracks, albums in a line,
POST a job for history, the data looks fine.
The nav grows by three, the schemas reborn—
A cleaner API blooms on this morn! 🌸

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: introducing documentation for measurements and measurement-jobs resources as part of the chat#1796 contract implementation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/measurements-measurement-jobs-contract

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mintlify

mintlify Bot commented Jun 16, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
recoup-docs 🟢 Ready View Preview Jun 16, 2026, 12:11 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 6 files

Re-trigger cubic

Comment thread api-reference/openapi/research.json Outdated
"/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.",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

YAGNI - delete deprecated docs / code.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — deleted track/historic-stats (endpoint, MDX page, nav entry, orphaned schemas). Not deprecated, removed outright per YAGNI.

Comment thread api-reference/openapi/research.json Outdated
"/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.",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

YAGNI - delete deprecated docs / code.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — deleted playcounts outright (page + nav + orphaned schemas).

Comment thread api-reference/openapi/research.json Outdated
"/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).",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

YAGNI - delete deprecated docs / code.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — deleted snapshots outright (page + nav + ResearchSnapshotRequest/Response).

Comment thread api-reference/openapi/research.json Outdated
"/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).",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

YAGNI - delete deprecated docs / code.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — deleted track/playcount-deltas outright (page + nav + orphaned schemas).

Comment thread api-reference/openapi/research.json Outdated
"/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.",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KISS

Suggested change
"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.",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied.

Comment thread api-reference/openapi/research.json Outdated
"/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.",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KISS

Suggested change
"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.",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied.

Comment thread api-reference/openapi/research.json Outdated
"/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`.",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KISS

Suggested change
"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`.",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied.

Comment thread api-reference/openapi/research.json Outdated
}
}
},
"/api/research/measurement-jobs/{id}": {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DRY - how is this endpoint different from https://developers.recoupable.com/api-reference/tasks/runs ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
api-reference/openapi/research.json (1)

2870-2911: 💤 Low value

Consider adding a 400 response for validation errors.

The sibling /tracks/{id}/measurements endpoint 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

📥 Commits

Reviewing files that changed from the base of the PR and between 5649e84 and d2efd84.

📒 Files selected for processing (6)
  • api-reference/openapi/research.json
  • api-reference/research/album-measurements.mdx
  • api-reference/research/measurement-jobs.mdx
  • api-reference/research/measurements.mdx
  • api-reference/research/track-playcount-deltas.mdx
  • docs.json
💤 Files with no reviewable changes (1)
  • api-reference/research/track-playcount-deltas.mdx

@sweetmantech sweetmantech merged commit 2b7459a into main Jun 16, 2026
3 checks passed
@sweetmantech sweetmantech deleted the feat/measurements-measurement-jobs-contract branch June 16, 2026 14:07
sweetmantech added a commit to recoupable/api that referenced this pull request Jun 16, 2026
…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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant