diff --git a/package.json b/package.json index 38ea1b5d6cebc..baab1f1ced367 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "remark-prism": "^1.3.6", "rss": "^1.2.2", "sass": "^1.69.5", - "search-insights": "^2.2.3", + "search-insights": "^2.17.2", "server-only": "^0.0.1", "sharp": "^0.33.4", "tailwindcss-scoped-preflight": "^3.0.4", @@ -137,4 +137,4 @@ "node": "20.11.0", "yarn": "1.22.21" } -} +} \ No newline at end of file diff --git a/src/components/search/index.tsx b/src/components/search/index.tsx index f4a37a691f236..5ac79ce9b4bb6 100644 --- a/src/components/search/index.tsx +++ b/src/components/search/index.tsx @@ -1,6 +1,7 @@ 'use client'; import {Fragment, useCallback, useEffect, useRef, useState} from 'react'; +import {captureException} from '@sentry/nextjs'; import { Hit, Result, @@ -9,7 +10,7 @@ import { } from '@sentry-internal/global-search'; import DOMPurify from 'dompurify'; import Link from 'next/link'; -import {useRouter} from 'next/navigation'; +import {usePathname, useRouter} from 'next/navigation'; import algoliaInsights from 'search-insights'; import {useOnClickOutside} from 'sentry-docs/clientUtils'; @@ -90,6 +91,7 @@ export function Search({path, autoFocus, searchPlatforms = [], showChatBot}: Pro const [showOffsiteResults, setShowOffsiteResults] = useState(false); const [loading, setLoading] = useState(true); const router = useRouter(); + const pathname = usePathname(); const handleClickOutside = useCallback((ev: MouseEvent) => { // don't close the search results if the user is clicking the expand button @@ -196,21 +198,66 @@ export function Search({path, autoFocus, searchPlatforms = [], showChatBot}: Pro }); const trackSearchResultClick = useCallback((hit: Hit, position: number): void => { - if (hit.id === undefined) { - return; + try { + algoliaInsights('clickedObjectIDsAfterSearch', { + eventName: 'documentation_search_result_click', + userToken: randomUserToken, + index: hit.index, + objectIDs: [hit.id], + // Positions in Algolia are 1 indexed + queryID: hit.queryID ?? '', + positions: [position + 1], + }); + } catch (error) { + captureException(error); } + }, []); - algoliaInsights('clickedObjectIDsAfterSearch', { - eventName: 'documentation_search_result_click', - userToken: randomUserToken, - index: hit.index, - objectIDs: [hit.id], - // Positions in Algolia are 1 indexed - queryID: hit.queryID ?? '', - positions: [position + 1], - }); + const removeTags = useCallback((str: string) => { + return str.replace(/<\/?[^>]+(>|$)/g, ''); }, []); + const handleSearchResultClick = useCallback( + (event: React.MouseEvent, hit: Hit, position: number): void => { + if (hit.id === undefined) { + return; + } + + trackSearchResultClick(hit, position); + + // edge case when the clicked search result is the currently visited paged + if (relativizeUrl(hit.url) === pathname) { + // do not navigate to the search result page in this case + event.preventDefault(); + + // sanitize the title to remove any html tags + const title = hit?.title && removeTags(hit.title); + + if (!title) { + return; + } + + // check for heading with the same text as the title + const headings = + document + .querySelector('main > div.prose') + ?.querySelectorAll('h1, h2, h3, h4, h5, h6') ?? []; + const foundHeading = Array.from(headings).find(heading => + heading.textContent?.toLowerCase().includes(title.toLowerCase()) + ); + + // close the search results and scroll to the heading if it exists + setInputFocus(false); + if (foundHeading) { + foundHeading.scrollIntoView({ + behavior: 'smooth', + }); + } + } + }, + [pathname, removeTags, trackSearchResultClick] + ); + return (
@@ -271,7 +318,7 @@ export function Search({path, autoFocus, searchPlatforms = [], showChatBot}: Pro focused?.id === hit.id ? styles['sgs-hit-focused'] : '' }`} ref={ - // Scroll to eleemnt on focus + // Scroll to element on focus hit.id === focused?.id ? el => el?.scrollIntoView({block: 'nearest'}) : undefined @@ -279,7 +326,7 @@ export function Search({path, autoFocus, searchPlatforms = [], showChatBot}: Pro > trackSearchResultClick(hit, index)} + onClick={e => handleSearchResultClick(e, hit, index)} > {hit.title && (
diff --git a/yarn.lock b/yarn.lock index ed1a1ddd3230a..4c2b25da6901a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11110,10 +11110,10 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" -search-insights@^2.2.3: - version "2.13.0" - resolved "https://registry.npmjs.org/search-insights/-/search-insights-2.13.0.tgz" - integrity sha512-Orrsjf9trHHxFRuo9/rzm0KIWmgzE8RMlZMzuhZOJ01Rnz3D0YBAe+V6473t6/H6c7irs6Lt48brULAiRWb3Vw== +search-insights@^2.17.2: + version "2.17.2" + resolved "https://registry.yarnpkg.com/search-insights/-/search-insights-2.17.2.tgz#d13b2cabd44e15ade8f85f1c3b65c8c02138629a" + integrity sha512-zFNpOpUO+tY2D85KrxJ+aqwnIfdEGi06UH2+xEb+Bp9Mwznmauqc9djbnBibJO5mpfUPPa8st6Sx65+vbeO45g== section-matter@^1.0.0: version "1.0.0"