import {Fragment, useMemo} from 'react'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import Alert from 'sentry/components/alert'; import LoadingContainer from 'sentry/components/loading/loadingContainer'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import type {CursorHandler} from 'sentry/components/pagination'; import SearchBar from 'sentry/components/performance/searchBar'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {NewQuery} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {defined} from 'sentry/utils'; import {browserHistory} from 'sentry/utils/browserHistory'; import EventView from 'sentry/utils/discover/eventView'; import type {AggregationOutputType} from 'sentry/utils/discover/fields'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {decodeScalar} from 'sentry/utils/queryString'; import {escapeFilterValue, MutableSearch} from 'sentry/utils/tokenizeSearch'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import useRouter from 'sentry/utils/useRouter'; import {prepareQueryForLandingPage} from 'sentry/views/performance/data'; import {TOP_SCREENS} from 'sentry/views/performance/mobile/constants'; import {MobileCursors} from 'sentry/views/performance/mobile/screenload/screens/constants'; import { DEFAULT_PLATFORM, PLATFORM_LOCAL_STORAGE_KEY, PLATFORM_QUERY_PARAM, } from 'sentry/views/performance/mobile/screenload/screens/platformSelector'; import {ScreensBarChart} from 'sentry/views/performance/mobile/screenload/screens/screenBarChart'; import { ScreensTable, useTableQuery, } from 'sentry/views/performance/mobile/screenload/screens/screensTable'; import {SETUP_CONTENT} from 'sentry/views/performance/mobile/screenload/screens/setupContent'; import {TabbedCodeSnippet} from 'sentry/views/performance/mobile/screenload/screens/tabbedCodeSnippets'; import { isCrossPlatform, transformReleaseEvents, } from 'sentry/views/performance/mobile/screenload/screens/utils'; import useTruncatedReleaseNames from 'sentry/views/performance/mobile/useTruncatedRelease'; import {getTransactionSearchQuery} from 'sentry/views/performance/utils'; import ChartPanel from 'sentry/views/starfish/components/chartPanel'; import {useTTFDConfigured} from 'sentry/views/starfish/queries/useHasTtfdConfigured'; import {useReleaseSelection} from 'sentry/views/starfish/queries/useReleases'; import {SpanMetricsField} from 'sentry/views/starfish/types'; import {appendReleaseFilters} from 'sentry/views/starfish/utils/releaseComparison'; export enum YAxis { WARM_START = 0, COLD_START = 1, TTID = 2, TTFD = 3, SLOW_FRAME_RATE = 4, FROZEN_FRAME_RATE = 5, THROUGHPUT = 6, COUNT = 7, SLOW_FRAMES = 8, FROZEN_FRAMES = 9, FRAMES_DELAY = 10, } export const YAXIS_COLUMNS: Readonly> = { [YAxis.WARM_START]: 'avg(measurements.app_start_warm)', [YAxis.COLD_START]: 'avg(measurements.app_start_cold)', [YAxis.TTID]: 'avg(measurements.time_to_initial_display)', [YAxis.TTFD]: 'avg(measurements.time_to_full_display)', [YAxis.SLOW_FRAME_RATE]: 'avg(measurements.frames_slow_rate)', [YAxis.FROZEN_FRAME_RATE]: 'avg(measurements.frames_frozen_rate)', [YAxis.THROUGHPUT]: 'tpm()', [YAxis.COUNT]: 'count()', // Using span metrics [YAxis.SLOW_FRAMES]: 'avg(mobile.slow_frames)', [YAxis.FROZEN_FRAMES]: 'avg(mobile.frozen_frames)', [YAxis.FRAMES_DELAY]: 'avg(mobile.frames_delay)', }; export const READABLE_YAXIS_LABELS: Readonly> = { [YAxis.WARM_START]: 'avg(app_start_warm)', [YAxis.COLD_START]: 'avg(app_start_cold)', [YAxis.TTID]: 'avg(time_to_initial_display)', [YAxis.TTFD]: 'avg(time_to_full_display)', [YAxis.SLOW_FRAME_RATE]: 'avg(frames_slow_rate)', [YAxis.FROZEN_FRAME_RATE]: 'avg(frames_frozen_rate)', [YAxis.THROUGHPUT]: 'tpm()', [YAxis.COUNT]: 'count()', [YAxis.SLOW_FRAMES]: 'avg(mobile.slow_frames)', [YAxis.FROZEN_FRAMES]: 'avg(mobile.frozen_frames)', [YAxis.FRAMES_DELAY]: 'avg(mobile.frames_delay)', }; export const CHART_TITLES: Readonly> = { [YAxis.WARM_START]: t('Warm Start'), [YAxis.COLD_START]: t('Cold Start'), [YAxis.TTID]: t('Time To Initial Display'), [YAxis.TTFD]: t('Time To Full Display'), [YAxis.SLOW_FRAME_RATE]: t('Slow Frame Rate'), [YAxis.FROZEN_FRAME_RATE]: t('Frozen Frame Rate'), [YAxis.THROUGHPUT]: t('Throughput'), [YAxis.COUNT]: t('Count'), [YAxis.SLOW_FRAMES]: t('Slow Frames'), [YAxis.FROZEN_FRAMES]: t('Frozen Frames'), [YAxis.FRAMES_DELAY]: t('Frames Delay'), }; export const OUTPUT_TYPE: Readonly> = { [YAxis.WARM_START]: 'duration', [YAxis.COLD_START]: 'duration', [YAxis.TTID]: 'duration', [YAxis.TTFD]: 'duration', [YAxis.SLOW_FRAME_RATE]: 'percentage', [YAxis.FROZEN_FRAME_RATE]: 'percentage', [YAxis.THROUGHPUT]: 'number', [YAxis.COUNT]: 'number', [YAxis.SLOW_FRAMES]: 'number', [YAxis.FROZEN_FRAMES]: 'number', [YAxis.FRAMES_DELAY]: 'duration', }; type Props = { yAxes: YAxis[]; additionalFilters?: string[]; chartHeight?: number; project?: Project | null; }; export function ScreensView({yAxes, additionalFilters, chartHeight, project}: Props) { const pageFilter = usePageFilters(); const {selection} = pageFilter; const location = useLocation(); const theme = useTheme(); const organization = useOrganization(); const {query: locationQuery} = location; const cursor = decodeScalar(location.query?.[MobileCursors.SCREENS_TABLE]); const hasPlatformSelectFeature = organization.features.includes('spans-first-ui'); const yAxisCols = yAxes.map(val => YAXIS_COLUMNS[val]); const platform = decodeScalar(locationQuery[PLATFORM_QUERY_PARAM]) ?? localStorage.getItem(PLATFORM_LOCAL_STORAGE_KEY) ?? DEFAULT_PLATFORM; const { primaryRelease, secondaryRelease, isLoading: isReleasesLoading, } = useReleaseSelection(); const {truncatedPrimaryRelease, truncatedSecondaryRelease} = useTruncatedReleaseNames(); const router = useRouter(); const {hasTTFD} = useTTFDConfigured(additionalFilters); const queryString = useMemo(() => { const query = new MutableSearch([ 'event.type:transaction', 'transaction.op:ui.load', ...(additionalFilters ?? []), ]); if (project && isCrossPlatform(project) && hasPlatformSelectFeature) { query.addFilterValue('os.name', platform); } const searchQuery = decodeScalar(locationQuery.query, ''); if (searchQuery) { query.addStringFilter(prepareQueryForLandingPage(searchQuery, false)); } return appendReleaseFilters(query, primaryRelease, secondaryRelease); }, [ additionalFilters, hasPlatformSelectFeature, locationQuery.query, platform, primaryRelease, project, secondaryRelease, ]); const orderby = decodeScalar(locationQuery.sort, `-count`); const newQuery: NewQuery = { name: '', fields: [ 'transaction', SpanMetricsField.PROJECT_ID, `avg_if(measurements.time_to_initial_display,release,${primaryRelease})`, `avg_if(measurements.time_to_initial_display,release,${secondaryRelease})`, `avg_if(measurements.time_to_full_display,release,${primaryRelease})`, `avg_if(measurements.time_to_full_display,release,${secondaryRelease})`, 'count()', ], query: queryString, dataset: DiscoverDatasets.METRICS, version: 2, projects: selection.projects, }; newQuery.orderby = orderby; const tableEventView = EventView.fromNewQueryWithLocation(newQuery, location); const { data: topTransactionsData, isLoading: topTransactionsLoading, pageLinks, } = useTableQuery({ eventView: tableEventView, enabled: !isReleasesLoading, referrer: 'api.starfish.mobile-screen-table', cursor, }); const topTransactions = useMemo(() => { return ( topTransactionsData?.data?.slice(0, 5).map(datum => datum.transaction as string) ?? [] ); }, [topTransactionsData?.data]); const topEventsQueryString = useMemo(() => { const topEventsQuery = new MutableSearch([ 'event.type:transaction', 'transaction.op:ui.load', ...(additionalFilters ?? []), ]); if (project && isCrossPlatform(project) && hasPlatformSelectFeature) { topEventsQuery.addFilterValue('os.name', platform); } return `${appendReleaseFilters(topEventsQuery, primaryRelease, secondaryRelease)} ${ topTransactions.length > 0 ? escapeFilterValue( `transaction:[${topTransactions.map(name => `"${name}"`).join()}]` ) : '' }`.trim(); }, [ additionalFilters, platform, primaryRelease, project, secondaryRelease, topTransactions, hasPlatformSelectFeature, ]); const {data: releaseEvents, isLoading: isReleaseEventsLoading} = useTableQuery({ eventView: EventView.fromNewQueryWithLocation( { name: '', fields: ['transaction', 'release', ...yAxisCols], orderby: yAxisCols[0], yAxis: yAxisCols, query: topEventsQueryString, dataset: DiscoverDatasets.METRICS, version: 2, }, location ), enabled: !topTransactionsLoading, referrer: 'api.starfish.mobile-screen-bar-chart', }); if (isReleasesLoading) { return ( ); } if (!defined(primaryRelease) && !isReleasesLoading) { return ( {t( 'No screens found on recent releases. Please try a single iOS or Android project, a single environment or a smaller date range.' )} ); } const transformedReleaseEvents = transformReleaseEvents({ yAxes, primaryRelease, secondaryRelease, colorPalette: theme.charts.getColorPalette(TOP_SCREENS - 2), releaseEvents, topTransactions, }); const derivedQuery = getTransactionSearchQuery(location, tableEventView.query); const tableSearchFilters = new MutableSearch(['transaction.op:ui.load']); const handleCursor: CursorHandler = (newCursor, pathname, query_) => { browserHistory.push({ pathname, query: {...query_, [MobileCursors.SCREENS_TABLE]: newCursor}, }); }; return (
{defined(hasTTFD) && !hasTTFD && yAxes[1] === YAxis.TTFD ? ( ) : ( )} { router.push({ pathname: router.location.pathname, query: { ...location.query, cursor: undefined, query: String(search).trim() || undefined, }, }); }} organization={organization} query={getFreeTextFromQuery(derivedQuery)} placeholder={t('Search for Screens')} additionalConditions={ new MutableSearch( appendReleaseFilters(tableSearchFilters, primaryRelease, secondaryRelease) ) } />
); } export function getFreeTextFromQuery(query: string) { const conditions = new MutableSearch(query); const transactionValues = conditions.getFilterValues('transaction'); if (transactionValues.length) { return transactionValues[0]; } if (conditions.freeText.length > 0) { // raw text query will be wrapped in wildcards in generatePerformanceEventView // so no need to wrap it here return conditions.freeText.join(' '); } return ''; } const ChartsContainer = styled('div')` display: flex; flex-direction: row; flex-wrap: wrap; gap: ${space(2)}; `; const ChartsContainerWithHiddenOverflow = styled('div')` flex: 1; overflow: hidden; `; const ChartsContainerItem = styled('div')` flex: 1; `; export const Spacer = styled('div')` margin-top: ${space(3)}; `; const StyledSearchBar = styled(SearchBar)` margin-bottom: ${space(1)}; `;