123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558 |
- import {createRef, Fragment, useLayoutEffect, useMemo, useState} from 'react';
- import styled from '@emotion/styled';
- import omit from 'lodash/omit';
- 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 {REPLAY_CLIP_OFFSETS} from 'sentry/components/events/eventReplay';
- import ReplayClipPreview from 'sentry/components/events/eventReplay/replayClipPreview';
- import {EventSdk} from 'sentry/components/events/eventSdk';
- import NewTagsUI from 'sentry/components/events/eventTagsAndScreenshot/tags';
- import {EventViewHierarchy} from 'sentry/components/events/eventViewHierarchy';
- import {Breadcrumbs} from 'sentry/components/events/interfaces/breadcrumbs';
- import {getFormattedTimeRangeWithLeadingAndTrailingZero} from 'sentry/components/events/interfaces/spans/utils';
- import {generateStats} from 'sentry/components/events/opsBreakdown';
- import {EventRRWebIntegration} from 'sentry/components/events/rrwebIntegration';
- import FileSize from 'sentry/components/fileSize';
- import ProjectBadge from 'sentry/components/idBadge/projectBadge';
- import {LazyRender, type LazyRenderProps} from 'sentry/components/lazyRender';
- import Link from 'sentry/components/links/link';
- import LoadingError from 'sentry/components/loadingError';
- import LoadingIndicator from 'sentry/components/loadingIndicator';
- import PerformanceDuration from 'sentry/components/performanceDuration';
- import QuestionTooltip from 'sentry/components/questionTooltip';
- import {Tooltip} from 'sentry/components/tooltip';
- import {PAGE_URL_PARAM} from 'sentry/constants/pageFilters';
- import {IconChevron, IconOpen} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import {
- type EntryBreadcrumbs,
- EntryType,
- type EventTransaction,
- type Organization,
- } from 'sentry/types';
- import {objectIsEmpty} from 'sentry/utils';
- import {trackAnalytics} from 'sentry/utils/analytics';
- import {getAnalyticsDataForEvent} from 'sentry/utils/events';
- import getDynamicText from 'sentry/utils/getDynamicText';
- import {WEB_VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants';
- import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes';
- import {useApiQuery} from 'sentry/utils/queryClient';
- import {getReplayIdFromEvent} from 'sentry/utils/replays/getReplayIdFromEvent';
- import {useLocation} from 'sentry/utils/useLocation';
- import useProjects from 'sentry/utils/useProjects';
- import {isCustomMeasurement} from 'sentry/views/dashboards/utils';
- import {CustomMetricsEventData} from 'sentry/views/metrics/customMetricsEventData';
- import type {TraceTreeNodeDetailsProps} from 'sentry/views/performance/newTraceDetails/traceDrawer/tabs/traceTreeNodeDetails';
- import {getTraceTabTitle} from 'sentry/views/performance/newTraceDetails/traceTabs';
- import type {
- TraceTree,
- TraceTreeNode,
- } from 'sentry/views/performance/newTraceDetails/traceTree';
- import {Row, Tags} from 'sentry/views/performance/traceDetails/styles';
- import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
- import {useTraceAverageTransactionDuration} from '../../useTraceAverageTransactionDuration';
- import {IssueList} from './issues/issues';
- import {TraceDrawerComponents} from './styles';
- 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={
- <TraceDrawerComponents.FlexBox style={{gap: '5px'}}>
- {t('Ops Breakdown')}
- <QuestionTooltip
- title={t('Applicable to the children of this event only')}
- size="xs"
- />
- </TraceDrawerComponents.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>();
- useLayoutEffect(() => {
- 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 ReplaySection({
- event,
- organization,
- }: {
- event: EventTransaction;
- organization: Organization;
- }) {
- const replayId = getReplayIdFromEvent(event);
- const startTimestampMS =
- 'startTimestamp' in event ? event.startTimestamp * 1000 : undefined;
- const timeOfEvent = event.dateCreated ?? startTimestampMS ?? event.dateReceived;
- const eventTimestampMs = timeOfEvent ? Math.floor(new Date(timeOfEvent).getTime()) : 0;
- return replayId ? (
- <ReplaySectionContainer>
- <ReplaySectionTitle>{t('Session Replay')}</ReplaySectionTitle>
- <ReplayClipPreview
- analyticsContext="trace-view"
- replaySlug={replayId}
- orgSlug={organization.slug}
- eventTimestampMs={eventTimestampMs}
- clipOffsets={REPLAY_CLIP_OFFSETS}
- fullReplayButtonProps={{
- analyticsEventKey: 'trace-view.drawer-open-replay-details-clicked',
- analyticsEventName: 'Trace View: Open Replay Details Clicked',
- analyticsParams: {
- ...getAnalyticsDataForEvent(event),
- organization,
- },
- }}
- />
- </ReplaySectionContainer>
- ) : null;
- }
- const LAZY_RENDER_PROPS: Partial<LazyRenderProps> = {
- observerOptions: {rootMargin: '50px'},
- };
- export function TransactionNodeDetails({
- node,
- organization,
- scrollToNode,
- onParentClick,
- }: TraceTreeNodeDetailsProps<TraceTreeNode<TraceTree.Transaction>>) {
- const location = useLocation();
- const {projects} = useProjects();
- const issues = useMemo(() => {
- return [...node.errors, ...node.performance_issues];
- }, [node.errors, node.performance_issues]);
- const {data: averageDurationQueryResult} = useTraceAverageTransactionDuration({
- node,
- location,
- organization,
- });
- const avgDurationInSeconds: number = useMemo(() => {
- return (
- Number(averageDurationQueryResult?.data[0]?.['avg(transaction.duration)']) / 1000
- );
- }, [averageDurationQueryResult]);
- const {
- data: event,
- isError,
- isLoading,
- } = useApiQuery<EventTransaction>(
- [
- `/organizations/${organization.slug}/events/${node.value.project_slug}:${node.value.event_id}/`,
- {
- query: {
- referrer: 'trace-details-summary',
- },
- },
- ],
- {
- staleTime: 0,
- enabled: !!node,
- }
- );
- if (isLoading) {
- return <LoadingIndicator />;
- }
- if (isError) {
- return <LoadingError message={t('Failed to fetch transaction details')} />;
- }
- const project = projects.find(proj => proj.slug === event?.projectSlug);
- const startTimestamp = Math.min(node.value.start_timestamp, node.value.timestamp);
- const endTimestamp = Math.max(node.value.start_timestamp, node.value.timestamp);
- const {start: startTimeWithLeadingZero, end: endTimeWithLeadingZero} =
- getFormattedTimeRangeWithLeadingAndTrailingZero(startTimestamp, endTimestamp);
- const durationInSeconds = endTimestamp - startTimestamp;
- const measurementNames = Object.keys(node.value.measurements ?? {})
- .filter(name => isCustomMeasurement(`measurements.${name}`))
- .filter(isNotMarkMeasurement)
- .filter(isNotPerformanceScoreMeasurement)
- .sort();
- const measurementKeys = Object.keys(event?.measurements ?? {})
- .filter(name => Boolean(WEB_VITAL_DETAILS[`measurements.${name}`]))
- .sort();
- const parentTransaction = node.parent_transaction;
- return (
- <TraceDrawerComponents.DetailContainer>
- <TraceDrawerComponents.HeaderContainer>
- <TraceDrawerComponents.Title>
- <Tooltip title={node.value.project_slug}>
- <ProjectBadge
- project={project ? project : {slug: node.value.project_slug}}
- avatarSize={30}
- hideName
- />
- </Tooltip>
- <TraceDrawerComponents.TitleText>
- <div>{t('transaction')}</div>
- <TraceDrawerComponents.TitleOp>
- {' '}
- {node.value['transaction.op']}
- </TraceDrawerComponents.TitleOp>
- </TraceDrawerComponents.TitleText>
- </TraceDrawerComponents.Title>
- <TraceDrawerComponents.Actions>
- <Button size="xs" onClick={_e => scrollToNode(node)}>
- {t('Show in view')}
- </Button>
- <TraceDrawerComponents.EventDetailsLink
- eventId={node.value.event_id}
- projectSlug={node.metadata.project_slug}
- />
- <Button
- size="xs"
- icon={<IconOpen />}
- href={`/api/0/projects/${organization.slug}/${node.value.project_slug}/events/${node.value.event_id}/json/`}
- external
- >
- {t('JSON')} (<FileSize bytes={event?.size} />)
- </Button>
- </TraceDrawerComponents.Actions>
- </TraceDrawerComponents.HeaderContainer>
- <IssueList node={node} organization={organization} issues={issues} />
- <TraceDrawerComponents.Table className="table key-value">
- <tbody>
- <Row title="Duration">
- <TraceDrawerComponents.Duration
- duration={durationInSeconds}
- baseline={avgDurationInSeconds}
- baseDescription={
- 'Average duration for this transaction over the last 24 hours'
- }
- />
- </Row>
- {parentTransaction ? (
- <Row title="Parent Transaction">
- <td className="value">
- <a href="#" onClick={() => onParentClick(parentTransaction)}>
- {getTraceTabTitle(parentTransaction)}
- </a>
- </td>
- </Row>
- ) : null}
- <Row title={t('Event ID')}>
- {node.value.event_id}
- <CopyToClipboardButton
- borderless
- size="zero"
- iconSize="xs"
- text={node.value.event_id}
- />
- </Row>
- <Row title={t('Description')}>
- <Link
- to={transactionSummaryRouteWithQuery({
- orgSlug: organization.slug,
- transaction: node.value.transaction,
- query: omit(location.query, Object.values(PAGE_URL_PARAM)),
- projectID: String(node.value.project_id),
- })}
- >
- {node.value.transaction}
- </Link>
- </Row>
- {node.value.profile_id ? (
- <Row
- title="Profile ID"
- extra={
- <TraceDrawerComponents.Button
- size="xs"
- to={generateProfileFlamechartRoute({
- orgSlug: organization.slug,
- projectSlug: node.value.project_slug,
- profileId: node.value.profile_id,
- })}
- onClick={function handleOnClick() {
- trackAnalytics('profiling_views.go_to_flamegraph', {
- organization,
- source: 'performance.trace_view',
- });
- }}
- >
- {t('View Profile')}
- </TraceDrawerComponents.Button>
- }
- >
- {node.value.profile_id}
- </Row>
- ) : null}
- <Row title="Date Range">
- {getDynamicText({
- fixed: 'Mar 19, 2021 11:06:27 AM UTC',
- value: (
- <Fragment>
- <DateTime date={startTimestamp * node.multiplier} />
- {` (${startTimeWithLeadingZero})`}
- </Fragment>
- ),
- })}
- <br />
- {getDynamicText({
- fixed: 'Mar 19, 2021 11:06:28 AM UTC',
- value: (
- <Fragment>
- <DateTime date={endTimestamp * node.multiplier} />
- {` (${endTimeWithLeadingZero})`}
- </Fragment>
- ),
- })}
- </Row>
- <OpsBreakdown event={event} />
- {!event || !event.measurements || measurementKeys.length <= 0 ? null : (
- <Fragment>
- {measurementKeys.map(measurement => (
- <Row
- key={measurement}
- title={WEB_VITAL_DETAILS[`measurements.${measurement}`]?.name}
- >
- <PerformanceDuration
- milliseconds={Number(
- event.measurements?.[measurement].value.toFixed(3)
- )}
- abbreviation
- />
- </Row>
- ))}
- </Fragment>
- )}
- {measurementNames.length > 0 && (
- <tr>
- <td className="key">{t('Measurements')}</td>
- <td className="value">
- <Measurements>
- {measurementNames.map(name => {
- return (
- event && (
- <TraceEventCustomPerformanceMetric
- key={name}
- event={event}
- name={name}
- location={location}
- organization={organization}
- source={undefined}
- isHomepage={false}
- />
- )
- );
- })}
- </Measurements>
- </td>
- </tr>
- )}
- </tbody>
- </TraceDrawerComponents.Table>
- <LazyRender {...LAZY_RENDER_PROPS} containerHeight={200}>
- {organization.features.includes('event-tags-tree-ui') ? (
- <TagsWrapper>
- <NewTagsUI event={event} projectSlug={node.value.project_slug} />
- </TagsWrapper>
- ) : (
- <TraceDrawerComponents.Table className="table key-value">
- <tbody>
- <Tags
- enableHiding
- location={location}
- organization={organization}
- tags={event.tags}
- event={node.value}
- />
- </tbody>
- </TraceDrawerComponents.Table>
- )}
- </LazyRender>
- {project ? <EventEvidence event={event} project={project} /> : null}
- <LazyRender {...LAZY_RENDER_PROPS} containerHeight={480}>
- <ReplaySection event={event} organization={organization} />
- </LazyRender>
- {event.projectSlug ? (
- <Entries
- definedEvent={event}
- projectSlug={event.projectSlug}
- group={undefined}
- organization={organization}
- isShare
- hideBeforeReplayEntries
- hideBreadCrumbs
- />
- ) : null}
- {!objectIsEmpty(event.contexts?.feedback ?? {}) ? (
- <Chunk
- key="feedback"
- type="feedback"
- alias="feedback"
- group={undefined}
- event={event}
- value={event.contexts?.feedback ?? {}}
- />
- ) : null}
- {event.user && !objectIsEmpty(event.user) ? (
- <Chunk
- key="user"
- type="user"
- alias="user"
- group={undefined}
- event={event}
- value={event.user}
- />
- ) : null}
- <EventExtraData event={event} />
- <EventSdk sdk={event.sdk} meta={event._meta?.sdk} />
- {event._metrics_summary ? (
- <CustomMetricsEventData
- metricsSummary={event._metrics_summary}
- startTimestamp={event.startTimestamp}
- />
- ) : null}
- <BreadCrumbsSection event={event} organization={organization} />
- {event.projectSlug ? (
- <EventAttachments event={event} projectSlug={event.projectSlug} />
- ) : null}
- {project ? <EventViewHierarchy event={event} project={project} /> : null}
- {event.projectSlug ? (
- <EventRRWebIntegration
- event={event}
- orgId={organization.slug}
- projectSlug={event.projectSlug}
- />
- ) : null}
- </TraceDrawerComponents.DetailContainer>
- );
- }
- const ReplaySectionContainer = styled('div')`
- display: flex;
- flex-direction: column;
- `;
- const ReplaySectionTitle = styled('div')`
- font-size: ${p => p.theme.fontSizeMedium};
- font-weight: 600;
- margin-bottom: ${space(2)};
- `;
- const Measurements = styled('div')`
- display: flex;
- flex-wrap: wrap;
- gap: ${space(1)};
- padding-top: 10px;
- `;
- const TagsWrapper = styled('div')`
- h3 {
- color: ${p => p.theme.textColor};
- }
- `;
|