Skip to content
Open
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
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
201 changes: 177 additions & 24 deletions packages/app/src/__tests__/source.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,184 @@
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();
});

// --- exact match ---

it('matches when valueExpression exactly equals durationExpression', () => {
expect(
getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: 'Duration', aggFn: 'avg' },
]),
).toEqual({ output: 'duration', factor: 1e-9 });
});

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

// --- non-matching expressions ---

it('does not match expressions that only contain the duration name', () => {
expect(
getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: 'avg(Duration)' },
]),
).toBeUndefined();
});

it('does not match division expressions', () => {
expect(
getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: 'Duration/1e6' },
]),
).toBeUndefined();
expect(
getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: '(Duration)/1e6' },
]),
).toBeUndefined();
expect(
getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: 'Duration / 1e9' },
]),
).toBeUndefined();
});

it('does not match modified or similar-named expressions', () => {
expect(
getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: 'Duration * 2' },
]),
).toBeUndefined();
expect(
getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: 'LongerDuration' },
]),
).toBeUndefined();
expect(
getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: 'round(Duration / 1e6, 2)' },
]),
).toBeUndefined();
});

// --- aggFn filtering ---

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

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

it.each(['sum', 'min', 'max', 'quantile', 'avg', 'any', 'last_value'])(
'detects duration with %s aggFn',
aggFn => {
expect(
getTraceDurationNumberFormat(TRACE_SOURCE, [
{ valueExpression: 'Duration', aggFn },
]),
).toEqual({ output: 'duration', factor: 1e-9 });
},
);

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

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

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

it('returns undefined when select is empty', () => {
expect(getTraceDurationNumberFormat(TRACE_SOURCE, [])).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