OSPulse

Monitor packages continuously

A dependency that was healthy when you adopted it can decay quietly over years — a maintainer walks away, releases stop, the bus factor drops to one. This guide shows how to put the packages that matter on a watchlist, read their health history, detect degradation before it becomes an incident, and find credible replacements.

Scanning finds packages; watching tracks them

Every scan scores every dependency. A watch is a deliberate, named subscription to one package — it is how you say "tell me about this one specifically" and it is what alert rules can be scoped to. Watch a few dozen packages deliberately, not everything you depend on.

Choose what to watch

Do not watch by popularity or by dependency count. Watch by what a failure would cost you. Two signals, both available from the API, do most of the work.

Blast radius — how much of your estate breaks. GET /api/v1/packages/{packageId}/usage tells you which repositories pull the package, at what version, whether the dependency is direct or transitive, and how business-critical each consuming repository is:

bash
curl https://api.ospulse.app/api/v1/packages/7c4b8d10-3e92-4f61-b8a0-15d2c9e73f88/usage \
  -H "Authorization: Bearer $OSPULSE_TOKEN"
json
{
  "items": [
    {
      "repositoryId": "b28f4d51-77c0-4f2a-9d16-3ea5c7180b44",
      "repositoryName": "checkout-api",
      "fullName": "acme-engineering/checkout-api",
      "version": "2.4.1",
      "dependencyType": "Runtime",
      "isDirect": true,
      "manifestPath": "src/Checkout.Api/packages.lock.json",
      "businessCriticality": "Critical"
    }
  ],
  "totalEstimate": 14
}

Fourteen repositories, one of them Critical, and a direct runtime dependency. That is a watch candidate. A build-time package used by one archived prototype is not.

Current health — how likely a failure is. GET /api/v1/packages/{packageId}/health returns the composite score and its eleven component scores, so you can see why a score is what it is:

json
{
  "snapshotId": "e21a4c7f-98b3-4d05-a6e1-b7f30c245d99",
  "capturedAtUtc": "2026-07-28T04:12:55Z",
  "overallHealthScore": 61,
  "confidenceScore": 84,
  "riskLevel": "Medium",
  "evidenceBundleId": "44f0b8c2-7a19-4e63-95d8-0cb1e7423a56",
  "componentScores": {
    "maintainer": 38, "activity": 52, "release": 60, "issue": 71,
    "pullRequest": 66, "security": 88, "licence": 100, "provenance": 74,
    "popularity": 92, "busFactor": 30, "blastRadius": 81
  },
  "weightsSnapshotJson": "{\"maintainer\":0.15,\"activity\":0.15}"
}

A busFactor of 30 against a popularity of 92 is the classic dangerous shape: widely used, barely maintained. See Health scoring for what each component measures.

A practical starting watchlist

Direct runtime dependencies of repositories marked Critical; any package with a busFactor below 40; anything you have forked or patched locally; and anything whose licence component is not 100. That is usually 20–50 packages, not 5,000.

Watch and unwatch

Find the package id

Watches are keyed on OSPulse's package id, not on the ecosystem name. Search for it:

bash
curl "https://api.ospulse.app/api/v1/packages?ecosystem=Npm&search=left-pad&take=5" \
  -H "Authorization: Bearer $OSPULSE_TOKEN"
json
{
  "items": [
    {
      "packageId": "7c4b8d10-3e92-4f61-b8a0-15d2c9e73f88",
      "name": "left-pad",
      "ecosystem": "Npm",
      "latestVersion": "1.3.0",
      "status": "Tracked",
      "sourceRepositoryUrl": "https://github.com/left-pad/left-pad"
    }
  ],
  "take": 5,
  "skip": 0,
  "totalReturned": 1
}

Names are not unique across ecosystems, so always pass ecosystem alongside search.

Add the watch, with a reason

The reason is free text and it is the single most valuable field here. In six months, "watching because X" is what stops someone removing the watch during a tidy-up.

bash
curl -X POST https://api.ospulse.app/api/v1/packages/7c4b8d10-3e92-4f61-b8a0-15d2c9e73f88/watch \
  -H "Authorization: Bearer $OSPULSE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Direct runtime dep of checkout-api; bus factor 1 since 2025" }'
json
{
  "watchId": "d5e91b06-4c37-42fa-8e70-9a1f2b83c0d4",
  "packageId": "7c4b8d10-3e92-4f61-b8a0-15d2c9e73f88",
  "userId": "aa17f3d2-60b8-4c19-9d43-2e0587fa6c31",
  "reason": "Direct runtime dep of checkout-api; bus factor 1 since 2025",
  "alreadyWatching": false
}

