sparkline() emits literal "undefined" for fractional value ranges (tick index can exceed array bounds)
- 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
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