Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | 1x 1x 1x 1x 1x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 1x 1x 1x 9x 9x 9x 9x 9x 9x 8x 8x 8x 9x 4x 4x 1x 1x 1x 3x 3x 9x 2x 2x 2x 2x 2x 9x 2x 2x 2x 2x 2x 2x 9x 9x 9x 9x 9x 9x | import { onSearchEvent, SearchEvent, type SearchOutcomeDetail } from '@/events/searchEvents';
import { trackEvent } from '@/helpers/analytics';
import { useEffect } from 'react';
/**
* PostHog properties describing how a search run turned out, derived from a
* terminal search event's {@link SearchOutcomeDetail}.
* @param outcome - The terminal event's outcome detail.
* @param searchTerm - The term the search was started with.
* @returns Non-PII event properties, ready for {@link trackEvent}.
* @example
* ```ts
* outcomeParams({ count: 3, durationMs: 1240, suppliersQueried: 8, suppliersCompleted: 2 }, "acetone");
* // => { search_term: "acetone", result_count: 3, duration_ms: 1240,
* // suppliers_queried: 8, suppliers_completed: 2 }
* ```
* @source
*/
function outcomeParams(
outcome: SearchOutcomeDetail,
searchTerm: string,
): Record<string, string | number> {
return {
search_term: searchTerm,
result_count: outcome.count,
duration_ms: outcome.durationMs,
suppliers_queried: outcome.suppliersQueried,
suppliers_completed: outcome.suppliersCompleted,
};
}
/**
* 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 `useBadgeController` hook.
*
* - `SearchEvent.STARTED` → `search_query` (with the search term)
* - `SearchEvent.COMPLETED` → `search_results`, or `search_terminated` when the
* stream only drained because the search was aborted
* - `SearchEvent.ABORTED` / `SearchEvent.FAILED` → `search_terminated`
*
* A search that ended early reports `search_terminated` *instead of*
* `search_results`, so the duration and result-count distributions on
* `search_results` describe complete runs only. Mount once, near the app root.
* @returns Nothing.
* @example
* ```tsx
* function App() {
* useSearchAnalytics();
* // ...
* }
* ```
* @source
*/
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();
};
}, []);
}
|