A watch belongs to the identity that created it — note the userId. Re-posting is safe: you get alreadyWatching: true and the existing watch back, so an idempotent reconciliation script can simply re-post its whole desired list.

Review the watchlist

bash
curl https://api.ospulse.app/api/v1/packages/watched \
  -H "Authorization: Bearer $OSPULSE_TOKEN"
json
{
  "items": [
    {
      "watchId": "d5e91b06-4c37-42fa-8e70-9a1f2b83c0d4",
      "packageId": "7c4b8d10-3e92-4f61-b8a0-15d2c9e73f88",
      "name": "left-pad",
      "ecosystem": "Npm",
      "latestVersion": "1.3.0",
      "status": "Tracked",
      "reason": "Direct runtime dep of checkout-api; bus factor 1 since 2025",
      "watchedAtUtc": "2026-07-28T11:02:19Z",
      "health": { "overallHealthScore": 61, "confidenceScore": 84, "riskLevel": "Medium" }
    }
  ],
  "totalReturned": 1
}

Because each item carries its current health inline, this one call is enough for a dashboard or digest — you only need the per-package endpoints when you want history or detail.

Remove a watch when it stops earning its place

bash
curl -X DELETE https://api.ospulse.app/api/v1/packages/7c4b8d10-3e92-4f61-b8a0-15d2c9e73f88/watch \
  -H "Authorization: Bearer $OSPULSE_TOKEN"

Returns 204 No Content. Prune the watchlist when you drop the dependency — a watchlist full of packages you no longer ship is how a digest becomes something nobody reads.

Detect degradation over time

A single score tells you where a package stands. The trend tells you where it is going, and the trend is what you act on. GET /api/v1/packages/{packageId}/health/history returns score snapshots newest-first:

bash
curl "https://api.ospulse.app/api/v1/packages/7c4b8d10-3e92-4f61-b8a0-15d2c9e73f88/health/history?take=6" \
  -H "Authorization: Bearer $OSPULSE_TOKEN"
json
{
  "packageId": "7c4b8d10-3e92-4f61-b8a0-15d2c9e73f88",
  "items": [
    { "capturedAtUtc": "2026-07-28T04:12:55Z", "overallHealthScore": 61, "confidenceScore": 84, "riskLevel": "Medium", "blastRadiusScore": 81 },
    { "capturedAtUtc": "2026-06-28T04:11:02Z", "overallHealthScore": 68, "confidenceScore": 85, "riskLevel": "Medium", "blastRadiusScore": 80 },
    { "capturedAtUtc": "2026-05-28T04:10:47Z", "overallHealthScore": 74, "confidenceScore": 86, "riskLevel": "Low",    "blastRadiusScore": 78 },
    { "capturedAtUtc": "2026-04-28T04:09:33Z", "overallHealthScore": 79, "confidenceScore": 86, "riskLevel": "Low",    "blastRadiusScore": 78 },
    { "capturedAtUtc": "2026-03-28T04:12:10Z", "overallHealthScore": 81, "confidenceScore": 87, "riskLevel": "Low",    "blastRadiusScore": 77 },
    { "capturedAtUtc": "2026-02-28T04:08:58Z", "overallHealthScore": 82, "confidenceScore": 84, "riskLevel": "Low",    "blastRadiusScore": 77 }
  ],
  "totalReturned": 6
}

That is a 21-point slide over five months with confidence holding steady — a genuine decay, not a measurement artefact. Three rules for reading history well:

  • Compare like windows. A month-on-month delta is signal; a day-on-day wobble is noise.
  • Check confidenceScore before believing a drop. If confidence fell alongside the score, OSPulse has less evidence than it did, which is a different problem from the package getting worse.
  • Weight the drop by blastRadiusScore. A ten-point fall on something buried in one archived service does not deserve the same response as a ten-point fall on a package in fourteen repositories.

Poll on a cadence that matches the data

Health snapshots are produced by scans, so polling more often than your repositories are scanned returns the same snapshot repeatedly. Daily is plenty for a dashboard; weekly is right for a digest. If you want to react in minutes rather than days, do not poll at all — use alert rules, which push.

A weekly health digest

This script reads the watchlist, compares each package's newest score with the oldest snapshot inside a lookback window, and prints only the packages that moved by more than a threshold. Run it from cron or a scheduled CI job and post the output to your team channel.

python
#!/usr/bin/env python3
"""Weekly OSPulse watchlist digest: report packages whose health score dropped."""
import os
import sys
from datetime import datetime, timedelta, timezone
 
import requests
 
API = "https://api.ospulse.app/api/v1"
SESSION = requests.Session()
SESSION.headers["Authorization"] = f"Bearer {os.environ['OSPULSE_TOKEN']}"
 
