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/thirty-students-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---

Allow manually constructed /trace URLs to land in the existing search experience with the trace viewer opened from URL state. This keeps trace deep links user-friendly while reusing the search page for source selection, not-found handling, and trace inspection.
41 changes: 41 additions & 0 deletions packages/app/pages/trace/[traceId].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { Center, Text } from '@mantine/core';

import { withAppNav } from '@/layout';
import { buildTraceRedirectUrl } from '@/utils/directTrace';

export function TraceRedirectPage() {
const { isReady, query, replace } = useRouter();
const traceIdParam = Array.isArray(query.traceId)
? query.traceId[0]
: query.traceId;

useEffect(() => {
if (!isReady) return;

if (!traceIdParam) {
replace('/search');
return;
}

replace(
buildTraceRedirectUrl({
traceId: traceIdParam,
search: window.location.search,
}),
);
}, [isReady, replace, traceIdParam]);

return (
<Center h="100vh">
<Text size="sm" c="dimmed">
Redirecting to search...
</Text>
</Center>
);
}

TraceRedirectPage.getLayout = withAppNav;

export default TraceRedirectPage;
157 changes: 152 additions & 5 deletions packages/app/src/DBSearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,18 @@ import { SQLPreview } from './components/ChartSQLPreview';
import DBSqlRowTableWithSideBar from './components/DBSqlRowTableWithSidebar';
import PatternTable from './components/PatternTable';
import { DBSearchHeatmapChart } from './components/Search/DBSearchHeatmapChart';
import DirectTraceSidePanel from './components/Search/DirectTraceSidePanel';
import SourceSchemaPreview from './components/SourceSchemaPreview';
import {
getRelativeTimeOptionLabel,
LIVE_TAIL_DURATION_MS,
} from './components/TimePicker/utils';
import { useTableMetadata } from './hooks/useMetadata';
import { useSqlSuggestions } from './hooks/useSqlSuggestions';
import {
buildDirectTraceWhereClause,
getDefaultDirectTraceDateRange,
} from './utils/directTrace';
import {
parseAsJsonEncoded,
parseAsSortingStateString,
Expand Down Expand Up @@ -804,14 +809,18 @@ const queryStateMap = {
orderBy: parseAsStringEncoded,
};

function DBSearchPage() {
export function DBSearchPage() {
const brandName = useBrandDisplayName();
// Next router is laggy behind window.location, which causes race
// conditions with useQueryStates, so we'll parse it directly
const paths = window.location.pathname.split('/');
const savedSearchId = paths.length === 3 ? paths[2] : null;

const [searchedConfig, setSearchedConfig] = useQueryStates(queryStateMap);
const [directTraceId, setDirectTraceId] = useQueryState(
'traceId',
parseAsStringEncoded,
);

const { data: savedSearch } = useSavedSearch(
{ id: `${savedSearchId}` },
Expand All @@ -829,6 +838,12 @@ function DBSearchPage() {
id: searchedConfig.source,
kinds: [SourceKind.Log, SourceKind.Trace],
});
const directTraceSource =
searchedSource?.kind === SourceKind.Trace ? searchedSource : undefined;
const chartSourceId =
directTraceId != null && !directTraceSource
? ''
: (searchedConfig.source ?? '');

const [analysisMode, setAnalysisMode] = useQueryState(
'mode',
Expand Down Expand Up @@ -886,7 +901,9 @@ function DBSearchPage() {
where: searchedConfig.where || '',
whereLanguage:
searchedConfig.whereLanguage ?? getStoredLanguage() ?? 'lucene',
source: searchedConfig.source || (savedSearchId ? '' : defaultSourceId),
source:
searchedConfig.source ||
(savedSearchId || directTraceId ? '' : defaultSourceId),
filters: searchedConfig.filters ?? [],
orderBy: searchedConfig.orderBy ?? '',
},
Expand Down Expand Up @@ -985,6 +1002,10 @@ function DBSearchPage() {
return;
}

if (savedSearchId == null && directTraceId != null && !source) {
return;
}

// Landed on a new search - ensure we have a source selected
if (savedSearchId == null && defaultSourceId && isSearchConfigEmpty) {
setSearchedConfig({
Expand All @@ -1003,6 +1024,7 @@ function DBSearchPage() {
setSearchedConfig,
savedSearchId,
defaultSourceId,
directTraceId,
sources,
]);

Expand Down Expand Up @@ -1125,9 +1147,28 @@ function DBSearchPage() {
const [saveSearchModalState, setSaveSearchModalState] = useState<
'create' | 'update' | undefined
>(undefined);
const chartSearchConfig = useMemo(
() => ({
select: searchedConfig.select ?? '',
source: chartSourceId,
where: searchedConfig.where ?? '',
whereLanguage:
searchedConfig.whereLanguage ?? getStoredLanguage() ?? 'lucene',
filters: searchedConfig.filters ?? [],
orderBy: searchedConfig.orderBy ?? '',
}),
[
chartSourceId,
searchedConfig.filters,
searchedConfig.orderBy,
searchedConfig.select,
searchedConfig.where,
searchedConfig.whereLanguage,
],
);

const { data: chartConfig, isLoading: isChartConfigLoading } =
useSearchedConfigToChartConfig(searchedConfig, defaultSearchConfig);
useSearchedConfigToChartConfig(chartSearchConfig, defaultSearchConfig);

// query error handling
const { hasQueryError, queryError } = useMemo(() => {
Expand Down Expand Up @@ -1368,17 +1409,68 @@ function DBSearchPage() {

const [isAlertModalOpen, { open: openAlertModal, close: closeAlertModal }] =
useDisclosure();
const directTraceRangeAppliedRef = useRef<string | null>(null);
const directTraceFilterAppliedRef = useRef<string | null>(null);

useEffect(() => {
if (!isReady || !directTraceId) {
directTraceRangeAppliedRef.current = null;
return;
}

const searchParams = new URLSearchParams(window.location.search);
if (searchParams.has('from') && searchParams.has('to')) {
return;
}

if (directTraceRangeAppliedRef.current === directTraceId) {
return;
}

directTraceRangeAppliedRef.current = directTraceId;
setIsLive(false);
const [start, end] = getDefaultDirectTraceDateRange();
onTimeRangeSelect(start, end, null);
}, [directTraceId, isReady, onTimeRangeSelect, setIsLive]);

useEffect(() => {
if (!directTraceId || !directTraceSource) {
directTraceFilterAppliedRef.current = null;
return;
}

const nextKey = `${directTraceSource.id}:${directTraceId}`;
if (directTraceFilterAppliedRef.current === nextKey) {
return;
}

directTraceFilterAppliedRef.current = nextKey;
setIsLive(false);
setSearchedConfig({
source: directTraceSource.id,
where: buildDirectTraceWhereClause(
directTraceSource.traceIdExpression,
directTraceId,
),
whereLanguage: 'sql',
filters: [],
});
}, [directTraceId, directTraceSource, setIsLive, setSearchedConfig]);

// Add this effect to trigger initial search when component mounts
useEffect(() => {
if (isReady && queryReady && !isChartConfigLoading) {
// Only trigger if we haven't searched yet (no time range in URL)
const searchParams = new URLSearchParams(window.location.search);
if (!searchParams.has('from') && !searchParams.has('to')) {
if (
directTraceId == null &&
!searchParams.has('from') &&
!searchParams.has('to')
) {
onSearch('Live Tail');
}
}
}, [isReady, queryReady, isChartConfigLoading, onSearch]);
}, [directTraceId, isReady, queryReady, isChartConfigLoading, onSearch]);

const { data: aliasMap } = useAliasMapFromChartConfig(dbSqlRowTableConfig);

Expand Down Expand Up @@ -1546,6 +1638,52 @@ function DBSearchPage() {
},
[setIsLive, setInterval, onTimeRangeSelect],
);
const directTraceFocusDate = useMemo(
() =>
new Date(
(searchedTimeRange[0].getTime() + searchedTimeRange[1].getTime()) / 2,
),
[searchedTimeRange],
);

const onDirectTraceSourceChange = useCallback(
(sourceId: string | null) => {
setIsLive(false);
if (sourceId == null) {
directTraceFilterAppliedRef.current = null;
setSearchedConfig({
source: null,
where: '',
whereLanguage: getStoredLanguage() ?? 'lucene',
filters: [],
});
return;
}

const nextSource = sources?.find(
(source): source is Extract<TSource, { kind: SourceKind.Trace }> =>
source.id === sourceId && isTraceSource(source),
);
if (!nextSource || !directTraceId) {
return;
}

setSearchedConfig({
source: nextSource.id,
where: buildDirectTraceWhereClause(
nextSource.traceIdExpression,
directTraceId,
),
whereLanguage: 'sql',
filters: [],
});
},
[directTraceId, setIsLive, setSearchedConfig, sources],
);

const closeDirectTraceSidePanel = useCallback(() => {
setDirectTraceId(null);
}, [setDirectTraceId]);

const clearSaveSearchModalState = useCallback(
() => setSaveSearchModalState(undefined),
Expand Down Expand Up @@ -1841,6 +1979,15 @@ function DBSearchPage() {
savedSearchId={savedSearchId}
/>
)}
<DirectTraceSidePanel
opened={directTraceId != null}
traceId={directTraceId ?? ''}
traceSourceId={directTraceSource?.id ?? searchedConfig.source ?? null}
dateRange={searchedTimeRange}
focusDate={directTraceFocusDate}
onClose={closeDirectTraceSidePanel}
onSourceChange={onDirectTraceSourceChange}
/>
<Flex
direction="column"
style={{ overflow: 'hidden', height: '100%' }}
Expand Down
Loading
Loading