Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions packages/app/src/__tests__/DBSearchPageQueryKey.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ jest.mock('@/hooks/useChartConfig', () => ({

jest.mock('@/source', () => ({
useSource: () => ({ data: null, isLoading: false }),
useResolvedNumberFormat: () => undefined,
}));

jest.mock('@/ChartUtils', () => ({
Expand Down
199 changes: 175 additions & 24 deletions packages/app/src/__tests__/source.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,182 @@
import { SourceKind, TTraceSource } from '@hyperdx/common-utils/dist/types';
import {
SourceKind,
TLogSource,
TTraceSource,
} from '@hyperdx/common-utils/dist/types';

import { getEventBody } from '../source';
import { getEventBody, getTraceDurationNumberFormat } from '../source';

const TRACE_SOURCE: TTraceSource = {
kind: SourceKind.Trace,
from: {
databaseName: 'default',
tableName: 'otel_traces',
},
timestampValueExpression: 'Timestamp',
connection: 'test-connection',
name: 'Traces',
id: 'test-source-id',
spanNameExpression: 'SpanName',
durationExpression: 'Duration',
durationPrecision: 9,
traceIdExpression: 'TraceId',
spanIdExpression: 'SpanId',
parentSpanIdExpression: 'ParentSpanId',
spanKindExpression: 'SpanKind',
defaultTableSelectExpression: 'Timestamp, ServiceName',
} as TTraceSource;

describe('getEventBody', () => {
// Added to prevent regression back to HDX-3361
it('returns spanNameExpression for trace kind source when both bodyExpression and spanNameExpression are present', () => {
const source = {
kind: SourceKind.Trace,
from: {
databaseName: 'default',
tableName: 'otel_traces',
},
timestampValueExpression: 'Timestamp',
connection: 'test-connection',
name: 'Traces',
id: 'test-source-id',
spanNameExpression: 'SpanName',
durationExpression: 'Duration',
durationPrecision: 9,
traceIdExpression: 'TraceId',
spanIdExpression: 'SpanId',
parentSpanIdExpression: 'ParentSpanId',
spanKindExpression: 'SpanKind',
} as TTraceSource;

const result = getEventBody(source);

const result = getEventBody(TRACE_SOURCE);
expect(result).toBe('SpanName');
});
});

describe('getTraceDurationNumberFormat', () => {
it('returns undefined for non-trace sources', () => {
const logSource = {
kind: SourceKind.Log,
id: 'log-source',
} as TLogSource;
const result = getTraceDurationNumberFormat(logSource, [
{ valueExpression: 'count()' },
]);
expect(result).toBeUndefined();
});

it('returns undefined when source is undefined', () => {
const result = getTraceDurationNumberFormat(undefined, [
{ valueExpression: 'count()' },
]);
expect(result).toBeUndefined();
});

it('returns undefined when select expressions do not reference duration', () => {
const result = getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: 'count()' },
]);
expect(result).toBeUndefined();
});

it('detects raw duration expression with avg aggFn', () => {
const result = getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: 'Duration', aggFn: 'avg' },
]);
expect(result).toEqual({
output: 'duration',
factor: 1e-9,
});
});

it('detects duration expression without aggFn (raw expression)', () => {
const result = getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: 'avg(Duration)' },
]);
expect(result).toEqual({
output: 'duration',
factor: 1e-9,
});
});

it('detects duration ms expression reference', () => {
const result = getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: '(Duration)/1e6' },
]);
expect(result).toEqual({
output: 'duration',
factor: 0.001,
});
});

it('detects duration seconds expression reference', () => {
const result = getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: '(Duration)/1e9' },
]);
expect(result).toEqual({
output: 'duration',
factor: 1,
});
});

it('returns undefined for count aggFn on duration', () => {
const result = getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: 'Duration', aggFn: 'count' },
]);
expect(result).toBeUndefined();
});

it('returns undefined for count_distinct aggFn on duration', () => {
const result = getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: 'Duration', aggFn: 'count_distinct' },
]);
expect(result).toBeUndefined();
});

it('detects duration with sum aggFn', () => {
const result = getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: 'Duration', aggFn: 'sum' },
]);
expect(result).toEqual({
output: 'duration',
factor: 1e-9,
});
});

it('detects duration with quantile aggFn', () => {
const result = getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: 'Duration', aggFn: 'quantile' },
]);
expect(result).toEqual({
output: 'duration',
factor: 1e-9,
});
});

it('detects duration with min/max aggFn', () => {
expect(
getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: 'Duration', aggFn: 'min' },
]),
).toEqual({ output: 'duration', factor: 1e-9 });
expect(
getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: 'Duration', aggFn: 'max' },
]),
).toEqual({ output: 'duration', factor: 1e-9 });
});

