Skip to content

[Bug]: teamHealth.ts's calculateTeamHealthScore and calculateSprintProgress divide by zero — NaN/Infinity for empty or zero-contribution teams #7960

Description

@ChetanSenta

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:

  1. 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.

  2. 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

  1. 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.

  2. 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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions