-
Platform-specific docs
+
If you use it, we probably support it.
diff --git a/src/components/platformSelector/index.tsx b/src/components/platformSelector/index.tsx
index e151f71d04c31..9f7b35345d35c 100644
--- a/src/components/platformSelector/index.tsx
+++ b/src/components/platformSelector/index.tsx
@@ -84,10 +84,8 @@ export function PlatformSelector({
const currentPlatformKey = currentPlatform?.key;
const pathname = usePathname();
- // Auto-open selector when on /platforms/ index page (no SDK selected)
- const isOnPlatformsIndex = pathname === '/platforms/' || pathname === '/platforms';
-
- const [open, setOpen] = useState(alwaysOpen || isOnPlatformsIndex);
+ // Not auto-opened on /platforms/: Radix Select locks body scroll while open.
+ const [open, setOpen] = useState(alwaysOpen);
const [searchValue, setSearchValue] = useState('');
const matches = useMemo(() => {
diff --git a/src/components/search/index.tsx b/src/components/search/index.tsx
index 52cfecf08fbd8..3365a200b4f37 100644
--- a/src/components/search/index.tsx
+++ b/src/components/search/index.tsx
@@ -11,7 +11,7 @@ import {
} from '@sentry-internal/global-search';
import {usePathname} from 'next/navigation';
import {Fragment, useCallback, useEffect, useRef, useState} from 'react';
-import algoliaInsights from 'search-insights';
+import {createInsightsClient} from 'search-insights';
import {useOnClickOutside} from 'sentry-docs/clientUtils';
import {isDeveloperDocs} from 'sentry-docs/isDeveloperDocs';
import {DocMetrics} from 'sentry-docs/metrics';
@@ -22,12 +22,6 @@ import styles from './search.module.scss';
import {SearchResultItems} from './searchResultItems';
import {relativizeUrl} from './util';
-// Initialize Algolia Insights
-algoliaInsights('init', {
- appId: process.env.NEXT_PUBLIC_ALGOLIA_APP_ID,
- apiKey: process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY,
-});
-
// We dont want to track anyone cross page/sessions or use cookies
// so just generate a random token each time the page is loaded and
// treat it as a random user.
@@ -58,6 +52,45 @@ const userDocsSites: SentryGlobalSearchConfig = [
const config = isDeveloperDocs ? developerDocsSites : userDocsSites;
const search = new SentryGlobalSearch(config);
+// Insights events are fire-and-forget, so rejected credentials are invisible: a
+// stale NEXT_PUBLIC_ALGOLIA_SEARCH_KEY silently 401'd every click for months.
+// Send them ourselves so a rejection gets reported once per page.
+let insightsRejectionReported = false;
+const algoliaInsights = createInsightsClient(async (url, data) => {
+ try {
+ const response = await fetch(url, {
+ method: 'POST',
+ body: JSON.stringify(data),
+ // text/plain keeps this a CORS simple request: an application/json
+ // preflight can be cancelled by the navigation a result click triggers.
+ headers: {'Content-Type': 'text/plain'},
+ keepalive: true,
+ });
+ if (!response.ok && !insightsRejectionReported) {
+ insightsRejectionReported = true;
+ captureException(
+ new Error(`Algolia Insights rejected an event with ${response.status}`)
+ );
+ }
+ return response.ok;
+ } catch (error) {
+ if (!insightsRejectionReported) {
+ insightsRejectionReported = true;
+ captureException(error);
+ }
+ return false;
+ }
+});
+
+const insightsAppId = process.env.NEXT_PUBLIC_ALGOLIA_APP_ID;
+const insightsApiKey = process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY;
+// Unconfigured deploys (previews) stay silent: search-insights throws on every
+// call until it is initialized, so nothing may be sent either.
+const insightsEnabled = Boolean(insightsAppId && insightsApiKey);
+if (insightsEnabled) {
+ algoliaInsights('init', {appId: insightsAppId, apiKey: insightsApiKey});
+}
+
type Props = {
autoFocus?: boolean;
/** Called before the Kapa modal opens, so a parent overlay can close itself first. */
@@ -178,6 +211,42 @@ export function Search({
};
}, [autoFocus]);
+ const resultsRef = useRef(null);
+ const [placement, setPlacement] = useState<'bottom' | 'top'>('bottom');
+ const showResults = query.length >= 2 && inputFocus;
+
+ // Keep the dropdown inside the viewport: cap it to whichever side of the input
+ // has more room, and flip above only when that side is the top.
+ useEffect(() => {
+ if (!showResults) {
+ return undefined;
+ }
+ const GUTTER = 16;
+ const update = () => {
+ const input = inputRef.current;
+ const dropdown = resultsRef.current;
+ if (!input || !dropdown) {
+ return;
+ }
+ const {top, bottom} = input.getBoundingClientRect();
+ const spaceBelow = window.innerHeight - bottom - GUTTER * 2;
+ const spaceAbove = top - GUTTER * 2;
+ const flip = spaceAbove > spaceBelow;
+ setPlacement(flip ? 'top' : 'bottom');
+ dropdown.style.setProperty(
+ '--sgs-available-space',
+ `${Math.round(Math.max(flip ? spaceAbove : spaceBelow, 200))}px`
+ );
+ };
+ update();
+ window.addEventListener('resize', update);
+ window.addEventListener('scroll', update, {passive: true});
+ return () => {
+ window.removeEventListener('resize', update);
+ window.removeEventListener('scroll', update);
+ };
+ }, [showResults]);
+
const searchFor = useCallback(
async (
inputQuery: string,
@@ -273,6 +342,9 @@ export function Search({
const totalHits = results.reduce((a, x) => a + x.hits.length, 0);
const trackSearchResultClick = useCallback((hit: Hit, position: number): void => {
+ if (!insightsEnabled) {
+ return;
+ }
try {
algoliaInsights('clickedObjectIDsAfterSearch', {
eventName: 'documentation_search_result_click',
@@ -368,8 +440,12 @@ export function Search({
- {query.length >= 2 && inputFocus && (
-
+ {showResults && (
+