Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add milliseconds and fractional second support to humanFriendlyDuration #25313

Merged
merged 1 commit into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading