123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271 |
- import {Fragment} from 'react';
- import {RouteComponentProps} from 'react-router';
- import styled from '@emotion/styled';
- import {BarChart} from 'sentry/components/charts/barChart';
- import {LineChart, LineChartSeries} from 'sentry/components/charts/lineChart';
- import DateTime from 'sentry/components/dateTime';
- import Link from 'sentry/components/links/link';
- import {Panel, PanelBody, PanelFooter, PanelHeader} from 'sentry/components/panels';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import {Organization, SentryApp} from 'sentry/types';
- import withOrganization from 'sentry/utils/withOrganization';
- import AsyncView from 'sentry/views/asyncView';
- import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
- import RequestLog from './requestLog';
- type Props = RouteComponentProps<{appSlug: string}, {}> & {
- organization: Organization;
- };
- type State = AsyncView['state'] & {
- app: SentryApp;
- interactions: {
- componentInteractions: {
- [key: string]: [number, number][];
- };
- views: [number, number][];
- };
- stats: {
- installStats: [number, number][];
- totalInstalls: number;
- totalUninstalls: number;
- uninstallStats: [number, number][];
- };
- };
- class SentryApplicationDashboard extends AsyncView<Props, State> {
- getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
- const {appSlug} = this.props.params;
- // Default time range for now: 90 days ago to now
- const now = Math.floor(new Date().getTime() / 1000);
- const ninety_days_ago = 3600 * 24 * 90;
- return [
- [
- 'stats',
- `/sentry-apps/${appSlug}/stats/`,
- {query: {since: now - ninety_days_ago, until: now}},
- ],
- [
- 'interactions',
- `/sentry-apps/${appSlug}/interaction/`,
- {query: {since: now - ninety_days_ago, until: now}},
- ],
- ['app', `/sentry-apps/${appSlug}/`],
- ];
- }
- getTitle() {
- return t('Integration Dashboard');
- }
- renderInstallData() {
- const {app, stats} = this.state;
- const {totalUninstalls, totalInstalls} = stats;
- return (
- <Fragment>
- <h5>{t('Installation & Interaction Data')}</h5>
- <Row>
- {app.datePublished ? (
- <StatsSection>
- <StatsHeader>{t('Date published')}</StatsHeader>
- <DateTime dateOnly date={app.datePublished} />
- </StatsSection>
- ) : null}
- <StatsSection data-test-id="installs">
- <StatsHeader>{t('Total installs')}</StatsHeader>
- <p>{totalInstalls}</p>
- </StatsSection>
- <StatsSection data-test-id="uninstalls">
- <StatsHeader>{t('Total uninstalls')}</StatsHeader>
- <p>{totalUninstalls}</p>
- </StatsSection>
- </Row>
- {this.renderInstallCharts()}
- </Fragment>
- );
- }
- renderInstallCharts() {
- const {installStats, uninstallStats} = this.state.stats;
- const installSeries = {
- data: installStats.map(point => ({
- name: point[0] * 1000,
- value: point[1],
- })),
- seriesName: t('installed'),
- };
- const uninstallSeries = {
- data: uninstallStats.map(point => ({
- name: point[0] * 1000,
- value: point[1],
- })),
- seriesName: t('uninstalled'),
- };
- return (
- <Panel>
- <PanelHeader>{t('Installations/Uninstallations over Last 90 Days')}</PanelHeader>
- <ChartWrapper>
- <BarChart
- series={[installSeries, uninstallSeries]}
- height={150}
- stacked
- isGroupedByDate
- legend={{
- show: true,
- orient: 'horizontal',
- data: ['installed', 'uninstalled'],
- itemWidth: 15,
- }}
- yAxis={{type: 'value', minInterval: 1, max: 'dataMax'}}
- xAxis={{type: 'time'}}
- grid={{left: space(4), right: space(4)}}
- />
- </ChartWrapper>
- </Panel>
- );
- }
- renderIntegrationViews() {
- const {views} = this.state.interactions;
- const {organization} = this.props;
- const {appSlug} = this.props.params;
- return (
- <Panel>
- <PanelHeader>{t('Integration Views')}</PanelHeader>
- <PanelBody>
- <InteractionsChart data={{Views: views}} />
- </PanelBody>
- <PanelFooter>
- <StyledFooter>
- {t('Integration views are measured through views on the ')}
- <Link to={`/sentry-apps/${appSlug}/external-install/`}>
- {t('external installation page')}
- </Link>
- {t(' and views on the Learn More/Install modal on the ')}
- <Link to={`/settings/${organization.slug}/integrations/`}>
- {t('integrations page')}
- </Link>
- </StyledFooter>
- </PanelFooter>
- </Panel>
- );
- }
- renderComponentInteractions() {
- const {componentInteractions} = this.state.interactions;
- const componentInteractionsDetails = {
- 'stacktrace-link': t(
- 'Each link click or context menu open counts as one interaction'
- ),
- 'issue-link': t('Each open of the issue link modal counts as one interaction'),
- };
- return (
- <Panel>
- <PanelHeader>{t('Component Interactions')}</PanelHeader>
- <PanelBody>
- <InteractionsChart data={componentInteractions} />
- </PanelBody>
- <PanelFooter>
- <StyledFooter>
- {Object.keys(componentInteractions).map(
- (component, idx) =>
- componentInteractionsDetails[component] && (
- <Fragment key={idx}>
- <strong>{`${component}: `}</strong>
- {componentInteractionsDetails[component]}
- <br />
- </Fragment>
- )
- )}
- </StyledFooter>
- </PanelFooter>
- </Panel>
- );
- }
- renderBody() {
- const {app} = this.state;
- return (
- <div>
- <SettingsPageHeader title={`${t('Integration Dashboard')} - ${app.name}`} />
- {app.status === 'published' && this.renderInstallData()}
- {app.status === 'published' && this.renderIntegrationViews()}
- {app.schema.elements && this.renderComponentInteractions()}
- <RequestLog app={app} />
- </div>
- );
- }
- }
- export default withOrganization(SentryApplicationDashboard);
- type InteractionsChartProps = {
- data: {
- [key: string]: [number, number][];
- };
- };
- const InteractionsChart = ({data}: InteractionsChartProps) => {
- const elementInteractionsSeries: LineChartSeries[] = Object.keys(data).map(
- (key: string) => {
- const seriesData = data[key].map(point => ({
- value: point[1],
- name: point[0] * 1000,
- }));
- return {
- seriesName: key,
- data: seriesData,
- };
- }
- );
- return (
- <ChartWrapper>
- <LineChart
- isGroupedByDate
- series={elementInteractionsSeries}
- grid={{left: space(4), right: space(4)}}
- legend={{
- show: true,
- orient: 'horizontal',
- data: Object.keys(data),
- }}
- />
- </ChartWrapper>
- );
- };
- const Row = styled('div')`
- display: flex;
- `;
- const StatsSection = styled('div')`
- margin-right: ${space(4)};
- `;
- const StatsHeader = styled('h6')`
- margin-bottom: ${space(1)};
- font-size: 12px;
- text-transform: uppercase;
- color: ${p => p.theme.subText};
- `;
- const StyledFooter = styled('div')`
- padding: ${space(1.5)};
- `;
- const ChartWrapper = styled('div')`
- padding-top: ${space(3)};
- `;
|