Skip to content

Commit

Permalink
#35 - added weekly active users endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
okalenyk authored and andkom committed Dec 26, 2022
1 parent a89146c commit 009acca
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 20 deletions.
4 changes: 3 additions & 1 deletion apps/api/src/general/general.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { TransactionService } from '@dao-stats/transaction';
import { GeneralTotalResponse } from './dto';
import { MetricService } from '../common/metric.service';
import { getDailyIntervals, getGrowth } from '../utils';
import { ActivityInterval } from '@dao-stats/common/types/activity-interval';

@Injectable()
export class GeneralService {
Expand Down Expand Up @@ -68,9 +69,10 @@ export class GeneralService {
metricQuery: MetricQuery,
): Promise<MetricResponse> {
const metrics =
await this.transactionService.getContractActivityCountWeekly(
await this.transactionService.getContractActivityCountHistory(
context,
metricQuery,
ActivityInterval.Week,
);

return {
Expand Down
5 changes: 5 additions & 0 deletions apps/api/src/pipes/metric-query.pipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class MetricQueryPipe implements PipeTransform {
const { contractId }: ContractContext = RequestContext.get();

let { from, to } = query;
const { interval } = query;

if (from) {
from = moment(isNaN(from) ? from : parseInt(from));
Expand All @@ -43,6 +44,10 @@ export class MetricQueryPipe implements PipeTransform {
throw new BadRequestException(`Invalid 'to' query parameter.`);
}

if (interval) {
to = to.subtract(1, interval).endOf(interval);
}

return {
...query,
from: from.valueOf(),
Expand Down
3 changes: 3 additions & 0 deletions apps/api/src/users/dto/users-total.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ export class UsersTotalResponse {

@ApiProperty()
averageInteractions: TotalMetric;

@ApiProperty()
activeUsers: TotalMetric;
}
47 changes: 47 additions & 0 deletions apps/api/src/users/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { UsersTotalResponse } from './dto';
import { UsersService } from './users.service';
import { ContractContextPipe, MetricQueryPipe } from '../pipes';
import { HasDaoContractContext } from '../decorators';
import { IntervalMetricQuery } from '@dao-stats/common/dto/interval-metric-query.dto';

@ApiTags('Users')
@Controller('users')
Expand Down Expand Up @@ -61,6 +62,36 @@ export class UsersController {
return this.usersService.usersLeaderboard(context);
}

@ApiResponse({
status: 200,
type: MetricResponse,
})
@ApiBadRequestResponse({
description: 'Bad Request Response based on the query params set',
})
@Get('/active-users')
async activeUsers(
@Param(ContractContextPipe) context: ContractContext,
@Query(MetricQueryPipe) metricQuery: IntervalMetricQuery,
): Promise<MetricResponse> {
return this.usersService.activeUsers(context, metricQuery);
}

@ApiResponse({
status: 200,
type: LeaderboardMetricResponse,
})
@ApiBadRequestResponse({
description: 'Bad Request Response based on the query params set',
})
@Get('/active-users/leaderboard')
async activeUsersLeaderboard(
@Param(ContractContextPipe) context: ContractContext,
@Query(MetricQueryPipe) metricQuery: IntervalMetricQuery,
): Promise<LeaderboardMetricResponse> {
return this.usersService.usersLeaderboard(context, metricQuery.interval);
}

@ApiResponse({
status: 200,
type: MetricResponse,
Expand Down Expand Up @@ -180,6 +211,22 @@ export class UsersController {
return this.usersService.users(context, metricQuery);
}

@ApiResponse({
status: 200,
type: MetricResponse,
})
@ApiBadRequestResponse({
description: 'Bad Request Response based on the query params set',
})
@HasDaoContractContext()
@Get('/:dao/active-users')
async daoActiveUsers(
@Param(ContractContextPipe) context: DaoContractContext,
@Query(MetricQueryPipe) metricQuery: IntervalMetricQuery,
): Promise<MetricResponse> {
return this.usersService.activeUsers(context, metricQuery);
}

@ApiResponse({
status: 200,
type: MetricResponse,
Expand Down
62 changes: 47 additions & 15 deletions apps/api/src/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
getGrowth,
patchMetricDays,
} from '../utils';
import { ActivityInterval } from '@dao-stats/common/types/activity-interval';
import { IntervalMetricQuery } from '@dao-stats/common/dto/interval-metric-query.dto';

@Injectable()
export class UsersService {
Expand All @@ -40,6 +42,9 @@ export class UsersService {
): Promise<UsersTotalResponse> {
const dayAgo = moment().subtract(1, 'days');

const weekAgo = moment().subtract(1, 'week');
const twoWeeksAgo = moment().subtract(2, 'week');

const [
usersCount,
dayAgoUsersCount,
Expand All @@ -50,6 +55,8 @@ export class UsersService {
daoInteractions,
dayAgoDaoInteractions,
members,
activeUsers,
weekAgoActiveUsers,
] = await Promise.all([
this.transactionService.getUsersTotalCount(context),
this.transactionService.getUsersTotalCount(context, {
Expand All @@ -68,6 +75,13 @@ export class UsersService {
to: dayAgo.valueOf(),
}),
this.metricService.total(context, DaoStatsMetric.MembersCount),
this.transactionService.getUsersTotalCount(context, {
from: weekAgo.valueOf(),
}),
this.transactionService.getUsersTotalCount(context, {
from: twoWeeksAgo.valueOf(),
to: weekAgo.valueOf(),
}),
]);

const avgDaoUsers = getAverage(daoUsers.map(({ count }) => count));
Expand Down Expand Up @@ -100,6 +114,10 @@ export class UsersService {
count: avgDaoInteractions,
growth: getGrowth(avgDaoInteractions, dayAgoAvgDaoInteractions),
},
activeUsers: {
count: activeUsers.count,
growth: getGrowth(activeUsers.count, weekAgoActiveUsers.count),
},
};
}

Expand All @@ -109,8 +127,9 @@ export class UsersService {
): Promise<MetricResponse> {
const { from, to } = metricQuery;
// TODO: optimize day-by-day querying
const { results: byDays, errors } = await PromisePool.withConcurrency(5)
const { results: byDays } = await PromisePool.withConcurrency(5)
.for(getDailyIntervals(from, to || moment().valueOf()))
.handleError((e) => this.logger.error(e))
.process(async ({ start, end }) => {
const qr = await this.transactionService.getUsersTotalCount(context, {
to: end,
Expand All @@ -119,39 +138,35 @@ export class UsersService {
return { ...qr, start, end };
});

if (errors && errors.length) {
errors.map((error) => this.logger.error(error));
}

return this.combineDailyMetrics(byDays);
}

async usersLeaderboard(
context: ContractContext,
interval?: ActivityInterval,
): Promise<LeaderboardMetricResponse> {
const monthAgo = moment().subtract(1, 'month');
const days = getDailyIntervals(monthAgo.valueOf(), moment().valueOf());

const { results: byDays, errors } = await PromisePool.withConcurrency(5)
const { results: byDays } = await PromisePool.withConcurrency(5)
.for(days)
.handleError((e) => this.logger.error(e))
.process(async ({ start, end }) => {
const qr = await this.transactionService.getDaoUsers(context, {
to: end,
});
const query = interval ? { from: start, to: end } : { to: end };
const qr = await this.transactionService.getDaoUsers(context, query);

return { usersCount: [...qr], start, end };
});

if (errors && errors.length) {
errors.map((error) => this.logger.error(error));
}

const dayAgo = moment().subtract(1, 'days');
const currentInterval = moment().subtract(1, interval || 'days');
const intervalAgo = moment().subtract(2, interval);
const [dayAgoActivity, totalActivity] = await Promise.all([
this.transactionService.getDaoUsers(context, {
to: dayAgo.valueOf(),
from: interval ? intervalAgo.valueOf() : null,
to: interval ? currentInterval.valueOf() : currentInterval.valueOf(),
}),
this.transactionService.getDaoUsers(context, {
from: interval ? currentInterval.valueOf() : null,
to: moment().valueOf(),
}),
]);
Expand Down Expand Up @@ -185,6 +200,23 @@ export class UsersService {
return { metrics };
}

async activeUsers(
context: DaoContractContext | ContractContext,
metricQuery: IntervalMetricQuery,
): Promise<MetricResponse> {
const metrics = await this.transactionService.getUsersTotalCountHistory(
context,
metricQuery,
);

return {
metrics: metrics.map(({ day, count }) => ({
timestamp: moment(day).valueOf(),
count,
})),
};
}

async members(
context: ContractContext | DaoContractContext,
metricQuery: MetricQuery,
Expand Down
12 changes: 12 additions & 0 deletions libs/common/src/dto/interval-metric-query.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { MetricQuery } from '.';
import { ActivityInterval } from '../types/activity-interval';

export class IntervalMetricQuery extends MetricQuery {
@ApiProperty({
name: 'interval',
description: `Activity Interval: e.g ${ActivityInterval.Week}`,
enum: ActivityInterval,
})
interval: ActivityInterval = ActivityInterval.Day;
}
5 changes: 5 additions & 0 deletions libs/common/src/types/activity-interval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum ActivityInterval {
Month = 'month',
Week = 'week',
Day = 'day',
}
24 changes: 20 additions & 4 deletions libs/transaction/src/transaction.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
TransactionType,
} from '@dao-stats/common';
import { TransactionLeaderboardDto } from './dto/transaction-leaderboard.dto';
import { ActivityInterval } from '@dao-stats/common/types/activity-interval';
import { IntervalMetricQuery } from '@dao-stats/common/dto/interval-metric-query.dto';

@Injectable()
export class TransactionService {
Expand Down Expand Up @@ -83,12 +85,13 @@ export class TransactionService {
return this.getContractActivityCountQuery(context, metricQuery).getRawOne();
}

async getContractActivityCountWeekly(
async getContractActivityCountHistory(
context: DaoContractContext | ContractContext,
metricQuery?: MetricQuery,
interval?: ActivityInterval,
): Promise<DailyCountDto[]> {
let queryBuilder = this.getContractActivityCountQuery(context, metricQuery);
queryBuilder = this.addWeeklySelection(queryBuilder);
queryBuilder = this.addIntervalSelection(queryBuilder, interval);

return queryBuilder.execute();
}
Expand All @@ -100,6 +103,18 @@ export class TransactionService {
return this.getUsersTotalQueryBuilder(context, metricQuery).getRawOne();
}

async getUsersTotalCountHistory(
context: DaoContractContext | ContractContext,
metricQuery?: IntervalMetricQuery,
): Promise<DailyCountDto[]> {
const { interval } = metricQuery;

let queryBuilder = this.getUsersTotalQueryBuilder(context, metricQuery);
queryBuilder = this.addIntervalSelection(queryBuilder, interval);

return queryBuilder.execute();
}

async getInteractionsCount(
context: DaoContractContext | ContractContext,
metricQuery?: MetricQuery,
Expand Down Expand Up @@ -250,12 +265,13 @@ export class TransactionService {
.orderBy('day', 'ASC');
}

private addWeeklySelection(
private addIntervalSelection(
qb: SelectQueryBuilder<Transaction>,
interval: ActivityInterval,
): SelectQueryBuilder<Transaction> {
return qb
.addSelect(
`date_trunc('week', to_timestamp(block_timestamp / 1e9)) as day`,
`date_trunc('${interval}', to_timestamp(block_timestamp / 1e9) + '1 ${interval}'::interval) as day`,
)
.groupBy('day')
.orderBy('day', 'ASC');
Expand Down

0 comments on commit 009acca

Please sign in to comment.