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:
curl https://api.ospulse.app/api/v1/packages/7c4b8d10-3e92-4f61-b8a0-15d2c9e73f88/usage \
-H "Authorization: Bearer $OSPULSE_TOKEN"{
"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:
{
"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:
curl "https://api.ospulse.app/api/v1/packages?ecosystem=Npm&search=left-pad&take=5" \
-H "Authorization: Bearer $OSPULSE_TOKEN"{
"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.
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" }'{
"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
curl https://api.ospulse.app/api/v1/packages/watched \
-H "Authorization: Bearer $OSPULSE_TOKEN"{
"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
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:
curl "https://api.ospulse.app/api/v1/packages/7c4b8d10-3e92-4f61-b8a0-15d2c9e73f88/health/history?take=6" \
-H "Authorization: Bearer $OSPULSE_TOKEN"{
"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
confidenceScorebefore 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.
#!/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")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 scheduledSorting 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:
curl https://api.ospulse.app/api/v1/packages/7c4b8d10-3e92-4f61-b8a0-15d2c9e73f88/replacement-candidates \
-H "Authorization: Bearer $OSPULSE_TOKEN"{
"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.
