Skip to content

Commit e12ad28

Browse files
authored
feat: add metrics in packages api (CM-1285) (#4259)
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
1 parent e67136b commit e12ad28

7 files changed

Lines changed: 165 additions & 53 deletions

File tree

backend/src/api/public/v1/ossprey/openapi.yaml

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@ components:
5050

5151
HealthBand:
5252
type: string
53-
enum: [healthy, fair, concerning, critical]
53+
enum: [excellent, healthy, fair, concerning, critical]
5454
description: >
55-
Server-derived from `scorecardScore` thresholds:
56-
`null or < 3.0` → critical · `< 5.0` → concerning · `< 7.0` → fair · `≥ 7.0` → healthy
55+
Tinybird band when enriched (excellent ≥85, healthy 70–84, fair 50–69,
56+
concerning 30–49, critical <30). Falls back to scorecard thresholds:
57+
`null or < 3.0` → critical · `< 5.0` → concerning · `< 7.0` → fair · `≥ 7.0` → healthy.
5758
5859
StewardshipStatus:
5960
type: string
@@ -90,7 +91,7 @@ components:
9091
- ecosystem
9192
- openVulns
9293
- maintainerCount
93-
- healthBand
94+
- health
9495
- stewards
9596
properties:
9697
purl:
@@ -102,12 +103,6 @@ components:
102103
ecosystem:
103104
type: string
104105
example: npm
105-
criticalityScore:
106-
type:
107-
- number
108-
- 'null'
109-
description: packages.impact (0–1 float). Multiply × 100 for display score.
110-
example: 0.94
111106
stewardshipId:
112107
type:
113108
- string
@@ -131,14 +126,52 @@ components:
131126
type: integer
132127
description: Count of package_maintainers rows. Bus factor proxy.
133128
example: 2
129+
criticalityScore:
130+
type:
131+
- number
132+
- 'null'
133+
description: Raw criticality score (0–1 float). Use `impact` for display.
134+
example: 0.94
135+
impact:
136+
type:
137+
- integer
138+
- 'null'
139+
description: Display score (0–100). criticalityScore × 100, rounded. Null if no score.
140+
example: 94
134141
scorecardScore:
135142
type:
136143
- number
137144
- 'null'
138145
description: OpenSSF Scorecard score (0–10). Null if no repo mapped.
139146
example: 5.2
140-
healthBand:
141-
$ref: '#/components/schemas/HealthBand'
147+
health:
148+
type: object
149+
required: [score, label]
150+
properties:
151+
score:
152+
type:
153+
- integer
154+
- 'null'
155+
description: >
156+
Health score (0–100). Tinybird composite score when enriched,
157+
OpenSSF Scorecard × 10 otherwise. Null if neither is available.
158+
example: 52
159+
label:
160+
type:
161+
- string
162+
- 'null'
163+
enum: [excellent, healthy, fair, concerning, critical, null]
164+
description: >
165+
Tinybird band when enriched (excellent ≥85, healthy 70–84,
166+
fair 50–69, concerning 30–49, critical <30), scorecard band otherwise.
167+
example: fair
168+
lifecycle:
169+
type:
170+
- string
171+
- 'null'
172+
enum: [active, stable, declining, abandoned, archived, null]
173+
description: Tinybird-enriched lifecycle label. Null if not yet enriched.
174+
example: active
142175
latestReleaseAt:
143176
type:
144177
- string
@@ -230,7 +263,11 @@ components:
230263
description: ROUND(scorecard_score × 10). X-axis position (0–100). 0 if no repo.
231264
example: 52
232265
healthBand:
233-
$ref: '#/components/schemas/HealthBand'
266+
type: string
267+
enum: [healthy, fair, concerning, critical]
268+
description: >
269+
Falls back to scorecard thresholds — never 'excellent' (scatter uses
270+
scorecard score only, not Tinybird health_label).
234271
stewardshipStatus:
235272
oneOf:
236273
- $ref: '#/components/schemas/StewardshipStatus'
@@ -545,7 +582,7 @@ paths:
545582
in: query
546583
schema:
547584
type: string
548-
enum: [active, stable, declining, abandoned]
585+
enum: [active, stable, declining, abandoned, archived]
549586
- name: status
550587
in: query
551588
description: >
@@ -625,13 +662,17 @@ paths:
625662
name: slf4j-api
626663
ecosystem: maven
627664
criticalityScore: 0.998
665+
impact: 100
628666
stewardshipId: '101'
629667
stewardshipStatus: active
630668
openVulns: 0
631669
maxVulnSeverity: null
632670
maintainerCount: 2
633671
scorecardScore: 7.5
634-
healthBand: healthy
672+
health:
673+
score: 75
674+
label: healthy
675+
lifecycle: active
635676
latestReleaseAt: '2026-04-10T00:00:00Z'
636677
lastActivity:
637678
type: state_changed

backend/src/api/public/v1/ossprey/packageList.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,18 @@ import { ok } from '@/utils/api'
1313
import { validateOrThrow } from '@/utils/validation'
1414

1515
import { purlFilterSchema } from '../packages/purl'
16+
import { HEALTH_BAND_SET, HEALTH_BAND_VALUES, LIFECYCLE_VALUES } from '../packages/types'
1617

1718
const MAX_PAGE_SIZE = 250
19+
const LIFECYCLE_SET = new Set<string>(LIFECYCLE_VALUES)
1820

1921
const boolParam = z.preprocess((v) => v === 'true', z.boolean()).default(false)
2022

2123
const querySchema = z.object({
2224
page: z.coerce.number().int().min(1).default(1),
2325
pageSize: z.coerce.number().int().min(1).max(MAX_PAGE_SIZE).default(25),
2426
ecosystem: z.string().trim().optional(),
25-
lifecycle: z.enum(['active', 'stable', 'declining', 'abandoned']).optional(),
27+
lifecycle: z.enum(LIFECYCLE_VALUES).optional(),
2628
name: z.string().trim().optional(),
2729
purl: purlFilterSchema,
2830
status: z
@@ -37,7 +39,7 @@ const querySchema = z.object({
3739
'inactive',
3840
])
3941
.optional(),
40-
healthBand: z.enum(['healthy', 'fair', 'concerning', 'critical']).optional(),
42+
healthBand: z.enum(HEALTH_BAND_VALUES).optional(),
4143
vulnSeverity: z.enum(['any', 'high', 'critical', 'none']).optional(),
4244
staleOnly: boolParam,
4345
unstewardedOnly: boolParam,
@@ -73,13 +75,23 @@ export async function packageListHandler(req: Request, res: Response): Promise<v
7375
name: r.name,
7476
ecosystem: r.ecosystem,
7577
criticalityScore: r.criticalityScore,
78+
impact: r.criticalityScore != null ? Math.round(r.criticalityScore * 100) : null,
7679
stewardshipId: r.stewardshipId ?? null,
7780
stewardshipStatus: r.stewardshipStatus ?? null,
7881
openVulns: r.openVulns,
7982
maxVulnSeverity: r.maxVulnSeverity ?? null,
8083
maintainerCount: r.maintainerCount,
8184
scorecardScore: r.scorecardScore,
82-
healthBand: computeHealthBand(r.scorecardScore != null ? Number(r.scorecardScore) : null),
85+
health: {
86+
score:
87+
r.healthScore ?? (r.scorecardScore != null ? Math.round(r.scorecardScore * 10) : null),
88+
label:
89+
r.healthLabel != null && HEALTH_BAND_SET.has(r.healthLabel)
90+
? r.healthLabel
91+
: computeHealthBand(r.scorecardScore),
92+
},
93+
lifecycle:
94+
r.lifecycleLabel != null && LIFECYCLE_SET.has(r.lifecycleLabel) ? r.lifecycleLabel : null,
8395
latestReleaseAt: r.latestReleaseAt ? r.latestReleaseAt.toISOString() : null,
8496
lastActivity: r.lastActivityAt
8597
? {

backend/src/api/public/v1/packages/getPackage.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import { ok } from '@/utils/api'
1313
import { validateOrThrow } from '@/utils/validation'
1414

1515
import { purlQuerySchema } from './purl'
16-
import type { StewardshipStatus } from './types'
16+
import { HEALTH_BAND_SET, LIFECYCLE_VALUES, type StewardshipStatus } from './types'
17+
18+
const LIFECYCLE_SET = new Set<string>(LIFECYCLE_VALUES)
1719

1820
function snakeToCamelKeys(obj: Record<string, unknown> | null): Record<string, unknown> | null {
1921
if (obj === null) return null
@@ -47,7 +49,6 @@ export async function getPackage(req: Request, res: Response): Promise<void> {
4749
const scorecardScore = pkg.scorecardScore != null ? Number(pkg.scorecardScore) : null
4850
const mappingConfidence =
4951
pkg.repoMappingConfidence != null ? Number(pkg.repoMappingConfidence) : null
50-
const healthBandScore = pkg.healthScore !== null ? pkg.healthScore / 10 : scorecardScore
5152

5253
ok(res, {
5354
purl: pkg.purl,
@@ -58,22 +59,28 @@ export async function getPackage(req: Request, res: Response): Promise<void> {
5859
healthScore: pkg.healthScore,
5960
healthScoreDetails: {
6061
total: pkg.healthScore,
61-
label: pkg.healthLabel,
62+
label:
63+
pkg.healthLabel != null && HEALTH_BAND_SET.has(pkg.healthLabel) ? pkg.healthLabel : null,
6264
maintainerHealth: pkg.maintainerHealthScore,
6365
securitySupplyChain: pkg.securitySupplyChainScore,
6466
developmentActivity: pkg.developmentActivityScore,
6567
},
66-
healthBand: computeHealthBand(healthBandScore),
68+
healthBand:
69+
pkg.healthLabel != null && HEALTH_BAND_SET.has(pkg.healthLabel)
70+
? pkg.healthLabel
71+
: computeHealthBand(scorecardScore),
6772
impact: {
68-
impactScore:
69-
pkg.criticalityScore != null ? Math.round(Number(pkg.criticalityScore) * 100) : null,
73+
impactScore: pkg.criticalityScore != null ? Math.round(pkg.criticalityScore * 100) : null,
7074
downloadsLastMonth: pkg.downloadsLast30d ?? null,
7175
dependentPackages: pkg.dependentPackagesCount ?? null,
7276
dependentRepos: pkg.dependentReposCount ?? null,
7377
transitiveReach: pkg.transitiveReach,
7478
},
7579
riskSignals: {
76-
lifecycle: pkg.lifecycleLabel,
80+
lifecycle:
81+
pkg.lifecycleLabel != null && LIFECYCLE_SET.has(pkg.lifecycleLabel)
82+
? pkg.lifecycleLabel
83+
: null,
7784
maintainerBusFactor: pkg.maintainerCount,
7885
lastRelease: pkg.latestReleaseAt ? pkg.latestReleaseAt.toISOString() : null,
7986
hasSecurityFile: pkg.hasSecurityFile,

backend/src/api/public/v1/packages/listPackages.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,42 @@
11
import type { Request, Response } from 'express'
22
import { z } from 'zod'
33

4-
import { getPackageStatusCounts, listPackagesForApi } from '@crowd/data-access-layer'
4+
import {
5+
computeHealthBand,
6+
getPackageStatusCounts,
7+
listPackagesForApi,
8+
} from '@crowd/data-access-layer'
59

610
import { getPackagesQx } from '@/db/packagesDb'
711
import { ok } from '@/utils/api'
812
import { validateOrThrow } from '@/utils/validation'
913

1014
import { purlFilterSchema } from './purl'
11-
import { STEWARDSHIP_STATUS_VALUES, type StewardshipStatus } from './types'
15+
import {
16+
HEALTH_BAND_SET,
17+
HEALTH_BAND_VALUES,
18+
LIFECYCLE_VALUES,
19+
STEWARDSHIP_STATUS_VALUES,
20+
type StewardshipStatus,
21+
} from './types'
1222

1323
const DEFAULT_PAGE_SIZE = 20
1424
const MAX_PAGE_SIZE = 100
1525

1626
const booleanQueryParam = z.preprocess((v) => v === 'true', z.boolean()).default(false)
1727

18-
const lifecycleValues = ['active', 'stable', 'declining', 'abandoned'] as const
19-
const healthBandValues = ['healthy', 'fair', 'concerning', 'critical'] as const
28+
const LIFECYCLE_SET = new Set<string>(LIFECYCLE_VALUES)
2029
const vulnSeverityValues = ['any', 'high', 'critical', 'none'] as const
2130

2231
const querySchema = z.object({
2332
page: z.coerce.number().int().min(1).default(1),
2433
pageSize: z.coerce.number().int().min(1).max(MAX_PAGE_SIZE).default(DEFAULT_PAGE_SIZE),
2534
ecosystem: z.string().trim().optional(),
26-
lifecycle: z.enum(lifecycleValues).optional(),
35+
lifecycle: z.enum(LIFECYCLE_VALUES).optional(),
2736
name: z.string().trim().optional(),
2837
purl: purlFilterSchema,
2938
status: z.enum(STEWARDSHIP_STATUS_VALUES).optional(),
30-
healthBand: z.enum(healthBandValues).optional(),
39+
healthBand: z.enum(HEALTH_BAND_VALUES).optional(),
3140
vulnSeverity: z.enum(vulnSeverityValues).optional(),
3241
busFactor1Only: booleanQueryParam,
3342
staleOnly: booleanQueryParam,
@@ -84,9 +93,16 @@ export async function listPackages(req: Request, res: Response): Promise<void> {
8493
purl: r.purl,
8594
name: r.name,
8695
ecosystem: r.ecosystem,
87-
health: r.scorecardScore != null ? Math.round(Number(r.scorecardScore) * 10) : null,
88-
impact: r.criticalityScore != null ? Math.round(Number(r.criticalityScore) * 100) : null,
89-
lifecycle: null,
96+
health: {
97+
score: r.healthScore ?? (r.scorecardScore != null ? Math.round(r.scorecardScore * 10) : null),
98+
label:
99+
r.healthLabel != null && HEALTH_BAND_SET.has(r.healthLabel)
100+
? r.healthLabel
101+
: computeHealthBand(r.scorecardScore),
102+
},
103+
impact: r.criticalityScore != null ? Math.round(r.criticalityScore * 100) : null,
104+
lifecycle:
105+
r.lifecycleLabel != null && LIFECYCLE_SET.has(r.lifecycleLabel) ? r.lifecycleLabel : null,
90106
maintainerBusFactor: r.maintainerCount,
91107
openVulns: r.openVulns,
92108
stewardshipId: r.stewardshipId ?? null,

backend/src/api/public/v1/packages/openapi.yaml

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ components:
127127
type:
128128
- string
129129
- 'null'
130-
enum: [active, stable, declining, abandoned, null]
130+
enum: [active, stable, declining, abandoned, archived, null]
131131
health:
132132
type:
133133
- integer
@@ -180,10 +180,20 @@ components:
180180
type: string
181181
example: npm
182182
health:
183-
type:
184-
- integer
185-
- 'null'
186-
example: 18
183+
type: object
184+
required: [score, label]
185+
properties:
186+
score:
187+
type:
188+
- integer
189+
- 'null'
190+
description: Health score (0–100). Tinybird composite score when enriched, OpenSSF Scorecard × 10 otherwise. Null if neither is available.
191+
example: 18
192+
label:
193+
type: string
194+
enum: [excellent, healthy, fair, concerning, critical]
195+
description: 'Tinybird band when enriched (excellent ≥85, healthy 70–84, fair 50–69, concerning 30–49, critical <30), scorecard band otherwise.'
196+
example: critical
187197
impact:
188198
type:
189199
- integer
@@ -193,7 +203,7 @@ components:
193203
type:
194204
- string
195205
- 'null'
196-
enum: [active, stable, declining, abandoned, null]
206+
enum: [active, stable, declining, abandoned, archived, null]
197207
maintainerBusFactor:
198208
type:
199209
- integer
@@ -307,7 +317,7 @@ components:
307317
type:
308318
- string
309319
- 'null'
310-
enum: [healthy, fair, concerning, critical, null]
320+
enum: [excellent, healthy, fair, concerning, critical, null]
311321
description: Derived from Tinybird health score when available, OpenSSF Scorecard otherwise.
312322
example: concerning
313323
impact:
@@ -350,7 +360,7 @@ components:
350360
type:
351361
- string
352362
- 'null'
353-
enum: [active, stable, declining, abandoned, null]
363+
enum: [active, stable, declining, abandoned, archived, null]
354364
maintainerBusFactor:
355365
type:
356366
- integer
@@ -541,7 +551,7 @@ paths:
541551
in: query
542552
schema:
543553
type: string
544-
enum: [active, stable, declining, abandoned]
554+
enum: [active, stable, declining, abandoned, archived]
545555
- name: busFactor1Only
546556
in: query
547557
schema:
@@ -814,7 +824,9 @@ paths:
814824
name: lodash
815825
ecosystem: npm
816826
lifecycle: declining
817-
health: 18
827+
health:
828+
score: 18
829+
label: critical
818830
impact: 71
819831
openVulns:
820832
low: 0

0 commit comments

Comments
 (0)