123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287 |
- import {Fragment} from 'react';
- import styled from '@emotion/styled';
- import {BarChart} from 'sentry/components/charts/barChart';
- import type {LineChartSeries} from 'sentry/components/charts/lineChart';
- import {LineChart} from 'sentry/components/charts/lineChart';
- import {DateTime} from 'sentry/components/dateTime';
- import Link from 'sentry/components/links/link';
- import LoadingError from 'sentry/components/loadingError';
- import LoadingIndicator from 'sentry/components/loadingIndicator';
- import Panel from 'sentry/components/panels/panel';
- import PanelBody from 'sentry/components/panels/panelBody';
- import PanelFooter from 'sentry/components/panels/panelFooter';
- import PanelHeader from 'sentry/components/panels/panelHeader';
- import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {SentryApp} from 'sentry/types/integrations';
- import {useApiQuery} from 'sentry/utils/queryClient';
- import useOrganization from 'sentry/utils/useOrganization';
- import {useParams} from 'sentry/utils/useParams';
- import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
- import RequestLog from './requestLog';
- type Interactions = {
- componentInteractions: {
- [key: string]: Array<[number, number]>;
- };
- views: Array<[number, number]>;
- };
- type Stats = {
- installStats: Array<[number, number]>;
- totalInstalls: number;
- totalUninstalls: number;
- uninstallStats: Array<[number, number]>;
- };
- function SentryApplicationDashboard() {
- const organization = useOrganization();
- const {appSlug} = useParams<{appSlug: string}>();
- // 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;
- const {
- data: app,
- isPending: isAppPending,
- isError: isAppError,
- } = useApiQuery<SentryApp>([`/sentry-apps/${appSlug}/`], {staleTime: 0});
- const {
- data: interactions,
- isPending: isInteractionsPending,
- isError: isInteractionsError,
- } = useApiQuery<Interactions>(
- [
- `/sentry-apps/${appSlug}/interaction/`,
- {query: {since: now - ninety_days_ago, until: now}},
- ],
- {staleTime: 0}
- );
- const {
- data: stats,
- isPending: isStatsPending,
- isError: isStatsError,
- } = useApiQuery<Stats>(
- [
- `/sentry-apps/${appSlug}/stats/`,
- {query: {since: now - ninety_days_ago, until: now}},
- ],
- {staleTime: 0}
- );
- if (isAppPending || isStatsPending || isInteractionsPending) {
- return <LoadingIndicator />;
- }
- if (isAppError || isStatsError || isInteractionsError) {
- return <LoadingError />;
- }
- const {installStats, uninstallStats, totalUninstalls, totalInstalls} = stats;
- const {views, componentInteractions} = interactions;
- const renderInstallData = () => {
- 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>
- {renderInstallCharts()}
- </Fragment>
- );
- };
- const renderInstallCharts = () => {
- 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>
- );
- };
- const renderIntegrationViews = () => {
- 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>
- );
- };
- const renderComponentInteractions = () => {
- 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 as keyof typeof componentInteractionsDetails
- ] && (
- <Fragment key={idx}>
- <strong>{`${component}: `}</strong>
- {
- componentInteractionsDetails[
- component as keyof typeof componentInteractionsDetails
- ]
- }
- <br />
- </Fragment>
- )
- )}
- </StyledFooter>
- </PanelFooter>
- </Panel>
- );
- };
- return (
- <div>
- <SentryDocumentTitle title={t('Integration Dashboard')} />
- <SettingsPageHeader title={`${t('Integration Dashboard')} - ${app.name}`} />
- {app.status === 'published' && renderInstallData()}
- {app.status === 'published' && renderIntegrationViews()}
- {app.schema.elements && renderComponentInteractions()}
- <RequestLog app={app} />
- </div>
- );
- }
- export default SentryApplicationDashboard;
- type InteractionsChartProps = {
- data: {
- [key: string]: Array<[number, number]>;
- };
- };
- function 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)};
- `;
|