import {Fragment} from 'react'; import styled from '@emotion/styled'; import memoize from 'lodash/memoize'; import type moment from 'moment-timezone'; import Tag from 'sentry/components/badge/tag'; import {Button, StyledButton} from 'sentry/components/button'; import Checkbox from 'sentry/components/checkbox'; import {CompactSelect} from 'sentry/components/compactSelect'; import {DateTime} from 'sentry/components/dateTime'; import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent'; 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'; 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 as SentryAppSchemaIssueLink)) { 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: React.ComponentProps['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 ? ( ) : ( ); } type Props = DeprecatedAsyncComponent['props'] & { app: SentryApp; }; type State = DeprecatedAsyncComponent['state'] & { currentPage: number; errorsOnly: boolean; eventType: string; requests: SentryAppWebhookRequest[]; }; export default class RequestLog extends DeprecatedAsyncComponent { shouldReload = true; get hasNextPage() { return (this.state.currentPage + 1) * MAX_PER_PAGE < this.state.requests.length; } get hasPrevPage() { return this.state.currentPage > 0; } getEndpoints(): ReturnType { const {slug} = this.props.app; const query: any = {}; if (this.state) { if (this.state.eventType !== ALL_EVENTS) { query.eventType = this.state.eventType; } if (this.state.errorsOnly) { query.errorsOnly = true; } } return [['requests', `/sentry-apps/${slug}/requests/`, {query}]]; } getDefaultState() { return { ...super.getDefaultState(), requests: [], eventType: ALL_EVENTS, errorsOnly: false, currentPage: 0, }; } handleChangeEventType = (eventType: string) => { this.setState( { eventType, currentPage: 0, }, this.remountComponent ); }; handleChangeErrorsOnly = () => { this.setState( { errorsOnly: !this.state.errorsOnly, currentPage: 0, }, this.remountComponent ); }; handleNextPage = () => { this.setState({ currentPage: this.state.currentPage + 1, }); }; handlePrevPage = () => { this.setState({ currentPage: this.state.currentPage - 1, }); }; renderLoading() { return this.renderBody(); } renderBody() { const {requests, eventType, errorsOnly, currentPage} = this.state; const {app} = this.props; const currentRequests = requests.slice( currentPage * MAX_PER_PAGE, (currentPage + 1) * MAX_PER_PAGE ); 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 => this.handleChangeEventType(opt?.value)} /> {}} /> {t('Errors Only')}
{t('Time')}
{t('Status Code')}
{app.status !== 'internal' &&
{t('Organization')}
}
{t('Event Type')}
{t('Webhook URL')}
{!this.state.loading ? ( {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.')} )}
) : ( )}