mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
265 lines
7.5 KiB
TypeScript
265 lines
7.5 KiB
TypeScript
import type {CSSProperties} from 'react';
|
|
import {Fragment} from 'react';
|
|
import styled from '@emotion/styled';
|
|
|
|
import DateTime from 'sentry/components/dateTime';
|
|
import type {GridColumnHeader} from 'sentry/components/gridEditable';
|
|
import GridEditable, {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
|
|
import Link from 'sentry/components/links/link';
|
|
import QuestionTooltip from 'sentry/components/questionTooltip';
|
|
import {t} from 'sentry/locale';
|
|
import type {NewQuery} from 'sentry/types';
|
|
import EventView from 'sentry/utils/discover/eventView';
|
|
import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
|
|
import {SPAN_OP_RELATIVE_BREAKDOWN_FIELD} from 'sentry/utils/discover/fields';
|
|
import {DiscoverDatasets} from 'sentry/utils/discover/types';
|
|
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
|
|
import {useLocation} from 'sentry/utils/useLocation';
|
|
import useOrganization from 'sentry/utils/useOrganization';
|
|
import {normalizeUrl} from 'sentry/utils/withDomainRequired';
|
|
import {DurationComparisonCell} from 'sentry/views/starfish/components/samplesTable/common';
|
|
import useErrorSamples from 'sentry/views/starfish/components/samplesTable/useErrorSamples';
|
|
import {DurationCell} from 'sentry/views/starfish/components/tableCells/durationCell';
|
|
import {
|
|
OverflowEllipsisTextContainer,
|
|
TextAlignLeft,
|
|
TextAlignRight,
|
|
} from 'sentry/views/starfish/components/textAlign';
|
|
import {DataTitles} from 'sentry/views/starfish/views/spans/types';
|
|
import type {SampleFilter} from 'sentry/views/starfish/views/webServiceView/endpointOverview';
|
|
|
|
type Keys =
|
|
| 'id'
|
|
| 'profile_id'
|
|
| 'timestamp'
|
|
| 'transaction.duration'
|
|
| 'avg_comparison'
|
|
| 'span_ops_breakdown.relative'
|
|
| 'http.status_code'
|
|
| 'transaction.status';
|
|
type TableColumnHeader = GridColumnHeader<Keys>;
|
|
|
|
type Props = {
|
|
data: DataRow[];
|
|
isLoading: boolean;
|
|
queryConditions: string[];
|
|
sampleFilter: SampleFilter;
|
|
setHighlightedId: (string) => void;
|
|
averageDuration?: number;
|
|
highlightedId?: string;
|
|
};
|
|
|
|
export type DataRow = {
|
|
'http.status_code': number;
|
|
id: string;
|
|
profile_id: string;
|
|
'spans.browser': number;
|
|
'spans.db': number;
|
|
'spans.http': number;
|
|
'spans.resource': number;
|
|
'spans.ui': number;
|
|
timestamp: string;
|
|
'transaction.duration': number;
|
|
'transaction.status': string;
|
|
};
|
|
|
|
export function TransactionSamplesTable({
|
|
queryConditions,
|
|
sampleFilter,
|
|
data,
|
|
isLoading,
|
|
averageDuration,
|
|
setHighlightedId,
|
|
highlightedId,
|
|
}: Props) {
|
|
const location = useLocation();
|
|
const organization = useOrganization();
|
|
const query = new MutableSearch(queryConditions);
|
|
|
|
const savedQuery: NewQuery = {
|
|
id: undefined,
|
|
name: 'Endpoint Overview Samples',
|
|
query: query.formatString(),
|
|
fields: [],
|
|
dataset: DiscoverDatasets.DISCOVER,
|
|
version: 2,
|
|
};
|
|
|
|
const columnOrder: TableColumnHeader[] = [
|
|
{
|
|
key: 'id',
|
|
name: 'Event ID',
|
|
width: 100,
|
|
},
|
|
...(sampleFilter === 'ALL'
|
|
? [
|
|
{
|
|
key: 'profile_id',
|
|
name: 'Profile ID',
|
|
width: 140,
|
|
} as TableColumnHeader,
|
|
{
|
|
key: SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
|
|
name: 'Operation Duration',
|
|
width: 180,
|
|
} as TableColumnHeader,
|
|
{
|
|
key: 'timestamp',
|
|
name: 'Timestamp',
|
|
width: 230,
|
|
} as TableColumnHeader,
|
|
{
|
|
key: 'transaction.duration',
|
|
name: DataTitles.duration,
|
|
width: 100,
|
|
} as TableColumnHeader,
|
|
{
|
|
key: 'avg_comparison',
|
|
name: 'Compared to Average',
|
|
width: 100,
|
|
} as TableColumnHeader,
|
|
]
|
|
: [
|
|
{
|
|
key: 'http.status_code',
|
|
name: 'Response Code',
|
|
width: COL_WIDTH_UNDEFINED,
|
|
} as TableColumnHeader,
|
|
{
|
|
key: 'transaction.status',
|
|
name: 'Status',
|
|
width: COL_WIDTH_UNDEFINED,
|
|
} as TableColumnHeader,
|
|
{
|
|
key: 'timestamp',
|
|
name: 'Timestamp',
|
|
width: 230,
|
|
} as TableColumnHeader,
|
|
]),
|
|
];
|
|
|
|
const eventView = EventView.fromNewQueryWithLocation(savedQuery, location);
|
|
const {isLoading: isErrorSamplesLoading, data: errorSamples} =
|
|
useErrorSamples(eventView);
|
|
|
|
function renderHeadCell(column: GridColumnHeader): React.ReactNode {
|
|
if (column.key === 'avg_comparison' || column.key === 'transaction.duration') {
|
|
return (
|
|
<TextAlignRight>
|
|
<OverflowEllipsisTextContainer>{column.name}</OverflowEllipsisTextContainer>
|
|
</TextAlignRight>
|
|
);
|
|
}
|
|
|
|
if (column.key === SPAN_OP_RELATIVE_BREAKDOWN_FIELD) {
|
|
return (
|
|
<Fragment>
|
|
{column.name}
|
|
<StyledIconQuestion
|
|
size="xs"
|
|
position="top"
|
|
title={t(
|
|
`Span durations are summed over the course of an entire transaction. Any overlapping spans are only counted once.`
|
|
)}
|
|
/>
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
return <OverflowEllipsisTextContainer>{column.name}</OverflowEllipsisTextContainer>;
|
|
}
|
|
|
|
function renderBodyCell(column: TableColumnHeader, row: DataRow): React.ReactNode {
|
|
const commonProps = {
|
|
style: (row.id === highlightedId
|
|
? {fontWeight: 'bold'}
|
|
: {}) satisfies CSSProperties,
|
|
onMouseEnter: () => setHighlightedId(row.id),
|
|
};
|
|
|
|
if (column.key === 'id') {
|
|
return (
|
|
<Link
|
|
{...commonProps}
|
|
to={normalizeUrl(
|
|
`/organizations/${organization.slug}/performance/${row['project.name']}:${row.id}`
|
|
)}
|
|
>
|
|
{row.id.slice(0, 8)}
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
if (column.key === 'profile_id') {
|
|
return row.profile_id ? (
|
|
<Link
|
|
{...commonProps}
|
|
to={normalizeUrl(
|
|
`/organizations/${organization.slug}/profiling/profile/${row['project.name']}/${row.profile_id}/flamegraph/`
|
|
)}
|
|
>
|
|
{row.profile_id.slice(0, 8)}
|
|
</Link>
|
|
) : (
|
|
<div {...commonProps}>(no value)</div>
|
|
);
|
|
}
|
|
|
|
if (column.key === 'transaction.duration') {
|
|
return <DurationCell milliseconds={row['transaction.duration']} />;
|
|
}
|
|
|
|
if (column.key === 'timestamp') {
|
|
return <DateTime {...commonProps} date={row[column.key]} year timeZone seconds />;
|
|
}
|
|
|
|
if (column.key === 'avg_comparison') {
|
|
return (
|
|
<DurationComparisonContainer {...commonProps}>
|
|
<DurationComparisonCell
|
|
duration={row['transaction.duration']}
|
|
compareToDuration={averageDuration ?? 0}
|
|
/>
|
|
</DurationComparisonContainer>
|
|
);
|
|
}
|
|
|
|
if (column.key === SPAN_OP_RELATIVE_BREAKDOWN_FIELD) {
|
|
return getFieldRenderer(column.key, {})(row, {
|
|
location,
|
|
organization,
|
|
eventView,
|
|
});
|
|
}
|
|
|
|
return <TextAlignLeft {...commonProps}>{row[column.key]}</TextAlignLeft>;
|
|
}
|
|
|
|
return (
|
|
<div onMouseLeave={() => setHighlightedId(undefined)}>
|
|
<GridEditable
|
|
isLoading={sampleFilter === 'ALL' ? isLoading : isErrorSamplesLoading}
|
|
data={sampleFilter === 'ALL' ? (data as DataRow[]) : (errorSamples as DataRow[])}
|
|
columnOrder={columnOrder}
|
|
columnSortBy={[]}
|
|
location={location}
|
|
grid={{
|
|
renderHeadCell,
|
|
renderBodyCell,
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const DurationComparisonContainer = styled('div')`
|
|
text-align: right;
|
|
width: 100%;
|
|
display: inline-block;
|
|
`;
|
|
|
|
const StyledIconQuestion = styled(QuestionTooltip)`
|
|
position: relative;
|
|
left: 4px;
|
|
`;
|