heroku / heroku/cli

sparkline() emits literal "undefined" for fractional value ranges (tick index can exceed array bounds)

Open Beginner friendly
#3,885 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
889
Forks
236
Avg merge
2d 10h
Merged PRs (30d)
33

Description

## Summary

`sparkline()` indexes the 8-element `ticks` array with a value that can reach **8**, yielding `undefined` for the peak data point. The output then contains the literal string `"undefined"` wherever the maximum (or values near it) lands. This is reachable from `heroku dashboard` metrics, which feed fractional (float) values into the function.

## Location

- File: [`src/lib/utils/sparkline.ts`](https://github.com/heroku/cli/blob/5118f40a868e7c07604e2986efc68ea45e9feaee/src/lib/utils/sparkline.ts)
- Function: `sparkline` (lines 27–38)

```ts
const ticks = ['▁','▂','▃','▄','▅','▆','▇','█'] // indices 0..7

const f = Math.floor(lshift(max - min, 8) / (ticks.length - 1)) // lshift(n,8) = n*256
...
const value = ticks[Math.floor(lshift(validValue - min, 8) / f)] // can be ticks[8]
```

## Problem

The index math is not clamped. For `range = max - min`, `f = Math.floor(range * 256 / 7)`; the peak value maps to `Math.floor(range * 256 / f)`, which exceeds 7 whenever `range * 256` is a small multiple of 8 but not of 7.

Concrete failing ranges (`max - min`):

| range | range*256 | f | max index | result |
|---|---|---|---|---|
| 0.03125 (=1/32) | 8 | 1 | 8 | `undefined` |
| 0.0625 | 16 | 2 | 8 | `undefined` |
| 0.125 | 32 | 4 | 8 | `undefined` |
| 0.1875 | 48 | 6 | 8 | `undefined` |

With integer inputs this never happens (`range*256` is a multiple of 256), which is why the existing unit tests pass — they only exercise integers. But `src/commands/dashboard.ts` builds sparkline points by summing router-latency metric samples (`points[j] = (points[j] || 0) + element`), and latency values are fractional milliseconds, so float ranges occur in practice.

## Trigger / Reproduction

Static analysis finding — behavior derived from source at `main` (`5118f40a`); not confirmed by execution:

```ts
sparkline([0, 0.03125])
// → '▁' + ticks[Math.floor(256/1)] === '▁undefined'
```

and via the CLI: `heroku dashboard --app ` where the summed per-hour latency points happen to span one of the failing ranges — the rendered dashboard line shows `undefined` characters instead of blocks.

## Expected Behavior

The tick index should be clamped to the last bucket, e.g.

```ts
const idx = Math.min(7, Math.floor(lshift(validValue - min, 8) / f))
```

so every valid value renders as a block character.

## Actual Behavior

Out-of-range indices produce `undefined`, which stringifies into the returned sparkline.

## Impact

Corrupted/ugly output in `heroku dashboard` for ordinary float-valued metrics; any other consumer of the exported `sparkline()` utility is affected identically. Purely cosmetic but user-visible and trivially reproducible once the value distribution matches.

## Suggested Direction

Clamp the computed index to `ticks.length - 1` (and add float-range unit tests such as `[0, 1/32]`, `[0, 0.0625]`). Alternatively compute the bucket as `Math.round((v - min) / range * 7)` with an explicit guard for `range === 0`.

Contributor guide

Open the contributing guide

Research direction

Start in src/lib/utils/sparkline.ts at the sparkline function and inspect the tick-index calculation, then review the existing unit tests that currently cover integer inputs. Add coverage for the fractional ranges described in the issue and verify that every valid value renders a block character without literal "undefined" output; dashboard usage is shown in src/commands/dashboard.ts.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
cli
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
88/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.