diff --git a/develop-docs/integrations/jira/index.mdx b/develop-docs/integrations/jira/index.mdx index 04dff5327df0e..00b2b57e73b49 100644 --- a/develop-docs/integrations/jira/index.mdx +++ b/develop-docs/integrations/jira/index.mdx @@ -30,7 +30,9 @@ To enable issue-sync between Jira and a locally hosted Sentry Jira integration, ![Navigating to Connected Apps](./images/jira-setup2.png) 6. In the new modal, select JIRA for the product to install the app on (8). -7. For the "App descriptor URL" (9), use the following URL, `https://{YOUR_SENTRY_DOMAIN}/extensions/jira/descriptor/`. Note that if you are running a local devserver, `YOUR_SENTRY_DOMAIN` will be your ngrok (or other tunneling service) domain. +7. For the "App descriptor URL" (9), use the following URL, `https://{YOUR_SENTRY_DOMAIN}/extensions/jira/descriptor/`. + * Note that if you are running a local devserver, `YOUR_SENTRY_DOMAIN` will be your ngrok (or other tunneling service) domain. + * For self-hosted Sentry users: Replace `https://{YOUR_SENTRY_DOMAIN}` with your Sentry address. ![Filling out Modal](./images/jira-setup3.png) 8. Click "Install app" (10), now if you select the "Installed Apps" (11) tab next to "Settings", you should see your newly installed app listed under "Sentry" (12). (Note: that "Sentry for Jira" is the SaaS integration). diff --git a/docs/contributing/pages/components.mdx b/docs/contributing/pages/components.mdx index 3b20613f1b150..efdbd14badc58 100644 --- a/docs/contributing/pages/components.mdx +++ b/docs/contributing/pages/components.mdx @@ -94,12 +94,36 @@ Render an expandable section. This is some content +This is some content + +This is some warning content + +This is some success content + ```markdown {tabTitle:Example} This is some content ``` +```markdown {tabTitle:Permalink} + + This is some content + +``` + +```markdown {tabTitle:Warning} + + This is some content + +``` + +```markdown {tabTitle:Success} + + This is some content + +``` + Attributes: - `title` (string) diff --git a/docs/organization/integrations/issue-tracking/jira/index.mdx b/docs/organization/integrations/issue-tracking/jira/index.mdx index c1095e7e977dd..3a281da41c65f 100644 --- a/docs/organization/integrations/issue-tracking/jira/index.mdx +++ b/docs/organization/integrations/issue-tracking/jira/index.mdx @@ -143,6 +143,10 @@ Confirm [Sentry's IP ranges](/security-legal-pii/security/ip-ranges/) are allowe Jira should now be authorized for all projects under your Sentry organization. +### Self-Hosted Sentry + Jira integration + +To install the Jira integration with a self-hosted Sentry instance, please follow our [develop-docs](/develop-docs/integrations/jira/index.mdx/), skip to the "Installing Local Jira App" section. + ## Configure Use Jira to leverage [issue management](#issue-management), [issue syncing](#issue-sync), and receive [notifications](#issue-notifications) about changes to issue status. Additionally, add [ignored fields](#ignored-fields) to hide specified fields in the issue creation form. diff --git a/docs/platforms/android/enriching-events/scopes/index.mdx b/docs/platforms/android/enriching-events/scopes/index.mdx index f6ee529e430af..cc78544b98c6e 100644 --- a/docs/platforms/android/enriching-events/scopes/index.mdx +++ b/docs/platforms/android/enriching-events/scopes/index.mdx @@ -70,6 +70,26 @@ Sentry.captureException(new Exception("my error")); // --> Will have the following extra: // { shared: 'current', global: 'data', isolation: 'data', current: 'data' } ``` +```kotlin +Sentry.configureScope(ScopeType.GLOBAL) { scope -> + scope.setExtra("shared", "global") + scope.setExtra("global", "data") +} + +Sentry.configureScope(ScopeType.ISOLATION) { scope -> + scope.setExtra("shared", "isolation") + scope.setExtra("isolation", "data") +} + +Sentry.configureScope(ScopeType.CURRENT) { scope -> + scope.setExtra("shared", "current") + scope.setExtra("current", "data") +} + +Sentry.captureException(Exception("my error")) +// --> Will have the following extra: +// { shared: 'current', global: 'data', isolation: 'data', current: 'data' } +``` ## Configuring the Scope diff --git a/docs/platforms/java/common/enriching-events/scopes/index.mdx b/docs/platforms/java/common/enriching-events/scopes/index.mdx index 2e13f0b9ddd00..58e678cad75df 100644 --- a/docs/platforms/java/common/enriching-events/scopes/index.mdx +++ b/docs/platforms/java/common/enriching-events/scopes/index.mdx @@ -21,7 +21,7 @@ routes or request handlers. ## How Scopes Work -Scopes are basically a stacks of data that are attached to events. When an event is captured, the SDK will merge the data from the active scopes into the event. This allows you to attach data to events that is relevant to the context in which the event was captured. +Scopes are basically stacks of data that are attached to events. When an event is captured, the SDK will merge the data from the active scopes into the event. This allows you to attach data to events that is relevant to the context in which the event was captured. A scope is generally valid inside of a callback or an execution context. This means that multiple parts of your application may have different scopes active at the same time. For instance, a web server might handle multiple requests at the same time, and each request may have different scope data to apply to its events. @@ -54,6 +54,13 @@ Sentry.configureScope(ScopeType.ISOLATION, scope -> { scope.setTag("my-tag", "my value"); }); ``` +```kotlin +Sentry.setTag("my-tag", "my value") +// Is identical to: +Sentry.configureScope(ScopeType.ISOLATION) { scope -> + scope.setTag("my-tag", "my value") +} +``` ### Current Scope @@ -70,6 +77,17 @@ Sentry.withScope(scope -> { // this event will not have the tag: Sentry.captureException(new Exception("my other error")); ``` +```kotlin +Sentry.withScope { scope -> + // scope is the current scope inside of this callback! + scope.setTag("my-tag", "my value") + // this tag will only be applied to events captured inside of this callback + // the following event will have the tag: + Sentry.captureException(Exception("my error")) +} +// this event will not have the tag: +Sentry.captureException(Exception("my other error")) +``` You can modify the current scope via `Sentry.configureScope(ScopeType.CURRENT, scope -> { ... })`, but usually you should use `Sentry.withScope()` to interact with local scopes instead. @@ -100,6 +118,26 @@ Sentry.captureException(new Exception("my error")); // --> Will have the following extra: // { shared: 'current', global: 'data', isolation: 'data', current: 'data' } ``` +```kotlin +Sentry.configureScope(ScopeType.GLOBAL) { scope -> + scope.setExtra("shared", "global") + scope.setExtra("global", "data") +} + +Sentry.configureScope(ScopeType.ISOLATION) { scope -> + scope.setExtra("shared", "isolation") + scope.setExtra("isolation", "data") +} + +Sentry.configureScope(ScopeType.CURRENT) { scope -> + scope.setExtra("shared", "current") + scope.setExtra("current", "data") +} + +Sentry.captureException(Exception("my error")) +// --> Will have the following extra: +// { shared: 'current', global: 'data', isolation: 'data', current: 'data' } +``` ## Configuring the Scope diff --git a/docs/platforms/javascript/common/tracing/instrumentation/custom-instrumentation/index.mdx b/docs/platforms/javascript/common/tracing/instrumentation/custom-instrumentation/index.mdx index c932c33236e38..8c175ad2a52e6 100644 --- a/docs/platforms/javascript/common/tracing/instrumentation/custom-instrumentation/index.mdx +++ b/docs/platforms/javascript/common/tracing/instrumentation/custom-instrumentation/index.mdx @@ -225,6 +225,34 @@ if (span) { } ``` +### Adding attributes to all spans + +To add an attribute to all spans, use the `beforeSendTransaction` callback: + +```javascript +Sentry.init({ + // dsn, ... + beforeSendTransaction(event) { + + // set the attribute on the root span + event.contexts.trace.data = { + ...event.contexts.trace.data, + myAttribute: "myValue", + } + + // and on all child spans + event.spans.forEach(span => { + span.data = { + ...span.data, + myAttribute: "myValue", + } + }); + } +}); +``` + + + ### Adding Span Operations ("op") Spans can have an operation associated with them, which help activate Sentry identify additional context about the span. For example database related spans have the `db` span operation associated with them. The Sentry product offers additional controls, visualizations and filters for spans with known operations. diff --git a/docs/platforms/javascript/common/troubleshooting/index.mdx b/docs/platforms/javascript/common/troubleshooting/index.mdx index 40143b02f353e..5bc1cd72e101d 100644 --- a/docs/platforms/javascript/common/troubleshooting/index.mdx +++ b/docs/platforms/javascript/common/troubleshooting/index.mdx @@ -137,14 +137,11 @@ You can work around our CDN bundles being blocked by [using our NPM packages](#u - - The Sentry Next.js SDK has a separate option to make setting up tunnels very - straight-forward, allowing you to skip the setup below. See - - Configure Tunneling to avoid Ad-Blockers - - to learn how to set up tunneling on Next.js. - +The Sentry Next.js SDK has a separate option to make setting up tunnels very +straight-forward, allowing you to skip the setup below. See +Configure Tunneling to avoid Ad-Blockers to learn how to set up tunneling on Next.js. + +If you do not want to configure `tunnelRoute`, you can follow the guide below. @@ -543,7 +540,7 @@ shamefully-hoist=true The problem here is related to memory consumption during the build process, especially when generating source maps. Here are some potential solutions and workarounds: - + - Update your `@sentry/nextjs` package to the latest version. - Increase Node.js memory limit: You can try increasing the memory limit for Node.js during the build process. Add this to your build command: `NODE_OPTIONS="--max-old-space-size=8192" next build`. This flag will increase the memory available to the node process to 8 GB. We have found that Next.js consumes around 4 GB in most cases. Decrease the size depending on your memory availability. - Disable source maps entirely: As a last resort, you can disable source map generation completely: diff --git a/docs/platforms/native/common/tracing/instrumentation/custom-instrumentation.mdx b/docs/platforms/native/common/tracing/instrumentation/custom-instrumentation.mdx index 15cfe5aa9f1df..1a9305e2ba822 100644 --- a/docs/platforms/native/common/tracing/instrumentation/custom-instrumentation.mdx +++ b/docs/platforms/native/common/tracing/instrumentation/custom-instrumentation.mdx @@ -38,6 +38,8 @@ When using these functions, you should ensure that the provided timestamps are c + + ## Distributed Tracing In order to use distributed tracing with the Native SDK, follow the custom instrumentation steps. diff --git a/package.json b/package.json index 2a2493c01a84e..b7d98d47d5639 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ }, "devDependencies": { "@babel/preset-typescript": "^7.15.0", - "@codecov/nextjs-webpack-plugin": "^1.0.0", + "@codecov/nextjs-webpack-plugin": "^1.8.0", "@spotlightjs/spotlight": "^2.5.0", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.10", diff --git a/platform-includes/performance/improving-data/native.mdx b/platform-includes/performance/improving-data/native.mdx new file mode 100644 index 0000000000000..cb1b96e225dde --- /dev/null +++ b/platform-includes/performance/improving-data/native.mdx @@ -0,0 +1,81 @@ +## Improving Data on Transactions and Spans +You can add Data Attributes to both Spans and Transactions. This data is visible in the trace explorer in Sentry. +The data must be of type `sentry_value_t`, which can store: +* 32-bit signed integers, +* double-precision floating-points, +* null-terminated strings, as well as +* lists and string-keyed maps containing `sentry_value_t` entries + +### Adding Data Attributes to Transactions + +You can add data attributes to your transactions using the following API: + +```c +sentry_transaction_context_t *tx_ctx = + sentry_transaction_context_new("processOrderBatch()", "task"); +sentry_transaction_t *tx = + sentry_transaction_start(tx_ctx, sentry_value_new_null()); + +sentry_transaction_set_data(tx, "my-data-attribute-1", + sentry_value_new_string("value1")); +sentry_transaction_set_data(tx, "my-data-attribute-2", + sentry_value_new_int32(42)); +sentry_transaction_set_data(tx, "my-data-attribute-3", + sentry_value_new_double(3.14)); +sentry_transaction_set_data(tx, "my-data-attribute-4", + sentry_value_new_bool(true)); + +sentry_value_t value_list = sentry_value_new_list(); +sentry_value_append(value_list, sentry_value_new_string("value1")); +sentry_value_append(value_list, sentry_value_new_int32(42)); +sentry_value_append(value_list, sentry_value_new_double(3.14)); +sentry_value_append(value_list, sentry_value_new_bool(true)); + +sentry_transaction_set_data(tx, "my-data-attribute-5", value_list); + +sentry_value_t value_object = sentry_value_new_object(); +sentry_value_set_by_key(value_object, "key_1", sentry_value_new_string("value1")); +sentry_value_set_by_key(value_object, "key_2", sentry_value_new_int32(42)); +sentry_value_set_by_key(value_object, "key_3", sentry_value_new_double(3.14)); +sentry_value_set_by_key(value_object, "key_4", sentry_value_new_bool(true)); + +sentry_transaction_set_data(tx, "my-data-attribute-6", value_object); +``` + +### Adding Data Attributes to Spans + +You can add data attributes to your spans using the following API: + +```c +sentry_transaction_context_t *tx_ctx = + sentry_transaction_context_new("processOrderBatch()", "task"); +sentry_transaction_t *tx = + sentry_transaction_start(tx_ctx, sentry_value_new_null()); +sentry_span_t *span = + sentry_transaction_start_child(tx, "task", "operation"); + +sentry_span_set_data(span, "my-data-attribute-1", + sentry_value_new_string("value1")); +sentry_span_set_data(span, "my-data-attribute-2", + sentry_value_new_int32(42)); +sentry_span_set_data(span, "my-data-attribute-3", + sentry_value_new_double(3.14)); +sentry_span_set_data(span, "my-data-attribute-4", + sentry_value_new_bool(true)); + +sentry_value_t value_list = sentry_value_new_list(); +sentry_value_append(value_list, sentry_value_new_string("value1")); +sentry_value_append(value_list, sentry_value_new_int32(42)); +sentry_value_append(value_list, sentry_value_new_double(3.14)); +sentry_value_append(value_list, sentry_value_new_bool(true)); + +sentry_span_set_data(span, "my-data-attribute-5", value_list); + +sentry_value_t value_object = sentry_value_new_object(); +sentry_value_set_by_key(value_object, "key_1", sentry_value_new_string("value1")); +sentry_value_set_by_key(value_object, "key_2", sentry_value_new_int32(42)); +sentry_value_set_by_key(value_object, "key_3", sentry_value_new_double(3.14)); +sentry_value_set_by_key(value_object, "key_4", sentry_value_new_bool(true)); + +sentry_span_set_data(span, "my-data-attribute-6", value_object); +``` diff --git a/src/build/resolveOpenAPI.ts b/src/build/resolveOpenAPI.ts index 09387c19dff15..848960528e954 100644 --- a/src/build/resolveOpenAPI.ts +++ b/src/build/resolveOpenAPI.ts @@ -8,7 +8,7 @@ import {DeRefedOpenAPI} from './open-api/types'; // SENTRY_API_SCHEMA_SHA is used in the sentry-docs GHA workflow in getsentry/sentry-api-schema. // DO NOT change variable name unless you change it in the sentry-docs GHA workflow in getsentry/sentry-api-schema. -const SENTRY_API_SCHEMA_SHA = 'ab06b0e825d0dd4f350fd269fefd3cc24ed121e2'; +const SENTRY_API_SCHEMA_SHA = '421589ca453b898372204963465c7d856feec7c7'; const activeEnv = process.env.GATSBY_ENV || process.env.NODE_ENV || 'development'; diff --git a/src/components/alert/index.tsx b/src/components/alert.tsx similarity index 61% rename from src/components/alert/index.tsx rename to src/components/alert.tsx index 3a49aba2340f7..6c1cd0548a743 100644 --- a/src/components/alert/index.tsx +++ b/src/components/alert.tsx @@ -5,9 +5,7 @@ import { InfoCircledIcon, } from '@radix-ui/react-icons'; -// explicitly not usig CSS modules here -// because there's some prerendered content that depends on these exact class names -import './styles.scss'; +import {Callout} from './callout'; type AlertProps = { children?: ReactNode; @@ -29,13 +27,9 @@ export function Alert({title, children, level = 'info'}: AlertProps) { } return ( -
- -
- {title &&
{title}
} -
{children}
-
-
+ + {children} + ); } diff --git a/src/components/alert/styles.scss b/src/components/alert/styles.scss deleted file mode 100644 index 71ef90ca2b699..0000000000000 --- a/src/components/alert/styles.scss +++ /dev/null @@ -1,57 +0,0 @@ -.alert { - margin-bottom: 1rem; - padding: 0.5rem 1rem; - border-radius: 6px; - display: flex; - flex-direction: row; - gap: 0.7em; - line-height: 1.75em; - border: 1px solid var(--alert-highlight-color); - - p { - margin-bottom: 0.5rem; - margin-top: 0; - } - - strong { - font-weight: 500; - } - - ul, - ol { - padding-inline-start: 2rem; - margin-top: 0; - } - - li { - padding-inline-start: 0; - margin: 0; - } - - .alert-header { - font-weight: 500; - color: inherit; - } -} - -.alert-icon { - flex: 1em 0 0; - height: 1.75em; - color: var(--alert-highlight-color); -} - -.alert-info { - --alert-highlight-color: var(--accent-11); - background: var(--accent-2); -} - -.alert-success { - --alert-highlight-color: var(--successGreen); - background: var(--successGreen); - background: color-mix(in srgb, var(--successGreen), transparent 85%); -} - -.alert-warning { - --alert-highlight-color: var(--amber-10); - background: var(--amber-2); -} diff --git a/src/components/callout/index.tsx b/src/components/callout/index.tsx new file mode 100644 index 0000000000000..903d382e8e265 --- /dev/null +++ b/src/components/callout/index.tsx @@ -0,0 +1,88 @@ +import { + ForwardRefExoticComponent, + MouseEventHandler, + ReactNode, + useCallback, +} from 'react'; + +// explicitly not usig CSS modules here +// because there's some prerendered content that depends on these exact class names +import './styles.scss'; + +type CalloutProps = { + Icon: ForwardRefExoticComponent; + children?: ReactNode; + /** If defined, the title of the callout will receive this ID and render a link to this ID. */ + id?: string; + level?: 'info' | 'warning' | 'success'; + role?: string; + title?: string; + titleOnClick?: MouseEventHandler; +}; + +function Header({ + title, + id, + onClick, +}: { + title: string; + id?: string; + onClick?: MouseEventHandler; +}) { + // We want to avoid actually triggering the link + const preventDefaultOnClick = useCallback( + (event: React.MouseEvent) => { + if (!onClick) { + return; + } + + event.preventDefault(); + onClick(event); + }, + [onClick] + ); + + if (!id) { + return ( +
+ {title} +
+ ); + } + + return ( +
+ + {title} + +
+ ); +} + +export function Callout({ + title, + children, + level = 'info', + Icon, + role, + id, + titleOnClick, +}: CalloutProps) { + return ( +
+ +
+ {title &&
} +
{children}
+
+
+ ); +} diff --git a/src/components/callout/styles.scss b/src/components/callout/styles.scss new file mode 100644 index 0000000000000..312e6aa69cff2 --- /dev/null +++ b/src/components/callout/styles.scss @@ -0,0 +1,74 @@ +.callout { + margin-bottom: 1rem; + padding: 0.5rem 1rem; + border-radius: 6px; + display: flex; + flex-direction: row; + gap: 0.7rem; + line-height: 1.75rem; + border: 1px solid var(--callout-highlight-color); + + p { + margin-bottom: 1rem; + margin-top: 0; + } + + strong { + font-weight: 500; + } + + ul, + ol { + padding-inline-start: 2rem; + margin-top: 0; + margin-bottom: 1rem; + } + + li { + padding-inline-start: 0; + margin: 0; + } + + .callout-header { + font-weight: 500; + color: inherit; + + & a { + color: inherit; + } + + &[role="button"] { + cursor: pointer; + } + } +} + +.callout-icon { + flex: 1em 0 0; + height: 1.75rem; + color: var(--callout-highlight-color); + + &[role="button"] { + cursor: pointer; + } +} + +.callout-content { + min-width: 0; +} + +.callout-info { + --callout-highlight-color: var(--accent-11); + background: var(--accent-2); +} + +.callout-success { + --callout-highlight-color: var(--successGreen); + background: var(--successGreen); + background: color-mix(in srgb, var(--successGreen), transparent 85%); +} + +.callout-warning { + --callout-highlight-color: var(--amber-10); + background: var(--amber-2); +} diff --git a/src/components/expandable.tsx b/src/components/expandable.tsx index 28368464d68c4..e57e91de0c84e 100644 --- a/src/components/expandable.tsx +++ b/src/components/expandable.tsx @@ -1,38 +1,17 @@ 'use client'; import {ReactNode, useEffect, useState} from 'react'; -import {ArrowDown} from 'react-feather'; -import styled from '@emotion/styled'; +import {ChevronDownIcon, ChevronRightIcon} from '@radix-ui/react-icons'; + +import {Callout} from './callout'; type Props = { children: ReactNode; title: string; + level?: 'info' | 'warning' | 'success'; permalink?: boolean; }; -const Arrow = styled(({...props}) => )<{className: string}>` - user-select: none; - transition: transform 200ms ease-in-out; - stroke-width: 3px; - position: absolute; - right: 0; - top: 4px; -`; - -const Details = styled.details` - background: var(--accent-2); - border-color: var(--accent-12); - border-left: 3px solid var(--accent-12); - margin-bottom: 1rem; - padding: 0.5rem 1rem; - h2 { - margin-top: 0; - } - &[open] .expandable-arrow { - transform: rotate(180deg); - } -`; - function slugify(str: string) { return str .toLowerCase() @@ -40,31 +19,28 @@ function slugify(str: string) { .replace(/[^a-z0-9-]/g, ''); } -const header = (title: string, permalink?: boolean) => - permalink ? ( -

- - {title} - -

- ) : ( - title - ); +export function Expandable({title, level, children, permalink}: Props) { + const id = permalink ? slugify(title) : undefined; -export function Expandable({title, children, permalink}: Props) { const [isExpanded, setIsExpanded] = useState(false); + // Ensure we scroll to the element if the URL hash matches useEffect(() => { - // if the url hash matches the title, expand the section - if (permalink && window.location.hash === `#${slugify(title)}`) { + if (!id) { + return () => {}; + } + + if (window.location.hash === `#${id}`) { + document.querySelector(`#${id}`)?.scrollIntoView(); setIsExpanded(true); } + + // When the hash changes (e.g. when the back/forward browser buttons are used), + // we want to ensure to jump to the correct section const onHashChange = () => { - if (window.location.hash === `#${slugify(title)}`) { + if (window.location.hash === `#${id}`) { setIsExpanded(true); + document.querySelector(`#${id}`)?.scrollIntoView(); } }; // listen for hash changes and expand the section if the hash matches the title @@ -72,15 +48,31 @@ export function Expandable({title, children, permalink}: Props) { return () => { window.removeEventListener('hashchange', onHashChange); }; - }, [title, permalink]); + }, [id]); + + function toggleIsExpanded() { + const newVal = !isExpanded; + + if (id) { + if (newVal) { + window.history.pushState({}, '', `#${id}`); + } else { + window.history.pushState({}, '', '#'); + } + } + + setIsExpanded(newVal); + } return ( -
- - {header(title, permalink)} - - - {children} -
+ + {isExpanded ? children : undefined} + ); } diff --git a/src/components/search/index.tsx b/src/components/search/index.tsx index 5b75b757852e8..a5fc3c2f0068e 100644 --- a/src/components/search/index.tsx +++ b/src/components/search/index.tsx @@ -9,19 +9,19 @@ import { SentryGlobalSearch, standardSDKSlug, } from '@sentry-internal/global-search'; -import DOMPurify from 'dompurify'; -import Link from 'next/link'; -import {usePathname, useRouter} from 'next/navigation'; +import {usePathname} from 'next/navigation'; import algoliaInsights from 'search-insights'; import {useOnClickOutside} from 'sentry-docs/clientUtils'; -import {useKeyboardNavigate} from 'sentry-docs/hooks/useKeyboardNavigate'; import {isDeveloperDocs} from 'sentry-docs/isDeveloperDocs'; import styles from './search.module.scss'; import {Logo} from '../logo'; +import {SearchResultItems} from './searchResultItems'; +import {relativizeUrl} from './util'; + // Initialize Algolia Insights algoliaInsights('init', { appId: process.env.NEXT_PUBLIC_ALGOLIA_APP_ID, @@ -33,8 +33,6 @@ algoliaInsights('init', { // treat it as a random user. const randomUserToken = crypto.randomUUID(); -const MAX_HITS = 10; - // this type is not exported from the global-search package type SentryGlobalSearchConfig = ConstructorParameters[0]; @@ -59,12 +57,6 @@ const userDocsSites: SentryGlobalSearchConfig = [ const config = isDeveloperDocs ? developerDocsSites : userDocsSites; const search = new SentryGlobalSearch(config); -function relativizeUrl(url: string) { - return isDeveloperDocs - ? url - : url.replace(/^(https?:\/\/docs\.sentry\.io)(?=\/|$)/, ''); -} - type Props = { autoFocus?: boolean; path?: string; @@ -79,7 +71,7 @@ export function Search({path, autoFocus, searchPlatforms = [], showChatBot}: Pro const [inputFocus, setInputFocus] = useState(false); const [showOffsiteResults, setShowOffsiteResults] = useState(false); const [loading, setLoading] = useState(true); - const router = useRouter(); + const pathname = usePathname(); const handleClickOutside = useCallback((ev: MouseEvent) => { @@ -176,16 +168,6 @@ export function Search({path, autoFocus, searchPlatforms = [], showChatBot}: Pro const totalHits = results.reduce((a, x) => a + x.hits.length, 0); - const flatHits = results.reduce( - (items, item) => [...items, ...item.hits.slice(0, MAX_HITS)], - [] - ); - - const {focused} = useKeyboardNavigate({ - list: flatHits, - onSelect: hit => router.push(relativizeUrl(hit.url)), - }); - const trackSearchResultClick = useCallback((hit: Hit, position: number): void => { try { algoliaInsights('clickedObjectIDsAfterSearch', { @@ -305,77 +287,13 @@ export function Search({path, autoFocus, searchPlatforms = [], showChatBot}: Pro {loading && } {!loading && totalHits > 0 && ( -
- {results - .filter(x => x.hits.length > 0) - .map((result, i) => ( - - {showOffsiteResults && ( -

- From {result.name} -

- )} -
    - {result.hits.slice(0, MAX_HITS).map((hit, index) => ( -
  • el?.scrollIntoView({block: 'nearest'}) - : undefined - } - > - handleSearchResultClick(e, hit, index)} - > - {hit.title && ( -
    - -
    - )} - {hit.text && ( - - )} - {hit.context && ( -
    - {hit.context.context1 && ( -
    - {hit.context.context1} -
    - )} - {hit.context.context2 && ( -
    - {hit.context.context2} -
    - )} -
    - )} - -
  • - ))} -
-
- ))} -
+ + handleSearchResultClick(event, hit, position) + } + showOffsiteResults={showOffsiteResults} + /> )} {!loading && totalHits === 0 && ( diff --git a/src/components/search/searchResultItems.tsx b/src/components/search/searchResultItems.tsx new file mode 100644 index 0000000000000..06585d682751c --- /dev/null +++ b/src/components/search/searchResultItems.tsx @@ -0,0 +1,111 @@ +import {Fragment} from 'react'; +import {Hit, Result} from '@sentry-internal/global-search'; +import DOMPurify from 'dompurify'; +import Link from 'next/link'; +import {useRouter} from 'next/navigation'; + +import {useListKeyboardNavigate} from 'sentry-docs/hooks/useListKeyboardNavigate'; + +import styles from './search.module.scss'; + +import {relativizeUrl} from './util'; + +const MAX_HITS = 10; + +interface SearchResultClickHandler { + event: React.MouseEvent; + hit: Hit; + position: number; +} + +export function SearchResultItems({ + results, + showOffsiteResults, + onSearchResultClick, +}: { + onSearchResultClick: (params: SearchResultClickHandler) => void; + results: Result[]; + showOffsiteResults: boolean; +}) { + const router = useRouter(); + const flatHits = results.reduce( + (items, item) => [...items, ...item.hits.slice(0, MAX_HITS)], + [] + ); + const {focused} = useListKeyboardNavigate({ + list: flatHits, + onSelect: hit => router.push(relativizeUrl(hit.url)), + }); + + return ( +
+ {results + .filter(x => x.hits.length > 0) + .map((result, i) => ( + + {showOffsiteResults && ( +

From {result.name}

+ )} +
    + {result.hits.slice(0, MAX_HITS).map((hit, index) => ( +
  • el?.scrollIntoView({block: 'nearest'}) + : undefined + } + > + onSearchResultClick({event, hit, position: index})} + > + {hit.title && ( +
    + +
    + )} + {hit.text && ( + + )} + {hit.context && ( +
    + {hit.context.context1 && ( +
    + {hit.context.context1} +
    + )} + {hit.context.context2 && ( +
    + {hit.context.context2} +
    + )} +
    + )} + +
  • + ))} +
+
+ ))} +
+ ); +} diff --git a/src/components/search/util.ts b/src/components/search/util.ts new file mode 100644 index 0000000000000..f5191b930da18 --- /dev/null +++ b/src/components/search/util.ts @@ -0,0 +1,7 @@ +import {isDeveloperDocs} from 'sentry-docs/isDeveloperDocs'; + +export function relativizeUrl(url: string) { + return isDeveloperDocs + ? url + : url.replace(/^(https?:\/\/docs\.sentry\.io)(?=\/|$)/, ''); +} diff --git a/src/hooks/useKeyboardNavigate.tsx b/src/hooks/useListKeyboardNavigate.tsx similarity index 95% rename from src/hooks/useKeyboardNavigate.tsx rename to src/hooks/useListKeyboardNavigate.tsx index ffe6bf0b41bde..9a841146c4d11 100644 --- a/src/hooks/useKeyboardNavigate.tsx +++ b/src/hooks/useListKeyboardNavigate.tsx @@ -5,6 +5,7 @@ type Props = { * The list of values to navigate through */ list: T[]; + /** * Callback triggered when the item is selected */ @@ -14,7 +15,7 @@ type Props = { /** * Navigate a list of items using the up/down arrow and ^j/^k keys */ -function useKeyboardNavigate({list, onSelect}: Props) { +function useListKeyboardNavigate({list, onSelect}: Props) { const [focused, setFocus] = useState(null); const setFocusIndex = useCallback( @@ -92,4 +93,4 @@ function useKeyboardNavigate({list, onSelect}: Props) { return {focused, setFocus}; } -export {useKeyboardNavigate}; +export {useListKeyboardNavigate}; diff --git a/yarn.lock b/yarn.lock index b8342d2f144d0..c74d2435ea0d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -624,33 +624,34 @@ resolved "https://registry.yarnpkg.com/@chevrotain/utils/-/utils-11.0.3.tgz#e39999307b102cff3645ec4f5b3665f5297a2224" integrity sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ== -"@codecov/bundler-plugin-core@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@codecov/bundler-plugin-core/-/bundler-plugin-core-1.4.0.tgz#6035d8fe2a321b125c883ab77b9e6c36c9c08abd" - integrity sha512-/Rglx52KLdyqoZBW3DH2E/31c9/zWWZ4efTf+qxV0FSLb7oJ9/JZT3IBKL7f6fbVujR8PDMLIoG4Q0pmVY7LzA== +"@codecov/bundler-plugin-core@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@codecov/bundler-plugin-core/-/bundler-plugin-core-1.8.0.tgz#662369cc13efecc860759dcbdf90636cb665b21e" + integrity sha512-D1aeA8u3RHOkQVLImLHxW6zFdUrw5wgoeDzYrKZeDExGp5ePs4RpJxKwklg1N0e1JxRcAgKj+zZo/y3Q4nV+sA== dependencies: "@actions/core" "^1.10.1" "@actions/github" "^6.0.0" + "@sentry/core" "^8.42.0" chalk "4.1.2" semver "^7.5.4" unplugin "^1.10.1" zod "^3.22.4" -"@codecov/nextjs-webpack-plugin@^1.0.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@codecov/nextjs-webpack-plugin/-/nextjs-webpack-plugin-1.4.0.tgz#fa266ae311668d7d9afbb22b0c25b757e15c6af1" - integrity sha512-zRvXgpQTwaRqUX/28Z9QHzXAoheCQELe3C7J85ZzmTLoiCPNMmj1IS3YqMzQ6y3mv4myAqbWqOBssjFzuQ2LuA== +"@codecov/nextjs-webpack-plugin@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@codecov/nextjs-webpack-plugin/-/nextjs-webpack-plugin-1.8.0.tgz#22fe73ea887d7fb3696e13bce610fdf5dded40d1" + integrity sha512-EjsY8AyBKzpMeKbd74/S8zi3dav/7QhPWCWrJBgnyV1tFYRlq3sE2dyKghgASJe7SVAbvK2suiX72SwvFIieUQ== dependencies: - "@codecov/bundler-plugin-core" "^1.4.0" - "@codecov/webpack-plugin" "^1.4.0" + "@codecov/bundler-plugin-core" "^1.8.0" + "@codecov/webpack-plugin" "^1.8.0" unplugin "^1.10.1" -"@codecov/webpack-plugin@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@codecov/webpack-plugin/-/webpack-plugin-1.4.0.tgz#2e60d9c13ba2e8c08784265f93f8eb509ed0bc14" - integrity sha512-2mkaa7W5zyn1vcJIe2JIRIJ+VMjJH5r8HgdhdY274WhsT5sJdgnCtStFvFZBH/ysRFbU0OybMdDpF/75POQXFg== +"@codecov/webpack-plugin@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@codecov/webpack-plugin/-/webpack-plugin-1.8.0.tgz#21430775530cde139c22a55318dccbfda3c0eb32" + integrity sha512-G62kbpAFuutQIrbuw97MAf5vkgIKXf64Nj0pRe6OPbC5Aps0nWrEL9v0Sr0WPdHcbQnr3i6noIXyUDdV1lUZ5g== dependencies: - "@codecov/bundler-plugin-core" "^1.4.0" + "@codecov/bundler-plugin-core" "^1.8.0" unplugin "^1.10.1" "@cspotcode/source-map-support@^0.8.0": @@ -1820,6 +1821,13 @@ dependencies: "@octokit/openapi-types" "^22.2.0" +"@opentelemetry/api-logs@0.53.0": + version "0.53.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz#c478cbd8120ec2547b64edfa03a552cfe42170be" + integrity sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw== + dependencies: + "@opentelemetry/api" "^1.0.0" + "@opentelemetry/api-logs@0.56.0": version "0.56.0" resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.56.0.tgz#68f8c51ca905c260b610c8a3c67d3f9fa3d59a45" @@ -1834,7 +1842,7 @@ dependencies: "@opentelemetry/api" "^1.3.0" -"@opentelemetry/api@^1.3.0", "@opentelemetry/api@^1.9.0": +"@opentelemetry/api@^1.0.0", "@opentelemetry/api@^1.3.0", "@opentelemetry/api@^1.8", "@opentelemetry/api@^1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== @@ -2070,6 +2078,18 @@ semver "^7.5.2" shimmer "^1.2.1" +"@opentelemetry/instrumentation@^0.49 || ^0.50 || ^0.51 || ^0.52.0 || ^0.53.0": + version "0.53.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz#e6369e4015eb5112468a4d45d38dcada7dad892d" + integrity sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A== + dependencies: + "@opentelemetry/api-logs" "0.53.0" + "@types/shimmer" "^1.2.0" + import-in-the-middle "^1.8.1" + require-in-the-middle "^7.1.1" + semver "^7.5.2" + shimmer "^1.2.1" + "@opentelemetry/instrumentation@^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0": version "0.56.0" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.56.0.tgz#3330ce16d9235a548efa1019a4a7f01414edd44a" @@ -2095,7 +2115,7 @@ "@opentelemetry/core" "1.30.1" "@opentelemetry/semantic-conventions" "1.28.0" -"@opentelemetry/sdk-trace-base@^1.30.1": +"@opentelemetry/sdk-trace-base@^1.22", "@opentelemetry/sdk-trace-base@^1.30.1": version "1.30.1" resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz#41a42234096dc98e8f454d24551fc80b816feb34" integrity sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg== @@ -2280,6 +2300,15 @@ dependencies: "@opentelemetry/instrumentation" "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0" +"@prisma/instrumentation@^5.8.1": + version "5.22.0" + resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-5.22.0.tgz#c39941046e9886e17bdb47dbac45946c24d579aa" + integrity sha512-LxccF392NN37ISGxIurUljZSh1YWnphO34V5a0+T7FVQG2u9bhAXRTJpgmQ3483woVhkraQZFF7cbRrpbw/F4Q== + dependencies: + "@opentelemetry/api" "^1.8" + "@opentelemetry/instrumentation" "^0.49 || ^0.50 || ^0.51 || ^0.52.0 || ^0.53.0" + "@opentelemetry/sdk-trace-base" "^1.22" + "@radix-ui/colors@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@radix-ui/colors/-/colors-3.0.0.tgz#e8a591a303c44e503bd1212cacf40a09511165e0" @@ -3139,6 +3168,11 @@ resolved "https://registry.yarnpkg.com/@sentry/core/-/core-9.0.0-alpha.0.tgz#2347326f7808bfc4523f2093de79f5f306c2d302" integrity sha512-rznbdRvLTbb3zvujXAhaYRgcwB5P8GSmyR/2Qdr6RdGu6qWWQMG2j4iIcolLlXj1DUJfSTCFmbeRB+sk3WqaPQ== +"@sentry/core@^8.42.0": + version "8.51.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.51.0.tgz#d0c73dfe3489788911b7ce784d3ef8458344482c" + integrity sha512-Go0KxCYLw+OBIlLSv5YsYX+x9NW43fNVcyB6rhkSp2Q5Zme3tAE6KtZFvyu4SO7G/903wisW5Q6qV6UuK/ee4A== + "@sentry/nextjs@9.0.0-alpha.0": version "9.0.0-alpha.0" resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-9.0.0-alpha.0.tgz#02af24c05edb6d5d3e33638437fc66c5a5edc991"