Skip to content

Commit 57a209e

Browse files
committed
Added Search support in Trace details
1 parent 97dfa76 commit 57a209e

File tree

7 files changed

+112
-24
lines changed

7 files changed

+112
-24
lines changed

.changeset/plenty-lizards-rule.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@spotlightjs/overlay': minor
3+
---
4+
5+
Added Search support in Trace details

packages/overlay/src/integrations/sentry/components/explore/traces/TraceDetails/components/TraceTreeview.tsx

+35-2
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
import { useState } from 'react';
22
import { useParams } from 'react-router-dom';
3+
import { SearchProvider, useSearch } from '~/integrations/sentry/context/SearchContext';
4+
import useSearchInput from '~/integrations/sentry/hooks/useSearchInput';
5+
import { ReactComponent as CrossIcon } from '../../../../../../../assets/cross.svg';
36
import sentryDataCache from '../../../../../data/sentryDataCache';
7+
import { getFormattedSpanDuration } from '../../../../../utils/duration';
48
import DateTime from '../../../../DateTime';
59
import SpanDetails from '../../spans/SpanDetails';
610
import SpanTree from '../../spans/SpanTree';
7-
import { getFormattedSpanDuration } from '../../../../../utils/duration';
811

912
type TraceTreeViewProps = { traceId: string };
1013

1114
export const DEFAULT_SPAN_NODE_WIDTH = 50;
1215

