Skip to content

Commit 6adfab0

Browse files
authored
feat(perf): Show link to docs when some web vitals data is missing (#31764)
In the Transaction Vitals view, sometimes some of the vitals details are not available, possibly due to the fact that some of these web vitals are not supported by all browsers. We should link troubleshooting docs to explain to the users in these cases why they may not be getting vitals. Fixes VIS-740
1 parent 44a1767 commit 6adfab0

File tree

3 files changed

+158
-77
lines changed

3 files changed

+158
-77
lines changed

static/app/views/performance/transactionSummary/transactionVitals/content.tsx

Lines changed: 97 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
1+
import React from 'react';
12
import {browserHistory} from 'react-router';
23
import styled from '@emotion/styled';
34
import {Location} from 'history';
45

6+
import Alert from 'sentry/components/alert';
57
import Button from 'sentry/components/button';
68
import DropdownControl, {DropdownItem} from 'sentry/components/dropdownControl';
79
import SearchBar from 'sentry/components/events/searchBar';
810
import * as Layout from 'sentry/components/layouts/thirds';
11+
import ExternalLink from 'sentry/components/links/externalLink';
912
import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
10-
import {t} from 'sentry/locale';
13+
import {IconInfo} from 'sentry/icons';
14+
import {t, tct} from 'sentry/locale';
1115
import space from 'sentry/styles/space';
1216
import {Organization} from 'sentry/types';
1317
import {trackAnalyticsEvent} from 'sentry/utils/analytics';
1418
import EventView from 'sentry/utils/discover/eventView';
19+
import {WebVital} from 'sentry/utils/discover/fields';
1520
import Histogram from 'sentry/utils/performance/histogram';
1621
import {FILTER_OPTIONS} from 'sentry/utils/performance/histogram/constants';
22+
import VitalsCardsDiscoverQuery from 'sentry/utils/performance/vitals/vitalsCardsDiscoverQuery';
1723
import {decodeScalar} from 'sentry/utils/queryString';
1824

19-
import {ZOOM_KEYS} from './constants';
25+
import {VITAL_GROUPS, ZOOM_KEYS} from './constants';
2026
import VitalsPanel from './vitalsPanel';
2127

2228
type Props = {
@@ -44,63 +50,100 @@ function VitalsContent(props: Props) {
4450
});
4551
};
4652

