Description
lib/analytics/teamHealth.ts exports several pure functions individually
(calculateBurnoutRisk, calculateVelocityTrends, calculateSprintProgress,
calculateTeamMetrics — see the export { ... } block at the bottom) as a
public API, in addition to the aggregate aggregateTeamData(). None of them
guard against zero-length or zero-value inputs:
-
calculateTeamHealthScore — divides by metrics.totalMembers twice, with
no zero check:
const productivity = Math.min(100, Math.round(
(metrics.activeMembers / metrics.totalMembers) * 100
));
...
const collaboration = Math.min(100, Math.round(
(metrics.combinedCurrentStreak / (metrics.totalMembers * 7)) * 100
));
If members is an empty array, metrics.totalMembers is 0, so both
lines compute 0/0 = NaN (or x/0 = Infinity if combinedCurrentStreak
somehow isn't also 0). overall then becomes NaN too, and every
overall >= N comparison against NaN evaluates false, so the function
silently falls through to level = 'critical' — an empty team gets
reported as having "critical" health, along with NaN fields in the
response payload.
-
calculateSprintProgress — divides by targetContributions with no zero
check:
progressPercentage: Math.round((currentContributions / targetContributions) * 100)
If every member's totalContributions is 0 (e.g. brand-new team members
with no activity yet), dailyRate is 0, so targetContributions is 0,
and progressPercentage becomes Math.round(0/0 * 100) = NaN.
The current app/api/enterprise/route.ts caller happens to always supply
non-empty, non-zero mock data, so this isn't visibly broken in the running
demo today — but these are exported, individually-importable functions
with no input guards, and the very next real (non-mock) integration that
wires actual GitHub data through this module (e.g. a newly-onboarded team
with 0 contributions so far) will hit this exact NaN/Infinity path and
render broken numbers straight into the enterprise dashboard.
Steps to Reproduce
-
Guard at the aggregateTeamData() entry point instead (reject empty
members arrays before calling the sub-functions)
→ Rejected: the sub-functions are individually exported and imported
elsewhere as a public module contract (see the export { ... } block
at the bottom of the file) — a caller of calculateTeamHealthScore or
calculateSprintProgress directly would still hit the bug. Fixing at
the source functions covers both the aggregate and direct-import
paths.
-
Throw an error on empty/zero input instead of returning 0
→ Rejected: this is analytics/reporting code feeding a dashboard, not
a place where a thrown error would be handled gracefully by the
caller (app/api/enterprise/route.ts has no try/catch around this
call). A defined fallback value (0, meaning "no data yet") is more
appropriate and consistent with how the rest of the file already
handles the "no members" case in calculateBurnoutRisk.
Expected Behavior
Add explicit zero-guards to both functions, matching the pattern the file
already uses elsewhere (e.g. calculateTeamMetrics already guards its own
average calculation implicitly via .length checks in some branches, and
calculateBurnoutRisk already does members.length > 0 ? ... : 0 for
avgContributions).
calculateTeamHealthScore:
const productivity =
metrics.totalMembers > 0
? Math.min(100, Math.round((metrics.activeMembers / metrics.totalMembers) * 100))
: 0;
const collaboration =
metrics.totalMembers > 0
? Math.min(
100,
Math.round((metrics.combinedCurrentStreak / (metrics.totalMembers * 7)) * 100)
)
: 0;
calculateSprintProgress:
progressPercentage:
targetContributions > 0
? Math.round((currentContributions / targetContributions) * 100)
: 0,
Screenshots / Logs
No response
GitHub Username (If applicable)
No response
Environment
Chrome
Description
lib/analytics/teamHealth.ts exports several pure functions individually
(calculateBurnoutRisk, calculateVelocityTrends, calculateSprintProgress,
calculateTeamMetrics — see the
export { ... }block at the bottom) as apublic API, in addition to the aggregate aggregateTeamData(). None of them
guard against zero-length or zero-value inputs:
calculateTeamHealthScore — divides by metrics.totalMembers twice, with
no zero check:
const productivity = Math.min(100, Math.round(
(metrics.activeMembers / metrics.totalMembers) * 100
));
...
const collaboration = Math.min(100, Math.round(
(metrics.combinedCurrentStreak / (metrics.totalMembers * 7)) * 100
));
If
membersis an empty array,metrics.totalMembersis 0, so bothlines compute 0/0 = NaN (or x/0 = Infinity if combinedCurrentStreak
somehow isn't also 0).
overallthen becomes NaN too, and everyoverall >= Ncomparison against NaN evaluates false, so the functionsilently falls through to
level = 'critical'— an empty team getsreported as having "critical" health, along with NaN fields in the
response payload.
calculateSprintProgress — divides by targetContributions with no zero
check:
progressPercentage: Math.round((currentContributions / targetContributions) * 100)
If every member's totalContributions is 0 (e.g. brand-new team members
with no activity yet), dailyRate is 0, so targetContributions is 0,
and progressPercentage becomes Math.round(0/0 * 100) = NaN.
The current app/api/enterprise/route.ts caller happens to always supply
non-empty, non-zero mock data, so this isn't visibly broken in the running
demo today — but these are exported, individually-importable functions
with no input guards, and the very next real (non-mock) integration that
wires actual GitHub data through this module (e.g. a newly-onboarded team
with 0 contributions so far) will hit this exact NaN/Infinity path and
render broken numbers straight into the enterprise dashboard.
Steps to Reproduce
Guard at the aggregateTeamData() entry point instead (reject empty
members arrays before calling the sub-functions)
→ Rejected: the sub-functions are individually exported and imported
elsewhere as a public module contract (see the
export { ... }blockat the bottom of the file) — a caller of calculateTeamHealthScore or
calculateSprintProgress directly would still hit the bug. Fixing at
the source functions covers both the aggregate and direct-import
paths.
Throw an error on empty/zero input instead of returning 0
→ Rejected: this is analytics/reporting code feeding a dashboard, not
a place where a thrown error would be handled gracefully by the
caller (app/api/enterprise/route.ts has no try/catch around this
call). A defined fallback value (0, meaning "no data yet") is more
appropriate and consistent with how the rest of the file already
handles the "no members" case in calculateBurnoutRisk.
Expected Behavior
Add explicit zero-guards to both functions, matching the pattern the file
already uses elsewhere (e.g. calculateTeamMetrics already guards its own
average calculation implicitly via .length checks in some branches, and
calculateBurnoutRisk already does
members.length > 0 ? ... : 0foravgContributions).
calculateTeamHealthScore:
const productivity =
metrics.totalMembers > 0
? Math.min(100, Math.round((metrics.activeMembers / metrics.totalMembers) * 100))
: 0;
const collaboration =
metrics.totalMembers > 0
? Math.min(
100,
Math.round((metrics.combinedCurrentStreak / (metrics.totalMembers * 7)) * 100)
)
: 0;
calculateSprintProgress:
progressPercentage:
targetContributions > 0
? Math.round((currentContributions / targetContributions) * 100)
: 0,
Screenshots / Logs
No response
GitHub Username (If applicable)
No response
Environment
Chrome