LOOKBACK_DAYS = int(os.environ.get("LOOKBACK_DAYS", "30"))
DROP_THRESHOLD = int(os.environ.get("DROP_THRESHOLD", "5"))
 
 
def get(path, **params):
    res = SESSION.get(f"{API}{path}", params=params, timeout=30)
    if not res.ok:  # RFC 7807 problem detail — log the correlationId, always
        problem = res.json() if "json" in res.headers.get("content-type", "") else {}
        print(f"{path} -> {res.status_code} (correlationId "
              f"{problem.get('correlationId')})", file=sys.stderr)
        res.raise_for_status()
    return res.json()
 
 
def parse(ts):
    return datetime.fromisoformat(ts.replace("Z", "+00:00"))
 
 
cutoff = datetime.now(timezone.utc) - timedelta(days=LOOKBACK_DAYS)
watched = get("/packages/watched")["items"]
findings = []
 
for pkg in watched:
    history = get(f"/packages/{pkg['packageId']}/health/history", take=30)["items"]
    if len(history) < 2:
        continue
 
    newest = history[0]
    # Oldest snapshot still inside the window, else the oldest we have.
    in_window = [h for h in history if parse(h["capturedAtUtc"]) >= cutoff]
    baseline = in_window[-1] if len(in_window) > 1 else history[-1]
 
    delta = newest["overallHealthScore"] - baseline["overallHealthScore"]
    if delta > -DROP_THRESHOLD:
        continue
 
    findings.append({
        "name": f"{pkg['name']} ({pkg['ecosystem']})",
        "score": newest["overallHealthScore"],
        "delta": delta,
        "confidence": newest["confidenceScore"],
        "risk": newest["riskLevel"],
        "repos": get(f"/packages/{pkg['packageId']}/usage")["totalEstimate"],
        "reason": pkg.get("reason") or "-",
    })
 
# Biggest drop first, then widest blast radius.
findings.sort(key=lambda f: (f["delta"], -f["repos"]))
 
print(f"OSPulse watchlist digest — {len(watched)} watched, "
      f"{len(findings)} declining over {LOOKBACK_DAYS} days\n")
 
for f in findings:
    print(f"{f['name']}")
    print(f"  score {f['score']} ({f['delta']:+d})  "
          f"confidence {f['confidence']}  risk {f['risk']}")
    print(f"  used by {f['repos']} repositories")
    print(f"  watching because: {f['reason']}\n")
text
OSPulse watchlist digest — 34 watched, 2 declining over 30 days
 
left-pad (Npm)
  score 61 (-7)  confidence 84  risk Medium
  used by 14 repositories
  watching because: Direct runtime dep of checkout-api; bus factor 1 since 2025
 
request-promise (Npm)
  score 44 (-6)  confidence 79  risk High
  used by 3 repositories
  watching because: Deprecated upstream, migration not yet scheduled

Sorting by the size of the drop and then by blast radius puts the packages that need a decision at the top. Anything that appears three weeks running is not a monitoring problem — it is a replacement decision you have been deferring.

Find a replacement

When a watched package is genuinely decaying, ask OSPulse what else in the ecosystem does the same job:

bash
curl https://api.ospulse.app/api/v1/packages/7c4b8d10-3e92-4f61-b8a0-15d2c9e73f88/replacement-candidates \
  -H "Authorization: Bearer $OSPULSE_TOKEN"
json
{
  "packageId": "7c4b8d10-3e92-4f61-b8a0-15d2c9e73f88",
  "items": [
    {
      "candidatePackageId": "b90f7412-cd58-4e02-91a7-6d3ba0417cc5",
      "name": "pad-left",
      "ecosystem": "Npm",
      "confidenceScore": 0.72,
      "rationale": "Same public surface; actively released; 4 maintainers"
    }
  ],
  "totalEstimate": 3
}

confidenceScore is how confident OSPulse is that the candidate is a genuine substitute, not a recommendation to migrate. Treat the list as a shortlist and check each candidate the way you checked the original: pull its health, its risk-indicators, and its history before committing. A candidate with a great score and three months of data is a worse bet than one with a decent score and five years of it.

Before you migrate, use usage to scope the work — it gives you the exact repositories, versions and manifest paths to change.

Common pitfalls

Watching everything. A 4,000-entry watchlist produces a digest nobody opens. Watch what you would be paged about.

Reacting to a single snapshot. Scores move for benign reasons — a quiet month between releases, a metadata source going briefly stale. Require a sustained move across two or three snapshots before you act.

Ignoring confidenceScore. A low-confidence score is a statement about the evidence, not the package. Chasing one wastes an afternoon.

Forgetting watches are per-identity. A watchlist created by one user's token is that user's. For team-wide monitoring, create the watches with a service token and keep that token as the owner of record.

Next steps