123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442 |
- import {Fragment, useCallback, useEffect, useMemo} from 'react';
- import {css} from '@emotion/react';
- import styled from '@emotion/styled';
- import {Alert} from 'sentry/components/alert';
- import {Button} from 'sentry/components/button';
- import Checkbox from 'sentry/components/checkbox';
- import ExternalLink from 'sentry/components/links/externalLink';
- import {Tooltip} from 'sentry/components/tooltip';
- import {PlatformKey} from 'sentry/data/platformCategories';
- import {IconQuestion} from 'sentry/icons';
- import {t, tct} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import {decodeList} from 'sentry/utils/queryString';
- import useRouter from 'sentry/utils/useRouter';
- import TextBlock from 'sentry/views/settings/components/text/textBlock';
- export enum ProductSolution {
- ERROR_MONITORING = 'error-monitoring',
- PERFORMANCE_MONITORING = 'performance-monitoring',
- SESSION_REPLAY = 'session-replay',
- PROFILING = 'profiling',
- }
- // This is the list of products that are available for each platform
- // Since the ProductSelection component is rendered in the onboarding/project creation flow only, it is ok to have this list here
- // NOTE: Please keep the prefix in alphabetical order
- export const platformProductAvailability = {
- javascript: [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.SESSION_REPLAY],
- 'javascript-react': [
- ProductSolution.PERFORMANCE_MONITORING,
- ProductSolution.SESSION_REPLAY,
- ],
- 'javascript-vue': [
- ProductSolution.PERFORMANCE_MONITORING,
- ProductSolution.SESSION_REPLAY,
- ],
- 'javascript-angular': [
- ProductSolution.PERFORMANCE_MONITORING,
- ProductSolution.SESSION_REPLAY,
- ],
- 'javascript-ember': [
- ProductSolution.PERFORMANCE_MONITORING,
- ProductSolution.SESSION_REPLAY,
- ],
- 'javascript-gatsby': [
- ProductSolution.PERFORMANCE_MONITORING,
- ProductSolution.SESSION_REPLAY,
- ],
- 'javascript-nextjs': [
- ProductSolution.PERFORMANCE_MONITORING,
- ProductSolution.SESSION_REPLAY,
- ],
- 'javascript-remix': [
- ProductSolution.PERFORMANCE_MONITORING,
- ProductSolution.SESSION_REPLAY,
- ],
- 'javascript-svelte': [
- ProductSolution.PERFORMANCE_MONITORING,
- ProductSolution.SESSION_REPLAY,
- ],
- 'javascript-sveltekit': [
- ProductSolution.PERFORMANCE_MONITORING,
- ProductSolution.SESSION_REPLAY,
- ],
- node: [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING],
- 'node-azurefunctions': [
- ProductSolution.PERFORMANCE_MONITORING,
- ProductSolution.PROFILING,
- ],
- 'node-connect': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING],
- python: [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING],
- 'python-aiohttp': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING],
- 'python-awslambda': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING],
- 'python-bottle': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING],
- 'python-celery': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING],
- 'python-chalice': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING],
- 'python-django': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING],
- 'python-falcon': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING],
- 'python-fastapi': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING],
- 'python-flask': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING],
- 'python-gcpfunctions': [
- ProductSolution.PERFORMANCE_MONITORING,
- ProductSolution.PROFILING,
- ],
- 'python-pyramid': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING],
- 'python-quart': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING],
- 'python-rq': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING],
- 'python-sanic': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING],
- 'python-serverless': [
- ProductSolution.PERFORMANCE_MONITORING,
- ProductSolution.PROFILING,
- ],
- 'python-tornado': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING],
- 'python-starlette': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING],
- 'python-tryton': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING],
- 'python-wsgi': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING],
- } as Record<PlatformKey, ProductSolution[]>;
- export type DisabledProduct = {
- reason: string;
- onClick?: () => void;
- product?: ProductSolution;
- };
- type ProductProps = {
- /**
- * If the product is checked. This information is grabbed from the URL.
- */
- checked: boolean;
- /**
- * The name of the product
- */
- label: string;
- /**
- * Brief product description
- */
- description?: string;
- /**
- * If the product is disabled. It contains a reason and an optional onClick handler
- */
- disabled?: DisabledProduct;
- /**
- * Link of the product documentation. Rendered if there is also a description.
- */
- docLink?: string;
- /**
- * Click handler. If the product is enabled, by clicking on the button, the product is added or removed from the URL.
- */
- onClick?: () => void;
- /**
- * A permanent disabled product is always disabled and cannot be enabled.
- */
- permanentDisabled?: boolean;
- };
- function Product({
- disabled,
- permanentDisabled,
- checked,
- label,
- onClick,
- docLink,
- description,
- }: ProductProps) {
- const ProductWrapper = permanentDisabled
- ? PermanentDisabledProductWrapper
- : disabled
- ? DisabledProductWrapper
- : ProductButtonWrapper;
- return (
- <Tooltip
- title={
- disabled?.reason ??
- (description && (
- <TooltipDescription>
- {description}
- {docLink && <ExternalLink href={docLink}>{t('Read the Docs')}</ExternalLink>}
- </TooltipDescription>
- ))
- }
- isHoverable
- >
- <ProductWrapper
- onClick={disabled?.onClick ?? onClick}
- disabled={disabled?.onClick ?? permanentDisabled ? false : !!disabled}
- priority={permanentDisabled || checked ? 'primary' : 'default'}
- aria-label={label}
- >
- <ProductButtonInner>
- <Checkbox
- checked={checked}
- disabled={permanentDisabled ? false : !!disabled}
- aria-label={label}
- size="xs"
- readOnly
- />
- <span>{label}</span>
- <IconQuestion size="xs" color="subText" />
- </ProductButtonInner>
- </ProductWrapper>
- </Tooltip>
- );
- }
- export type ProductSelectionProps = {
- /**
- * List of products that are disabled. All of them have to contain a reason by default and optionally an onClick handler.
- */
- disabledProducts?: DisabledProduct[];
- /**
- * If true, the loader script is used instead of the npm/yarn guide.
- */
- lazyLoader?: boolean;
- /**
- * The platform key of the project (e.g. javascript-react, python-django, etc.)
- */
- platform?: PlatformKey;
- /**
- * A custom list of products per platform. If not provided, the default list is used.
- */
- productsPerPlatform?: Record<PlatformKey, ProductSolution[]>;
- skipLazyLoader?: () => void;
- /**
- * If true, the component has a bottom margin of 20px
- */
- withBottomMargin?: boolean;
- };
- export function ProductSelection({
- disabledProducts,
- lazyLoader,
- skipLazyLoader,
- platform,
- withBottomMargin,
- productsPerPlatform = platformProductAvailability,
- }: ProductSelectionProps) {
- const router = useRouter();
- const urlProducts = decodeList(router.location.query.product);
- const products: ProductSolution[] | undefined = platform
- ? productsPerPlatform[platform]
- : undefined;
- const defaultProducts = useMemo(() => {
- return (
- products?.filter(
- product =>
- !disabledProducts?.some(disabledProduct => disabledProduct.product === product)
- ) ?? []
- );
- }, [products, disabledProducts]);
- useEffect(() => {
- router.replace({
- pathname: router.location.pathname,
- query: {
- ...router.location.query,
- product: defaultProducts,
- },
- });
- // Adding defaultProducts to the dependency array causes an max-depth error
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [router]);
- const handleClickProduct = useCallback(
- (product: ProductSolution) => {
- let newProduct = urlProducts.includes(product)
- ? urlProducts.filter(p => p !== product)
- : [...urlProducts, product];
- if (defaultProducts?.includes(ProductSolution.PROFILING)) {
- if (
- !newProduct.includes(ProductSolution.PERFORMANCE_MONITORING) &&
- newProduct.includes(ProductSolution.PROFILING)
- ) {
- newProduct = [...newProduct, ProductSolution.PERFORMANCE_MONITORING];
- }
- }
- router.replace({
- pathname: router.location.pathname,
- query: {
- ...router.location.query,
- product: newProduct,
- },
- });
- },
- [router, urlProducts, defaultProducts]
- );
- if (!products) {
- // if the platform does not support any product, we don't render anything
- return null;
- }
- // TODO(aknaus): clean up
- // The package manager info is only shown for javascript platforms
- // until we improve multi snippet suppport
- const showPackageManagerInfo = platform?.indexOf('javascript') === 0;
- return (
- <Fragment>
- {showPackageManagerInfo && (
- <TextBlock>
- {lazyLoader
- ? tct('In this quick guide you’ll use our [loaderScript] to set up:', {
- loaderScript: <strong>Loader Script</strong>,
- })
- : tct('In this quick guide you’ll use [npm] or [yarn] to set up:', {
- npm: <strong>npm</strong>,
- yarn: <strong>yarn</strong>,
- })}
- </TextBlock>
- )}
- <Products>
- <Product
- label={t('Error Monitoring')}
- disabled={{reason: t("Let's admit it, we all have errors.")}}
- checked
- permanentDisabled
- />
- {products.includes(ProductSolution.PERFORMANCE_MONITORING) && (
- <Product
- label={t('Performance Monitoring')}
- description={t(
- 'Automatic performance issue detection across services and context on who is impacted, outliers, regressions, and the root cause of your slowdown.'
- )}
- docLink="https://docs.sentry.io/platforms/javascript/guides/react/performance/"
- onClick={() => handleClickProduct(ProductSolution.PERFORMANCE_MONITORING)}
- disabled={
- urlProducts.includes(ProductSolution.PROFILING)
- ? {
- reason: t(
- 'You must have Performance Monitoring set up to use Profiling. Disabling it is not possible while Profiling is selected.'
- ),
- }
- : disabledProducts?.find(
- disabledProduct =>
- disabledProduct.product === ProductSolution.PERFORMANCE_MONITORING
- )
- }
- checked={urlProducts.includes(ProductSolution.PERFORMANCE_MONITORING)}
- permanentDisabled={urlProducts.includes(ProductSolution.PROFILING)}
- />
- )}
- {products.includes(ProductSolution.SESSION_REPLAY) && (
- <Product
- label={t('Session Replay')}
- description={t(
- 'Video-like reproductions of user sessions with debugging context to help you confirm issue impact and troubleshoot faster.'
- )}
- docLink="https://docs.sentry.io/platforms/javascript/guides/react/session-replay/"
- onClick={() => handleClickProduct(ProductSolution.SESSION_REPLAY)}
- disabled={disabledProducts?.find(
- disabledProduct =>
- disabledProduct.product === ProductSolution.SESSION_REPLAY
- )}
- checked={urlProducts.includes(ProductSolution.SESSION_REPLAY)}
- />
- )}
- {products.includes(ProductSolution.PROFILING) && (
- <Product
- label={t('Profiling')}
- description={t(
- 'See the exact functions and lines of code causing your performance bottlenecks, so you can speed up troubleshooting and optimize resource consumption.'
- )}
- docLink="https://docs.sentry.io/platforms/python/profiling/"
- onClick={() => handleClickProduct(ProductSolution.PROFILING)}
- disabled={disabledProducts?.find(
- disabledProduct => disabledProduct.product === ProductSolution.PROFILING
- )}
- checked={urlProducts.includes(ProductSolution.PROFILING)}
- />
- )}
- </Products>
- {showPackageManagerInfo && lazyLoader && (
- <AlternativeInstallationAlert type="info" showIcon>
- {tct('Prefer to set up Sentry using [npm:npm] or [yarn:yarn]? [goHere].', {
- npm: <strong />,
- yarn: <strong />,
- goHere: (
- <Button onClick={skipLazyLoader} priority="link">
- {t('Go here')}
- </Button>
- ),
- })}
- </AlternativeInstallationAlert>
- )}
- <Divider withBottomMargin={withBottomMargin} />
- </Fragment>
- );
- }
- const Products = styled('div')`
- display: flex;
- flex-wrap: wrap;
- gap: ${space(1)};
- `;
- const ProductButtonWrapper = styled(Button)`
- ${p =>
- p.priority === 'primary' &&
- css`
- &,
- :hover {
- background: ${p.theme.purple100};
- color: ${p.theme.purple300};
- }
- `}
- `;
- const DisabledProductWrapper = styled(Button)`
- && {
- cursor: ${p => (p.disabled ? 'not-allowed' : 'pointer')};
- input {
- cursor: ${p =>
- p.disabled || p.priority === 'default' ? 'not-allowed' : 'pointer'};
- }
- }
- `;
- const PermanentDisabledProductWrapper = styled(Button)`
- && {
- &,
- :hover {
- background: ${p => p.theme.purple100};
- color: ${p => p.theme.purple300};
- opacity: 0.5;
- cursor: not-allowed;
- input {
- cursor: not-allowed;
- }
- }
- }
- `;
- const ProductButtonInner = styled('div')`
- display: grid;
- grid-template-columns: repeat(3, max-content);
- gap: ${space(1)};
- align-items: center;
- `;
- const Divider = styled('hr')<{withBottomMargin?: boolean}>`
- height: 1px;
- width: 100%;
- background: ${p => p.theme.border};
- border: none;
- ${p => p.withBottomMargin && `margin-bottom: ${space(3)}`}
- `;
- const TooltipDescription = styled('div')`
- display: flex;
- flex-direction: column;
- gap: ${space(0.5)};
- justify-content: flex-start;
- `;
- const AlternativeInstallationAlert = styled(Alert)`
- margin-top: ${space(3)};
- `;
|