|
@@ -0,0 +1,399 @@
|
|
|
+import {Fragment, useMemo, useState} from 'react';
|
|
|
+import styled from '@emotion/styled';
|
|
|
+import pick from 'lodash/pick';
|
|
|
+
|
|
|
+import Accordion from 'sentry/components/accordion/accordion';
|
|
|
+import {LinkButton} from 'sentry/components/button';
|
|
|
+import EventsRequest, {RenderProps} from 'sentry/components/charts/eventsRequest';
|
|
|
+import {getInterval} from 'sentry/components/charts/utils';
|
|
|
+import LoadingIndicator from 'sentry/components/loadingIndicator';
|
|
|
+import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
|
|
|
+import PerformanceDuration from 'sentry/components/performanceDuration';
|
|
|
+import Truncate from 'sentry/components/truncate';
|
|
|
+import {CHART_PALETTE} from 'sentry/constants/chartPalette';
|
|
|
+import {t} from 'sentry/locale';
|
|
|
+import {space} from 'sentry/styles/space';
|
|
|
+import {Series, SeriesDataUnit} from 'sentry/types/echarts';
|
|
|
+import {defined} from 'sentry/utils';
|
|
|
+import {tooltipFormatterUsingAggregateOutputType} from 'sentry/utils/discover/charts';
|
|
|
+import DiscoverQuery from 'sentry/utils/discover/discoverQuery';
|
|
|
+import {DiscoverDatasets} from 'sentry/utils/discover/types';
|
|
|
+import {formatVersion} from 'sentry/utils/formatters';
|
|
|
+import {useMEPSettingContext} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
|
|
|
+import {usePageError} from 'sentry/utils/performance/contexts/pageError';
|
|
|
+import {MutableSearch} from 'sentry/utils/tokenizeSearch';
|
|
|
+import useApi from 'sentry/utils/useApi';
|
|
|
+import {useLocation} from 'sentry/utils/useLocation';
|
|
|
+import usePageFilters from 'sentry/utils/usePageFilters';
|
|
|
+import {normalizeUrl} from 'sentry/utils/withDomainRequired';
|
|
|
+import {GenericPerformanceWidget} from 'sentry/views/performance/landing/widgets/components/performanceWidget';
|
|
|
+import {
|
|
|
+ GrowLink,
|
|
|
+ WidgetEmptyStateWarning,
|
|
|
+} from 'sentry/views/performance/landing/widgets/components/selectableList';
|
|
|
+import {transformDiscoverToList} from 'sentry/views/performance/landing/widgets/transforms/transformDiscoverToList';
|
|
|
+import {transformEventsRequestToArea} from 'sentry/views/performance/landing/widgets/transforms/transformEventsToArea';
|
|
|
+import {
|
|
|
+ PerformanceWidgetProps,
|
|
|
+ QueryDefinition,
|
|
|
+ QueryDefinitionWithKey,
|
|
|
+ WidgetDataConstraint,
|
|
|
+ WidgetDataResult,
|
|
|
+ WidgetPropUnion,
|
|
|
+} from 'sentry/views/performance/landing/widgets/types';
|
|
|
+import {
|
|
|
+ eventsRequestQueryProps,
|
|
|
+ getMEPParamsIfApplicable,
|
|
|
+ QUERY_LIMIT_PARAM,
|
|
|
+ TOTAL_EXPANDABLE_ROWS_HEIGHT,
|
|
|
+} from 'sentry/views/performance/landing/widgets/utils';
|
|
|
+import {Subtitle} from 'sentry/views/profiling/landing/styles';
|
|
|
+import {RightAlignedCell} from 'sentry/views/replays/deadRageClick/deadRageSelectorCards';
|
|
|
+import Chart from 'sentry/views/starfish/components/chart';
|
|
|
+import {useReleaseSelection} from 'sentry/views/starfish/queries/useReleases';
|
|
|
+import {STARFISH_CHART_INTERVAL_FIDELITY} from 'sentry/views/starfish/utils/constants';
|
|
|
+import {appendReleaseFilters} from 'sentry/views/starfish/utils/releaseComparison';
|
|
|
+import {OUTPUT_TYPE, YAxis, YAXIS_COLUMNS} from 'sentry/views/starfish/views/screens';
|
|
|
+
|
|
|
+type DataType = {
|
|
|
+ chart: WidgetDataResult & ReturnType<typeof transformEventsRequestToArea>;
|
|
|
+ list: WidgetDataResult & ReturnType<typeof transformDiscoverToList>;
|
|
|
+};
|
|
|
+
|
|
|
+export function transformEventsChartRequest<T extends WidgetDataConstraint>(
|
|
|
+ widgetProps: WidgetPropUnion<T>,
|
|
|
+ results: RenderProps,
|
|
|
+ _: QueryDefinitionWithKey<T>
|
|
|
+) {
|
|
|
+ const {start, end, utc, interval, statsPeriod} = normalizeDateTimeParams(
|
|
|
+ widgetProps.location.query
|
|
|
+ );
|
|
|
+
|
|
|
+ const data = results.results ?? [];
|
|
|
+
|
|
|
+ const childData = {
|
|
|
+ ...results,
|
|
|
+ isLoading: results.loading || results.reloading,
|
|
|
+ isErrored: results.errored,
|
|
|
+ hasData: defined(data) && !!data.length && !!data[0].data.length,
|
|
|
+ data,
|
|
|
+ previousData: results.previousTimeseriesData ?? undefined,
|
|
|
+
|
|
|
+ utc: utc === 'true',
|
|
|
+ interval,
|
|
|
+ statsPeriod: statsPeriod ?? undefined,
|
|
|
+ start: start ?? '',
|
|
|
+ end: end ?? '',
|
|
|
+ };
|
|
|
+
|
|
|
+ return childData;
|
|
|
+}
|
|
|
+
|
|
|
+function SlowScreensByTTID(props: PerformanceWidgetProps) {
|
|
|
+ const api = useApi();
|
|
|
+ const pageFilter = usePageFilters();
|
|
|
+ const mepSetting = useMEPSettingContext();
|
|
|
+ const {
|
|
|
+ isLoading: isLoadingReleases,
|
|
|
+ primaryRelease,
|
|
|
+ secondaryRelease,
|
|
|
+ } = useReleaseSelection();
|
|
|
+ const location = useLocation();
|
|
|
+ const [selectedListIndex, setSelectListIndex] = useState<number>(0);
|
|
|
+ const {organization, InteractiveTitle} = props;
|
|
|
+ const pageError = usePageError();
|
|
|
+
|
|
|
+ const field = props.fields[0];
|
|
|
+
|
|
|
+ const listQuery = useMemo<QueryDefinition<DataType, WidgetDataResult>>(
|
|
|
+ () => ({
|
|
|
+ fields: field,
|
|
|
+ component: provided => {
|
|
|
+ if (isLoadingReleases) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const eventView = provided.eventView.clone();
|
|
|
+ let extraQueryParams = getMEPParamsIfApplicable(mepSetting, props.chartSetting);
|
|
|
+
|
|
|
+ // Set fields
|
|
|
+ eventView.fields = [
|
|
|
+ {field: 'transaction'},
|
|
|
+ {field: YAXIS_COLUMNS[YAxis.TTID]},
|
|
|
+ {field: 'count()'},
|
|
|
+ ];
|
|
|
+ eventView.sorts = [
|
|
|
+ {
|
|
|
+ field: 'count()',
|
|
|
+ kind: 'desc',
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
+ // Change data set to metrics
|
|
|
+ eventView.dataset = DiscoverDatasets.METRICS;
|
|
|
+ extraQueryParams = {
|
|
|
+ ...extraQueryParams,
|
|
|
+ dataset: DiscoverDatasets.METRICS,
|
|
|
+ };
|
|
|
+
|
|
|
+ // Update query
|
|
|
+ const mutableSearch = new MutableSearch(eventView.query);
|
|
|
+ mutableSearch.addFilterValue('event.type', 'transaction');
|
|
|
+ mutableSearch.addFilterValue('transaction.op', 'ui.load');
|
|
|
+ eventView.query = appendReleaseFilters(
|
|
|
+ mutableSearch,
|
|
|
+ primaryRelease,
|
|
|
+ secondaryRelease
|
|
|
+ );
|
|
|
+
|
|
|
+ return (
|
|
|
+ <DiscoverQuery
|
|
|
+ {...provided}
|
|
|
+ eventView={eventView}
|
|
|
+ location={location}
|
|
|
+ limit={QUERY_LIMIT_PARAM}
|
|
|
+ cursor="0:0:1"
|
|
|
+ noPagination
|
|
|
+ queryExtras={extraQueryParams}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ },
|
|
|
+ transform: transformDiscoverToList,
|
|
|
+ }),
|
|
|
+ // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
+ [props.chartSetting, mepSetting.memoizationKey, primaryRelease, secondaryRelease]
|
|
|
+ );
|
|
|
+
|
|
|
+ const chartQuery = useMemo<QueryDefinition<DataType, WidgetDataResult>>(
|
|
|
+ () => {
|
|
|
+ return {
|
|
|
+ enabled: widgetData => {
|
|
|
+ return !!widgetData?.list?.data?.length;
|
|
|
+ },
|
|
|
+ fields: field,
|
|
|
+ component: provided => {
|
|
|
+ const eventView = props.eventView.clone();
|
|
|
+ let extraQueryParams = getMEPParamsIfApplicable(mepSetting, props.chartSetting);
|
|
|
+ const pageFilterDatetime = {
|
|
|
+ start: provided.start,
|
|
|
+ end: provided.end,
|
|
|
+ period: provided.period,
|
|
|
+ };
|
|
|
+
|
|
|
+ // Chart options
|
|
|
+ const currentSeriesNames = [field];
|
|
|
+ const includePreviousParam = false;
|
|
|
+ const yAxis = provided.yAxis;
|
|
|
+ const interval = getInterval(pageFilterDatetime, 'medium');
|
|
|
+ const partialDataParam = true;
|
|
|
+
|
|
|
+ eventView.additionalConditions.setFilterValues('transaction', [
|
|
|
+ provided.widgetData.list.data[selectedListIndex].transaction as string,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ eventView.dataset = DiscoverDatasets.METRICS;
|
|
|
+ extraQueryParams = {
|
|
|
+ ...extraQueryParams,
|
|
|
+ dataset: DiscoverDatasets.METRICS,
|
|
|
+ };
|
|
|
+
|
|
|
+ eventView.fields = [
|
|
|
+ {field: 'avg(measurements.time_to_initial_display)'},
|
|
|
+ {field: 'release'},
|
|
|
+ ];
|
|
|
+ const mutableSearch = new MutableSearch(eventView.query);
|
|
|
+ mutableSearch.addFilterValue('event.type', 'transaction');
|
|
|
+ mutableSearch.addFilterValue('transaction.op', 'ui.load');
|
|
|
+ eventView.query = appendReleaseFilters(
|
|
|
+ mutableSearch,
|
|
|
+ primaryRelease,
|
|
|
+ secondaryRelease
|
|
|
+ );
|
|
|
+ eventView.interval = getInterval(
|
|
|
+ pageFilter.selection.datetime,
|
|
|
+ STARFISH_CHART_INTERVAL_FIDELITY
|
|
|
+ );
|
|
|
+
|
|
|
+ return (
|
|
|
+ <EventsRequest
|
|
|
+ {...pick(provided, eventsRequestQueryProps)}
|
|
|
+ api={api}
|
|
|
+ yAxis={yAxis}
|
|
|
+ includePrevious={includePreviousParam}
|
|
|
+ includeTransformedData
|
|
|
+ partial={partialDataParam}
|
|
|
+ currentSeriesNames={currentSeriesNames}
|
|
|
+ field={eventView.getFields()}
|
|
|
+ query={eventView.getQueryWithAdditionalConditions()}
|
|
|
+ interval={interval}
|
|
|
+ hideError
|
|
|
+ onError={pageError.setPageError}
|
|
|
+ queryExtras={extraQueryParams}
|
|
|
+ topEvents={2}
|
|
|
+ referrer="performance-line-chart-widget"
|
|
|
+ />
|
|
|
+ );
|
|
|
+ },
|
|
|
+ transform: transformEventsChartRequest,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
+ [
|
|
|
+ props.chartSetting,
|
|
|
+ selectedListIndex,
|
|
|
+ mepSetting.memoizationKey,
|
|
|
+ primaryRelease,
|
|
|
+ secondaryRelease,
|
|
|
+ ]
|
|
|
+ );
|
|
|
+
|
|
|
+ const Queries = {
|
|
|
+ list: listQuery,
|
|
|
+ chart: chartQuery,
|
|
|
+ };
|
|
|
+
|
|
|
+ const assembleAccordionItems = provided =>
|
|
|
+ getItems(provided).map(item => ({header: item, content: getChart(provided)}));
|
|
|
+
|
|
|
+ const getChart = provided =>
|
|
|
+ function () {
|
|
|
+ const transformedReleaseSeries: {
|
|
|
+ [releaseVersion: string]: Series;
|
|
|
+ } = {};
|
|
|
+
|
|
|
+ const series = provided.widgetData.chart.data;
|
|
|
+ if (defined(series)) {
|
|
|
+ series.forEach(({seriesName: release, data}) => {
|
|
|
+ const isPrimary = release === primaryRelease;
|
|
|
+
|
|
|
+ const label = release;
|
|
|
+ const seriesData =
|
|
|
+ data.map(datum => {
|
|
|
+ return {
|
|
|
+ name: datum.name,
|
|
|
+ value: datum.value,
|
|
|
+ } as SeriesDataUnit;
|
|
|
+ }) ?? [];
|
|
|
+
|
|
|
+ const color = isPrimary ? CHART_PALETTE[3][0] : CHART_PALETTE[3][1];
|
|
|
+ transformedReleaseSeries[release] = {
|
|
|
+ seriesName: formatVersion(label),
|
|
|
+ color,
|
|
|
+ data: seriesData,
|
|
|
+ };
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Chart
|
|
|
+ height={props.chartHeight}
|
|
|
+ data={Object.values(transformedReleaseSeries)}
|
|
|
+ loading={provided.widgetData.chart.isLoading}
|
|
|
+ utc={false}
|
|
|
+ grid={{
|
|
|
+ left: '0',
|
|
|
+ right: '0',
|
|
|
+ top: '8px',
|
|
|
+ bottom: '0',
|
|
|
+ }}
|
|
|
+ isLineChart
|
|
|
+ aggregateOutputFormat={OUTPUT_TYPE[YAxis.TTID]}
|
|
|
+ tooltipFormatterOptions={{
|
|
|
+ valueFormatter: value =>
|
|
|
+ tooltipFormatterUsingAggregateOutputType(value, OUTPUT_TYPE[YAxis.TTID]),
|
|
|
+ }}
|
|
|
+ errored={provided.widgetData.chart.isErrored}
|
|
|
+ disableXAxis
|
|
|
+ showLegend={false}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ const getItems = provided =>
|
|
|
+ provided.widgetData.list.data.map(
|
|
|
+ listItem =>
|
|
|
+ function () {
|
|
|
+ const transaction = (listItem.transaction as string | undefined) ?? '';
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Fragment>
|
|
|
+ <GrowLink
|
|
|
+ to={normalizeUrl({
|
|
|
+ pathname: `/performance/mobile/screens/spans/`,
|
|
|
+ query: {
|
|
|
+ project: listItem['project.id'],
|
|
|
+ transaction,
|
|
|
+ primaryRelease,
|
|
|
+ secondaryRelease,
|
|
|
+ ...normalizeDateTimeParams(location.query),
|
|
|
+ },
|
|
|
+ })}
|
|
|
+ >
|
|
|
+ <Truncate value={transaction} maxLength={40} />
|
|
|
+ </GrowLink>
|
|
|
+ <RightAlignedCell>
|
|
|
+ <StyledDurationWrapper>
|
|
|
+ <PerformanceDuration
|
|
|
+ milliseconds={listItem['avg(measurements.time_to_initial_display)']}
|
|
|
+ abbreviation
|
|
|
+ />
|
|
|
+ </StyledDurationWrapper>
|
|
|
+ </RightAlignedCell>
|
|
|
+ </Fragment>
|
|
|
+ );
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ const Visualizations = [
|
|
|
+ {
|
|
|
+ component: provided =>
|
|
|
+ isLoadingReleases || provided.widgetData.chart.loading ? (
|
|
|
+ <LoadingIndicator />
|
|
|
+ ) : (
|
|
|
+ <Accordion
|
|
|
+ expandedIndex={selectedListIndex}
|
|
|
+ setExpandedIndex={setSelectListIndex}
|
|
|
+ items={assembleAccordionItems(provided)}
|
|
|
+ />
|
|
|
+ ),
|
|
|
+ // accordion items height + chart height
|
|
|
+ height: TOTAL_EXPANDABLE_ROWS_HEIGHT + props.chartHeight,
|
|
|
+ noPadding: true,
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <GenericPerformanceWidget
|
|
|
+ {...props}
|
|
|
+ location={location}
|
|
|
+ Subtitle={() => <Subtitle>{props.subTitle}</Subtitle>}
|
|
|
+ HeaderActions={() => (
|
|
|
+ <LinkButton
|
|
|
+ to={normalizeUrl({
|
|
|
+ pathname: `/organizations/${organization.slug}/performance/mobile/screens/`,
|
|
|
+ query: {
|
|
|
+ ...normalizeDateTimeParams(pageFilter),
|
|
|
+ primaryRelease,
|
|
|
+ secondaryRelease,
|
|
|
+ },
|
|
|
+ })}
|
|
|
+ size="sm"
|
|
|
+ >
|
|
|
+ {t('View All')}
|
|
|
+ </LinkButton>
|
|
|
+ )}
|
|
|
+ InteractiveTitle={
|
|
|
+ InteractiveTitle ? () => <InteractiveTitle isLoading={false} /> : null
|
|
|
+ }
|
|
|
+ EmptyComponent={WidgetEmptyStateWarning}
|
|
|
+ Queries={Queries}
|
|
|
+ Visualizations={Visualizations}
|
|
|
+ />
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export default SlowScreensByTTID;
|
|
|
+
|
|
|
+const StyledDurationWrapper = styled('div')`
|
|
|
+ padding: 0 ${space(1)};
|
|
|
+`;
|