Skip to content

Promote test → main: measurements + measurement-jobs (chat#1796)#670

Merged
sweetmantech merged 2 commits into
mainfrom
test
Jun 16, 2026
Merged

Promote test → main: measurements + measurement-jobs (chat#1796)#670
sweetmantech merged 2 commits into
mainfrom
test

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Promote the two merged chat#1796 PRs from test to main (prod).

Includes:

Both verified on preview (results in the respective PRs). Additive — legacy endpoints keep serving. After this lands, the catalog-value-estimator seed (skills#43) works against prod.


Summary by cubic

Promotes consolidated research measurement APIs to prod: adds POST /api/research/measurement-jobs for ingest and GET /api/research/tracks|albums/{id}/measurements for reads. Also adds a Songstats card-on-file gate and makes the historical backfill drain retryable with automatic reclaim in maintenance. Legacy endpoints remain available.

  • New Features

    • POST /api/research/measurement-jobs{ scope, source }:
      • source: "current" uses the snapshot pipeline (maps snapshot_idid).
      • source: "historical" enqueues Songstats backfill ranked by latest counts; skips songs already backfilled; bounded concurrency; returns 202.
      • Validation: auth + research credits; historical requires a payment method; current does not.
    • GET /api/research/tracks/{id}/measurements — daily series; ?aggregate=run_rate over a trailing window. {id} can be ISRC or a Spotify track id (resolved to ISRC).
    • GET /api/research/albums/{id}/measurements — latest count per track (thin remap of playcounts). CORS preflight added for new routes.
  • Reliability

    • Backfill drain: 408/429/5xx are marked failed (reclaimable); 404 and other permanent 4xx are marked done (terminal).
    • Maintenance cron reclaims failed and stale in_progress rows before draining and reports the reclaimed count.

Written for commit 318ae4a. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added research measurement endpoints for retrieving album and track playcount data
    • Introduced measurement job creation supporting historical backfills and current snapshots
    • Added measurement aggregation options, including run-rate projections
    • Implemented payment method validation for measurement features
  • Improvements

    • Enhanced error handling with better categorization for failed requests
    • Improved queue management with staleness detection and recovery mechanisms

The read half of the chat#1796 consolidation, against docs#242, TDD throughout.

- GET /api/research/tracks/{id}/measurements — a track's measured series from
  the store (granularity=daily); aggregate=run_rate returns the trailing-window
  annualized run-rate (a projection via computePlaycountDeltas). Provider-neutral
  {id} (ISRC or Spotify track id, resolved to ISRC). Replaces track/historic-stats
  + track/playcount-deltas.
- GET /api/research/albums/{id}/measurements — latest count per track; a thin
  remap over getAlbumPlaycounts. Replaces playcounts.

Heavy reuse of existing store reads (selectSongMeasurements, computePlaycountDeltas,
getAlbumPlaycounts); legacy endpoints keep serving (deprecated in docs#242 only).

24 new unit tests (resolver, both data fns, both validators, both handlers);
full lib/research suite green (268); tsc + lint clean on touched files.
…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)
@vercel

vercel Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
api Ready Ready Preview Jun 16, 2026 3:35pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Three new REST endpoints are added: GET /api/research/albums/{id}/measurements, GET /api/research/tracks/{id}/measurements, and POST /api/research/measurement-jobs. The PR also introduces historical Songstats backfill enqueueing, Stripe payment-method gating, stale queue row reclamation, and reclassifies non-200 Songstats responses in the backfill worker as retryable vs. terminal.

Changes

Research Measurements & Measurement Jobs API

Layer / File(s) Summary
Stale backfill queue reclamation and error classification
lib/supabase/songstats_backfill_queue/updateSongstatsBackfillQueue.ts, app/workflows/backfillTrackStep.ts, lib/research/playcounts/playcountMaintenanceHandler.ts
Adds reclaimStaleSongstatsBackfillRows (resets stale failed/in_progress rows to pending after 1 hour), reclassifies non-200 Songstats responses into no data 404, retryable, and terminal outcomes with matching done/failed queue statuses, and wires reclamation into playcountMaintenanceHandler before the drain workflow.
Measurement job validation, schema, and payment gating
lib/research/measurement_jobs/validateCreateMeasurementJobRequest.ts, lib/research/measurement_jobs/ensureSongstatsPaymentMethod.ts
Defines Zod schema (scope union, source, platforms), implements validateCreateMeasurementJobRequest with auth/JSON/schema validation, and adds ensureSongstatsPaymentMethod which returns a 402 Stripe Checkout redirect when no default payment method is set for historical requests.
Measurement job creation: scope resolution and backfill dispatch
lib/research/measurement_jobs/resolveScopeSongs.ts, lib/research/measurement_jobs/enqueueHistoricalBackfill.ts, lib/research/measurement_jobs/createMeasurementJob.ts
resolveScopeSongs maps ISRC/album/catalog scope to a deduplicated ISRC list; enqueueHistoricalBackfill queries existing measurements, skips already-backfilled ISRCs, and upserts queue entries in batches of 25; createMeasurementJob routes historical requests to the backfill enqueue and current requests to createSnapshot with normalized result mapping.
POST measurement-jobs handler and route
lib/research/measurement_jobs/createMeasurementJobHandler.ts, app/api/research/measurement-jobs/route.ts
Adds createMeasurementJobHandler with validation-first flow, 202 CORS response on success, structured error mapping, and 500 fallback; wires it to the Next.js route with OPTIONS/POST and maxDuration = 60.
Album measurements: validation, fetch, handler, and route
lib/research/measurements/validateGetAlbumMeasurementsRequest.ts, lib/research/measurements/getAlbumMeasurements.ts, lib/research/measurements/getAlbumMeasurementsHandler.ts, app/api/research/albums/[id]/measurements/route.ts
Validates auth and research credits; calls getAlbumPlaycounts and remaps rows to a measurements array keyed by platform_displayed_play_count; handler applies validation-first pattern with success/error response helpers; route wires OPTIONS/GET with maxDuration = 60.
Track measurements: ISRC resolution, validation, data fetch, aggregation, and route
lib/research/measurements/resolveTrackIsrc.ts, lib/research/measurements/validateGetTrackMeasurementsRequest.ts, lib/research/measurements/getTrackMeasurements.ts, lib/research/measurements/getTrackMeasurementsHandler.ts, app/api/research/tracks/[id]/measurements/route.ts
resolveTrackIsrc accepts ISRC patterns or Spotify track IDs; validator parses aggregate/window/platform/metric query params with defaults; getTrackMeasurements deducts credits only after data exists and returns either a time series or a run_rate aggregation; handler and route follow the same validation-first pattern.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Route as Next.js Route
    participant Validator
    participant Logic as Business Logic
    participant DB as Supabase / Stripe

    rect rgba(100, 149, 237, 0.5)
        Note over Client,DB: POST /api/research/measurement-jobs (historical)
        Client->>Route: POST with scope + source=historical
        Route->>Validator: validateCreateMeasurementJobRequest
        Validator->>DB: validateAuthContext
        Validator->>DB: ensureSongstatsPaymentMethod → Stripe customer lookup
        alt No payment method
            DB-->>Validator: 402 + Stripe Checkout URL
            Validator-->>Client: 402 checkoutUrl
        end
        Validator-->>Logic: ValidatedCreateMeasurementJobRequest
        Logic->>DB: resolveScopeSongs → ISRC list
        Logic->>DB: enqueueHistoricalBackfill → upsert songstats_backfill_queue
        Logic-->>Client: 202 enqueued + skipped counts
    end

    rect rgba(144, 238, 144, 0.5)
        Note over Client,DB: GET /api/research/tracks/{id}/measurements
        Client->>Route: GET with id + ?aggregate=run_rate&window=90d
        Route->>Validator: validateGetTrackMeasurementsRequest
        Validator->>DB: auth + ensureResearchCredits
        Validator-->>Logic: ValidatedGetTrackMeasurementsRequest
        Logic->>DB: resolveTrackIsrc → ISRC
        Logic->>DB: fetch song_measurements rows
        Logic->>Logic: computePlaycountDeltas → annualized run_rate
        Logic-->>Client: 200 data.aggregate
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • recoupable/api#663: Introduced playcountMaintenanceHandler, which this PR extends by adding the reclaimStaleSongstatsBackfillRows pre-step and updating the cron response shape.
  • recoupable/api#662: Both PRs modify backfillTrackStep.ts around non-200 Songstats response handling and backfill queue/quota outcome updates.
  • recoupable/api#668: Overlaps with this PR's measurement-jobs write endpoint, stale row reclamation, and backfill-drain retry classification changes.

Poem

🎵 Three routes awoke at measurement time,
Albums and tracks now served in their prime.
Historical depths get queued with care,
Stale rows reclaimed from the job-queue snare.
Stripe stands guard at the historical gate—
Pay up or wait, we'll backfill your fate! 🎸

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning Five explicit code review comments remain unaddressed: double credit deduction in album path, missing Zod ID validation, empty platforms array acceptance, premature credit deduction in track path,... Apply all five review comment fixes: add .min(1) to platforms schema, validate album id with Zod before credit check, remove pre-deduction in album validator, defer credit deduction to success returns in track measurements, reject invali...
✅ Passed checks (2 passed)
Check name Status Explanation
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test

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.

@sweetmantech sweetmantech merged commit dfd9004 into main Jun 16, 2026
6 of 7 checks passed
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