import {Fragment, useCallback, useMemo, useState} from 'react'; import styled from '@emotion/styled'; import memoize from 'lodash/memoize'; import type moment from 'moment-timezone'; import {Button, StyledButton} from 'sentry/components/button'; import {CompactSelect} from 'sentry/components/compactSelect'; import {Tag, type TagProps} from 'sentry/components/core/badge/tag'; import {Checkbox} from 'sentry/components/core/checkbox'; import {DateTime} from 'sentry/components/dateTime'; import EmptyMessage from 'sentry/components/emptyMessage'; import ExternalLink from 'sentry/components/links/externalLink'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import Panel from 'sentry/components/panels/panel'; import PanelBody from 'sentry/components/panels/panelBody'; import PanelHeader from 'sentry/components/panels/panelHeader'; import PanelItem from 'sentry/components/panels/panelItem'; import {IconChevron, IconFlag, IconOpen} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type { SentryApp, SentryAppSchemaIssueLink, SentryAppWebhookRequest, } from 'sentry/types/integrations'; import {shouldUse24Hours} from 'sentry/utils/dates'; import {type ApiQueryKey, useApiQuery} from 'sentry/utils/queryClient'; const ALL_EVENTS = t('All Events'); const MAX_PER_PAGE = 10; const is24Hours = shouldUse24Hours(); const componentHasSelectUri = (issueLinkComponent: SentryAppSchemaIssueLink): boolean => { const hasSelectUri = (fields: any[]): boolean => fields.some(field => field.type === 'select' && 'uri' in field); const createHasSelectUri = hasSelectUri(issueLinkComponent.create.required_fields) || hasSelectUri(issueLinkComponent.create.optional_fields || []); const linkHasSelectUri = hasSelectUri(issueLinkComponent.link.required_fields) || hasSelectUri(issueLinkComponent.link.optional_fields || []); return createHasSelectUri || linkHasSelectUri; }; const getEventTypes = memoize((app: SentryApp) => { // TODO(nola): ideally this would be kept in sync with EXTENDED_VALID_EVENTS on the backend let issueLinkEvents: string[] = []; const issueLinkComponent = (app.schema.elements || []).find( element => element.type === 'issue-link' ); if (issueLinkComponent) { issueLinkEvents = ['external_issue.created', 'external_issue.linked']; if (componentHasSelectUri(issueLinkComponent)) { issueLinkEvents.push('select_options.requested'); } } const events = [ ALL_EVENTS, // Internal apps don't have installation webhooks ...(app.status !== 'internal' ? ['installation.created', 'installation.deleted'] : []), ...(app.events.includes('error') ? ['error.created'] : []), ...(app.events.includes('issue') ? ['issue.created', 'issue.resolved', 'issue.ignored', 'issue.assigned'] : []), ...(app.isAlertable ? [ 'event_alert.triggered', 'metric_alert.open', 'metric_alert.resolved', 'metric_alert.critical', 'metric_alert.warning', ] : []), ...issueLinkEvents, ]; return events; }); function ResponseCode({code}: {code: number}) { let type: TagProps['type'] = 'error'; if (code <= 399 && code >= 300) { type = 'warning'; } else if (code <= 299 && code >= 100) { type = 'success'; } return ( {code === 0 ? 'timeout' : code} ); } function TimestampLink({date, link}: {date: moment.MomentInput; link?: string}) { return link ? ( ) : ( ); } interface RequestLogProps { app: SentryApp; } function makeRequestLogQueryKey( slug: string, query: Record ): ApiQueryKey { return [`/sentry-apps/${slug}/webhook-requests/`, {query}]; } export default function RequestLog({app}: RequestLogProps) { const [currentPage, setCurrentPage] = useState(0); const [errorsOnly, setErrorsOnly] = useState(false); const [eventType, setEventType] = useState(ALL_EVENTS); const {slug} = app; const query: any = {}; if (eventType !== ALL_EVENTS) { query.eventType = eventType; } if (errorsOnly) { query.errorsOnly = true; } const { data: requests = [], isLoading, refetch, } = useApiQuery(makeRequestLogQueryKey(slug, query), { staleTime: Infinity, }); const currentRequests = useMemo( () => requests.slice(currentPage * MAX_PER_PAGE, (currentPage + 1) * MAX_PER_PAGE), [currentPage, requests] ); const hasNextPage = useMemo( () => (currentPage + 1) * MAX_PER_PAGE < requests.length, [currentPage, requests] ); const hasPrevPage = useMemo(() => currentPage > 0, [currentPage]); const handleChangeEventType = useCallback( (newEventType: string) => { setEventType(newEventType); setCurrentPage(0); refetch(); }, [refetch] ); const handleChangeErrorsOnly = useCallback(() => { setErrorsOnly(!errorsOnly); setCurrentPage(0); refetch(); }, [errorsOnly, refetch]); const handleNextPage = useCallback(() => { setCurrentPage(currentPage + 1); }, [currentPage]); const handlePrevPage = useCallback(() => { setCurrentPage(currentPage - 1); }, [currentPage]); return (
{t('Request Log')}

{t( 'This log shows the status of any outgoing webhook requests from Sentry to your integration.' )}

({ value: type, label: type, }))} onChange={opt => handleChangeEventType(opt?.value)} /> {}} /> {t('Errors Only')}
{t('Time')}
{t('Status Code')}
{app.status !== 'internal' &&
{t('Organization')}
}
{t('Event Type')}
{t('Webhook URL')}
{!isLoading ? ( {currentRequests.length > 0 ? ( currentRequests.map((request, idx) => ( {app.status !== 'internal' && (
{request.organization ? request.organization.name : null}
)}
{request.eventType}
{request.webhookUrl}
)) ) : ( }> {t('No requests found in the last 30 days.')} )}
) : ( )}