Files
2024-02-15 17:01:50 +00:00

1003 lines
28 KiB
TypeScript

import {Fragment} from 'react';
import {browserHistory} from 'react-router';
import styled from '@emotion/styled';
import type {Location} from 'history';
import partial from 'lodash/partial';
import {Button} from 'sentry/components/button';
import Count from 'sentry/components/count';
import type {MenuItemProps} from 'sentry/components/dropdownMenu';
import {DropdownMenu} from 'sentry/components/dropdownMenu';
import Duration from 'sentry/components/duration';
import FileSize from 'sentry/components/fileSize';
import ProjectBadge from 'sentry/components/idBadge/projectBadge';
import UserBadge from 'sentry/components/idBadge/userBadge';
import ExternalLink from 'sentry/components/links/externalLink';
import Link from 'sentry/components/links/link';
import {RowRectangle} from 'sentry/components/performance/waterfall/rowBar';
import {pickBarColor} from 'sentry/components/performance/waterfall/utils';
import {Tooltip} from 'sentry/components/tooltip';
import UserMisery from 'sentry/components/userMisery';
import Version from 'sentry/components/version';
import {IconDownload} from 'sentry/icons';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {AvatarProject, IssueAttachment, Organization, Project} from 'sentry/types';
import {defined, isUrl} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
import type {EventData, MetaType} from 'sentry/utils/discover/eventView';
import type EventView from 'sentry/utils/discover/eventView';
import type {RateUnit} from 'sentry/utils/discover/fields';
import {
AGGREGATIONS,
getAggregateAlias,
getSpanOperationName,
isEquation,
isRelativeSpanOperationBreakdownField,
SPAN_OP_BREAKDOWN_FIELDS,
SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
} from 'sentry/utils/discover/fields';
import {getShortEventId} from 'sentry/utils/events';
import {formatFloat, formatPercentage, formatRate} from 'sentry/utils/formatters';
import getDynamicText from 'sentry/utils/getDynamicText';
import toPercent from 'sentry/utils/number/toPercent';
import Projects from 'sentry/utils/projects';
import toArray from 'sentry/utils/toArray';
import {QuickContextHoverWrapper} from 'sentry/views/discover/table/quickContext/quickContextWrapper';
import {ContextType} from 'sentry/views/discover/table/quickContext/utils';
import {
filterToLocationQuery,
SpanOperationBreakdownFilter,
stringToFilter,
} from 'sentry/views/performance/transactionSummary/filter';
import {PercentChangeCell} from 'sentry/views/starfish/components/tableCells/percentChangeCell';
import {TimeSpentCell} from 'sentry/views/starfish/components/tableCells/timeSpentCell';
import {SpanMetricsField} from 'sentry/views/starfish/types';
import {decodeScalar} from '../queryString';
import ArrayValue from './arrayValue';
import {
BarContainer,
Container,
FieldDateTime,
FieldShortId,
FlexContainer,
NumberContainer,
OverflowFieldShortId,
OverflowLink,
UserIcon,
VersionContainer,
} from './styles';
import TeamKeyTransactionField from './teamKeyTransactionField';
/**
* Types, functions and definitions for rendering fields in discover results.
*/
export type RenderFunctionBaggage = {
location: Location;
organization: Organization;
eventView?: EventView;
projectSlug?: string;
unit?: string;
};
type RenderFunctionOptions = {
enableOnClick?: boolean;
};
type FieldFormatterRenderFunction = (
field: string,
data: EventData,
baggage?: RenderFunctionBaggage
) => React.ReactNode;
type FieldFormatterRenderFunctionPartial = (
data: EventData,
baggage: RenderFunctionBaggage
) => React.ReactNode;
type FieldFormatter = {
isSortable: boolean;
renderFunc: FieldFormatterRenderFunction;
};
type FieldFormatters = {
array: FieldFormatter;
boolean: FieldFormatter;
date: FieldFormatter;
duration: FieldFormatter;
integer: FieldFormatter;
number: FieldFormatter;
percent_change: FieldFormatter;
percentage: FieldFormatter;
rate: FieldFormatter;
size: FieldFormatter;
string: FieldFormatter;
};
export type FieldTypes = keyof FieldFormatters;
const EmptyValueContainer = styled('span')`
color: ${p => p.theme.gray300};
`;
const emptyValue = <EmptyValueContainer>{t('(no value)')}</EmptyValueContainer>;
const emptyStringValue = <EmptyValueContainer>{t('(empty string)')}</EmptyValueContainer>;
export function nullableValue(value: string | null): string | React.ReactElement {
switch (value) {
case null:
return emptyValue;
case '':
return emptyStringValue;
default:
return value;
}
}
export const SIZE_UNITS = {
bit: 1 / 8,
byte: 1,
kibibyte: 1024,
mebibyte: 1024 ** 2,
gibibyte: 1024 ** 3,
tebibyte: 1024 ** 4,
pebibyte: 1024 ** 5,
exbibyte: 1024 ** 6,
kilobyte: 1000,
megabyte: 1000 ** 2,
gigabyte: 1000 ** 3,
terabyte: 1000 ** 4,
petabyte: 1000 ** 5,
exabyte: 1000 ** 6,
};
export const ABYTE_UNITS = [
'kilobyte',
'megabyte',
'gigabyte',
'terabyte',
'petabyte',
'exabyte',
];
export const DURATION_UNITS = {
nanosecond: 1 / 1000 ** 2,
microsecond: 1 / 1000,
millisecond: 1,
second: 1000,
minute: 1000 * 60,
hour: 1000 * 60 * 60,
day: 1000 * 60 * 60 * 24,
week: 1000 * 60 * 60 * 24 * 7,
};
export const PERCENTAGE_UNITS = ['ratio', 'percent'];
/**
* A mapping of field types to their rendering function.
* This mapping is used when a field is not defined in SPECIAL_FIELDS
* and the field is not being coerced to a link.
*
* This mapping should match the output sentry.utils.snuba:get_json_type
*/
export const FIELD_FORMATTERS: FieldFormatters = {
boolean: {
isSortable: true,
renderFunc: (field, data) => {
const value = data[field] ? t('true') : t('false');
return <Container>{value}</Container>;
},
},
date: {
isSortable: true,
renderFunc: (field, data, baggage) => (
<Container>
{data[field]
? getDynamicText({
value: (
<FieldDateTime
date={data[field]}
year
seconds
timeZone
utc={decodeScalar(baggage?.location?.query?.utc) === 'true'}
/>
),
fixed: 'timestamp',
})
: emptyValue}
</Container>
),
},
duration: {
isSortable: true,
renderFunc: (field, data, baggage) => {
const {unit} = baggage ?? {};
return (
<NumberContainer>
{typeof data[field] === 'number' ? (
<Duration
seconds={(data[field] * ((unit && DURATION_UNITS[unit]) ?? 1)) / 1000}
fixedDigits={2}
abbreviation
/>
) : (
emptyValue
)}
</NumberContainer>
);
},
},
rate: {
isSortable: true,
renderFunc: (field, data, baggage) => {
const {unit} = baggage ?? {};
return (
<NumberContainer>
{formatRate(data[field], unit as RateUnit, {minimumValue: 0.01})}
</NumberContainer>
);
},
},
integer: {
isSortable: true,
renderFunc: (field, data) => (
<NumberContainer>
{typeof data[field] === 'number' ? <Count value={data[field]} /> : emptyValue}
</NumberContainer>
),
},
number: {
isSortable: true,
renderFunc: (field, data) => (
<NumberContainer>
{typeof data[field] === 'number' ? formatFloat(data[field], 4) : emptyValue}
</NumberContainer>
),
},
percentage: {
isSortable: true,
renderFunc: (field, data) => (
<NumberContainer>
{typeof data[field] === 'number' ? formatPercentage(data[field]) : emptyValue}
</NumberContainer>
),
},
size: {
isSortable: true,
renderFunc: (field, data, baggage) => {
const {unit} = baggage ?? {};
return (
<NumberContainer>
{unit && SIZE_UNITS[unit] && typeof data[field] === 'number' ? (
<FileSize
bytes={data[field] * SIZE_UNITS[unit]}
base={ABYTE_UNITS.includes(unit) ? 10 : 2}
/>
) : (
emptyValue
)}
</NumberContainer>
);
},
},
string: {
isSortable: true,
renderFunc: (field, data) => {
// Some fields have long arrays in them, only show the tail of the data.
const value = Array.isArray(data[field])
? data[field].slice(-1)
: defined(data[field])
? data[field]
: emptyValue;
if (isUrl(value)) {
return (
<Container>
<ExternalLink href={value} data-test-id="group-tag-url">
{value}
</ExternalLink>
</Container>
);
}
return <Container>{nullableValue(value)}</Container>;
},
},
array: {
isSortable: true,
renderFunc: (field, data) => {
const value = toArray(data[field]);
return <ArrayValue value={value} />;
},
},
percent_change: {
isSortable: true,
renderFunc: (fieldName, data) => {
return <PercentChangeCell deltaValue={data[fieldName]} />;
},
},
};
type SpecialFieldRenderFunc = (
data: EventData,
baggage: RenderFunctionBaggage
) => React.ReactNode;
type SpecialField = {
renderFunc: SpecialFieldRenderFunc;
sortField: string | null;
};
type SpecialFields = {
attachments: SpecialField;
'count_unique(user)': SpecialField;
'error.handled': SpecialField;
id: SpecialField;
issue: SpecialField;
'issue.id': SpecialField;
minidump: SpecialField;
'profile.id': SpecialField;
project: SpecialField;
release: SpecialField;
replayId: SpecialField;
team_key_transaction: SpecialField;
'timestamp.to_day': SpecialField;
'timestamp.to_hour': SpecialField;
trace: SpecialField;
'trend_percentage()': SpecialField;
user: SpecialField;
'user.display': SpecialField;
};
const DownloadCount = styled('span')`
padding-left: ${space(0.75)};
`;
const RightAlignedContainer = styled('span')`
margin-left: auto;
margin-right: 0;
`;
/**
* "Special fields" either do not map 1:1 to an single column in the event database,
* or they require custom UI formatting that can't be handled by the datatype formatters.
*/
const SPECIAL_FIELDS: SpecialFields = {
// This is a custom renderer for a field outside discover
// TODO - refactor code and remove from this file or add ability to query for attachments in Discover
attachments: {
sortField: null,
renderFunc: (data, {organization, projectSlug}) => {
const attachments: Array<IssueAttachment> = data.attachments;
const items: MenuItemProps[] = attachments
.filter(attachment => attachment.type !== 'event.minidump')
.map(attachment => ({
key: attachment.id,
label: attachment.name,
onAction: () =>
window.open(
`/api/0/projects/${organization.slug}/${projectSlug}/events/${attachment.event_id}/attachments/${attachment.id}/?download=1`
),
}));
return (
<RightAlignedContainer>
<DropdownMenu
position="left"
size="xs"
triggerProps={{
showChevron: false,
icon: (
<Fragment>
<IconDownload color="gray500" size="sm" />
<DownloadCount>{items.length}</DownloadCount>
</Fragment>
),
}}
items={items}
/>
</RightAlignedContainer>
);
},
},
minidump: {
sortField: null,
renderFunc: (data, {organization, projectSlug}) => {
const attachments: Array<IssueAttachment & {url: string}> = data.attachments;
const minidump = attachments.find(
attachment => attachment.type === 'event.minidump'
);
return (
<RightAlignedContainer>
<Button
size="xs"
disabled={!minidump}
onClick={
minidump
? () => {
window.open(
`/api/0/projects/${organization.slug}/${projectSlug}/events/${minidump.event_id}/attachments/${minidump.id}/?download=1`
);
}
: undefined
}
>
<IconDownload color="gray500" size="sm" />
<DownloadCount>{minidump ? 1 : 0}</DownloadCount>
</Button>
</RightAlignedContainer>
);
},
},
id: {
sortField: 'id',
renderFunc: data => {
const id: string | unknown = data?.id;
if (typeof id !== 'string') {
return null;
}
return <Container>{getShortEventId(id)}</Container>;
},
},
trace: {
sortField: 'trace',
renderFunc: data => {
const id: string | unknown = data?.trace;
if (typeof id !== 'string') {
return emptyValue;
}
return <Container>{getShortEventId(id)}</Container>;
},
},
'issue.id': {
sortField: 'issue.id',
renderFunc: (data, {organization}) => {
const target = {
pathname: `/organizations/${organization.slug}/issues/${data['issue.id']}/`,
};
return (
<Container>
<OverflowLink to={target} aria-label={data['issue.id']}>
{data['issue.id']}
</OverflowLink>
</Container>
);
},
},
replayId: {
sortField: 'replayId',
renderFunc: data => {
const replayId = data?.replayId;
if (typeof replayId !== 'string' || !replayId) {
return emptyValue;
}
return <Container>{getShortEventId(replayId)}</Container>;
},
},
'profile.id': {
sortField: 'profile.id',
renderFunc: data => {
const id: string | unknown = data?.['profile.id'];
if (typeof id !== 'string') {
return emptyValue;
}
return <Container>{getShortEventId(id)}</Container>;
},
},
issue: {
sortField: null,
renderFunc: (data, {organization}) => {
const issueID = data['issue.id'];
if (!issueID) {
return (
<Container>
<FieldShortId shortId={`${data.issue}`} />
</Container>
);
}
const target = {
pathname: `/organizations/${organization.slug}/issues/${issueID}/`,
};
return (
<Container>
<QuickContextHoverWrapper
dataRow={data}
contextType={ContextType.ISSUE}
organization={organization}
>
<StyledLink to={target} aria-label={issueID}>
<OverflowFieldShortId shortId={`${data.issue}`} />
</StyledLink>
</QuickContextHoverWrapper>
</Container>
);
},
},
project: {
sortField: 'project',
renderFunc: (data, {organization}) => {
let slugs: string[] | undefined = undefined;
let projectIds: number[] | undefined = undefined;
if (typeof data.project === 'number') {
projectIds = [data.project];
} else {
slugs = [data.project];
}
return (
<Container>
<Projects orgId={organization.slug} slugs={slugs} projectIds={projectIds}>
{({projects}) => {
let project: Project | AvatarProject | undefined;
if (typeof data.project === 'number') {
project = projects.find(p => p.id === data.project.toString());
} else {
project = projects.find(p => p.slug === data.project);
}
return (
<ProjectBadge
project={project ? project : {slug: data.project}}
avatarSize={16}
/>
);
}}
</Projects>
</Container>
);
},
},
user: {
sortField: 'user',
renderFunc: data => {
if (data.user) {
const [key, value] = data.user.split(':');
const userObj = {
id: '',
name: '',
email: '',
username: '',
ip_address: '',
};
userObj[key] = value;
const badge = <UserBadge user={userObj} hideEmail avatarSize={16} />;
return <Container>{badge}</Container>;
}
return <Container>{emptyValue}</Container>;
},
},
'user.display': {
sortField: 'user.display',
renderFunc: data => {
if (data['user.display']) {
const userObj = {
id: '',
name: data['user.display'],
email: '',
username: '',
ip_address: '',
};
const badge = <UserBadge user={userObj} hideEmail avatarSize={16} />;
return <Container>{badge}</Container>;
}
return <Container>{emptyValue}</Container>;
},
},
'count_unique(user)': {
sortField: 'count_unique(user)',
renderFunc: data => {
const count = data.count_unique_user ?? data['count_unique(user)'];
if (typeof count === 'number') {
return (
<FlexContainer>
<NumberContainer>
<Count value={count} />
</NumberContainer>
<UserIcon size="md" />
</FlexContainer>
);
}
return <Container>{emptyValue}</Container>;
},
},
release: {
sortField: 'release',
renderFunc: (data, {organization}) =>
data.release ? (
<VersionContainer>
<QuickContextHoverWrapper
dataRow={data}
contextType={ContextType.RELEASE}
organization={organization}
>
<Version version={data.release} truncate />
</QuickContextHoverWrapper>
</VersionContainer>
) : (
<Container>{emptyValue}</Container>
),
},
'error.handled': {
sortField: 'error.handled',
renderFunc: data => {
const values = data['error.handled'];
// Transactions will have null, and default events have no handled attributes.
if (values === null || values?.length === 0) {
return <Container>{emptyValue}</Container>;
}
const value = Array.isArray(values) ? values : [values];
return (
<Container>
{value.every(v => [1, null].includes(v)) ? 'true' : 'false'}
</Container>
);
},
},
team_key_transaction: {
sortField: null,
renderFunc: (data, {organization}) => (
<TeamKeyTransactionField
isKeyTransaction={(data.team_key_transaction ?? 0) !== 0}
organization={organization}
projectSlug={data.project}
transactionName={data.transaction}
/>
),
},
'trend_percentage()': {
sortField: 'trend_percentage()',
renderFunc: data => (
<NumberContainer>
{typeof data.trend_percentage === 'number'
? formatPercentage(data.trend_percentage - 1)
: emptyValue}
</NumberContainer>
),
},
'timestamp.to_hour': {
sortField: 'timestamp.to_hour',
renderFunc: data => (
<Container>
{getDynamicText({
value: <FieldDateTime date={data['timestamp.to_hour']} year timeZone />,
fixed: 'timestamp.to_hour',
})}
</Container>
),
},
'timestamp.to_day': {
sortField: 'timestamp.to_day',
renderFunc: data => (
<Container>
{getDynamicText({
value: <FieldDateTime date={data['timestamp.to_day']} dateOnly year utc />,
fixed: 'timestamp.to_day',
})}
</Container>
),
},
};
type SpecialFunctionFieldRenderer = (
fieldName: string
) => (data: EventData, baggage: RenderFunctionBaggage) => React.ReactNode;
type SpecialFunctions = {
time_spent_percentage: SpecialFunctionFieldRenderer;
user_misery: SpecialFunctionFieldRenderer;
};
/**
* "Special functions" are functions whose values either do not map 1:1 to a single column,
* or they require custom UI formatting that can't be handled by the datatype formatters.
*/
const SPECIAL_FUNCTIONS: SpecialFunctions = {
user_misery: fieldName => data => {
const userMiseryField = fieldName;
if (!(userMiseryField in data)) {
return <NumberContainer>{emptyValue}</NumberContainer>;
}
const userMisery = data[userMiseryField];
if (userMisery === null || isNaN(userMisery)) {
return <NumberContainer>{emptyValue}</NumberContainer>;
}
const projectThresholdConfig = 'project_threshold_config';
let countMiserableUserField: string = '';
let miseryLimit: number | undefined = parseInt(
userMiseryField.split('(').pop()?.slice(0, -1) || '',
10
);
if (isNaN(miseryLimit)) {
countMiserableUserField = 'count_miserable(user)';
if (projectThresholdConfig in data) {
miseryLimit = data[projectThresholdConfig][1];
} else {
miseryLimit = undefined;
}
} else {
countMiserableUserField = `count_miserable(user,${miseryLimit})`;
}
const uniqueUsers = data['count_unique(user)'];
let miserableUsers: number | undefined;
if (countMiserableUserField in data) {
const countMiserableMiseryLimit = parseInt(
userMiseryField.split('(').pop()?.slice(0, -1) || '',
10
);
miserableUsers =
countMiserableMiseryLimit === miseryLimit ||
(isNaN(countMiserableMiseryLimit) && projectThresholdConfig)
? data[countMiserableUserField]
: undefined;
}
return (
<BarContainer>
<UserMisery
bars={10}
barHeight={20}
miseryLimit={miseryLimit}
totalUsers={uniqueUsers}
userMisery={userMisery}
miserableUsers={miserableUsers}
/>
</BarContainer>
);
},
time_spent_percentage: fieldName => data => {
return (
<TimeSpentCell
percentage={data[fieldName]}
total={data[`sum(${SpanMetricsField.SPAN_SELF_TIME})`]}
op={data[`span.op`]}
/>
);
},
};
/**
* Get the sort field name for a given field if it is special or fallback
* to the generic type formatter.
*/
export function getSortField(
field: string,
tableMeta: MetaType | undefined
): string | null {
if (SPECIAL_FIELDS.hasOwnProperty(field)) {
return SPECIAL_FIELDS[field as keyof typeof SPECIAL_FIELDS].sortField;
}
if (!tableMeta) {
return field;
}
if (isEquation(field)) {
return field;
}
for (const alias in AGGREGATIONS) {
if (field.startsWith(alias)) {
return AGGREGATIONS[alias].isSortable ? field : null;
}
}
const fieldType = tableMeta[field];
if (FIELD_FORMATTERS.hasOwnProperty(fieldType)) {
return FIELD_FORMATTERS[fieldType as keyof typeof FIELD_FORMATTERS].isSortable
? field
: null;
}
return null;
}
const isDurationValue = (data: EventData, field: string): boolean => {
return field in data && typeof data[field] === 'number';
};
export const spanOperationRelativeBreakdownRenderer = (
data: EventData,
{location, organization, eventView}: RenderFunctionBaggage,
options?: RenderFunctionOptions
): React.ReactNode => {
const {enableOnClick = true} = options ?? {};
const sumOfSpanTime = SPAN_OP_BREAKDOWN_FIELDS.reduce(
(prev, curr) => (isDurationValue(data, curr) ? prev + data[curr] : prev),
0
);
const cumulativeSpanOpBreakdown = Math.max(sumOfSpanTime, data['transaction.duration']);
if (
SPAN_OP_BREAKDOWN_FIELDS.every(field => !isDurationValue(data, field)) ||
cumulativeSpanOpBreakdown === 0
) {
return FIELD_FORMATTERS.duration.renderFunc(SPAN_OP_RELATIVE_BREAKDOWN_FIELD, data);
}
let otherPercentage = 1;
let orderedSpanOpsBreakdownFields;
const sortingOnField = eventView?.sorts?.[0]?.field;
if (sortingOnField && (SPAN_OP_BREAKDOWN_FIELDS as string[]).includes(sortingOnField)) {
orderedSpanOpsBreakdownFields = [
sortingOnField,
...SPAN_OP_BREAKDOWN_FIELDS.filter(op => op !== sortingOnField),
];
} else {
orderedSpanOpsBreakdownFields = SPAN_OP_BREAKDOWN_FIELDS;
}
return (
<RelativeOpsBreakdown data-test-id="relative-ops-breakdown">
{orderedSpanOpsBreakdownFields.map(field => {
if (!isDurationValue(data, field)) {
return null;
}
const operationName = getSpanOperationName(field) ?? 'op';
const spanOpDuration: number = data[field];
const widthPercentage = spanOpDuration / cumulativeSpanOpBreakdown;
otherPercentage = otherPercentage - widthPercentage;
if (widthPercentage === 0) {
return null;
}
return (
<div key={operationName} style={{width: toPercent(widthPercentage || 0)}}>
<Tooltip
title={
<div>
<div>{operationName}</div>
<div>
<Duration
seconds={spanOpDuration / 1000}
fixedDigits={2}
abbreviation
/>
</div>
</div>
}
containerDisplayMode="block"
>
<RectangleRelativeOpsBreakdown
style={{
backgroundColor: pickBarColor(operationName),
cursor: enableOnClick ? 'pointer' : 'default',
}}
onClick={event => {
if (!enableOnClick) {
return;
}
event.stopPropagation();
const filter = stringToFilter(operationName);
if (filter === SpanOperationBreakdownFilter.NONE) {
return;
}
trackAnalytics('performance_views.relative_breakdown.selection', {
action: filter,
organization,
});
browserHistory.push({
pathname: location.pathname,
query: {
...location.query,
...filterToLocationQuery(filter),
},
});
}}
/>
</Tooltip>
</div>
);
})}
<div key="other" style={{width: toPercent(otherPercentage || 0)}}>
<Tooltip title={<div>{t('Other')}</div>} containerDisplayMode="block">
<OtherRelativeOpsBreakdown />
</Tooltip>
</div>
</RelativeOpsBreakdown>
);
};
const RelativeOpsBreakdown = styled('div')`
position: relative;
display: flex;
`;
const RectangleRelativeOpsBreakdown = styled(RowRectangle)`
position: relative;
width: 100%;
`;
const OtherRelativeOpsBreakdown = styled(RectangleRelativeOpsBreakdown)`
background-color: ${p => p.theme.gray100};
`;
const StyledLink = styled(Link)`
max-width: 100%;
`;
/**
* Get the field renderer for the named field and metadata
*
* @param {String} field name
* @param {object} metadata mapping.
* @param {boolean} isAlias convert the name with getAggregateAlias
* @returns {Function}
*/
export function getFieldRenderer(
field: string,
meta: MetaType,
isAlias: boolean = true
): FieldFormatterRenderFunctionPartial {
if (SPECIAL_FIELDS.hasOwnProperty(field)) {
return SPECIAL_FIELDS[field].renderFunc;
}
if (isRelativeSpanOperationBreakdownField(field)) {
return spanOperationRelativeBreakdownRenderer;
}
const fieldName = isAlias ? getAggregateAlias(field) : field;
const fieldType = meta[fieldName];
for (const alias in SPECIAL_FUNCTIONS) {
if (fieldName.startsWith(alias)) {
return SPECIAL_FUNCTIONS[alias](fieldName);
}
}
if (FIELD_FORMATTERS.hasOwnProperty(fieldType)) {
return partial(FIELD_FORMATTERS[fieldType].renderFunc, fieldName);
}
return partial(FIELD_FORMATTERS.string.renderFunc, fieldName);
}
type FieldTypeFormatterRenderFunctionPartial = (
data: EventData,
baggage?: RenderFunctionBaggage
) => React.ReactNode;
/**
* Get the field renderer for the named field only based on its type from the given
* metadata.
*
* @param {String} field name
* @param {object} metadata mapping.
* @param {boolean} isAlias convert the name with getAggregateAlias
* @returns {Function}
*/
export function getFieldFormatter(
field: string,
meta: MetaType,
isAlias: boolean = true
): FieldTypeFormatterRenderFunctionPartial {
const fieldName = isAlias ? getAggregateAlias(field) : field;
const fieldType = meta[fieldName];
if (FIELD_FORMATTERS.hasOwnProperty(fieldType)) {
return partial(FIELD_FORMATTERS[fieldType].renderFunc, fieldName);
}
return partial(FIELD_FORMATTERS.string.renderFunc, fieldName);
}