123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615 |
- import {createRef, Fragment, useEffect, useState} from 'react';
- import styled from '@emotion/styled';
- import {Location} from 'history';
- import omit from 'lodash/omit';
- import Alert from 'sentry/components/alert';
- import {Button} from 'sentry/components/button';
- import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton';
- import DateTime from 'sentry/components/dateTime';
- import {Chunk} from 'sentry/components/events/contexts/chunk';
- import {EventAttachments} from 'sentry/components/events/eventAttachments';
- import {
- isNotMarkMeasurement,
- isNotPerformanceScoreMeasurement,
- TraceEventCustomPerformanceMetric,
- } from 'sentry/components/events/eventCustomPerformanceMetrics';
- import {Entries} from 'sentry/components/events/eventEntries';
- import {EventEvidence} from 'sentry/components/events/eventEvidence';
- import {EventExtraData} from 'sentry/components/events/eventExtraData';
- import {EventSdk} from 'sentry/components/events/eventSdk';
- import {EventViewHierarchy} from 'sentry/components/events/eventViewHierarchy';
- import {Breadcrumbs} from 'sentry/components/events/interfaces/breadcrumbs';
- import NewTraceDetailsSpanDetail, {
- SpanDetailContainer,
- SpanDetailProps,
- SpanDetails,
- } from 'sentry/components/events/interfaces/spans/newTraceDetailsSpanDetails';
- import {
- getFormattedTimeRangeWithLeadingAndTrailingZero,
- getSpanOperation,
- } from 'sentry/components/events/interfaces/spans/utils';
- import {generateStats} from 'sentry/components/events/opsBreakdown';
- import {EventRRWebIntegration} from 'sentry/components/events/rrwebIntegration';
- import {DataSection} from 'sentry/components/events/styles';
- import FileSize from 'sentry/components/fileSize';
- import ProjectBadge from 'sentry/components/idBadge/projectBadge';
- import Link from 'sentry/components/links/link';
- import LoadingIndicator from 'sentry/components/loadingIndicator';
- import {
- ErrorDot,
- ErrorLevel,
- ErrorMessageContent,
- ErrorMessageTitle,
- ErrorTitle,
- } from 'sentry/components/performance/waterfall/rowDetails';
- import PerformanceDuration from 'sentry/components/performanceDuration';
- import QuestionTooltip from 'sentry/components/questionTooltip';
- import {generateIssueEventTarget} from 'sentry/components/quickTrace/utils';
- import {Tooltip} from 'sentry/components/tooltip';
- import {PAGE_URL_PARAM} from 'sentry/constants/pageFilters';
- import {IconChevron, IconOpen} from 'sentry/icons';
- import {t, tn} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import {EntryBreadcrumbs, EntryType, EventTransaction, Organization} from 'sentry/types';
- import {objectIsEmpty} from 'sentry/utils';
- import {trackAnalytics} from 'sentry/utils/analytics';
- import getDynamicText from 'sentry/utils/getDynamicText';
- import {PageErrorProvider} from 'sentry/utils/performance/contexts/pageError';
- import {WEB_VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants';
- import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes';
- import {useLocation} from 'sentry/utils/useLocation';
- import useOrganization from 'sentry/utils/useOrganization';
- import useProjects from 'sentry/utils/useProjects';
- import {isCustomMeasurement} from 'sentry/views/dashboards/utils';
- import {ProfileGroupProvider} from 'sentry/views/profiling/profileGroupProvider';
- import {ProfileContext, ProfilesProvider} from 'sentry/views/profiling/profilesProvider';
- import DetailPanel from 'sentry/views/starfish/components/detailPanel';
- import {transactionSummaryRouteWithQuery} from '../transactionSummary/utils';
- import {EventDetail} from './newTraceDetailsContent';
- import {Row, Tags} from './styles';
- type DetailPanelProps = {
- detail: EventDetail | SpanDetailProps | undefined;
- onClose: () => void;
- };
- type EventDetailProps = {
- detail: EventDetail;
- location: Location;
- organization: Organization;
- };
- function OpsBreakdown({event}: {event: EventTransaction}) {
- const [showingAll, setShowingAll] = useState(false);
- const breakdown = event && generateStats(event, {type: 'no_filter'});
- if (!breakdown) {
- return null;
- }
- const renderText = showingAll ? t('Show less') : t('Show more') + '...';
- return (
- breakdown && (
- <Row
- title={
- <FlexBox style={{gap: '5px'}}>
- {t('Ops Breakdown')}
- <QuestionTooltip
- title={t('Applicable to the children of this event only')}
- size="xs"
- />
- </FlexBox>
- }
- >
- <div style={{display: 'flex', flexDirection: 'column', gap: space(0.25)}}>
- {breakdown.slice(0, showingAll ? breakdown.length : 5).map(currOp => {
- const {name, percentage, totalInterval} = currOp;
- const operationName = typeof name === 'string' ? name : t('Other');
- const pctLabel = isFinite(percentage) ? Math.round(percentage * 100) : '∞';
- return (
- <div key={operationName}>
- {operationName}:{' '}
- <PerformanceDuration seconds={totalInterval} abbreviation /> ({pctLabel}%)
- </div>
- );
- })}
- {breakdown.length > 5 && (
- <a onClick={() => setShowingAll(prev => !prev)}>{renderText}</a>
- )}
- </div>
- </Row>
- )
- );
- }
- function BreadCrumbsSection({
- event,
- organization,
- }: {
- event: EventTransaction;
- organization: Organization;
- }) {
- const [showBreadCrumbs, setShowBreadCrumbs] = useState(false);
- const breadCrumbsContainerRef = createRef<HTMLDivElement>();
- useEffect(() => {
- setTimeout(() => {
- if (showBreadCrumbs) {
- breadCrumbsContainerRef.current?.scrollIntoView({
- behavior: 'smooth',
- block: 'end',
- });
- }
- }, 100);
- }, [showBreadCrumbs, breadCrumbsContainerRef]);
- const matchingEntry: EntryBreadcrumbs | undefined = event?.entries.find(
- (entry): entry is EntryBreadcrumbs => entry.type === EntryType.BREADCRUMBS
- );
- if (!matchingEntry) {
- return null;
- }
- const renderText = showBreadCrumbs ? t('Hide Breadcrumbs') : t('Show Breadcrumbs');
- const chevron = <IconChevron size="xs" direction={showBreadCrumbs ? 'up' : 'down'} />;
- return (
- <Fragment>
- <a
- style={{display: 'flex', alignItems: 'center', gap: space(0.5)}}
- onClick={() => {
- setShowBreadCrumbs(prev => !prev);
- }}
- >
- {renderText} {chevron}
- </a>
- <div ref={breadCrumbsContainerRef}>
- {showBreadCrumbs && (
- <Breadcrumbs
- hideTitle
- data={matchingEntry.data}
- event={event}
- organization={organization}
- />
- )}
- </div>
- </Fragment>
- );
- }
- function EventDetails({detail, organization, location}: EventDetailProps) {
- const {projects} = useProjects();
- if (!detail.event) {
- return <LoadingIndicator />;
- }
- const {user, contexts, projectSlug} = detail.event;
- const {feedback} = contexts ?? {};
- const eventJsonUrl = `/api/0/projects/${organization.slug}/${detail.traceFullDetailedEvent.project_slug}/events/${detail.traceFullDetailedEvent.event_id}/json/`;
- const project = projects.find(proj => proj.slug === detail.event?.projectSlug);
- const {errors, performance_issues} = detail.traceFullDetailedEvent;
- const hasIssues = errors.length + performance_issues.length > 0;
- const startTimestamp = Math.min(
- detail.traceFullDetailedEvent.start_timestamp,
- detail.traceFullDetailedEvent.timestamp
- );
- const endTimestamp = Math.max(
- detail.traceFullDetailedEvent.start_timestamp,
- detail.traceFullDetailedEvent.timestamp
- );
- const {start: startTimeWithLeadingZero, end: endTimeWithLeadingZero} =
- getFormattedTimeRangeWithLeadingAndTrailingZero(startTimestamp, endTimestamp);
- const duration = (endTimestamp - startTimestamp) * 1000;
- const durationString = `${Number(duration.toFixed(3)).toLocaleString()}ms`;
- const measurementNames = Object.keys(detail.traceFullDetailedEvent.measurements ?? {})
- .filter(name => isCustomMeasurement(`measurements.${name}`))
- .filter(isNotMarkMeasurement)
- .filter(isNotPerformanceScoreMeasurement)
- .sort();
- const renderMeasurements = () => {
- if(!detail.event){
- return null;
- }
- const {measurements} = detail.event;
- const measurementKeys = Object.keys(measurements ?? {})
- .filter(name => Boolean(WEB_VITAL_DETAILS[`measurements.${name}`]))
- .sort();
- if (!measurements || measurementKeys.length <= 0) {
- return null;
- }
- return (
- <Fragment>
- {measurementKeys.map(measurement => (
- <Row
- key={measurement}
- title={WEB_VITAL_DETAILS[`measurements.${measurement}`]?.name}
- >
- <PerformanceDuration milliseconds={Number(measurements[measurement].value.toFixed(3))} abbreviation />
- </Row>
- ))}
- </Fragment>
- );
- };
- const renderGoToProfileButton = () => {
- if (!detail.traceFullDetailedEvent.profile_id) {
- return null;
- }
- const target = generateProfileFlamechartRoute({
- orgSlug: organization.slug,
- projectSlug: detail.traceFullDetailedEvent.project_slug,
- profileId: detail.traceFullDetailedEvent.profile_id,
- });
- function handleOnClick() {
- trackAnalytics('profiling_views.go_to_flamegraph', {
- organization,
- source: 'performance.trace_view',
- });
- }
- return (
- <StyledButton size="xs" to={target} onClick={handleOnClick}>
- {t('View Profile')}
- </StyledButton>
- );
- };
- return (
- <Wrapper>
- <Actions>
- <Button
- size="sm"
- icon={<IconOpen />}
- href={eventJsonUrl}
- external
- onClick={() =>
- trackAnalytics('performance_views.event_details.json_button_click', {
- organization,
- })
- }
- >
- {t('JSON')} (<FileSize bytes={detail.event?.size} />)
- </Button>
- </Actions>
- <Title>
- <Tooltip title={detail.traceFullDetailedEvent.project_slug}>
- <ProjectBadge
- project={
- project ? project : {slug: detail.traceFullDetailedEvent.project_slug}
- }
- avatarSize={50}
- hideName
- />
- </Tooltip>
- <div>
- <div>{t('Event')}</div>
- <TransactionOp>
- {' '}
- {detail.traceFullDetailedEvent['transaction.op']}
- </TransactionOp>
- </div>
- </Title>
- {hasIssues && (
- <Alert
- system
- defaultExpanded
- type="error"
- expand={[
- ...detail.traceFullDetailedEvent.errors,
- ...detail.traceFullDetailedEvent.performance_issues,
- ].map(error => (
- <ErrorMessageContent key={error.event_id}>
- <ErrorDot level={error.level} />
- <ErrorLevel>{error.level}</ErrorLevel>
- <ErrorTitle>
- <Link to={generateIssueEventTarget(error, organization)}>
- {error.title}
- </Link>
- </ErrorTitle>
- </ErrorMessageContent>
- ))}
- >
- <ErrorMessageTitle>
- {tn(
- '%s issue occurred in this transaction.',
- '%s issues occurred in this transaction.',
- detail.traceFullDetailedEvent.errors.length +
- detail.traceFullDetailedEvent.performance_issues.length
- )}
- </ErrorMessageTitle>
- </Alert>
- )}
- <StyledTable className="table key-value">
- <tbody>
- <Row title={<TransactionIdTitle>{t('Event ID')}</TransactionIdTitle>}>
- {detail.traceFullDetailedEvent.event_id}
- <CopyToClipboardButton
- borderless
- size="zero"
- iconSize="xs"
- text={`${window.location.href.replace(window.location.hash, '')}#txn-${
- detail.traceFullDetailedEvent.event_id
- }`}
- />
- </Row>
- <Row title={t('Description')}>
- <Link
- to={transactionSummaryRouteWithQuery({
- orgSlug: organization.slug,
- transaction: detail.traceFullDetailedEvent.transaction,
- query: omit(location.query, Object.values(PAGE_URL_PARAM)),
- projectID: String(detail.traceFullDetailedEvent.project_id),
- })}
- >
- {detail.traceFullDetailedEvent.transaction}
- </Link>
- </Row>
- {detail.traceFullDetailedEvent.profile_id && (
- <Row title="Profile ID" extra={renderGoToProfileButton()}>
- {detail.traceFullDetailedEvent.profile_id}
- </Row>
- )}
- <Row title="Duration">{durationString}</Row>
- <Row title="Date Range">
- {getDynamicText({
- fixed: 'Mar 19, 2021 11:06:27 AM UTC',
- value: (
- <Fragment>
- <DateTime date={startTimestamp * 1000} />
- {` (${startTimeWithLeadingZero})`}
- </Fragment>
- ),
- })}
- <br />
- {getDynamicText({
- fixed: 'Mar 19, 2021 11:06:28 AM UTC',
- value: (
- <Fragment>
- <DateTime date={endTimestamp * 1000} />
- {` (${endTimeWithLeadingZero})`}
- </Fragment>
- ),
- })}
- </Row>
- <OpsBreakdown event={detail.event} />
- {renderMeasurements()}
- <Tags
- enableHiding
- location={location}
- organization={organization}
- transaction={detail.traceFullDetailedEvent}
- />
- {measurementNames.length > 0 && (
- <tr>
- <td className="key">{t('Custom Metrics')}</td>
- <td className="value">
- <Measurements>
- {measurementNames.map(name => {
- return (
- detail.event && (
- <TraceEventCustomPerformanceMetric
- key={name}
- event={detail.event}
- name={name}
- location={location}
- organization={organization}
- source={undefined}
- isHomepage={false}
- />
- )
- );
- })}
- </Measurements>
- </td>
- </tr>
- )}
- </tbody>
- </StyledTable>
- {project && <EventEvidence event={detail.event} project={project} />}
- {projectSlug && (
- <Entries
- definedEvent={detail.event}
- projectSlug={projectSlug}
- group={undefined}
- organization={organization}
- isShare={false}
- hideBeforeReplayEntries
- hideBreadCrumbs
- />
- )}
- {!objectIsEmpty(feedback) && (
- <Chunk
- key="feedback"
- type="feedback"
- alias="feedback"
- group={undefined}
- event={detail.event}
- value={feedback}
- />
- )}
- {user && !objectIsEmpty(user) && (
- <Chunk
- key="user"
- type="user"
- alias="user"
- group={undefined}
- event={detail.event}
- value={user}
- />
- )}
- <EventExtraData event={detail.event} />
- <EventSdk sdk={detail.event.sdk} meta={detail.event._meta?.sdk} />
- <BreadCrumbsSection event={detail.event} organization={organization} />
- {projectSlug && <EventAttachments event={detail.event} projectSlug={projectSlug} />}
- {project && <EventViewHierarchy event={detail.event} project={project} />}
- {projectSlug && (
- <EventRRWebIntegration
- event={detail.event}
- orgId={organization.slug}
- projectSlug={projectSlug}
- />
- )}
- </Wrapper>
- );
- }
- function SpanDetailsBody({
- detail,
- organization,
- }: {
- detail: SpanDetailProps;
- organization: Organization;
- }) {
- const {projects} = useProjects();
- const project = projects.find(proj => proj.slug === detail.event?.projectSlug);
- const profileId = detail?.event?.contexts?.profile?.profile_id ?? null;
- return (
- <Wrapper>
- <Title>
- <Tooltip title={detail.event.projectSlug}>
- <ProjectBadge
- project={project ? project : {slug: detail.event.projectSlug || ''}}
- avatarSize={50}
- hideName
- />
- </Tooltip>
- <div>
- <div>{t('Span')}</div>
- <TransactionOp> {getSpanOperation(detail.span)}</TransactionOp>
- </div>
- </Title>
- {detail.event.projectSlug && (
- <ProfilesProvider
- orgSlug={organization.slug}
- projectSlug={detail.event.projectSlug}
- profileId={profileId || ''}
- >
- <ProfileContext.Consumer>
- {profiles => (
- <ProfileGroupProvider
- type="flamechart"
- input={profiles?.type === 'resolved' ? profiles.data : null}
- traceID={profileId || ''}
- >
- <NewTraceDetailsSpanDetail {...detail} />
- </ProfileGroupProvider>
- )}
- </ProfileContext.Consumer>
- </ProfilesProvider>
- )}
- </Wrapper>
- );
- }
- export function isEventDetail(
- detail: EventDetail | SpanDetailProps
- ): detail is EventDetail {
- return !('span' in detail);
- }
- function TraceViewDetailPanel({detail, onClose}: DetailPanelProps) {
- const organization = useOrganization();
- const location = useLocation();
- return (
- <PageErrorProvider>
- <DetailPanel detailKey={detail ? 'open' : undefined} onClose={onClose}>
- {detail &&
- (isEventDetail(detail) ? (
- <EventDetails
- location={location}
- organization={organization}
- detail={detail}
- />
- ) : (
- <SpanDetailsBody organization={organization} detail={detail} />
- ))}
- </DetailPanel>
- </PageErrorProvider>
- );
- }
- const Wrapper = styled('div')`
- display: flex;
- flex-direction: column;
- gap: ${space(2)};
- ${DataSection} {
- padding: 0;
- }
- ${SpanDetails} {
- padding: 0;
- }
- ${SpanDetailContainer} {
- border-bottom: none;
- }
- `;
- const FlexBox = styled('div')`
- display: flex;
- align-items: center;
- `;
- const Actions = styled('div')`
- display: flex;
- align-items: center;
- justify-content: flex-end;
- `;
- const Title = styled(FlexBox)`
- gap: ${space(2)};
- `;
- const TransactionOp = styled('div')`
- font-size: 25px;
- font-weight: bold;
- max-width: 600px;
- ${p => p.theme.overflowEllipsis}
- `;
- const TransactionIdTitle = styled('a')`
- display: flex;
- color: ${p => p.theme.textColor};
- :hover {
- color: ${p => p.theme.textColor};
- }
- `;
- const Measurements = styled('div')`
- display: flex;
- flex-wrap: wrap;
- gap: ${space(1)};
- padding-top: 10px;
- `;
- const StyledButton = styled(Button)`
- position: absolute;
- top: ${space(0.75)};
- right: ${space(0.5)};
- `;
- const StyledTable = styled('table')`
- margin-bottom: 0 !important;
- `;
- export default TraceViewDetailPanel;
|