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
5 changes: 5 additions & 0 deletions .changeset/twelve-beers-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---

feat: Include displayed timestamp in default order by
48 changes: 38 additions & 10 deletions packages/app/src/DBSearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -532,28 +532,44 @@ function useSearchedConfigToChartConfig({

function optimizeDefaultOrderBy(
timestampExpr: string,
displayedTimestampExpr: string | undefined,
sortingKey: string | undefined,
) {
const defaultModifier = 'DESC';
const fallbackOrderByItems = [
getFirstTimestampValueExpression(timestampExpr ?? ''),
defaultModifier,
];
const fallbackOrderBy = fallbackOrderByItems.join(' ');
const firstTimestampValueExpression =
getFirstTimestampValueExpression(timestampExpr ?? '') ?? '';
const defaultOrderByItems = [firstTimestampValueExpression];
const trimmedDisplayedTimestampExpr = displayedTimestampExpr?.trim();

if (
trimmedDisplayedTimestampExpr &&
trimmedDisplayedTimestampExpr !== firstTimestampValueExpression
) {
defaultOrderByItems.push(trimmedDisplayedTimestampExpr);
}

const fallbackOrderBy =
defaultOrderByItems.length > 1
? `(${defaultOrderByItems.join(', ')}) ${defaultModifier}`
: `${defaultOrderByItems[0]} ${defaultModifier}`;

if (!sortingKey) return fallbackOrderBy;

const orderByArr = [];
const sortKeys = splitAndTrimWithBracket(sortingKey);
for (let i = 0; i < sortKeys.length; i++) {
const sortKey = sortKeys[i];
if (sortKey.includes('toStartOf') && sortKey.includes(timestampExpr)) {
if (
sortKey.includes('toStartOf') &&
sortKey.includes(firstTimestampValueExpression)
) {
orderByArr.push(sortKey);
} else if (
sortKey === timestampExpr ||
sortKey === firstTimestampValueExpression ||
(sortKey.startsWith('toUnixTimestamp') &&
sortKey.includes(timestampExpr)) ||
(sortKey.startsWith('toDateTime') && sortKey.includes(timestampExpr))
sortKey.includes(firstTimestampValueExpression)) ||
(sortKey.startsWith('toDateTime') &&
sortKey.includes(firstTimestampValueExpression))
) {
if (orderByArr.length === 0) {
// fallback if the first sort key is the timestamp sort key
Expand All @@ -562,6 +578,8 @@ function optimizeDefaultOrderBy(
orderByArr.push(sortKey);
break;
}
} else if (sortKey === trimmedDisplayedTimestampExpr) {
orderByArr.push(sortKey);
}
}

Expand All @@ -570,7 +588,16 @@ function optimizeDefaultOrderBy(
return fallbackOrderBy;
}

return `(${orderByArr.join(', ')}) ${defaultModifier}`;
if (
trimmedDisplayedTimestampExpr &&
!orderByArr.includes(trimmedDisplayedTimestampExpr)
) {
orderByArr.push(trimmedDisplayedTimestampExpr);
}

return orderByArr.length > 1
? `(${orderByArr.join(', ')}) ${defaultModifier}`
: `${orderByArr[0]} ${defaultModifier}`;
}

export function useDefaultOrderBy(sourceID: string | undefined | null) {
Expand All @@ -582,6 +609,7 @@ export function useDefaultOrderBy(sourceID: string | undefined | null) {
() =>
optimizeDefaultOrderBy(
source?.timestampValueExpression ?? '',
source?.displayedTimestampValueExpression,
tableMetadata?.sorting_key,
),
[source, tableMetadata],
Expand Down
113 changes: 94 additions & 19 deletions packages/app/src/__tests__/DBSearchPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,82 +17,157 @@ describe('useDefaultOrderBy', () => {

describe('optimizeOrderBy function', () => {
describe('should handle', () => {
const mockSource = {
timestampValueExpression: 'Timestamp',
};

const testCases = [
{
input: undefined,
sortingKey: undefined,
expected: 'Timestamp DESC',
},
{
input: '',
sortingKey: '',
expected: 'Timestamp DESC',
},
{
// Traces Table
input: 'ServiceName, SpanName, toDateTime(Timestamp)',
sortingKey: 'ServiceName, SpanName, toDateTime(Timestamp)',
expected: 'Timestamp DESC',
},
{
// Optimized Traces Table
input:
sortingKey:
'toStartOfHour(Timestamp), ServiceName, SpanName, toDateTime(Timestamp)',
expected: '(toStartOfHour(Timestamp), toDateTime(Timestamp)) DESC',
},
{
// Unsupported for now as it's not a great sort key, want to just
// use default behavior for this
input: 'toDateTime(Timestamp), ServiceName, SpanName, Timestamp',
sortingKey: 'toDateTime(Timestamp), ServiceName, SpanName, Timestamp',
expected: 'Timestamp DESC',
},
{
// Unsupported prefix sort key
input: 'toDateTime(Timestamp), ServiceName, SpanName',
sortingKey: 'toDateTime(Timestamp), ServiceName, SpanName',
expected: 'Timestamp DESC',
},
{
// Inverted sort key order, we should not try to optimize this
input:
sortingKey:
'ServiceName, toDateTime(Timestamp), SeverityText, toStartOfHour(Timestamp)',
expected: 'Timestamp DESC',
},
{
input: 'toStartOfHour(Timestamp), other_column, Timestamp',
sortingKey: 'toStartOfHour(Timestamp), other_column, Timestamp',
expected: '(toStartOfHour(Timestamp), Timestamp) DESC',
},
{
input: 'Timestamp, other_column',
sortingKey: 'Timestamp, other_column',
expected: 'Timestamp DESC',
},
{
input: 'user_id, toStartOfHour(Timestamp), status, Timestamp',
sortingKey: 'user_id, toStartOfHour(Timestamp), status, Timestamp',
expected: '(toStartOfHour(Timestamp), Timestamp) DESC',
},
{
input:
sortingKey:
'toStartOfMinute(Timestamp), user_id, status, toUnixTimestamp(Timestamp)',
expected:
'(toStartOfMinute(Timestamp), toUnixTimestamp(Timestamp)) DESC',
},
{
// test variation of toUnixTimestamp
input:
sortingKey:
'toStartOfMinute(Timestamp), user_id, status, toUnixTimestamp64Nano(Timestamp)',
expected:
'(toStartOfMinute(Timestamp), toUnixTimestamp64Nano(Timestamp)) DESC',
},
{
input:
sortingKey:
'toUnixTimestamp(toStartOfMinute(Timestamp)), user_id, status, Timestamp',
expected:
'(toUnixTimestamp(toStartOfMinute(Timestamp)), Timestamp) DESC',
},
{
sortingKey: 'toStartOfMinute(Timestamp), user_id, status, Timestamp',
timestampValueExpression: 'Timestamp, toStartOfMinute(Timestamp)',
expected: '(toStartOfMinute(Timestamp), Timestamp) DESC',
},
{
sortingKey: 'Timestamp',
displayedTimestampValueExpression: 'Timestamp64',
expected: '(Timestamp, Timestamp64) DESC',
},
{
sortingKey: 'Timestamp',
displayedTimestampValueExpression: 'Timestamp64 ',
expected: '(Timestamp, Timestamp64) DESC',
},
{
sortingKey: 'Timestamp',
displayedTimestampValueExpression: 'Timestamp',
expected: 'Timestamp DESC',
},
{
sortingKey: 'Timestamp',
displayedTimestampValueExpression: '',
expected: 'Timestamp DESC',
},
{
sortingKey: 'Timestamp, ServiceName, Timestamp64',
displayedTimestampValueExpression: 'Timestamp64',
expected: '(Timestamp, Timestamp64) DESC',
},
{
sortingKey:
'toStartOfMinute(Timestamp), Timestamp, ServiceName, Timestamp64',
displayedTimestampValueExpression: 'Timestamp64',
expected: '(toStartOfMinute(Timestamp), Timestamp, Timestamp64) DESC',
},
{
sortingKey:
'toStartOfMinute(Timestamp), Timestamp64, ServiceName, Timestamp',
displayedTimestampValueExpression: 'Timestamp64',
expected: '(toStartOfMinute(Timestamp), Timestamp64, Timestamp) DESC',
},
{
sortingKey: 'SomeOtherTimeColumn',
displayedTimestampValueExpression: 'Timestamp64',
expected: '(Timestamp, Timestamp64) DESC',
},
{
sortingKey: '',
displayedTimestampValueExpression: 'Timestamp64',
expected: '(Timestamp, Timestamp64) DESC',
},
{
sortingKey: 'ServiceName, TimestampTime, Timestamp',
timestampValueExpression: 'TimestampTime, Timestamp',
displayedTimestampValueExpression: 'Timestamp',
expected: '(TimestampTime, Timestamp) DESC',
},
{
sortingKey: 'ServiceName, TimestampTime, Timestamp',
timestampValueExpression: 'Timestamp, TimestampTime',
displayedTimestampValueExpression: 'Timestamp',
expected: 'Timestamp DESC',
},
{
sortingKey: '',
timestampValueExpression: 'Timestamp, TimestampTime',
displayedTimestampValueExpression: '',
expected: 'Timestamp DESC',
},
];
for (const testCase of testCases) {
it(`${testCase.input}`, () => {
const mockTableMetadata = { sorting_key: testCase.input };
it(`${testCase.sortingKey}`, () => {
const mockSource = {
timestampValueExpression:
testCase.timestampValueExpression || 'Timestamp',
displayedTimestampValueExpression:
testCase.displayedTimestampValueExpression,
};

const mockTableMetadata = {
sorting_key: testCase.sortingKey,
};

jest.spyOn(sourceModule, 'useSource').mockReturnValue({
data: mockSource,
Expand Down
17 changes: 16 additions & 1 deletion packages/app/src/components/SourceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ export function LogTableModelForm({ control, watch }: TableModelProps) {
</FormRow>
<FormRow
label={'Displayed Timestamp Column'}
helpText="This DateTime column is used to display search results."
helpText="This DateTime column is used to display and order search results."
>
<SQLInlineEditorControlled
tableConnection={{
Expand Down Expand Up @@ -631,6 +631,21 @@ export function TraceTableModelForm({ control, watch }: TableModelProps) {
placeholder="SpanName"
/>
</FormRow>
<FormRow
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field was already being inferred for the default OTEL trace table, now we allow the user to modify it if needed.

label={'Displayed Timestamp Column'}
helpText="This DateTime column is used to display and order search results."
>
<SQLInlineEditorControlled
tableConnection={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="displayedTimestampValueExpression"
disableKeywordAutocomplete
/>
</FormRow>
</Stack>
);
}
Expand Down
Loading