From d55dd13c1b301f6d3117b3c5fdd42a6934a257ba Mon Sep 17 00:00:00 2001 From: bkellam Date: Wed, 19 Nov 2025 13:45:47 -0800 Subject: [PATCH] Add back posthog search_finished event --- .../search/components/searchResultsPage.tsx | 81 +++++++++++-------- .../app/[domain]/search/useStreamedSearch.ts | 36 ++++++--- packages/web/src/lib/posthogEvents.ts | 6 +- 3 files changed, 79 insertions(+), 44 deletions(-) diff --git a/packages/web/src/app/[domain]/search/components/searchResultsPage.tsx b/packages/web/src/app/[domain]/search/components/searchResultsPage.tsx index 68c85f24..4e8982cc 100644 --- a/packages/web/src/app/[domain]/search/components/searchResultsPage.tsx +++ b/packages/web/src/app/[domain]/search/components/searchResultsPage.tsx @@ -32,6 +32,7 @@ import { CodePreviewPanel } from "./codePreviewPanel"; import { FilterPanel } from "./filterPanel"; import { useFilteredMatches } from "./filterPanel/useFilterMatches"; import { SearchResultsPanel, SearchResultsPanelHandle } from "./searchResultsPanel"; +import useCaptureEvent from "@/hooks/useCaptureEvent"; interface SearchResultsPageProps { searchQuery: string; @@ -50,6 +51,7 @@ export const SearchResultsPage = ({ const { setSearchHistory } = useSearchHistory(); const domain = useDomain(); const { toast } = useToast(); + const captureEvent = useCaptureEvent(); // Encodes the number of matches to return in the search response. const _maxMatchCount = parseInt(useNonEmptyQueryParam(SearchQueryParams.matches) ?? `${defaultMaxMatchCount}`); @@ -59,7 +61,8 @@ export const SearchResultsPage = ({ error, files, repoInfo, - durationMs, + timeToSearchCompletionMs, + timeToFirstSearchResultMs, isStreaming, numMatches, isExhaustive, @@ -98,40 +101,52 @@ export const SearchResultsPage = ({ ]) }, [searchQuery, setSearchHistory]); - // @todo: capture search stats on completion. - // useEffect(() => { - // if (!searchResponse) { - // return; - // } + useEffect(() => { + if (isStreaming || !stats) { + return; + } - // const fileLanguages = searchResponse.files?.map(file => file.language) || []; + const fileLanguages = files.map(file => file.language) || []; - // captureEvent("search_finished", { - // durationMs: searchResponse.totalClientSearchDurationMs, - // fileCount: searchResponse.stats.fileCount, - // matchCount: searchResponse.stats.totalMatchCount, - // actualMatchCount: searchResponse.stats.actualMatchCount, - // filesSkipped: searchResponse.stats.filesSkipped, - // contentBytesLoaded: searchResponse.stats.contentBytesLoaded, - // indexBytesLoaded: searchResponse.stats.indexBytesLoaded, - // crashes: searchResponse.stats.crashes, - // shardFilesConsidered: searchResponse.stats.shardFilesConsidered, - // filesConsidered: searchResponse.stats.filesConsidered, - // filesLoaded: searchResponse.stats.filesLoaded, - // shardsScanned: searchResponse.stats.shardsScanned, - // shardsSkipped: searchResponse.stats.shardsSkipped, - // shardsSkippedFilter: searchResponse.stats.shardsSkippedFilter, - // ngramMatches: searchResponse.stats.ngramMatches, - // ngramLookups: searchResponse.stats.ngramLookups, - // wait: searchResponse.stats.wait, - // matchTreeConstruction: searchResponse.stats.matchTreeConstruction, - // matchTreeSearch: searchResponse.stats.matchTreeSearch, - // regexpsConsidered: searchResponse.stats.regexpsConsidered, - // flushReason: searchResponse.stats.flushReason, - // fileLanguages, - // }); - // }, [captureEvent, searchQuery, searchResponse]); + console.debug('timeToFirstSearchResultMs:', timeToFirstSearchResultMs); + console.debug('timeToSearchCompletionMs:', timeToSearchCompletionMs); + captureEvent("search_finished", { + durationMs: timeToSearchCompletionMs, + timeToSearchCompletionMs, + timeToFirstSearchResultMs, + fileCount: stats.fileCount, + matchCount: stats.totalMatchCount, + actualMatchCount: stats.actualMatchCount, + filesSkipped: stats.filesSkipped, + contentBytesLoaded: stats.contentBytesLoaded, + indexBytesLoaded: stats.indexBytesLoaded, + crashes: stats.crashes, + shardFilesConsidered: stats.shardFilesConsidered, + filesConsidered: stats.filesConsidered, + filesLoaded: stats.filesLoaded, + shardsScanned: stats.shardsScanned, + shardsSkipped: stats.shardsSkipped, + shardsSkippedFilter: stats.shardsSkippedFilter, + ngramMatches: stats.ngramMatches, + ngramLookups: stats.ngramLookups, + wait: stats.wait, + matchTreeConstruction: stats.matchTreeConstruction, + matchTreeSearch: stats.matchTreeSearch, + regexpsConsidered: stats.regexpsConsidered, + flushReason: stats.flushReason, + fileLanguages, + isSearchExhaustive: isExhaustive, + }); + }, [ + captureEvent, + files, + isStreaming, + isExhaustive, + stats, + timeToSearchCompletionMs, + timeToFirstSearchResultMs, + ]); const onLoadMoreResults = useCallback(() => { const url = createPathWithQueryParams(`/${domain}/search`, @@ -170,7 +185,7 @@ export const SearchResultsPage = ({ onLoadMoreResults={onLoadMoreResults} numMatches={numMatches} repoInfo={repoInfo} - searchDurationMs={durationMs} + searchDurationMs={timeToSearchCompletionMs} isStreaming={isStreaming} searchStats={stats} isMoreResultsButtonVisible={!isExhaustive} diff --git a/packages/web/src/app/[domain]/search/useStreamedSearch.ts b/packages/web/src/app/[domain]/search/useStreamedSearch.ts index 04514329..810cab88 100644 --- a/packages/web/src/app/[domain]/search/useStreamedSearch.ts +++ b/packages/web/src/app/[domain]/search/useStreamedSearch.ts @@ -8,7 +8,8 @@ interface CacheEntry { files: SearchResultFile[]; repoInfo: Record; numMatches: number; - durationMs: number; + timeToSearchCompletionMs: number; + timeToFirstSearchResultMs: number; timestamp: number; isExhaustive: boolean; } @@ -39,7 +40,8 @@ export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegex error: Error | null, files: SearchResultFile[], repoInfo: Record, - durationMs: number, + timeToSearchCompletionMs: number, + timeToFirstSearchResultMs: number, numMatches: number, stats?: SearchStats, }>({ @@ -48,7 +50,8 @@ export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegex error: null, files: [], repoInfo: {}, - durationMs: 0, + timeToSearchCompletionMs: 0, + timeToFirstSearchResultMs: 0, numMatches: 0, stats: undefined, }); @@ -94,7 +97,8 @@ export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegex error: null, files: cachedEntry.files, repoInfo: cachedEntry.repoInfo, - durationMs: cachedEntry.durationMs, + timeToSearchCompletionMs: cachedEntry.timeToSearchCompletionMs, + timeToFirstSearchResultMs: cachedEntry.timeToFirstSearchResultMs, numMatches: cachedEntry.numMatches, }); return; @@ -106,7 +110,8 @@ export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegex error: null, files: [], repoInfo: {}, - durationMs: 0, + timeToSearchCompletionMs: 0, + timeToFirstSearchResultMs: 0, numMatches: 0, }); @@ -138,6 +143,7 @@ export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegex const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; + let numMessagesProcessed = 0; while (true as boolean) { const { done, value } = await reader.read(); @@ -175,6 +181,7 @@ export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegex } const response: StreamedSearchResponse = JSON.parse(data); + const isFirstMessage = numMessagesProcessed === 0; switch (response.type) { case 'chunk': setState(prev => ({ @@ -191,6 +198,9 @@ export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegex }, {} as Record), }, numMatches: prev.numMatches + response.stats.actualMatchCount, + ...(isFirstMessage ? { + timeToFirstSearchResultMs: performance.now() - startTime, + } : {}), })); break; case 'final': @@ -198,13 +208,18 @@ export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegex ...prev, isExhaustive: response.isSearchExhaustive, stats: response.accumulatedStats, + ...(isFirstMessage ? { + timeToFirstSearchResultMs: performance.now() - startTime, + } : {}), })); break; } + + numMessagesProcessed++; } } - const durationMs = performance.now() - startTime; + const timeToSearchCompletionMs = performance.now() - startTime; setState(prev => { // Cache the final results after the stream has completed. searchCache.set(cacheKey, { @@ -212,12 +227,13 @@ export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegex repoInfo: prev.repoInfo, isExhaustive: prev.isExhaustive, numMatches: prev.numMatches, - durationMs, + timeToFirstSearchResultMs: prev.timeToFirstSearchResultMs, + timeToSearchCompletionMs, timestamp: Date.now(), }); return { ...prev, - durationMs, + timeToSearchCompletionMs, isStreaming: false, } }); @@ -229,11 +245,11 @@ export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegex console.error(error); Sentry.captureException(error); - const durationMs = performance.now() - startTime; + const timeToSearchCompletionMs = performance.now() - startTime; setState(prev => ({ ...prev, isStreaming: false, - durationMs, + timeToSearchCompletionMs, error: error as Error, })); } diff --git a/packages/web/src/lib/posthogEvents.ts b/packages/web/src/lib/posthogEvents.ts index 9ed40fd9..12acbc5d 100644 --- a/packages/web/src/lib/posthogEvents.ts +++ b/packages/web/src/lib/posthogEvents.ts @@ -5,7 +5,10 @@ export type PosthogEventMap = { contentBytesLoaded: number, indexBytesLoaded: number, crashes: number, + /** @deprecated: use timeToFirstSearchResultMs and timeToSearchCompletionMs instead */ durationMs: number, + timeToFirstSearchResultMs: number, + timeToSearchCompletionMs: number, fileCount: number, shardFilesConsidered: number, filesConsidered: number, @@ -23,7 +26,8 @@ export type PosthogEventMap = { matchTreeSearch: number, regexpsConsidered: number, flushReason: number, - fileLanguages: string[] + fileLanguages: string[], + isSearchExhaustive: boolean }, share_link_created: {}, ////////////////////////////////////////////////////////////////