13-
export default function TraceTreeview({ traceId }: TraceTreeViewProps) {
16+
function TraceTreeviewContent({ traceId }: TraceTreeViewProps) {
1417
const { spanId } = useParams();
1518

19+
const { setQuery } = useSearch();
20+
21+
const { inputValue, showReset, handleChange, handleReset } = useSearchInput(setQuery, 500);
22+
1623
const [spanNodeWidth, setSpanNodeWidth] = useState<number>(DEFAULT_SPAN_NODE_WIDTH);
1724

1825
const trace = sentryDataCache.getTraceById(traceId)!;
@@ -34,6 +41,24 @@ export default function TraceTreeview({ traceId }: TraceTreeViewProps) {
3441
</span>
3542
</div>
3643
</div>
44+
{trace.spans.size > 0 && (
45+
<div className="bg-primary-950 text-primary-50 border-primary-600 hover:border-primary-500 relative mx-6 mb-4 mt-2 flex h-auto w-auto gap-2 rounded-md border py-1 pl-4 pr-6 outline-none transition-all">
46+
<input
47+
className="text-primary-50 h-auto w-full flex-1 bg-transparent outline-none transition-all"
48+
onChange={handleChange}
49+
value={inputValue}
50+
placeholder="Search in Trace"
51+
/>
52+
{showReset ? (
53+
<CrossIcon
54+
onClick={handleReset}
55+
className="fill-primary-50 absolute right-1 top-[5px] cursor-pointer"
56+
height={20}
57+
width={20}
58+
/>
59+
) : null}
60+
</div>
61+
)}
3762
<div className="flex-1 px-2 pb-6">
3863
<SpanTree
3964
traceContext={trace}
@@ -57,3 +82,11 @@ export default function TraceTreeview({ traceId }: TraceTreeViewProps) {
5782
</>
5883
);
5984
}
85+
86+
export default function TraceTreeview(props: TraceTreeViewProps) {
87+
return (
88+
<SearchProvider>
89+
<TraceTreeviewContent {...props} />
90+
</SearchProvider>
91+
);
92+
}

packages/overlay/src/integrations/sentry/components/explore/traces/spans/SpanItem.tsx

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { useRef, useState } from 'react';
1+
import { useMemo, useRef, useState } from 'react';
22
import { Link, useParams } from 'react-router-dom';
33
import { ReactComponent as ChevronIcon } from '~/assets/chevronDown.svg';
4+
import { useSearch } from '~/integrations/sentry/context/SearchContext';
45
import classNames from '../../../../../../lib/classNames';
56
import type { Span, TraceContext } from '../../../../types';
6-
import { getSpanDurationClassName, getFormattedDuration } from '../../../../utils/duration';
7+
import { getFormattedDuration, getSpanDurationClassName } from '../../../../utils/duration';
78
import PlatformIcon from '../../../PlatformIcon';
89
import SpanResizer from '../../../SpanResizer';
910
import SpanTree from './SpanTree';
@@ -17,7 +18,6 @@ const SpanItem = ({
1718
totalTransactions = 0,
1819
spanNodeWidth,
1920
setSpanNodeWidth = () => {},
20-
query,
2121
}: {
2222
span: Span;
2323
startTimestamp: number;
@@ -27,9 +27,9 @@ const SpanItem = ({
2727
totalTransactions?: number;
2828
spanNodeWidth: number;
2929
setSpanNodeWidth?: (val: number) => void;
30-
query?: string;
3130
}) => {
3231
const { spanId } = useParams();
32+
const { query } = useSearch();
3333
const containerRef = useRef<HTMLLIElement>(null);
3434
const childrenCount = span.children ? span.children.length : 0;
3535
const [isItemCollapsed, setIsItemCollapsed] = useState(
@@ -47,9 +47,10 @@ const SpanItem = ({
4747
setSpanNodeWidth(newLeftWidth);
4848
}
4949
};
50-
const isQueried = query
51-
? span.span_id.includes(query) || span.op?.includes(query) || span.description?.includes(query)
52-
: false;
50+
const isQueried = useMemo(() => {
51+
if (!query) return false;
52+
return span.span_id.includes(query) || span.op?.includes(query) || span.description?.includes(query);
53+
}, [query, span.span_id, span.op, span.description]);
5354

5455
return (
5556
<li key={span.span_id} ref={containerRef}>
@@ -134,7 +135,6 @@ const SpanItem = ({
134135
totalTransactions={totalTransactions}
135136
spanNodeWidth={spanNodeWidth}
136137
setSpanNodeWidth={setSpanNodeWidth}
137-
query={query}
138138
/>
139139
)}
140140
</li>

packages/overlay/src/integrations/sentry/components/explore/traces/spans/SpanTree.tsx

-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ export default function SpanTree({
1111
totalTransactions,
1212
spanNodeWidth,
1313
setSpanNodeWidth,
14-
query,
1514
}: {
1615
traceContext: TraceContext;
1716
tree: Span[];
@@ -21,7 +20,6 @@ export default function SpanTree({
2120
totalTransactions?: number;
2221
spanNodeWidth: number;
2322
setSpanNodeWidth?: (val: number) => void;
24-
query?: string;
2523
}) {
2624
if (!tree || !tree.length) return null;
2725
return (
@@ -38,7 +36,6 @@ export default function SpanTree({
3836
totalDuration={totalDuration}
3937
spanNodeWidth={spanNodeWidth}
4038
setSpanNodeWidth={setSpanNodeWidth}
41-
query={query}
4239
/>
4340
);
4441
})}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { createContext, ReactNode, useContext, useState } from 'react';
2+
3+
type SearchContextType = {
4+
query: string;
5+
setQuery: (query: string) => void;
6+
};
7+
8+
const SearchContext = createContext<SearchContextType | undefined>(undefined);
9+
10+
export function SearchProvider({ children }: { children: ReactNode }) {
11+
const [query, setQuery] = useState('');
12+
13+
return <SearchContext.Provider value={{ query, setQuery }}>{children}</SearchContext.Provider>;
14+
}
15+
16+
export function useSearch() {
17+
const context = useContext(SearchContext);
18+
if (context === undefined) {
19+
throw new Error('useSearch must be used within a SearchProvider');
20+
}
21+
return context;
22+
}

packages/overlay/src/integrations/sentry/hooks/useDebounce.ts

+10-11
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,23 @@ import { useLayoutEffect, useMemo, useRef } from 'react';
88
// eslint-disable-next-line @typescript-eslint/no-explicit-any
99
export default function useDebounce<T extends (...args: any[]) => void>(callback: T, delay: number) {
1010
const callbackRef = useRef(callback);
11+
const timerRef = useRef<number | null>(null);
1112

1213
useLayoutEffect(() => {
1314
callbackRef.current = callback;
1415
}, [callback]);
1516

16-
let timer: number;
17-
18-
const debounceFunction = (func: T, delayMs: number, ...args: Parameters<T>) => {
19-
clearTimeout(timer);
20-
timer = window.setTimeout(() => {
21-
func(...args);
22-
}, delayMs);
23-
};
24-
2517
return useMemo(
2618
() =>
27-
(...args: Parameters<T>) =>
28-
debounceFunction(callbackRef.current, delay, ...args),
19+
(...args: Parameters<T>) => {
20+
if (timerRef.current !== null) {
21+
clearTimeout(timerRef.current);
22+
}
23+
timerRef.current = window.setTimeout(() => {
24+
callbackRef.current(...args);
25+
timerRef.current = null;
26+
}, delay);
27+
},
2928
[delay],
3029
);
3130
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { FormEvent, useEffect, useState } from 'react';
2+
import useDebounce from './useDebounce';
3+
4+
export default function useSearchInput(onSearch: (value: string) => void, delay = 500) {
5+
const [inputValue, setInputValue] = useState('');
6+
const [showReset, setShowReset] = useState(false);
7+
8+
const debouncedSearch = useDebounce(onSearch, delay);
9+
10+
useEffect(() => {
11+
debouncedSearch(inputValue);
12+
}, [inputValue, debouncedSearch]);
13+
14+
const handleChange = (e: FormEvent<HTMLInputElement>) => {
15+
const value = e.currentTarget.value;
16+
setInputValue(value);
17+
setShowReset(!!value);
18+
};
19+
20+
const handleReset = () => {
21+
setInputValue('');
22+
setShowReset(false);
23+
onSearch('');
24+
};
25+
26+
return {
27+
inputValue,
28+
showReset,
29+
handleChange,
30+
handleReset,
31+
};
32+
}

0 commit comments

Comments
 (0)