From d4883bc38bd756c872fa359583222544a4d41e4b Mon Sep 17 00:00:00 2001 From: Abdellah Hariti Date: Thu, 23 May 2024 16:53:18 +0100 Subject: [PATCH 01/34] feat: onboarding options on getting started pages --- docs/platforms/php/index.mdx | 40 ++++++-- src/components/docPage/type.scss | 4 + src/components/onboarding/index.tsx | 136 ++++++++++++++++++++++++++++ src/mdx.ts | 2 + src/mdxComponents.ts | 3 + src/rehype-onboarding-lines.js | 111 +++++++++++++++++++++++ 6 files changed, 289 insertions(+), 7 deletions(-) create mode 100644 src/components/onboarding/index.tsx create mode 100644 src/rehype-onboarding-lines.js diff --git a/docs/platforms/php/index.mdx b/docs/platforms/php/index.mdx index 4350150524f5b..61f59a3c72df8 100644 --- a/docs/platforms/php/index.mdx +++ b/docs/platforms/php/index.mdx @@ -19,6 +19,14 @@ Using a framework? Check out the other SDKs we support in the left-hand dropdown This Sentry PHP SDK provides support for PHP 7.2 or later. If you are using our previous PHP SDK, you can access the legacy SDK documentation, until further notice. + + ## Install Sentry captures data by using an SDK within your application’s runtime. These are platform-specific and allow Sentry to have a deep understanding of how your application works. @@ -29,24 +37,42 @@ Install the SDK using [Composer](https://getcomposer.org/). composer require sentry/sentry ``` -## Configure + + +Install the Excimer extension via PECL: + +```bash +pecl install excimer +``` -After you’ve completed setting up a project in Sentry, Sentry will give you a value which we call a DSN or Data Source Name. It looks a lot like a standard URL, but it’s just a representation of the configuration required by the Sentry SDKs. It consists of a few pieces, including the protocol, public key, the server address, and the project identifier. +The Excimer PHP extension supports PHP 7.2 and up. Excimer requires Linux or macOS and doesn't support Windows. For additional ways to install Excimer, [see docs](/platforms/php/profiling/#installation). + + + +## Configure To capture all errors, even the one during the startup of your application, you should initialize the Sentry PHP SDK as soon as possible. -```php -\Sentry\init(['dsn' => '___PUBLIC_DSN___' ]); +```php {onboardingOptions: {performance: '3-4', profiling: '5-6'}} +\Sentry\init([ + 'dsn' => '___PUBLIC_DSN___' , + // Specify a fixed sample rate + 'traces_sample_rate' => 1.0, + // Set a sampling rate for profiling - this is relative to traces_sample_rate + 'profiles_sample_rate' => 1.0, +]); ``` -## Usage +# Verify + +In PHP you can either capture a caught exception or capture the last error with captureLastError. ```php try { - $this->functionFailsForSure(); + $this->functionFailsForSure(); } catch (\Throwable $exception) { - \Sentry\captureException($exception); + \Sentry\captureException($exception); } ``` diff --git a/src/components/docPage/type.scss b/src/components/docPage/type.scss index cf2a2220ef85d..0c4c4a9181086 100644 --- a/src/components/docPage/type.scss +++ b/src/components/docPage/type.scss @@ -143,6 +143,10 @@ dt+dd { margin-bottom: var(--paragraph-margin-bottom); } + + [data-onboarding-option].hidden { + display: none; + } } .sidebar { diff --git a/src/components/onboarding/index.tsx b/src/components/onboarding/index.tsx new file mode 100644 index 0000000000000..8df07bbd99ac1 --- /dev/null +++ b/src/components/onboarding/index.tsx @@ -0,0 +1,136 @@ +'use client'; + +import {useEffect, useState} from 'react'; +import {Button, Checkbox} from '@radix-ui/themes'; + +export function OnboardingOption({ + children, + optionId, + dependsOn = [], +}: { + children: React.ReactNode; + optionId: string; + dependsOn?: string[]; +}) { + return ( +
+ {children} +
+ ); +} + +type OnboardingOptionType = { + dependsOn: string[]; + id: string; + name: string; + disabled?: boolean; +}; + +const validateOptionDeps = (options: OnboardingOptionType[]) => { + options.forEach(option => { + option.dependsOn?.forEach(dep => { + if (!options.find(opt => opt.id === dep)) { + throw new Error( + `Option dependency with \`${dep}\` not found: \`${JSON.stringify(option, null, 2)}\`` + ); + } + }); + }); +}; + +export function OnboardingOptionsButtons({ + options: initialOptions, +}: { + options: OnboardingOptionType[]; +}) { + validateOptionDeps(initialOptions); + + const [options, setSelectedOptions] = useState< + (OnboardingOptionType & {selected: boolean})[] + >(initialOptions.map(option => ({...option, selected: option.disabled || false}))); + + function handleCheckedChange(option: OnboardingOptionType, selected: boolean) { + setSelectedOptions(prev => { + // - select option and all dependencies + // - disable dependencies + if (selected) { + return prev.map(opt => { + if (opt.id === option.id) { + return { + ...opt, + selected: true, + }; + } + if (option.dependsOn?.includes(opt.id)) { + return {...opt, disabled: true, selected: true}; + } + return opt; + }); + } + // unselect option and reenable dependencies + // Note: does not account for dependencies of multiple dependants + return prev.map(opt => { + if (opt.id === option.id) { + return { + ...opt, + selected: false, + }; + } + // reenable dependencies + return option.dependsOn?.includes(opt.id) ? {...opt, disabled: false} : opt; + }); + }); + } + useEffect(() => { + options.forEach(option => { + if (option.disabled) { + return; + } + const targetElements = document.querySelectorAll( + `[data-onboarding-option="${option.id}"]` + ); + targetElements.forEach(el => { + el.classList.toggle('hidden', !option.selected); + }); + if (option.selected && option.dependsOn) { + const dependenciesSelecor = option.dependsOn.map( + dep => `[data-onboarding-option="${dep}"]` + ); + const dependencies = document.querySelectorAll( + dependenciesSelecor.join(', ') + ); + + dependencies.forEach(dep => { + dep.classList.remove('hidden'); + }); + } + }); + }, [options]); + + return ( +
+ {options.map(option => ( + + ))} +
+ ); +} diff --git a/src/mdx.ts b/src/mdx.ts index d75f51c2275ca..43317371c2f03 100644 --- a/src/mdx.ts +++ b/src/mdx.ts @@ -17,6 +17,7 @@ import getAppRegistry from './build/appRegistry'; import getPackageRegistry from './build/packageRegistry'; import {apiCategories} from './build/resolveOpenAPI'; import getAllFilesRecursively from './files'; +import rehypeOnboardingLines from './rehype-onboarding-lines'; import remarkCodeTabs from './remark-code-tabs'; import remarkCodeTitles from './remark-code-title'; import remarkComponentSpacing from './remark-component-spacing'; @@ -333,6 +334,7 @@ export async function getFileBySlug(slug: string) { }, ], [rehypePrismPlus, {ignoreMissing: true}], + rehypeOnboardingLines, [rehypePrismDiff, {remove: true}], rehypePresetMinify, ]; diff --git a/src/mdxComponents.ts b/src/mdxComponents.ts index cf1b1eec91bd2..4cb552e36d9ac 100644 --- a/src/mdxComponents.ts +++ b/src/mdxComponents.ts @@ -11,6 +11,7 @@ import {GuideGrid} from './components/guideGrid'; import {JsBundleList} from './components/jsBundleList'; import {LambdaLayerDetail} from './components/lambdaLayerDetail'; import {LinkWithPlatformIcon} from './components/linkWithPlatformIcon'; +import {OnboardingOption, OnboardingOptionsButtons} from './components/onboarding'; import {OrgAuthTokenNote} from './components/orgAuthTokenNote'; import {PageGrid} from './components/pageGrid'; import {ParamTable} from './components/paramTable'; @@ -60,6 +61,8 @@ export function mdxComponents( PlatformCategorySection, PlatformOrGuideName, PlatformSdkPackageName, + OnboardingOption, + OnboardingOptionsButtons, RelayMetrics, SandboxLink, SignInNote, diff --git a/src/rehype-onboarding-lines.js b/src/rehype-onboarding-lines.js new file mode 100644 index 0000000000000..c40a48e1c50c2 --- /dev/null +++ b/src/rehype-onboarding-lines.js @@ -0,0 +1,111 @@ +/** + * @typedef {import('hast').Element} Element + * @typedef {import('hast').Root} Root + */ + +import rangeParser from 'parse-numeric-range'; +import {visit} from 'unist-util-visit'; + +/** + * Rehype plugin that adds the `data-onboarding-option="some-option-id"` attribute and `hidden` class name + * to each line of code based on the metastring of the code block. + * + * The metastring should be in the format of: + * `{onboardingOptions: {performance: '1, 3-4', profiling: '5-6'}}` + * where the keys are the onboarding options, the line numbers can be individual or ranges separated by a comma. + * + * These lines will be hidden by default and shown based on the user's selection of `` + * + * **Note**: This plugin should be used after `rehype-prism-plus` as it relies on the `span.code-highlight`s generated by it. + * + * @return {import('unified').Plugin<[], Root>} + */ +export default function rehypeOnboardingLines() { + return tree => { + visit(tree, onlyHighlitedCodeBlocks, visitor); + }; +} +/** + * Parse the line numbers from the metastring + * @param {string} meta + * @return {number[]} + * @example + * parseLines('1, 3-4') // [1, 3, 4] + * parseLines('') // [] + */ +const parseLines = meta => { + const RE = /([\d,-]+)/; + // Remove space between {} e.g. {1, 3} + const parsedMeta = meta + .split(',') + .map(str => str.trim()) + .join(','); + if (RE.test(parsedMeta)) { + const strlineNumbers = RE.exec(parsedMeta)?.[1]; + if (!strlineNumbers) { + return []; + } + const lineNumbers = rangeParser(strlineNumbers); + return lineNumbers; + } + return []; +}; + +/** + * Create a closure that returns an onboarding option `id` for a given line if it exists + * + * @param {string} meta + * @return { (index:number) => string | undefined } + */ +const getOptionForLine = meta => { + const optionsRE = /{.*onboardingOptions:(.*)}/i; + let linesForOptions = {}; + const options = optionsRE.exec(meta)?.[1]; + if (!options) { + return () => undefined; + } + // eval provides the convenience of not having to wrap the object properties in quotes + // eslint-disable-next-line no-eval + const parsedOptions = eval(`(${options})`); + linesForOptions = Object.keys(parsedOptions).reduce((acc, key) => { + acc[key] = parseLines(parsedOptions[key]); + return acc; + }, {}); + return index => { + for (const key in linesForOptions) { + if (linesForOptions[key].includes(index + 1)) { + return key; + } + } + return undefined; + }; +}; + +/** + * @param {Element} node + */ +function visitor(node) { + const meta = /** @type {string} */ ( + node?.data?.meta || node?.properties?.metastring || '' + ); + + const optionForLine = getOptionForLine(meta); + + node.children.forEach((line, index) => { + const option = optionForLine(index); + if (option) { + line.properties['data-onboarding-option'] = option; + line.properties.className.push('hidden'); + } + }); +} + +/** + * Check if the node is a code block with the class `code-highlight` + * @param {Element} node + * @return {boolean} + */ +const onlyHighlitedCodeBlocks = node => + node.tagName === 'code' && + // className is an array of strings (made sure by rehype-prism-plus) + node.properties.className.includes('code-highlight'); From 4313c7303fc82b6fe14e5a910ac40d563a5d9521 Mon Sep 17 00:00:00 2001 From: Abdellah Hariti Date: Thu, 23 May 2024 18:03:15 +0100 Subject: [PATCH 02/34] better node filter --- src/rehype-onboarding-lines.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/rehype-onboarding-lines.js b/src/rehype-onboarding-lines.js index c40a48e1c50c2..d32d291a11e13 100644 --- a/src/rehype-onboarding-lines.js +++ b/src/rehype-onboarding-lines.js @@ -16,7 +16,7 @@ import {visit} from 'unist-util-visit'; * * These lines will be hidden by default and shown based on the user's selection of `` * - * **Note**: This plugin should be used after `rehype-prism-plus` as it relies on the `span.code-highlight`s generated by it. + * **Note**: This plugin should be used after `rehype-prism-plus` as it relies on its output. * * @return {import('unified').Plugin<[], Root>} */ @@ -101,11 +101,10 @@ function visitor(node) { } /** - * Check if the node is a code block with the class `code-highlight` + * Check if the node is a code block with the metastring containing `onboardingOptions` * @param {Element} node * @return {boolean} */ const onlyHighlitedCodeBlocks = node => node.tagName === 'code' && - // className is an array of strings (made sure by rehype-prism-plus) - node.properties.className.includes('code-highlight'); + (node?.data?.meta || node?.properties?.metastring || '').includes('onboardingOptions'); From a60795d6a54ca3c6ac48753e25e1d7da040a9478 Mon Sep 17 00:00:00 2001 From: Abdellah Hariti Date: Thu, 23 May 2024 18:58:05 +0100 Subject: [PATCH 03/34] a faster node filter --- package.json | 1 + src/rehype-onboarding-lines.js | 15 +++++---------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index bce850b7f336b..d3d0c749eefc0 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "next-auth": "^4.24.5", "next-mdx-remote": "^4.4.1", "nextjs-toploader": "^1.6.6", + "parse-numeric-range": "^1.3.0", "platformicons": "^5.10.9", "prism-sentry": "^1.0.2", "prismjs": "^1.27.0", diff --git a/src/rehype-onboarding-lines.js b/src/rehype-onboarding-lines.js index d32d291a11e13..00ac4d5aba5d9 100644 --- a/src/rehype-onboarding-lines.js +++ b/src/rehype-onboarding-lines.js @@ -22,7 +22,7 @@ import {visit} from 'unist-util-visit'; */ export default function rehypeOnboardingLines() { return tree => { - visit(tree, onlyHighlitedCodeBlocks, visitor); + visit(tree, {type: 'element', tagName: 'code'}, visitor); }; } /** @@ -89,6 +89,10 @@ function visitor(node) { node?.data?.meta || node?.properties?.metastring || '' ); + if (!meta.includes('onboardingOptions')) { + return; + } + const optionForLine = getOptionForLine(meta); node.children.forEach((line, index) => { @@ -99,12 +103,3 @@ function visitor(node) { } }); } - -/** - * Check if the node is a code block with the metastring containing `onboardingOptions` - * @param {Element} node - * @return {boolean} - */ -const onlyHighlitedCodeBlocks = node => - node.tagName === 'code' && - (node?.data?.meta || node?.properties?.metastring || '').includes('onboardingOptions'); From 7d28ae0e74444681148b8e8b679ad090732b51c2 Mon Sep 17 00:00:00 2001 From: Abdellah Hariti Date: Thu, 23 May 2024 19:46:18 +0100 Subject: [PATCH 04/34] denser options UI --- docs/platforms/php/index.mdx | 12 ++++++------ src/components/onboarding/index.tsx | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/platforms/php/index.mdx b/docs/platforms/php/index.mdx index 61f59a3c72df8..1cdb45f191d88 100644 --- a/docs/platforms/php/index.mdx +++ b/docs/platforms/php/index.mdx @@ -19,6 +19,12 @@ Using a framework? Check out the other SDKs we support in the left-hand dropdown This Sentry PHP SDK provides support for PHP 7.2 or later. If you are using our previous PHP SDK, you can access the legacy SDK documentation, until further notice. +## Install + +Sentry captures data by using an SDK within your application’s runtime. These are platform-specific and allow Sentry to have a deep understanding of how your application works. + +Install the SDK using [Composer](https://getcomposer.org/). + -## Install - -Sentry captures data by using an SDK within your application’s runtime. These are platform-specific and allow Sentry to have a deep understanding of how your application works. - -Install the SDK using [Composer](https://getcomposer.org/). - ```bash composer require sentry/sentry ``` diff --git a/src/components/onboarding/index.tsx b/src/components/onboarding/index.tsx index 8df07bbd99ac1..4339e86828689 100644 --- a/src/components/onboarding/index.tsx +++ b/src/components/onboarding/index.tsx @@ -108,11 +108,11 @@ export function OnboardingOptionsButtons({ }, [options]); return ( -
+
{options.map(option => ( ))} diff --git a/src/components/onboarding/styles.module.scss b/src/components/onboarding/styles.module.scss new file mode 100644 index 0000000000000..97a3ed85705b6 --- /dev/null +++ b/src/components/onboarding/styles.module.scss @@ -0,0 +1,76 @@ +.TooltipContent { + border-radius: 4px; + user-select: none; + padding: 8px 12px; + overflow-wrap: break-word; + max-width: 225px; + font-size: 12px; + line-height: 1.2; + text-align: center; + color: var(--gray-11); + background-color: white; + box-shadow: var(--shadow-6); + animation-duration: 100ms; + animation-timing-function: ease-in; +} + +.TooltipContent[data-state='delayed-open'][data-side='top'] { + animation-name: slideDownAndFade; +} +.TooltipContent[data-state='delayed-open'][data-side='right'] { + animation-name: slideLeftAndFade; +} +.TooltipContent[data-state='delayed-open'][data-side='bottom'] { + animation-name: slideUpAndFade; +} +.TooltipContent[data-state='delayed-open'][data-side='left'] { + animation-name: slideRightAndFade; +} + +.TooltipArrow { + fill: white; +} + +@keyframes slideUpAndFade { + from { + opacity: 0; + transform: translateY(2px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideRightAndFade { + from { + opacity: 0; + transform: translateX(-2px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes slideDownAndFade { + from { + opacity: 0; + transform: translateY(-2px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideLeftAndFade { + from { + opacity: 0; + transform: translateX(2px); + } + to { + opacity: 1; + transform: translateX(0); + } +} From e42fc83996355e295d8f20e6717939b6de4f5261 Mon Sep 17 00:00:00 2001 From: Abdellah Hariti Date: Fri, 24 May 2024 12:34:50 +0100 Subject: [PATCH 11/34] skip formatting codeblocks with onboarding options --- src/remark-format-code.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/remark-format-code.js b/src/remark-format-code.js index 8f3e9ae20e9ce..474a7e23e59de 100644 --- a/src/remark-format-code.js +++ b/src/remark-format-code.js @@ -12,7 +12,10 @@ export default function remarkFormatCodeBlocks() { const formattingWork = codeNodes // skip code blocks with diff meta as they might have // broken syntax due to + and - characters - .filter(node => !node.meta?.includes('diff')) + // or with `onboardingOptions` as they need to have predictable line numbers + .filter( + node => !(node.meta?.includes('diff') || node.meta?.includes('onboardingOptions')) + ) .map(node => formatCode(node)); await Promise.all(formattingWork); From 800b6812ffd4d744eff6b5d00e640c52a3fa1dd4 Mon Sep 17 00:00:00 2001 From: Abdellah Hariti Date: Fri, 24 May 2024 13:37:04 +0100 Subject: [PATCH 12/34] sticky onboarding option buttons --- src/components/onboarding/index.tsx | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/components/onboarding/index.tsx b/src/components/onboarding/index.tsx index 0f2ad3296daae..4c24a47890281 100644 --- a/src/components/onboarding/index.tsx +++ b/src/components/onboarding/index.tsx @@ -1,6 +1,6 @@ 'use client'; -import {useEffect, useState} from 'react'; +import {useEffect, useRef, useState} from 'react'; import {QuestionMarkCircledIcon} from '@radix-ui/react-icons'; import * as Tooltip from '@radix-ui/react-tooltip'; import {Button, Checkbox, Theme} from '@radix-ui/themes'; @@ -120,8 +120,33 @@ export function OnboardingOptionsButtons({ }); }, [options]); + const buttonsRef = useRef(null); + const containerTopPx = 100; + const [isSticky, setIsSticky] = useState(false); + + useEffect(() => { + const observer = new IntersectionObserver( + function ([buttonsContainer]) { + setIsSticky(!buttonsContainer.isIntersecting); + }, + { + // the 1 exptra px is important to trigger the observer + // https://stackoverflow.com/questions/16302483/event-to-detect-when-positionsticky-is-triggered + rootMargin: `-${containerTopPx + 1}px 0px 0px 0px`, + threshold: [1], + } + ); + + observer.observe(buttonsRef.current!); + }, []); + return ( -
+
{options.map(option => (