Files
sentinel/static/app/views/ddm/customMetricsEventData.tsx
2024-02-15 17:01:50 +00:00

229 lines
6.8 KiB
TypeScript

import {Fragment, useState} from 'react';
import styled from '@emotion/styled';
import {LinkButton} from 'sentry/components/button';
import {EventDataSection} from 'sentry/components/events/eventDataSection';
import KeyValueList from 'sentry/components/events/interfaces/keyValueList';
import type {
MetricsSummary,
MetricsSummaryItem,
} from 'sentry/components/events/interfaces/spans/types';
import Link from 'sentry/components/links/link';
import {normalizeDateTimeString} from 'sentry/components/organizations/pageFilters/parse';
import Pill from 'sentry/components/pill';
import Pills from 'sentry/components/pills';
import TextOverflow from 'sentry/components/textOverflow';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {MRI, Organization} from 'sentry/types';
import {getDdmUrl, getDefaultMetricOp} from 'sentry/utils/metrics';
import {hasDDMFeature} from 'sentry/utils/metrics/features';
import {
formatMetricUsingUnit,
getReadableMetricType,
} from 'sentry/utils/metrics/formatters';
import {formatMRI, parseMRI} from 'sentry/utils/metrics/mri';
import {MetricDisplayType} from 'sentry/utils/metrics/types';
import useOrganization from 'sentry/utils/useOrganization';
function flattenMetricsSummary(
metricsSummary: MetricsSummary
): {item: MetricsSummaryItem; key: string; mri: MRI}[] {
return (
Object.entries(metricsSummary) as [
keyof MetricsSummary,
MetricsSummary[keyof MetricsSummary],
][]
).flatMap(([mri, items]) =>
(items || []).map((item, index) => ({item, mri, key: `${mri}${index}`}))
);
}
function tagToQuery(tagKey: string, tagValue: string) {
return `${tagKey}:"${tagValue}"`;
}
const HALF_HOUR_IN_MS = 30 * 60 * 1000;
export function CustomMetricsEventData({
metricsSummary,
startTimestamp,
}: {
startTimestamp: number;
metricsSummary?: MetricsSummary;
}) {
const organization = useOrganization();
const metricsSummaryEntries = metricsSummary
? flattenMetricsSummary(metricsSummary)
: [];
const widgetStart = new Date(startTimestamp * 1000 - HALF_HOUR_IN_MS);
const widgetEnd = new Date(startTimestamp * 1000 + HALF_HOUR_IN_MS);
if (!hasDDMFeature(organization) || metricsSummaryEntries.length === 0) {
return null;
}
return (
<EventDataSection type="custom-metrics" title={t('Emitted Metrics')}>
{metricsSummaryEntries.map(({mri, item, key}) => {
return (
<Fragment key={key}>
<KeyValueList
shouldSort={false}
data={[
{
key: 'name',
subject: t('Name'),
value: <TextOverflow>{formatMRI(mri)}</TextOverflow>,
actionButton: (
<LinkButton
size="xs"
to={getDdmUrl(organization.slug, {
start: normalizeDateTimeString(widgetStart),
end: normalizeDateTimeString(widgetEnd),
widgets: [
{
mri,
displayType: MetricDisplayType.LINE,
op: getDefaultMetricOp(mri),
query: Object.entries(item.tags ?? {})
.map(([tagKey, tagValue]) => tagToQuery(tagKey, tagValue))
.join(' '),
},
],
})}
>
{t('Open in Metrics')}
</LinkButton>
),
},
{
key: 'stats',
subject: t('Stats'),
value: <MetricStats item={item} mri={mri} />,
},
item.tags && Object.keys(item.tags).length > 0
? {
key: 'tags',
subject: t('Tags'),
value: (
<Tags tags={item.tags} organization={organization} mri={mri} />
),
}
: null,
].filter((row): row is Exclude<typeof row, null> => Boolean(row))}
/>
</Fragment>
);
})}
</EventDataSection>
);
}
function MetricStats({mri, item}: {item: MetricsSummaryItem; mri: MRI}) {
const parsedMRI = parseMRI(mri);
const unit = parsedMRI?.unit ?? 'none';
const type = parsedMRI?.type ?? 'c';
const typeLine = t(`Type: %s`, getReadableMetricType(type));
// We use formatMetricUsingUnit with unit 'none' to ensure uniform number formatting
const countLine = t(`Count: %s`, formatMetricUsingUnit(item.count, 'none'));
// For counters the other stats offer little value, so we only show type and count
if (type === 'c' || !item.count) {
return (
<pre>
{typeLine}
<br />
{countLine}
</pre>
);
}
// If there is only one value, min, max, avg and sum are all the same
if (item.count <= 1) {
return (
<pre>
{typeLine}
<br />
{t('Value: %s', formatMetricUsingUnit(item.sum, unit))}
</pre>
);
}
return (
<pre>
{typeLine}
<br />
{countLine}
<br />
{t('Sum: %s', formatMetricUsingUnit(item.sum, unit))}
<br />
{t('Min: %s', formatMetricUsingUnit(item.min, unit))}
<br />
{t('Max: %s', formatMetricUsingUnit(item.max, unit))}
<br />
{t(
'Avg: %s',
formatMetricUsingUnit(item.sum && item.count && item.sum / item.count, unit)
)}
</pre>
);
}
function Tags({
tags,
organization,
mri,
}: {
mri: MRI;
organization: Organization;
tags: Record<string, string>;
}) {
const [showingAll, setShowingAll] = useState(false);
const renderedTags = Object.entries(tags).slice(0, showingAll ? undefined : 5);
const renderText = showingAll ? t('Show less') : t('Show more') + '...';
return (
<StyledPills>
{renderedTags.map(([tagKey, tagValue]) => (
<StyledPill key={tagKey} name={tagKey}>
<Link
to={getDdmUrl(organization.slug, {
widgets: [
{
mri,
displayType: MetricDisplayType.LINE,
op: getDefaultMetricOp(mri),
query: tagToQuery(tagKey, tagValue),
},
],
})}
>
{tagValue}
</Link>
</StyledPill>
))}
{Object.entries(tags).length > 5 && (
<ShowMore onClick={() => setShowingAll(prev => !prev)}>{renderText}</ShowMore>
)}
</StyledPills>
);
}
const StyledPills = styled(Pills)`
padding-top: ${space(1)};
`;
const StyledPill = styled(Pill)`
width: min-content;
`;
const ShowMore = styled('a')`
white-space: nowrap;
align-self: center;
margin-bottom: ${space(1)};
padding: ${space(0.5)} ${space(0.5)};
`;