123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179 |
- import React from 'react';
- import {browserHistory, WithRouterProps} from 'react-router';
- import styled from '@emotion/styled';
- import {Location} from 'history';
- import Feature from 'app/components/acl/feature';
- import Alert from 'app/components/alert';
- import LightWeightNoProjectMessage from 'app/components/lightWeightNoProjectMessage';
- import GlobalSelectionHeader from 'app/components/organizations/globalSelectionHeader';
- import SentryDocumentTitle from 'app/components/sentryDocumentTitle';
- import {t} from 'app/locale';
- import {PageContent} from 'app/styles/organization';
- import {GlobalSelection, Organization, Project} from 'app/types';
- import EventView from 'app/utils/discover/eventView';
- import {isAggregateField, WebVital} from 'app/utils/discover/fields';
- import {WEB_VITAL_DETAILS} from 'app/utils/performance/vitals/constants';
- import {decodeScalar} from 'app/utils/queryString';
- import {stringifyQueryObject, tokenizeSearch} from 'app/utils/tokenizeSearch';
- import withGlobalSelection from 'app/utils/withGlobalSelection';
- import withOrganization from 'app/utils/withOrganization';
- import withProjects from 'app/utils/withProjects';
- import {getTransactionName} from '../utils';
- import {PERCENTILE, VITAL_GROUPS} from './constants';
- import RumContent from './content';
- type Props = {
- location: Location;
- organization: Organization;
- projects: Project[];
- selection: GlobalSelection;
- } & Pick<WithRouterProps, 'router'>;
- type State = {
- eventView: EventView | undefined;
- };
- class TransactionVitals extends React.Component<Props> {
- state: State = {
- eventView: generateRumEventView(
- this.props.location,
- getTransactionName(this.props.location)
- ),
- };
- static getDerivedStateFromProps(nextProps: Readonly<Props>, prevState: State): State {
- return {
- ...prevState,
- eventView: generateRumEventView(
- nextProps.location,
- getTransactionName(nextProps.location)
- ),
- };
- }
- getDocumentTitle(): string {
- const name = getTransactionName(this.props.location);
- const hasTransactionName = typeof name === 'string' && String(name).trim().length > 0;
- if (hasTransactionName) {
- return [String(name).trim(), t('Vitals')].join(' \u2014 ');
- }
- return [t('Summary'), t('Vitals')].join(' \u2014 ');
- }
- renderNoAccess = () => {
- return <Alert type="warning">{t("You don't have access to this feature")}</Alert>;
- };
- render() {
- const {organization, projects, location} = this.props;
- const {eventView} = this.state;
- const transactionName = getTransactionName(location);
- if (!eventView || transactionName === undefined) {
- // If there is no transaction name, redirect to the Performance landing page
- browserHistory.replace({
- pathname: `/organizations/${organization.slug}/performance/`,
- query: {
- ...location.query,
- },
- });
- return null;
- }
- const shouldForceProject = eventView.project.length === 1;
- const forceProject = shouldForceProject
- ? projects.find(p => parseInt(p.id, 10) === eventView.project[0])
- : undefined;
- const projectSlugs = eventView.project
- .map(projectId => projects.find(p => parseInt(p.id, 10) === projectId))
- .filter((p: Project | undefined): p is Project => p !== undefined)
- .map(p => p.slug);
- return (
- <SentryDocumentTitle
- title={this.getDocumentTitle()}
- orgSlug={organization.slug}
- projectSlug={forceProject?.slug}
- >
- <Feature
- features={['performance-view']}
- organization={organization}
- renderDisabled={this.renderNoAccess}
- >
- <GlobalSelectionHeader
- lockedMessageSubject={t('transaction')}
- shouldForceProject={shouldForceProject}
- forceProject={forceProject}
- specificProjectSlugs={projectSlugs}
- disableMultipleProjectSelection
- showProjectSettingsLink
- >
- <StyledPageContent>
- <LightWeightNoProjectMessage organization={organization}>
- <RumContent
- location={location}
- eventView={eventView}
- transactionName={transactionName}
- organization={organization}
- projects={projects}
- />
- </LightWeightNoProjectMessage>
- </StyledPageContent>
- </GlobalSelectionHeader>
- </Feature>
- </SentryDocumentTitle>
- );
- }
- }
- const StyledPageContent = styled(PageContent)`
- padding: 0;
- `;
- function generateRumEventView(
- location: Location,
- transactionName: string | undefined
- ): EventView | undefined {
- if (transactionName === undefined) {
- return undefined;
- }
- const query = decodeScalar(location.query.query, '');
- const conditions = tokenizeSearch(query);
- conditions
- .setTagValues('event.type', ['transaction'])
- .setTagValues('transaction.op', ['pageload'])
- .setTagValues('transaction', [transactionName]);
- Object.keys(conditions.tagValues).forEach(field => {
- if (isAggregateField(field)) conditions.removeTag(field);
- });
- const vitals = VITAL_GROUPS.reduce((allVitals: WebVital[], group) => {
- return allVitals.concat(group.vitals);
- }, []);
- return EventView.fromNewQueryWithLocation(
- {
- id: undefined,
- version: 2,
- name: transactionName,
- fields: [
- ...vitals.map(vital => `percentile(${vital}, ${PERCENTILE})`),
- ...vitals.map(vital => `count_at_least(${vital}, 0)`),
- ...vitals.map(
- vital => `count_at_least(${vital}, ${WEB_VITAL_DETAILS[vital].poorThreshold})`
- ),
- ],
- query: stringifyQueryObject(conditions),
- projects: [],
- },
- location
- );
- }
- export default withGlobalSelection(withProjects(withOrganization(TransactionVitals)));
|