import * as React from 'react'; import styled from '@emotion/styled'; import DateTime from 'app/components/dateTime'; import {SpanDetailContainer} from 'app/components/events/interfaces/spans/spanDetail'; import {rawSpanKeys, SpanType} from 'app/components/events/interfaces/spans/types'; import {getHumanDuration} from 'app/components/performance/waterfall/utils'; import Pill from 'app/components/pill'; import Pills from 'app/components/pills'; import {t} from 'app/locale'; import space from 'app/styles/space'; import getDynamicText from 'app/utils/getDynamicText'; import theme from 'app/utils/theme'; import SpanDetailContent from './spanDetailContent'; import {SpanBarRectangle} from './styles'; import { DiffSpanType, generateCSSWidth, getSpanDuration, SpanGeneratedBoundsType, SpanWidths, } from './utils'; type DurationDisplay = 'right' | 'inset'; const getDurationDisplay = (width: SpanWidths | undefined): DurationDisplay => { if (!width) { return 'right'; } switch (width.type) { case 'WIDTH_PIXEL': { return 'right'; } case 'WIDTH_PERCENTAGE': { const spaceNeeded = 0.3; if (width.width < 1 - spaceNeeded) { return 'right'; } return 'inset'; } default: { const _exhaustiveCheck: never = width; return _exhaustiveCheck; } } }; type Props = { span: Readonly; bounds: SpanGeneratedBoundsType; }; class SpanDetail extends React.Component { renderContent() { const {span, bounds} = this.props; switch (span.comparisonResult) { case 'matched': { return ( ); } case 'regression': { return ; } case 'baseline': { return ; } default: { const _exhaustiveCheck: never = span; return _exhaustiveCheck; } } } render() { return ( { // prevent toggling the span detail event.stopPropagation(); }} > {this.renderContent()} ); } } const MatchedSpanDetailsContent = (props: { baselineSpan: SpanType; regressionSpan: SpanType; bounds: SpanGeneratedBoundsType; }) => { const {baselineSpan, regressionSpan, bounds} = props; const dataKeys = new Set([ ...Object.keys(baselineSpan?.data ?? {}), ...Object.keys(regressionSpan?.data ?? {}), ]); const unknownKeys = new Set([ ...Object.keys(baselineSpan).filter(key => { return !rawSpanKeys.has(key as any); }), ...Object.keys(regressionSpan).filter(key => { return !rawSpanKeys.has(key as any); }), ]); return (
baselineSpan.span_id} renderRegressionContent={() => regressionSpan.span_id} /> baselineSpan.parent_span_id || ''} renderRegressionContent={() => regressionSpan.parent_span_id || ''} /> baselineSpan.trace_id} renderRegressionContent={() => regressionSpan.trace_id} /> baselineSpan.description ?? ''} renderRegressionContent={() => regressionSpan.description ?? ''} /> getDynamicText({ fixed: 'Mar 16, 2020 9:10:12 AM UTC', value: ( {` (${baselineSpan.start_timestamp})`} ), }) } renderRegressionContent={() => getDynamicText({ fixed: 'Mar 16, 2020 9:10:12 AM UTC', value: ( {` (${baselineSpan.start_timestamp})`} ), }) } /> getDynamicText({ fixed: 'Mar 16, 2020 9:10:12 AM UTC', value: ( {` (${baselineSpan.timestamp})`} ), }) } renderRegressionContent={() => getDynamicText({ fixed: 'Mar 16, 2020 9:10:12 AM UTC', value: ( {` (${regressionSpan.timestamp})`} ), }) } /> { const startTimestamp: number = baselineSpan.start_timestamp; const endTimestamp: number = baselineSpan.timestamp; const duration = (endTimestamp - startTimestamp) * 1000; return `${duration.toFixed(3)}ms`; }} renderRegressionContent={() => { const startTimestamp: number = regressionSpan.start_timestamp; const endTimestamp: number = regressionSpan.timestamp; const duration = (endTimestamp - startTimestamp) * 1000; return `${duration.toFixed(3)}ms`; }} /> baselineSpan.op || ''} renderRegressionContent={() => regressionSpan.op || ''} /> String(!!baselineSpan.same_process_as_parent)} renderRegressionContent={() => String(!!regressionSpan.same_process_as_parent)} /> {Array.from(dataKeys).map((dataTitle: string) => ( { const data = baselineSpan?.data ?? {}; const value: string | undefined = data[dataTitle]; return JSON.stringify(value, null, 4) || ''; }} renderRegressionContent={() => { const data = regressionSpan?.data ?? {}; const value: string | undefined = data[dataTitle]; return JSON.stringify(value, null, 4) || ''; }} /> ))} {Array.from(unknownKeys).map(key => ( { return JSON.stringify(baselineSpan[key], null, 4) || ''; }} renderRegressionContent={() => { return JSON.stringify(regressionSpan[key], null, 4) || ''; }} /> ))}
); }; const RowSplitter = styled('div')` display: flex; flex-direction: row; > * + * { border-left: 1px solid ${p => p.theme.border}; } `; const SpanBarContainer = styled('div')` position: relative; height: 16px; margin-top: ${space(3)}; margin-bottom: ${space(2)}; `; const SpanBars = (props: { bounds: SpanGeneratedBoundsType; baselineSpan: SpanType; regressionSpan: SpanType; }) => { const {bounds, baselineSpan, regressionSpan} = props; const baselineDurationDisplay = getDurationDisplay(bounds.baseline); const regressionDurationDisplay = getDurationDisplay(bounds.regression); return ( {getHumanDuration(getSpanDuration(baselineSpan))} {getHumanDuration(getSpanDuration(regressionSpan))} ); }; const Row = (props: { title?: string; baselineTitle?: string; regressionTitle?: string; renderBaselineContent: () => React.ReactNode; renderRegressionContent: () => React.ReactNode; }) => { const {title, baselineTitle, regressionTitle} = props; const baselineContent = props.renderBaselineContent(); const regressionContent = props.renderRegressionContent(); if (!baselineContent && !regressionContent) { return null; } return ( {baselineContent} {regressionContent} ); }; const RowContainer = styled('div')` width: 50%; min-width: 50%; max-width: 50%; flex-basis: 50%; padding-left: ${space(2)}; padding-right: ${space(2)}; `; const RowTitle = styled('div')` font-size: 13px; font-weight: 600; `; const RowCell = ({title, children}: {title: string; children: React.ReactNode}) => { return ( {title}
          {children}
        
); }; const getTags = (span: SpanType) => { const tags: {[tag_name: string]: string} | undefined = span?.tags; if (!tags) { return undefined; } const keys = Object.keys(tags); if (keys.length <= 0) { return undefined; } return tags; }; const TagPills = ({tags}: {tags: {[tag_name: string]: string} | undefined}) => { if (!tags) { return null; } const keys = Object.keys(tags); if (keys.length <= 0) { return null; } return ( {keys.map((key, index) => ( ))} ); }; const Tags = ({ baselineSpan, regressionSpan, }: { baselineSpan: SpanType; regressionSpan: SpanType; }) => { const baselineTags = getTags(baselineSpan); const regressionTags = getTags(regressionSpan); if (!baselineTags && !regressionTags) { return null; } return ( {t('Tags')}
{t('Tags')}
); }; const DurationPill = styled('div')<{ durationDisplay: DurationDisplay; fontColors: {right: string; inset: string}; }>` position: absolute; top: 50%; display: flex; align-items: center; transform: translateY(-50%); white-space: nowrap; font-size: ${p => p.theme.fontSizeExtraSmall}; color: ${p => p.fontColors.right}; ${p => { switch (p.durationDisplay) { case 'right': return `left: calc(100% + ${space(0.75)});`; default: return ` right: ${space(0.75)}; color: ${p.fontColors.inset}; `; } }}; @media (max-width: ${p => p.theme.breakpoints[1]}) { font-size: 10px; } `; export default SpanDetail;