123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424 |
- import {Fragment} from 'react';
- import styled from '@emotion/styled';
- import type {Location} from 'history';
- import {Button, LinkButton} from 'sentry/components/button';
- import {CompactSelect} from 'sentry/components/compactSelect';
- import type {DropdownOption} from 'sentry/components/discover/transactionsList';
- import {InvestigationRuleCreation} from 'sentry/components/dynamicSampling/investigationRule';
- import GridEditable, {
- COL_WIDTH_UNDEFINED,
- type GridColumnHeader,
- } from 'sentry/components/gridEditable';
- import Pagination, {type CursorHandler} from 'sentry/components/pagination';
- import {IconPlay, IconProfiling} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {Organization} from 'sentry/types/organization';
- import {parseCursor} from 'sentry/utils/cursor';
- import type EventView from 'sentry/utils/discover/eventView';
- import type {EventsMetaType} from 'sentry/utils/discover/eventView';
- import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
- import {decodeScalar} from 'sentry/utils/queryString';
- import {MutableSearch} from 'sentry/utils/tokenizeSearch';
- import {useLocation} from 'sentry/utils/useLocation';
- import {useNavigate} from 'sentry/utils/useNavigate';
- import useOrganization from 'sentry/utils/useOrganization';
- import useProjects from 'sentry/utils/useProjects';
- import {renderHeadCell} from 'sentry/views/insights/common/components/tableCells/renderHeadCell';
- import {SpanIdCell} from 'sentry/views/insights/common/components/tableCells/spanIdCell';
- import {useEAPSpans} from 'sentry/views/insights/common/queries/useDiscover';
- import {type EAPSpanResponse, ModuleName} from 'sentry/views/insights/types';
- import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHeader/breadcrumbs';
- import {TransactionFilterOptions} from 'sentry/views/performance/transactionSummary/utils';
- // TODO: When supported, also add span operation breakdown as a field
- type Row = Pick<
- EAPSpanResponse,
- | 'span_id'
- | 'user.display'
- | 'user.id'
- | 'user.email'
- | 'user.username'
- | 'user.ip'
- | 'span.duration'
- | 'trace'
- | 'timestamp'
- | 'replayId'
- | 'profile.id'
- | 'profiler.id'
- | 'thread.id'
- | 'precise.start_ts'
- | 'precise.finish_ts'
- >;
- type Column = GridColumnHeader<
- | 'span_id'
- | 'user.display'
- | 'span.duration'
- | 'trace'
- | 'timestamp'
- | 'replayId'
- | 'profile.id'
- >;
- const COLUMN_ORDER: Column[] = [
- {
- key: 'trace',
- name: t('Trace ID'),
- width: COL_WIDTH_UNDEFINED,
- },
- {
- key: 'span_id',
- name: t('Span ID'),
- width: COL_WIDTH_UNDEFINED,
- },
- {
- key: 'user.display',
- name: t('User'),
- width: COL_WIDTH_UNDEFINED,
- },
- {
- key: 'span.duration',
- name: t('Total Duration'),
- width: COL_WIDTH_UNDEFINED,
- },
- {
- key: 'timestamp',
- name: t('Timestamp'),
- width: COL_WIDTH_UNDEFINED,
- },
- {
- key: 'replayId',
- name: t('Replay'),
- width: COL_WIDTH_UNDEFINED,
- },
- {
- key: 'profile.id',
- name: t('Profile'),
- width: COL_WIDTH_UNDEFINED,
- },
- ];
- const LIMIT = 5;
- const PAGINATION_CURSOR_SIZE = 'xs';
- const CURSOR_NAME = 'serviceEntrySpansCursor';
- type Props = {
- eventView: EventView;
- handleDropdownChange: (k: string) => void;
- totalValues: Record<string, number> | null;
- transactionName: string;
- showViewSampledEventsButton?: boolean;
- supportsInvestigationRule?: boolean;
- };
- export function ServiceEntrySpansTable({
- eventView,
- handleDropdownChange,
- totalValues,
- transactionName,
- supportsInvestigationRule,
- showViewSampledEventsButton,
- }: Props) {
- const location = useLocation();
- const organization = useOrganization();
- const {projects} = useProjects();
- const navigate = useNavigate();
- const projectSlug = projects.find(p => p.id === `${eventView.project}`)?.slug;
- const cursor = decodeScalar(location.query?.[CURSOR_NAME]);
- const {selected, options} = getOTelTransactionsListSort(location);
- const p95 = totalValues?.['p95()'] ?? 0;
- const eventViewQuery = new MutableSearch(eventView.query);
- if (selected.value === TransactionFilterOptions.SLOW && p95) {
- eventViewQuery.addFilterValue('span.duration', `<=${p95.toFixed(0)}`);
- }
- const {
- data: tableData,
- isLoading,
- pageLinks,
- meta,
- error,
- } = useEAPSpans(
- {
- search: eventViewQuery.formatString(),
- fields: [
- 'span_id',
- 'user.id',
- 'user.email',
- 'user.username',
- 'user.ip',
- 'span.duration',
- 'trace',
- 'timestamp',
- 'replayId',
- 'profile.id',
- 'profiler.id',
- 'thread.id',
- 'precise.start_ts',
- 'precise.finish_ts',
- ],
- sorts: [selected.sort],
- limit: LIMIT,
- cursor,
- },
- 'api.performance.service-entry-spans-table',
- true
- );
- const consolidatedData = tableData?.map(row => {
- const user =
- row['user.username'] || row['user.email'] || row['user.ip'] || row['user.id'];
- return {
- ...row,
- 'user.display': user,
- };
- });
- const handleCursor: CursorHandler = (_cursor, pathname, query) => {
- navigate({
- pathname,
- query: {...query, [CURSOR_NAME]: _cursor},
- });
- };
- const cursorOffset = parseCursor(cursor)?.offset ?? 0;
- const totalNumSamples = cursorOffset;
- const handleViewSampledEvents = () => {
- if (!projectSlug) {
- return;
- }
- navigate({
- pathname: `${location.pathname}events/`,
- query: {
- ...location.query,
- transaction: transactionName,
- project: `${eventView.project}`,
- },
- });
- };
- return (
- <Fragment>
- <Header>
- <CompactSelect
- triggerProps={{prefix: t('Filter'), size: 'xs'}}
- value={selected.value}
- options={options}
- onChange={opt => handleDropdownChange(opt.value)}
- />
- <HeaderButtonWrapper>
- {supportsInvestigationRule && (
- <InvestigationRuleWrapper>
- <InvestigationRuleCreation
- buttonProps={{size: 'xs'}}
- eventView={eventView}
- numSamples={totalNumSamples}
- />
- </InvestigationRuleWrapper>
- )}
- {showViewSampledEventsButton && (
- <Button
- size="xs"
- data-test-id="transaction-events-open"
- onClick={handleViewSampledEvents}
- >
- {t('View Sampled Events')}
- </Button>
- )}
- </HeaderButtonWrapper>
- <CustomPagination
- pageLinks={pageLinks}
- onCursor={handleCursor}
- isLoading={isLoading}
- />
- </Header>
- <GridEditable
- isLoading={isLoading}
- error={error}
- data={consolidatedData}
- columnOrder={COLUMN_ORDER}
- columnSortBy={[]}
- grid={{
- renderHeadCell: column =>
- renderHeadCell({
- column,
- }),
- renderBodyCell: (column, row) =>
- renderBodyCell(column, row, meta, projectSlug, location, organization),
- }}
- />
- </Fragment>
- );
- }
- function renderBodyCell(
- column: Column,
- row: Row,
- meta: EventsMetaType | undefined,
- projectSlug: string | undefined,
- location: Location,
- organization: Organization
- ) {
- if (column.key === 'span_id') {
- return (
- <SpanIdCell
- moduleName={ModuleName.OTHER}
- projectSlug={projectSlug ?? ''}
- traceId={row.trace}
- timestamp={row.timestamp}
- transactionId={row.span_id}
- spanId={row.span_id}
- source={TraceViewSources.PERFORMANCE_TRANSACTION_SUMMARY}
- location={location}
- />
- );
- }
- if (column.key === 'profile.id') {
- return (
- <div>
- <LinkButton
- size="xs"
- icon={<IconProfiling size="xs" />}
- to={{
- pathname: `/organizations/${organization.slug}/profiling/profile/${projectSlug}/${row['profile.id']}/flamegraph/`,
- query: {
- referrer: 'performance',
- },
- }}
- aria-label={t('View Profile')}
- disabled={!row['profile.id']}
- />
- </div>
- );
- }
- if (column.key === 'replayId') {
- return (
- <div>
- <LinkButton
- size="xs"
- icon={<IconPlay size="xs" />}
- to={{
- pathname: `/organizations/${organization.slug}/replays/${row.replayId}/`,
- query: {
- referrer: 'performance',
- },
- }}
- disabled={!row.replayId}
- aria-label={t('View Replay')}
- />
- </div>
- );
- }
- if (!meta || !meta?.fields) {
- return row[column.key];
- }
- const renderer = getFieldRenderer(column.key, meta.fields, false);
- const rendered = renderer(row, {
- location,
- organization,
- unit: meta.units?.[column.key],
- });
- return rendered;
- }
- // A wrapper component that handles the isLoading state. This will allow the component to not disappear when the data is loading.
- function CustomPagination({
- pageLinks,
- onCursor,
- isLoading,
- }: {
- isLoading: boolean;
- onCursor: CursorHandler;
- pageLinks: string | undefined;
- }) {
- if (isLoading) {
- return (
- <StyledPagination
- pageLinks={'n/a'}
- disabled
- onCursor={() => {}}
- size={PAGINATION_CURSOR_SIZE}
- />
- );
- }
- return (
- <StyledPagination
- pageLinks={pageLinks}
- onCursor={onCursor}
- size={PAGINATION_CURSOR_SIZE}
- />
- );
- }
- // TODO: The span ops breakdown filter will not work here due to OTLP changes.
- // this may need to be adjusted in the future to handle the new breakdown filter that will replace it.
- function getOTelFilterOptions(): DropdownOption[] {
- return [
- {
- sort: {kind: 'asc', field: 'span.duration'},
- value: TransactionFilterOptions.FASTEST,
- label: t('Fastest Transactions'),
- },
- {
- sort: {kind: 'desc', field: 'span.duration'},
- value: TransactionFilterOptions.SLOW,
- label: t('Slow Transactions (p95)'),
- },
- {
- sort: {kind: 'desc', field: 'span.duration'},
- value: TransactionFilterOptions.OUTLIER,
- label: t('Outlier Transactions (p100)'),
- },
- {
- sort: {kind: 'desc', field: 'timestamp'},
- value: TransactionFilterOptions.RECENT,
- label: t('Recent Transactions'),
- },
- ];
- }
- function getOTelTransactionsListSort(location: Location): {
- options: DropdownOption[];
- selected: DropdownOption;
- } {
- const sortOptions = getOTelFilterOptions();
- const urlParam = decodeScalar(
- location.query.showTransactions,
- TransactionFilterOptions.SLOW
- );
- const selectedSort = sortOptions.find(opt => opt.value === urlParam) || sortOptions[0]!;
- return {selected: selectedSort, options: sortOptions};
- }
- const Header = styled('div')`
- display: grid;
- grid-template-columns: 1fr auto auto auto;
- margin-bottom: ${space(1)};
- align-items: center;
- `;
- const StyledPagination = styled(Pagination)`
- margin: 0 0 0 ${space(1)};
- `;
- const HeaderButtonWrapper = styled('div')`
- display: flex;
- `;
- const InvestigationRuleWrapper = styled('div')`
- margin-right: ${space(1)};
- `;
|