2021-10-15 05:44:59 +11:00
|
|
|
import PropTypes from 'prop-types';
|
2023-05-24 01:15:17 +10:00
|
|
|
import { PureComponent } from 'react';
|
|
|
|
|
2021-10-15 05:44:59 +11:00
|
|
|
import { FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl';
|
2023-05-24 01:15:17 +10:00
|
|
|
|
2021-10-15 05:44:59 +11:00
|
|
|
import classNames from 'classnames';
|
2023-05-24 01:15:17 +10:00
|
|
|
|
|
|
|
import api from 'mastodon/api';
|
2021-10-15 05:44:59 +11:00
|
|
|
import { roundTo10 } from 'mastodon/utils/numbers';
|
|
|
|
|
|
|
|
const dateForCohort = cohort => {
|
2023-09-28 18:16:15 +10:00
|
|
|
const timeZone = 'UTC';
|
2021-10-15 05:44:59 +11:00
|
|
|
switch(cohort.frequency) {
|
|
|
|
case 'day':
|
2023-09-28 18:16:15 +10:00
|
|
|
return <FormattedDate value={cohort.period} month='long' day='2-digit' timeZone={timeZone} />;
|
2021-10-15 05:44:59 +11:00
|
|
|
default:
|
2023-09-28 18:16:15 +10:00
|
|
|
return <FormattedDate value={cohort.period} month='long' year='numeric' timeZone={timeZone} />;
|
2021-10-15 05:44:59 +11:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-05-23 18:52:27 +10:00
|
|
|
export default class Retention extends PureComponent {
|
2021-10-15 05:44:59 +11:00
|
|
|
|
|
|
|
static propTypes = {
|
|
|
|
start_at: PropTypes.string,
|
|
|
|
end_at: PropTypes.string,
|
|
|
|
frequency: PropTypes.string,
|
|
|
|
};
|
|
|
|
|
|
|
|
state = {
|
|
|
|
loading: true,
|
|
|
|
data: null,
|
|
|
|
};
|
|
|
|
|
|
|
|
componentDidMount () {
|
|
|
|
const { start_at, end_at, frequency } = this.props;
|
|
|
|
|
|
|
|
api().post('/api/v1/admin/retention', { start_at, end_at, frequency }).then(res => {
|
|
|
|
this.setState({
|
|
|
|
loading: false,
|
|
|
|
data: res.data,
|
|
|
|
});
|
|
|
|
}).catch(err => {
|
|
|
|
console.error(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
render () {
|
|
|
|
const { loading, data } = this.state;
|
2021-12-18 09:00:41 +11:00
|
|
|
const { frequency } = this.props;
|
2021-10-15 05:44:59 +11:00
|
|
|
|
|
|
|
let content;
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />;
|
|
|
|
} else {
|
|
|
|
content = (
|
|
|
|
<table className='retention__table'>
|
|
|
|
<thead>
|
|
|
|
<tr>
|
|
|
|
<th>
|
|
|
|
<div className='retention__table__date retention__table__label'>
|
|
|
|
<FormattedMessage id='admin.dashboard.retention.cohort' defaultMessage='Sign-up month' />
|
|
|
|
</div>
|
|
|
|
</th>
|
|
|
|
|
|
|
|
<th>
|
|
|
|
<div className='retention__table__number retention__table__label'>
|
|
|
|
<FormattedMessage id='admin.dashboard.retention.cohort_size' defaultMessage='New users' />
|
|
|
|
</div>
|
|
|
|
</th>
|
|
|
|
|
|
|
|
{data[0].data.slice(1).map((retention, i) => (
|
|
|
|
<th key={retention.date}>
|
|
|
|
<div className='retention__table__number retention__table__label'>
|
|
|
|
{i + 1}
|
|
|
|
</div>
|
|
|
|
</th>
|
|
|
|
))}
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
<tr>
|
|
|
|
<td>
|
|
|
|
<div className='retention__table__date retention__table__average'>
|
|
|
|
<FormattedMessage id='admin.dashboard.retention.average' defaultMessage='Average' />
|
|
|
|
</div>
|
|
|
|
</td>
|
|
|
|
|
|
|
|
<td>
|
|
|
|
<div className='retention__table__size'>
|
|
|
|
<FormattedNumber value={data.reduce((sum, cohort, i) => sum + ((cohort.data[0].value * 1) - sum) / (i + 1), 0)} maximumFractionDigits={0} />
|
|
|
|
</div>
|
|
|
|
</td>
|
|
|
|
|
|
|
|
{data[0].data.slice(1).map((retention, i) => {
|
2022-01-24 02:01:25 +11:00
|
|
|
const average = data.reduce((sum, cohort, k) => cohort.data[i + 1] ? sum + (cohort.data[i + 1].rate - sum)/(k + 1) : sum, 0);
|
2021-10-15 05:44:59 +11:00
|
|
|
|
|
|
|
return (
|
|
|
|
<td key={retention.date}>
|
|
|
|
<div className={classNames('retention__table__box', 'retention__table__average', `retention__table__box--${roundTo10(average * 100)}`)}>
|
|
|
|
<FormattedNumber value={average} style='percent' />
|
|
|
|
</div>
|
|
|
|
</td>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</tr>
|
|
|
|
</thead>
|
|
|
|
|
|
|
|
<tbody>
|
|
|
|
{data.slice(0, -1).map(cohort => (
|
|
|
|
<tr key={cohort.period}>
|
|
|
|
<td>
|
|
|
|
<div className='retention__table__date'>
|
|
|
|
{dateForCohort(cohort)}
|
|
|
|
</div>
|
|
|
|
</td>
|
|
|
|
|
|
|
|
<td>
|
|
|
|
<div className='retention__table__size'>
|
|
|
|
<FormattedNumber value={cohort.data[0].value} />
|
|
|
|
</div>
|
|
|
|
</td>
|
|
|
|
|
|
|
|
{cohort.data.slice(1).map(retention => (
|
|
|
|
<td key={retention.date}>
|
2022-01-24 02:01:25 +11:00
|
|
|
<div className={classNames('retention__table__box', `retention__table__box--${roundTo10(retention.rate * 100)}`)}>
|
|
|
|
<FormattedNumber value={retention.rate} style='percent' />
|
2021-10-15 05:44:59 +11:00
|
|
|
</div>
|
|
|
|
</td>
|
|
|
|
))}
|
|
|
|
</tr>
|
|
|
|
))}
|
|
|
|
</tbody>
|
|
|
|
</table>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-12-18 09:00:41 +11:00
|
|
|
let title = null;
|
|
|
|
switch(frequency) {
|
|
|
|
case 'day':
|
|
|
|
title = <FormattedMessage id='admin.dashboard.daily_retention' defaultMessage='User retention rate by day after sign-up' />;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
title = <FormattedMessage id='admin.dashboard.monthly_retention' defaultMessage='User retention rate by month after sign-up' />;
|
2022-12-19 02:51:37 +11:00
|
|
|
}
|
2021-12-18 09:00:41 +11:00
|
|
|
|
2021-10-15 05:44:59 +11:00
|
|
|
return (
|
|
|
|
<div className='retention'>
|
2021-12-18 09:00:41 +11:00
|
|
|
<h4>{title}</h4>
|
2021-10-15 05:44:59 +11:00
|
|
|
|
|
|
|
{content}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|