From d467f600941662ed91e94487e7075b5610d6b327 Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Wed, 18 Dec 2024 13:00:10 -0500 Subject: [PATCH] send click events to algolia --- .../src/components/shared/command-hits.tsx | 29 +++++++++++++++++-- .../src/components/shared/command-link.tsx | 12 ++++++-- .../src/components/shared/hits.ts | 28 ------------------ .../src/hooks/use-search-hits.ts | 6 ++++ 4 files changed, 41 insertions(+), 34 deletions(-) diff --git a/packages/ui/fern-docs-search-ui/src/components/shared/command-hits.tsx b/packages/ui/fern-docs-search-ui/src/components/shared/command-hits.tsx index 2332b3e28c..8a8bfb14a9 100644 --- a/packages/ui/fern-docs-search-ui/src/components/shared/command-hits.tsx +++ b/packages/ui/fern-docs-search-ui/src/components/shared/command-hits.tsx @@ -1,8 +1,9 @@ import { TooltipPortal } from "@radix-ui/react-tooltip"; +import type { SendEventForHits } from "instantsearch.js/es/lib/utils"; import { PropsWithChildren, ReactNode, memo } from "react"; import { Snippet } from "react-instantsearch"; -import { useSearchHits } from "../../hooks/use-search-hits"; +import { useSearchHits, useSendEvent } from "../../hooks/use-search-hits"; import { AlgoliaRecordHit } from "../../types"; import * as Command from "../cmdk"; import { PageIcon } from "../icons/page"; @@ -23,6 +24,7 @@ export const CommandSearchHits = ({ }): ReactNode => { const isQueryEmpty = Command.useCommandState((state) => state.search.trimStart().length === 0) as boolean; const items = useSearchHits(); + const sendEvent = useSendEvent(); const { filters } = useFacetFilters(); @@ -30,18 +32,28 @@ export const CommandSearchHits = ({ return false; } - return ; + return ( + + ); }; const MemoizedCommandSearchHits = memo( ({ domain, items, + sendEvent, onSelect, prefetch, }: { domain: string; items: AlgoliaRecordHit[]; + sendEvent: SendEventForHits; onSelect: (path: string) => void; prefetch?: (path: string) => Promise; }) => { @@ -51,11 +63,19 @@ const MemoizedCommandSearchHits = memo( {groups.map((group, index) => ( - {group.hits.map((hit) => ( + {group.hits.map((hit, hitIndex) => ( { + if (hit.record) { + sendEvent("click", hit.record, eventName, { + search_position: hitIndex + 1, + ...additionalData, + }); + } + }} prefetch={prefetch} domain={domain} /> @@ -71,6 +91,7 @@ function CommandHit({ hit, domain, onSelect, + sendClickEvent, prefetch, }: { hit: GroupedHit; @@ -79,6 +100,7 @@ function CommandHit({ * @param path - the path to navigate to via nextjs router */ onSelect: (path: string) => void; + sendClickEvent?: (eventName?: string | undefined, additionalData?: Record | undefined) => void; prefetch?: (path: string) => Promise; }) { if (!hit.record) { @@ -93,6 +115,7 @@ function CommandHit({ prefetch={prefetch} onSelect={onSelect} domain={domain} + sendClickEvent={sendClickEvent} > Promise; + sendClickEvent?: (eventName?: string | undefined, additionalData?: Record | undefined) => void; } ->(({ href, target, rel, onSelect, prefetch, domain, ...props }, forwardedRef) => { +>(({ href, target, rel, onSelect, prefetch, domain, sendClickEvent, ...props }, forwardedRef) => { const ref = useRef(null); const isSelected = Command.useCommandState((state) => state.value === href) as boolean; const handleSelect = useCallback(() => { @@ -50,10 +51,13 @@ export const CommandLink = forwardRef< if (!element) { return; } - const listener = () => handleSelect(); + const listener = () => { + handleSelect(); + sendClickEvent?.("onselect"); + }; element.addEventListener(Command.SELECT_EVENT, listener); return () => element.removeEventListener(Command.SELECT_EVENT, listener); - }, [handleSelect]); + }, [handleSelect, sendClickEvent]); const Comp = props.asChild ? Slot : "a"; // Note: `onSelect` is purposely not passed in here because these command items must be rendered as @@ -72,6 +76,8 @@ export const CommandLink = forwardRef< } }} onClick={(e) => { + sendClickEvent?.("onclick"); + // if the user clicked this link without any modifier keys, and it's a left click, then we want to // navigate to the link using the `onSelect` handler to defer the behavior to the NextJS router. if (!e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey && (e.button === 0 || e.button === 1)) { diff --git a/packages/ui/fern-docs-search-ui/src/components/shared/hits.ts b/packages/ui/fern-docs-search-ui/src/components/shared/hits.ts index e0ce18ed10..9aae3ab181 100644 --- a/packages/ui/fern-docs-search-ui/src/components/shared/hits.ts +++ b/packages/ui/fern-docs-search-ui/src/components/shared/hits.ts @@ -12,41 +12,13 @@ export interface GroupedHits { hits: GroupedHit[]; } -// type SegmentType = "markdown" | "changelog" | "parameter" | "http" | "webhook" | "websocket"; -// const SEGMENT_DISPLAY_NAMES: Record = { -// markdown: "Guides", -// changelog: "Changelog", -// parameter: "Parameters", -// http: "Endpoints", -// webhook: "Webhooks", -// websocket: "WebSockets", -// }; - export function generateHits(items: AlgoliaRecordHit[]): GroupedHits[] { - // return Object.entries( - // groupBy(items, (item): SegmentType => { - // if (item.type === "api-reference") { - // return item.api_type; - // } - // return item.type; - // }), - // ).map(([type, hits]) => ({ - // title: SEGMENT_DISPLAY_NAMES[type as SegmentType] ?? type, - // hits: hits.map((hit) => ({ - // title: hit.title, - // path: `${hit.pathname}${hit.hash ?? ""}`, - // icon: hit.icon, - // record: hit, - // })), - // })); - return [ { title: "Results", hits: items.map((hit) => ({ title: hit.title, path: `${hit.pathname}${hit.hash ?? ""}`, - // category: SEGMENT_DISPLAY_NAMES[hit.type === "api-reference" ? hit.api_type : hit.type], icon: hit.icon, record: hit, })), diff --git a/packages/ui/fern-docs-search-ui/src/hooks/use-search-hits.ts b/packages/ui/fern-docs-search-ui/src/hooks/use-search-hits.ts index 8f20b860c1..d254a18acf 100644 --- a/packages/ui/fern-docs-search-ui/src/hooks/use-search-hits.ts +++ b/packages/ui/fern-docs-search-ui/src/hooks/use-search-hits.ts @@ -1,4 +1,5 @@ import type { AlgoliaRecord } from "@fern-ui/fern-docs-search-server/algolia/types"; +import type { SendEventForHits } from "instantsearch.js/es/lib/utils"; import { useEffect, useLayoutEffect, useState } from "react"; import { useHits } from "react-instantsearch"; @@ -11,6 +12,11 @@ export function useSearchHits(): AlgoliaRecordHit[] { return items; } +export function useSendEvent(): SendEventForHits { + const { sendEvent } = useHits(); + return sendEvent; +} + // this is a hack to force the `` component to re-render when the hits change, so that it can change its selected hit // this doesn't work all the time, but slightly improves the UX otherwise the selection is sometimes 1-render behind. export function useSearchHitsRerender(): void {