Nothing.
function App() {
useSearchAnalytics();
// ...
}
export function useSearchAnalytics(): void {
useEffect(() => {
// Remember the most recent started query so the terminal events (which carry
// only the outcome) can attach the term to their event.
let lastQuery = '';
const unsubscribers = [
onSearchEvent(SearchEvent.STARTED, ({ query }) => {
lastQuery = query.trim();
void trackEvent('search_query', { search_term: lastQuery });
}),
onSearchEvent(SearchEvent.COMPLETED, ({ abortReason, ...outcome }) => {
const params = outcomeParams(outcome, lastQuery);
if (abortReason === undefined) {
void trackEvent('search_results', params);
return;
}
void trackEvent('search_terminated', { ...params, reason: abortReason });
}),
onSearchEvent(SearchEvent.ABORTED, ({ reason, ...outcome }) => {
void trackEvent('search_terminated', {
...outcomeParams(outcome, lastQuery),
reason: reason ?? 'unknown',
});
}),
onSearchEvent(SearchEvent.FAILED, ({ error, ...outcome }) => {
void trackEvent('search_terminated', {
...outcomeParams(outcome, lastQuery),
reason: 'error',
...(error ? { error_message: error } : {}),
});
}),
];
return () => {
for (const unsubscribe of unsubscribers) unsubscribe();
};
}, []);
}
Subscribes to the search-lifecycle event bus and forwards it to PostHog, keeping analytics out of the search hook itself (the producers stay decoupled from consumers). Mirrors the
useBadgeControllerhook.SearchEvent.STARTED→search_query(with the search term)SearchEvent.COMPLETED→search_results, orsearch_terminatedwhen the stream only drained because the search was abortedSearchEvent.ABORTED/SearchEvent.FAILED→search_terminatedA search that ended early reports
search_terminatedinstead ofsearch_results, so the duration and result-count distributions onsearch_resultsdescribe complete runs only. Mount once, near the app root.