53+
const allVitals = VITAL_GROUPS.reduce((keys: WebVital[], {vitals}) => {
54+
return keys.concat(vitals);
55+
}, []);
56+
4757
return (
4858
<Histogram location={location} zoomKeys={ZOOM_KEYS}>
4959
{({activeFilter, handleFilterChange, handleResetView, isZoomed}) => (
5060
<Layout.Main fullWidth>
51-
<StyledActions>
52-
<StyledSearchBar
53-
organization={organization}
54-
projectIds={eventView.project}
55-
query={query}
56-
fields={eventView.fields}
57-
onSearch={handleSearch}
58-
/>
59-
<DropdownControl
60-
buttonProps={{prefix: t('Outliers')}}
61-
label={activeFilter.label}
62-
>
63-
{FILTER_OPTIONS.map(({label, value}) => (
64-
<DropdownItem
65-
key={value}
66-
onSelect={(filterOption: string) => {
67-
trackAnalyticsEvent({
68-
eventKey: 'performance_views.vitals.filter_changed',
69-
eventName: 'Performance Views: Change vitals filter',
70-
organization_id: organization.id,
71-
value: filterOption,
72-
});
73-
handleFilterChange(filterOption);
74-
}}
75-
eventKey={value}
76-
isActive={value === activeFilter.value}
77-
>
78-
{label}
79-
</DropdownItem>
80-
))}
81-
</DropdownControl>
82-
<Button
83-
onClick={() => {
84-
trackAnalyticsEvent({
85-
eventKey: 'performance_views.vitals.reset_view',
86-
eventName: 'Performance Views: Reset vitals view',
87-
organization_id: organization.id,
88-
});
89-
90-
handleResetView();
91-
}}
92-
disabled={!isZoomed}
93-
data-test-id="reset-view"
94-
>
95-
{t('Reset View')}
96-
</Button>
97-
</StyledActions>
98-
<VitalsPanel
99-
organization={organization}
100-
location={location}
61+
<VitalsCardsDiscoverQuery
10162
eventView={eventView}
102-
dataFilter={activeFilter.value}
103-
/>
63+
orgSlug={organization.slug}
64+
location={location}
65+
vitals={allVitals}
66+
>
67+
{results => {
68+
const isMissingVitalsData =
69+
!results.isLoading &&
70+
allVitals.some(vital => !results.vitalsData?.[vital]);
71+
72+
return (
73+
<React.Fragment>
74+
{isMissingVitalsData && (
75+
<Alert type="info" icon={<IconInfo size="md" />}>
76+
{tct(
77+
'If this page is looking a little bare, keep in mind not all browsers support these vitals. [link]',
78+
{
79+
link: (
80+
<ExternalLink href="https://docs.sentry.io/product/performance/web-vitals/#browser-support">
81+
{t('Read more about browser support.')}
82+
</ExternalLink>
83+
),
84+
}
85+
)}
86+
</Alert>
87+
)}
88+
89+
<StyledActions>
90+
<StyledSearchBar
91+
organization={organization}
92+
projectIds={eventView.project}
93+
query={query}
94+
fields={eventView.fields}
95+
onSearch={handleSearch}
96+
/>
97+
<DropdownControl
98+
buttonProps={{prefix: t('Outliers')}}
99+
label={activeFilter.label}
100+
>
101+
{FILTER_OPTIONS.map(({label, value}) => (
102+
<DropdownItem
103+
key={value}
104+
onSelect={(filterOption: string) => {
105+
trackAnalyticsEvent({
106+
eventKey: 'performance_views.vitals.filter_changed',
107+
eventName: 'Performance Views: Change vitals filter',
108+
organization_id: organization.id,
109+
value: filterOption,
110+
});
111+
handleFilterChange(filterOption);
112+
}}
113+
eventKey={value}
114+
isActive={value === activeFilter.value}
115+
>
116+
{label}
117+
</DropdownItem>
118+
))}
119+
</DropdownControl>
120+
<Button
121+
onClick={() => {
122+
trackAnalyticsEvent({
123+
eventKey: 'performance_views.vitals.reset_view',
124+
eventName: 'Performance Views: Reset vitals view',
125+
organization_id: organization.id,
126+
});
127+
128+
handleResetView();
129+
}}
130+
disabled={!isZoomed}
131+
data-test-id="reset-view"
132+
>
133+
{t('Reset View')}
134+
</Button>
135+
</StyledActions>
136+
<VitalsPanel
137+
organization={organization}
138+
location={location}
139+
eventView={eventView}
140+
dataFilter={activeFilter.value}
141+
results={results}
142+
/>
143+
</React.Fragment>
144+
);
145+
}}
146+
</VitalsCardsDiscoverQuery>
104147
</Layout.Main>
105148
)}
106149
</Histogram>

static/app/views/performance/transactionSummary/transactionVitals/vitalsPanel.tsx

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ import HistogramQuery from 'sentry/utils/performance/histogram/histogramQuery';
99
import {DataFilter, HistogramData} from 'sentry/utils/performance/histogram/types';
1010
import {WEB_VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants';
1111
import {VitalGroup} from 'sentry/utils/performance/vitals/types';
12-
import VitalsCardDiscoverQuery, {
13-
VitalData,
14-
} from 'sentry/utils/performance/vitals/vitalsCardsDiscoverQuery';
12+
import {VitalData} from 'sentry/utils/performance/vitals/vitalsCardsDiscoverQuery';
1513
import {decodeScalar} from 'sentry/utils/queryString';
1614

1715
import {NUM_BUCKETS, VITAL_GROUPS} from './constants';
@@ -21,6 +19,7 @@ type Props = {
2119
eventView: EventView;
2220
location: Location;
2321
organization: Organization;
22+
results: object;
2423
dataFilter?: DataFilter;
2524
};
2625

@@ -148,30 +147,17 @@ class VitalsPanel extends Component<Props> {
148147
}
149148

150149
render() {
151-
const {location, organization, eventView} = this.props;
152-
153-
const allVitals = VITAL_GROUPS.reduce((keys: WebVital[], {vitals}) => {
154-
return keys.concat(vitals);
155-
}, []);
150+
const {results} = this.props;
156151

157152
return (
158153
<Panel>
159-
<VitalsCardDiscoverQuery
160-
eventView={eventView}
161-
orgSlug={organization.slug}
162-
location={location}
163-
vitals={allVitals}
164-
>
165-
{results => (
166-
<Fragment>
167-
{VITAL_GROUPS.map(vitalGroup => (
168-
<Fragment key={vitalGroup.vitals.join('')}>
169-
{this.renderVitalGroup(vitalGroup, results)}
170-
</Fragment>
171-
))}
154+
<Fragment>
155+
{VITAL_GROUPS.map(vitalGroup => (
156+
<Fragment key={vitalGroup.vitals.join('')}>
157+
{this.renderVitalGroup(vitalGroup, results)}
172158
</Fragment>
173-
)}
174-
</VitalsCardDiscoverQuery>
159+
))}
160+
</Fragment>
175161
</Panel>
176162
);
177163
}

tests/js/spec/views/performance/transactionVitals.spec.jsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,5 +341,57 @@ describe('Performance > Web Vitals', function () {
341341
),
342342
});
343343
});
344+
345+
it('renders an info alert when missing web vitals data', async function () {
346+
MockApiClient.addMockResponse({
347+
url: '/organizations/org-slug/events-vitals/',
348+
body: {
349+
'measurements.fp': {poor: 1, meh: 2, good: 3, total: 6, p75: 4567},
350+
'measurements.fcp': {poor: 1, meh: 2, good: 3, total: 6, p75: 1456},
351+
},
352+
});
353+
354+
const {organization, router, routerContext} = initialize({
355+
query: {
356+
lcpStart: '20',
357+
},
358+
});
359+
360+
const wrapper = mountWithTheme(
361+
<WrappedComponent
362+
organization={organization}
363+
location={router.location}
364+
router={router}
365+
/>,
366+
routerContext
367+
);
368+
369+
await tick();
370+
wrapper.update();
371+
372+
expect(wrapper.find('Alert')).toHaveLength(1);
373+
});
374+
375+
it('does not render an info alert when data from all web vitals is present', async function () {
376+
const {organization, router, routerContext} = initialize({
377+
query: {
378+
lcpStart: '20',
379+
},
380+
});
381+
382+
const wrapper = mountWithTheme(
383+
<WrappedComponent
384+
organization={organization}
385+
location={router.location}
386+
router={router}
387+
/>,
388+
routerContext
389+
);
390+
391+
await tick();
392+
wrapper.update();
393+
394+
expect(wrapper.find('Alert')).toHaveLength(0);
395+
});
344396
});
345397
});

0 commit comments

Comments
 (0)