it('detects duration with combinator aggFn like avgIf', () => {
const result = getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: 'Duration', aggFn: 'avgIf' },
]);
expect(result).toEqual({
output: 'duration',
factor: 1e-9,
});
});

it('skips non-preserving aggFn and detects preserving one in mixed selects', () => {
const result = getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: 'Duration', aggFn: 'count' },
{ valueExpression: 'Duration', aggFn: 'avg' },
]);
expect(result).toEqual({
output: 'duration',
factor: 1e-9,
});
});

it('returns undefined when only non-preserving aggFns reference duration', () => {
const result = getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: 'Duration', aggFn: 'count' },
{ valueExpression: 'Duration', aggFn: 'count_distinct' },
]);
expect(result).toBeUndefined();
});

it('returns undefined when select is empty', () => {
const result = getTraceDurationNumberFormat(TRACE_SOURCE, []);
expect(result).toBeUndefined();
});
});
106 changes: 106 additions & 0 deletions packages/app/src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { MetricsDataType, NumberFormat } from '../types';
import * as utils from '../utils';
import {
formatAttributeClause,
formatDurationMs,
formatNumber,
getAllMetricTables,
getMetricTableName,
Expand Down Expand Up @@ -357,6 +358,68 @@ describe('formatNumber', () => {
});
});

describe('duration format', () => {
it('formats seconds input as adaptive duration', () => {
const format: NumberFormat = {
output: 'duration',
factor: 1,
};
expect(formatNumber(30.41, format)).toBe('30.41s');
expect(formatNumber(0.045, format)).toBe('45ms');
expect(formatNumber(3661, format)).toBe('1.02h');
});

it('formats milliseconds input as adaptive duration', () => {
const format: NumberFormat = {
output: 'duration',
factor: 0.001,
};
expect(formatNumber(30410, format)).toBe('30.41s');
expect(formatNumber(45, format)).toBe('45ms');
});

it('formats nanoseconds input as adaptive duration', () => {
const format: NumberFormat = {
output: 'duration',
factor: 0.000000001,
};
expect(formatNumber(30410000000, format)).toBe('30.41s');
expect(formatNumber(45000000, format)).toBe('45ms');
expect(formatNumber(500, format)).toBe('0.5µs');
});

it('handles zero value', () => {
const format: NumberFormat = {
output: 'duration',
factor: 1,
};
expect(formatNumber(0, format)).toBe('0ms');
});

it('defaults factor to 1 (seconds) when not specified', () => {
const format: NumberFormat = {
output: 'duration',
};
expect(formatNumber(1.5, format)).toBe('1.5s');
});

it('formats sub-millisecond values as microseconds', () => {
const format: NumberFormat = {
output: 'duration',
factor: 1,
};
expect(formatNumber(0.0003, format)).toBe('300µs');
});

it('formats large values as hours', () => {
const format: NumberFormat = {
output: 'duration',
factor: 1,
};
expect(formatNumber(7200, format)).toBe('2h');
});
});

describe('unit handling', () => {
it('appends unit to formatted number', () => {
const format: NumberFormat = {
Expand Down Expand Up @@ -596,6 +659,49 @@ describe('formatNumber', () => {
});
});

describe('formatDurationMs', () => {
it('formats zero', () => {
expect(formatDurationMs(0)).toBe('0ms');
});

it('formats microseconds', () => {
expect(formatDurationMs(0.5)).toBe('500µs');
expect(formatDurationMs(0.003)).toBe('3µs');
expect(formatDurationMs(0.01)).toBe('10µs');
});

it('formats milliseconds', () => {
expect(formatDurationMs(1)).toBe('1ms');
expect(formatDurationMs(45)).toBe('45ms');
expect(formatDurationMs(999)).toBe('999ms');
expect(formatDurationMs(5.5)).toBe('5.5ms');
});

it('formats seconds', () => {
expect(formatDurationMs(1000)).toBe('1s');
expect(formatDurationMs(1500)).toBe('1.5s');
expect(formatDurationMs(30410)).toBe('30.41s');
});

it('formats minutes', () => {
expect(formatDurationMs(60000)).toBe('1min');
expect(formatDurationMs(90000)).toBe('1.5min');
});

it('formats hours', () => {
expect(formatDurationMs(3600000)).toBe('1h');
expect(formatDurationMs(7200000)).toBe('2h');
});

it('handles negative values', () => {
expect(formatDurationMs(-1500)).toBe('-1.5s');
});

it('handles sub-microsecond precision', () => {
expect(formatDurationMs(0.0005)).toBe('0.5µs');
});
});

describe('useLocalStorage', () => {
// Create a mock for localStorage
let localStorageMock: jest.Mocked<Storage>;
Expand Down
Loading
Loading