123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493 |
- import type React from 'react';
- import {Fragment, useEffect, useMemo, useState} from 'react';
- import styled from '@emotion/styled';
- import startCase from 'lodash/startCase';
- import moment from 'moment';
- import Alert from 'sentry/components/alert';
- import {Button} from 'sentry/components/button';
- import type {EventErrorData} from 'sentry/components/events/errorItem';
- import KeyValueList from 'sentry/components/events/interfaces/keyValueList';
- import List from 'sentry/components/list';
- import ListItem from 'sentry/components/list/listItem';
- import {
- GenericSchemaErrors,
- HttpProcessingErrors,
- JavascriptProcessingErrors,
- NativeProcessingErrors,
- ProguardProcessingErrors,
- } from 'sentry/constants/eventErrors';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {Event, Project} from 'sentry/types';
- import {defined} from 'sentry/utils';
- import {trackAnalytics} from 'sentry/utils/analytics';
- import {getAnalyticsDataForEvent} from 'sentry/utils/events';
- import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
- import useOrganization from 'sentry/utils/useOrganization';
- import type {ActionableItemErrors, ActionableItemTypes} from './actionableItemsUtils';
- import {
- ActionableItemWarning,
- shouldErrorBeShown,
- useFetchProguardMappingFiles,
- } from './actionableItemsUtils';
- import type {ActionableItemsResponse} from './useActionableItems';
- import {useActionableItems} from './useActionableItems';
- interface ErrorMessage {
- desc: React.ReactNode;
- title: string;
- data?: {
- absPath?: string;
- image_path?: string;
- mage_name?: string;
- message?: string;
- name?: string;
- partialMatchPath?: string;
- sdk_time?: string;
- server_time?: string;
- url?: string;
- urlPrefix?: string;
- } & Record<string, any>;
- meta?: Record<string, any>;
- }
- const keyMapping = {
- image_uuid: 'Debug ID',
- image_name: 'File Name',
- image_path: 'File Path',
- };
- function getErrorMessage(
- error: ActionableItemErrors | EventErrorData,
- meta?: Record<string, any>
- ): Array<ErrorMessage> {
- const errorData = error.data ?? {};
- const metaData = meta ?? {};
- switch (error.type) {
- // Event Errors
- case ProguardProcessingErrors.PROGUARD_MISSING_LINENO:
- return [
- {
- title: t('A proguard mapping file does not contain line info'),
- desc: null,
- data: errorData,
- meta: metaData,
- },
- ];
- case ProguardProcessingErrors.PROGUARD_MISSING_MAPPING:
- return [
- {
- title: t('A proguard mapping file was missing'),
- desc: null,
- data: errorData,
- meta: metaData,
- },
- ];
- case NativeProcessingErrors.NATIVE_MISSING_OPTIONALLY_BUNDLED_DSYM:
- return [
- {
- title: t('An optional debug information file was missing'),
- desc: null,
- data: errorData,
- meta: metaData,
- },
- ];
- case NativeProcessingErrors.NATIVE_MISSING_DSYM:
- return [
- {
- title: t('A required debug information file was missing'),
- desc: null,
- data: errorData,
- meta: metaData,
- },
- ];
- case NativeProcessingErrors.NATIVE_BAD_DSYM:
- return [
- {
- title: t('The debug information file used was broken'),
- desc: null,
- data: errorData,
- meta: metaData,
- },
- ];
- case JavascriptProcessingErrors.JS_MISSING_SOURCES_CONTENT:
- return [
- {
- title: t('Missing Sources Context'),
- desc: null,
- data: errorData,
- meta: metaData,
- },
- ];
- case HttpProcessingErrors.FETCH_GENERIC_ERROR:
- return [
- {
- title: t('Unable to fetch HTTP resource'),
- desc: null,
- data: errorData,
- meta: metaData,
- },
- ];
- case HttpProcessingErrors.RESTRICTED_IP:
- return [
- {
- title: t('Cannot fetch resource due to restricted IP address'),
- desc: null,
- data: errorData,
- meta: metaData,
- },
- ];
- case HttpProcessingErrors.SECURITY_VIOLATION:
- return [
- {
- title: t('Cannot fetch resource due to security violation'),
- desc: null,
- data: errorData,
- meta: metaData,
- },
- ];
- case GenericSchemaErrors.FUTURE_TIMESTAMP:
- return [
- {
- title: t('Invalid timestamp (in future)'),
- desc: null,
- data: errorData,
- meta: metaData,
- },
- ];
- case GenericSchemaErrors.CLOCK_DRIFT:
- return [
- {
- title: t('Clock drift detected in SDK'),
- desc: null,
- data: errorData,
- meta: metaData,
- },
- ];
- case GenericSchemaErrors.PAST_TIMESTAMP:
- return [
- {
- title: t('Invalid timestamp (too old)'),
- desc: null,
- data: errorData,
- meta: metaData,
- },
- ];
- case GenericSchemaErrors.VALUE_TOO_LONG:
- return [
- {
- title: t('Discarded value due to exceeding maximum length'),
- desc: null,
- data: errorData,
- meta: metaData,
- },
- ];
- case GenericSchemaErrors.INVALID_DATA:
- return [
- {
- title: t('Discarded invalid value'),
- desc: null,
- data: errorData,
- meta: metaData,
- },
- ];
- case GenericSchemaErrors.INVALID_ENVIRONMENT:
- return [
- {
- title: t('Environment cannot contain "/" or newlines'),
- desc: null,
- data: errorData,
- meta: metaData,
- },
- ];
- case GenericSchemaErrors.INVALID_ATTRIBUTE:
- return [
- {
- title: t('Discarded unknown attribute'),
- desc: null,
- data: errorData,
- meta: metaData,
- },
- ];
- default:
- return [];
- }
- }
- interface ExpandableErrorListProps {
- errorList: ErrorMessageType[];
- handleExpandClick: (type: ActionableItemTypes) => void;
- }
- function ExpandableErrorList({handleExpandClick, errorList}: ExpandableErrorListProps) {
- const [expanded, setExpanded] = useState(false);
- const firstError = errorList[0];
- const {title, desc, type} = firstError;
- const numErrors = errorList.length;
- const errorDataList = errorList.map(error => error.data ?? {});
- const cleanedData = useMemo(() => {
- const cleaned = errorDataList.map(errorData => {
- const data = {...errorData};
- if (data.message === 'None') {
- // Python ensures a message string, but "None" doesn't make sense here
- delete data.message;
- }
- if (typeof data.image_path === 'string') {
- // Separate the image name for readability
- const separator = /^([a-z]:\\|\\\\)/i.test(data.image_path) ? '\\' : '/';
- const path = data.image_path.split(separator);
- data.image_name = path.splice(-1, 1)[0];
- data.image_path = path.length ? path.join(separator) + separator : '';
- }
- if (typeof data.server_time === 'string' && typeof data.sdk_time === 'string') {
- data.message = t(
- 'Adjusted timestamps by %s',
- moment
- .duration(moment.utc(data.server_time).diff(moment.utc(data.sdk_time)))
- .humanize()
- );
- }
- return Object.entries(data)
- .map(([key, value]) => ({
- key,
- value,
- subject: keyMapping[key] || startCase(key),
- }))
- .filter(d => {
- if (!d.value) {
- return true;
- }
- return !!d.value;
- });
- });
- return cleaned;
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [errorDataList]);
- return (
- <List symbol="bullet">
- <StyledListItem>
- <ErrorTitleFlex>
- <strong>
- {title} ({numErrors})
- </strong>
- <ToggleButton
- priority="link"
- size="zero"
- onClick={() => {
- setExpanded(!expanded);
- handleExpandClick(type);
- }}
- >
- {expanded ? t('Collapse') : t('Expand')}
- </ToggleButton>
- </ErrorTitleFlex>
- {expanded && (
- <div>
- {desc && <Description>{desc}</Description>}
- {cleanedData.map((data, idx) => {
- return (
- <div key={idx}>
- <KeyValueList data={data} isContextData />
- {idx !== numErrors - 1 && <hr />}
- </div>
- );
- })}
- </div>
- )}
- </StyledListItem>
- </List>
- );
- }
- interface ErrorMessageType extends ErrorMessage {
- type: ActionableItemTypes;
- }
- function groupedErrors(
- event: Event,
- data?: ActionableItemsResponse,
- progaurdErrors?: EventErrorData[]
- ): Record<ActionableItemTypes, ErrorMessageType[]> | {} {
- if (!data || !progaurdErrors || !event) {
- return {};
- }
- const {_meta} = event;
- const errors = [...data.errors, ...progaurdErrors]
- .filter(error => shouldErrorBeShown(error, event))
- .flatMap((error, errorIdx) =>
- getErrorMessage(error, _meta?.errors?.[errorIdx]).map(message => ({
- ...message,
- type: error.type,
- }))
- );
- const grouped = errors.reduce((rv, error) => {
- rv[error.type] = rv[error.type] || [];
- rv[error.type].push(error);
- return rv;
- }, Object.create(null));
- return grouped;
- }
- interface ActionableItemsProps {
- event: Event;
- isShare: boolean;
- project: Project;
- }
- export function ActionableItems({event, project, isShare}: ActionableItemsProps) {
- const organization = useOrganization();
- const {data, isLoading} = useActionableItems({
- eventId: event.id,
- orgSlug: organization.slug,
- projectSlug: project.slug,
- });
- const {proguardErrorsLoading, proguardErrors} = useFetchProguardMappingFiles({
- event,
- project,
- isShare,
- });
- useEffect(() => {
- if (proguardErrors?.length) {
- if (proguardErrors[0]?.type === 'proguard_potentially_misconfigured_plugin') {
- trackAnalytics('issue_error_banner.proguard_misconfigured.displayed', {
- organization,
- group: event?.groupID,
- platform: project.platform,
- });
- } else if (proguardErrors[0]?.type === 'proguard_missing_mapping') {
- trackAnalytics('issue_error_banner.proguard_missing_mapping.displayed', {
- organization,
- group: event?.groupID,
- platform: project.platform,
- });
- }
- }
- // Just for analytics, only track this once per visit
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
- const errorMessages = groupedErrors(event, data, proguardErrors);
- useRouteAnalyticsParams({
- show_actionable_items_cta: data ? data.errors.length > 0 : false,
- actionable_items: data ? Object.keys(errorMessages) : [],
- });
- if (
- isLoading ||
- !defined(data) ||
- data.errors.length === 0 ||
- Object.keys(errorMessages).length === 0
- ) {
- return null;
- }
- if (proguardErrorsLoading) {
- // XXX: This is necessary for acceptance tests to wait until removal since there is
- // no visual loading state.
- return <HiddenDiv data-test-id="event-errors-loading" />;
- }
- const analyticsParams = {
- organization,
- project_id: event.projectID,
- group_id: event.groupID,
- ...getAnalyticsDataForEvent(event),
- };
- const handleExpandClick = (type: ActionableItemTypes) => {
- trackAnalytics('actionable_items.expand_clicked', {
- ...analyticsParams,
- type,
- });
- };
- const hasErrorAlert = Object.keys(errorMessages).some(
- error =>
- !ActionableItemWarning.includes(
- error as ProguardProcessingErrors | NativeProcessingErrors | GenericSchemaErrors
- )
- );
- for (const errorKey in Object.keys(errorMessages)) {
- const isWarning = ActionableItemWarning.includes(
- errorKey as ProguardProcessingErrors | NativeProcessingErrors | GenericSchemaErrors
- );
- const shouldDelete = hasErrorAlert ? isWarning : !isWarning;
- if (shouldDelete) {
- delete errorMessages[errorKey];
- }
- }
- return (
- <StyledAlert
- defaultExpanded
- showIcon
- type={hasErrorAlert ? 'error' : 'warning'}
- expand={
- <Fragment>
- {Object.keys(errorMessages).map((error, idx) => {
- return (
- <ExpandableErrorList
- key={idx}
- errorList={errorMessages[error]}
- handleExpandClick={handleExpandClick}
- />
- );
- })}
- </Fragment>
- }
- >
- {hasErrorAlert
- ? t('Sentry has identified the following problems for you to fix')
- : t('Sentry has identified the following problems for you to monitor')}
- </StyledAlert>
- );
- }
- const Description = styled('div')`
- margin-top: ${space(0.5)};
- `;
- const StyledAlert = styled(Alert)`
- margin: 0 30px;
- `;
- const StyledListItem = styled(ListItem)`
- margin-bottom: ${space(0.75)};
- `;
- const ToggleButton = styled(Button)`
- color: ${p => p.theme.subText};
- text-decoration: underline;
- :hover,
- :focus {
- color: ${p => p.theme.textColor};
- }
- `;
- const ErrorTitleFlex = styled('div')`
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: ${space(1)};
- `;
- const HiddenDiv = styled('div')`
- display: none;
- `;
|