import {ReactNode} from 'react'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import {useQuery} from '@tanstack/react-query'; import CircleIndicator from 'sentry/components/circleIndicator'; import {IconOpen} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {getDuration} from 'sentry/utils/formatters'; import usePageFilters from 'sentry/utils/usePageFilters'; import { getHostStatusBreakdownEventView, getHostStatusBreakdownQuery, } from 'sentry/views/starfish/modules/APIModule/queries'; import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery'; type Props = { host: string; }; export function HostDetails({host}: Props) { const theme = useTheme(); const pageFilter = usePageFilters(); const {isLoading: isStatusBreakdownLoading, data: statusBreakdown} = useSpansQuery({ queryString: getHostStatusBreakdownQuery({ domain: host, datetime: pageFilter.selection.datetime, }), eventView: getHostStatusBreakdownEventView({ domain: host, datetime: pageFilter.selection.datetime, }), initialData: [], }); const hostMarketingName = Object.keys(EXTERNAL_APIS).find(key => host.includes(key)); const failures = statusBreakdown?.filter((item: any) => item.status > 299); const successes = statusBreakdown?.filter((item: any) => item.status < 300); const totalCount = statusBreakdown?.reduce( (acc: number, item: any) => acc + item.count, 0 ); const externalApi = hostMarketingName && EXTERNAL_APIS[hostMarketingName]; const {isLoading: isStatusLoading, data: statusData} = useQuery({ queryKey: ['domain-status', host], queryFn: () => fetch(`${externalApi?.statusPage}?format=json`).then(res => res.json()), retry: false, refetchOnWindowFocus: false, initialData: {}, enabled: !!externalApi, }); return ( {externalApi?.faviconLink && ( )} {hostMarketingName ? ( {hostMarketingName} {` (${host})`} ) : ( {host} )} {!isStatusLoading && statusData.status ? ( {' '} {statusData.status.description} ) : null} {externalApi?.statusPage && ( {t('Status')} )} {externalApi?.description} {isStatusBreakdownLoading ? null : failures?.map((item: any) => { const errorCodeDescription = ERROR_CODE_DESCRIPTIONS[item.status]; return ( {`${item.status}${ errorCodeDescription ? ` ${errorCodeDescription}` : '' } (${item.count})`} } /> ); })} {isStatusBreakdownLoading ? null : successes?.map((item: any) => ( ))} ); } const DetailsContainer = styled('div')` padding: ${space(2)}; border-radius: ${p => p.theme.borderRadius}; border: 1px solid ${p => p.theme.border}; margin-bottom: ${space(2)}; `; const FlexContainer = styled('div')` display: flex; flex-direction: row; `; const Host = styled('span')` font-weight: bold; `; const StatusText = styled('span')` margin-left: ${space(2)}; `; const StyledIconOpen = styled(IconOpen)` flex: 0; top: 2px; position: relative; margin-left: ${space(0.5)}; `; const LinkContainer = styled('span')` flex: 1; text-align: right; `; const StatusContainer = styled('span')` margin-top: ${space(1)}; flex: 1; height: 20px; display: flex; flex-direction: row; gap: ${space(1)}; `; const MeterBarContainer = styled('div')` width: 150px; top: -6px; position: relative; `; const Failure = styled('span')` font-weight: bold; color: ${p => p.theme.red300}; `; const ExternalApiDescription = styled('span')` font-size: ${p => p.theme.fontSizeSmall}; color: ${p => p.theme.gray300}; `; const ERROR_CODE_DESCRIPTIONS = { 400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 408: 'Request Timeout', 429: 'Too Many Requests', 500: 'Internal Server Error', }; const EXTERNAL_APIS = { stripe: { statusPage: 'https://status.stripe.com/', faviconLink: 'https://stripe.com/favicon.ico', description: t( 'Stripe is a suite of payment APIs that powers commerce for online businesses of all sizes' ), }, twilio: { statusPage: 'https://status.twilio.com/', faviconLink: 'https://www.twilio.com/favicon.ico', description: t('Twilio is a cloud communications platform as a service company.'), }, sendgrid: { statusPage: 'https://status.sendgrid.com/', faviconLink: 'https://sendgrid.com/favicon.ico', description: t( 'SendGrid is a cloud-based SMTP provider that allows you to send email without having to maintain email servers.' ), }, plaid: { statusPage: 'https://status.plaid.com/', faviconLink: 'https://plaid.com/favicon.ico', description: t( 'Plaid is a technology platform that enables applications to connect with users bank accounts.' ), }, paypal: {statusPage: 'https://www.paypal-status.com/'}, braintree: {statusPage: 'https://status.braintreepayments.com/'}, clickup: { statusPage: 'https://clickup.statuspage.io/', faviconLink: 'https://clickup.com/favicon.ico', description: t( 'ClickUp is a productivity platform that provides a fundamentally new way to work.' ), }, github: { statusPage: 'https://www.githubstatus.com/', faviconLink: 'https://github.com/favicon.ico', description: t( 'GitHub is a provider of Internet hosting for software development and version control.' ), }, gitlab: { statusPage: 'https://status.gitlab.com/', faviconLink: 'https://gitlab.com/favicon.ico', description: t( 'GitLab is a web-based DevOps lifecycle tool that provides a Git-repository manager providing wiki, issue-tracking and CI/CD pipeline features.' ), }, bitbucket: { statusPage: 'https://bitbucket.status.atlassian.com/', faviconLink: 'https://bitbucket.org/favicon.ico', description: t( 'Bitbucket is a web-based version control repository hosting service.' ), }, jira: { statusPage: 'https://jira.status.atlassian.com/', faviconLink: 'https://jira.com/favicon.ico', description: t( 'Jira is a proprietary issue tracking product developed by Atlassian.' ), }, asana: { statusPage: 'https://trust.asana.com/', faviconLink: 'https://asana.com/favicon.ico', description: t( 'Asana is a web and mobile application designed to help teams organize, track, and manage their work.' ), }, trello: {statusPage: 'https://trello.status.atlassian.com/'}, zendesk: {statusPage: 'https://status.zendesk.com/'}, intercom: {statusPage: 'https://www.intercomstatus.com/'}, freshdesk: {statusPage: 'https://status.freshdesk.com/'}, linear: {statusPage: 'https://status.linear.app/'}, gaussMoney: {}, }; export const INTERNAL_API_REGEX = /\d\.\d|localhost/; export function MeterBar({ minWidth, meterItems, row, total, color, meterText, }: { color: string; meterItems: string[]; minWidth: number; row: any; total: number; meterText?: ReactNode; }) { const widths = [] as number[]; meterItems.reduce((acc, item, index) => { const width = Math.max( Math.min( (100 * row[item]) / total - acc, 100 - acc - minWidth * (meterItems.length - index) ), minWidth ); widths.push(width); return acc + width; }, 0); return ( {meterText ?? `${getDuration(row[meterItems[0]] / 1000, 0, true, true)}`} ); } const MeterContainer = styled('span')<{width: number}>` display: flex; width: ${p => p.width}%; height: ${space(1)}; background-color: ${p => p.theme.gray100}; margin-bottom: 4px; `; const Meter = styled('span')<{ color: string; width: number; }>` display: block; width: ${p => p.width}%; height: 100%; background-color: ${p => p.color}; `; const MeterText = styled('span')` font-size: ${p => p.theme.fontSizeExtraSmall}; color: ${p => p.theme.gray300}; white-space: nowrap; `;