Skip to content

Commit

Permalink
feat: Add milliseconds and fractional second support to humanFriendly…
Browse files Browse the repository at this point in the history
…Duration (#25313)
  • Loading branch information
robbie-c authored Oct 1, 2024
1 parent b110a43 commit 38bbb90
Show file tree
Hide file tree
Showing 8 changed files with 58 additions and 31 deletions.
26 changes: 20 additions & 6 deletions frontend/src/lib/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,11 +500,25 @@ describe('lib/utils', () => {
})
})
describe('humanFriendlyDuration()', () => {
it('returns correct value for <= 60', () => {
it('returns correct value for 0 <= t < 1', () => {
expect(humanFriendlyDuration(0)).toEqual('0s')
expect(humanFriendlyDuration(0.001)).toEqual('1ms')
expect(humanFriendlyDuration(0.02)).toEqual('20ms')
expect(humanFriendlyDuration(0.3)).toEqual('300ms')
expect(humanFriendlyDuration(0.999)).toEqual('999ms')
})

it('returns correct value for 1 < t <= 60', () => {
expect(humanFriendlyDuration(60)).toEqual('1m')
expect(humanFriendlyDuration(45)).toEqual('45s')
expect(humanFriendlyDuration(44.8)).toEqual('45s')
expect(humanFriendlyDuration(45.2)).toEqual('45s')
expect(humanFriendlyDuration(45.2, { secondsFixed: 1 })).toEqual('45.2s')
expect(humanFriendlyDuration(1.23)).toEqual('1s')
expect(humanFriendlyDuration(1.23, { secondsPrecision: 3 })).toEqual('1.23s')
expect(humanFriendlyDuration(1, { secondsPrecision: 3 })).toEqual('1s')
expect(humanFriendlyDuration(1, { secondsFixed: 1 })).toEqual('1s')
expect(humanFriendlyDuration(1)).toEqual('1s')
})
it('returns correct value for 60 < t < 120', () => {
expect(humanFriendlyDuration(90)).toEqual('1m 30s')
Expand All @@ -524,13 +538,13 @@ describe('lib/utils', () => {
expect(humanFriendlyDuration(86400.12)).toEqual('1d')
})
it('truncates to specified # of units', () => {
expect(humanFriendlyDuration(3961, 2)).toEqual('1h 6m')
expect(humanFriendlyDuration(30, 2)).toEqual('30s') // no change
expect(humanFriendlyDuration(30, 0)).toEqual('') // returns no units (useless)
expect(humanFriendlyDuration(3961, { maxUnits: 2 })).toEqual('1h 6m')
expect(humanFriendlyDuration(30, { maxUnits: 2 })).toEqual('30s') // no change
expect(humanFriendlyDuration(30, { maxUnits: 0 })).toEqual('') // returns no units (useless)
})
it('returns an empty string for nullish inputs', () => {
expect(humanFriendlyDuration('', 2)).toEqual('')
expect(humanFriendlyDuration(null, 2)).toEqual('')
expect(humanFriendlyDuration('', { maxUnits: 2 })).toEqual('')
expect(humanFriendlyDuration(null, { maxUnits: 2 })).toEqual('')
})
})

Expand Down
29 changes: 26 additions & 3 deletions frontend/src/lib/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -497,13 +497,36 @@ export const humanFriendlyMilliseconds = (timestamp: number | undefined): string

return `${(timestamp / 1000).toFixed(2)}s`
}
export function humanFriendlyDuration(d: string | number | null | undefined, maxUnits?: number): string {
export function humanFriendlyDuration(
d: string | number | null | undefined,
{
maxUnits,
secondsPrecision,
secondsFixed,
}: { maxUnits?: number; secondsPrecision?: number; secondsFixed?: number } = {}
): string {
// Convert `d` (seconds) to a human-readable duration string.
// Example: `1d 10hrs 9mins 8s`
if (d === '' || d === null || d === undefined) {
if (d === '' || d === null || d === undefined || maxUnits === 0) {
return ''
}
d = Number(d)
if (d < 0) {
return `-${humanFriendlyDuration(-d)}`
}
if (d === 0) {
return `0s`
}
if (d < 1) {
return `${Math.round(d * 1000)}ms`
}
if (d < 60) {
if (secondsPrecision != null) {
return `${parseFloat(d.toPrecision(secondsPrecision))}s` // round to s.f. then throw away trailing zeroes
}
return `${parseFloat(d.toFixed(secondsFixed ?? 0))}s` // round to fixed point then throw away trailing zeroes
}

const days = Math.floor(d / 86400)
const h = Math.floor((d % 86400) / 3600)
const m = Math.floor((d % 3600) / 60)
Expand All @@ -520,7 +543,7 @@ export function humanFriendlyDuration(d: string | number | null | undefined, max
} else {
units = [hDisplay, mDisplay, sDisplay].filter(Boolean)
}
return units.slice(0, maxUnits).join(' ')
return units.slice(0, maxUnits ?? undefined).join(' ')
}

export function humanFriendlyDiff(from: dayjs.Dayjs | string, to: dayjs.Dayjs | string): string {
Expand Down
16 changes: 1 addition & 15 deletions frontend/src/queries/nodes/WebOverview/WebOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,20 +139,6 @@ const formatPercentage = (x: number, options?: { precise?: boolean }): string =>
return (x / 100).toLocaleString(undefined, { style: 'percent', maximumFractionDigits: 0 })
}

const formatSeconds = (x: number): string => {
// if over than a minute, show minutes and seconds
if (x >= 60) {
return humanFriendlyDuration(x)
}
// if over 1 second, show 3 significant figures
if (x >= 1) {
return `${x.toPrecision(3)}s`
}

// show the number of milliseconds
return `${x * 1000}ms`
}

const formatUnit = (x: number, options?: { precise?: boolean }): string => {
if (options?.precise) {
return x.toLocaleString()
Expand All @@ -170,7 +156,7 @@ const formatItem = (
} else if (kind === 'percentage') {
return formatPercentage(value, options)
} else if (kind === 'duration_s') {
return formatSeconds(value)
return humanFriendlyDuration(value, { secondsPrecision: 3 })
}
return formatUnit(value, options)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export function FunnelBarHorizontal({
{step.average_conversion_time && step.average_conversion_time >= Number.EPSILON ? (
<div className="text-muted-alt">
Average time to convert:{' '}
<b>{humanFriendlyDuration(step.average_conversion_time, 2)}</b>
<b>{humanFriendlyDuration(step.average_conversion_time, { maxUnits: 2 })}</b>
</div>
) : null}
</header>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export function StepLegend({ step, stepIndex, showTime, showPersonsModal }: Step
</LemonRow>
{showTime && (
<LemonRow icon={<IconClock />} title="Median time of conversion from previous step">
{humanFriendlyDuration(step.median_conversion_time, 3) || '–'}
{humanFriendlyDuration(step.median_conversion_time, { maxUnits: 3 }) || '–'}
</LemonRow>
)}
</>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/scenes/funnels/FunnelHistogram.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function FunnelHistogram(): JSX.Element | null {
width={width}
isDashboardItem={isInDashboardContext}
height={height}
formatXTickLabel={(v) => humanFriendlyDuration(v, 2)}
formatXTickLabel={(v) => humanFriendlyDuration(v, { maxUnits: 2 })}
/>
</div>
)
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/scenes/funnels/FunnelTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,13 @@ export function FunnelTooltip({
{stepIndex > 0 && series.median_conversion_time != null && (
<tr>
<td>Median time from previous</td>
<td>{humanFriendlyDuration(series.median_conversion_time, 3)}</td>
<td>{humanFriendlyDuration(series.median_conversion_time, { maxUnits: 3 })}</td>
</tr>
)}
{stepIndex > 0 && series.average_conversion_time != null && (
<tr>
<td>Average time from previous</td>
<td>{humanFriendlyDuration(series.average_conversion_time, 3)}</td>
<td>{humanFriendlyDuration(series.average_conversion_time, { maxUnits: 3 })}</td>
</tr>
)}
</tbody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,9 @@ export function FunnelStepsTable(): JSX.Element | null {
),
render: (_: void, breakdown: FlattenedFunnelStepByBreakdown) =>
breakdown.steps?.[step.order]?.median_conversion_time != undefined
? humanFriendlyDuration(breakdown.steps[step.order].median_conversion_time, 3)
? humanFriendlyDuration(breakdown.steps[step.order].median_conversion_time, {
maxUnits: 3,
})
: '–',
align: 'right',
width: 0,
Expand All @@ -268,7 +270,9 @@ export function FunnelStepsTable(): JSX.Element | null {
),
render: (_: void, breakdown: FlattenedFunnelStepByBreakdown) =>
breakdown.steps?.[step.order]?.average_conversion_time != undefined
? humanFriendlyDuration(breakdown.steps[step.order].average_conversion_time, 3)
? humanFriendlyDuration(breakdown.steps[step.order].average_conversion_time, {
maxUnits: 3,
})
: '–',
align: 'right',
width: 0,
Expand Down

0 comments on commit 38bbb90

Please sign in to comment.