Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'All time' and 'This hour' timebuckets #172

Merged
merged 15 commits into from
Jul 20, 2023
96 changes: 86 additions & 10 deletions apps/production/src/analytics/analytics.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as _isEmpty from 'lodash/isEmpty'
import * as _isArray from 'lodash/isArray'
import * as _toNumber from 'lodash/toNumber'
import * as _pick from 'lodash/pick'
import * as _includes from 'lodash/includes'
import * as _map from 'lodash/map'
import * as _uniqBy from 'lodash/uniqBy'
import * as _round from 'lodash/round'
Expand Down Expand Up @@ -35,6 +36,7 @@ import {
getSessionKey,
getHeartbeatKey,
DataType,
validPeriods,
} from './analytics.service'
import { TaskManagerService } from '../task-manager/task-manager.service'
import { CurrentUserId } from '../auth/decorators/current-user-id.decorator'
Expand Down Expand Up @@ -289,7 +291,26 @@ export class AnalyticsController {
this.analyticsService.validatePeriod(period)
}

this.analyticsService.validateTimebucket(timeBucket)
let newTimebucket = timeBucket
let allowedTumebucketForPeriodAll

let diff

if (period === 'all') {
const res = await this.analyticsService.getTimeBucketForAllTime(
pid,
period,
)

diff = res.diff
// eslint-disable-next-line prefer-destructuring
newTimebucket = _includes(res.timeBucket, timeBucket)
? timeBucket
: res.timeBucket[0]
allowedTumebucketForPeriodAll = res.timeBucket
}

this.analyticsService.validateTimebucket(newTimebucket)
const [filtersQuery, filtersParams, appliedFilters, customEVFilterApplied] =
this.analyticsService.getFiltersQuery(
filters,
Expand All @@ -301,9 +322,10 @@ export class AnalyticsController {
this.analyticsService.getGroupFromTo(
from,
to,
timeBucket,
newTimebucket,
period,
safeTimezone,
diff,
)
await this.analyticsService.checkProjectAccess(
pid,
Expand Down Expand Up @@ -335,7 +357,7 @@ export class AnalyticsController {

if (isCaptcha) {
result = await this.analyticsService.groupCaptchaByTimeBucket(
timeBucket,
newTimebucket,
groupFrom,
groupTo,
subQuery,
Expand All @@ -345,7 +367,7 @@ export class AnalyticsController {
)
} else {
result = await this.analyticsService.groupByTimeBucket(
timeBucket,
newTimebucket,
groupFrom,
groupTo,
subQuery,
Expand All @@ -361,6 +383,7 @@ export class AnalyticsController {
return {
...result,
appliedFilters,
timeBucket: allowedTumebucketForPeriodAll,
}
}

Expand All @@ -373,6 +396,7 @@ export class AnalyticsController {
...result,
customs,
appliedFilters,
timeBucket: allowedTumebucketForPeriodAll,
}
}

Expand Down Expand Up @@ -489,17 +513,36 @@ export class AnalyticsController {
this.analyticsService.validatePeriod(period)
}

this.analyticsService.validateTimebucket(timeBucket)
let newTimeBucket = timeBucket
let allowedTumebucketForPeriodAll
let diff

if (period === 'all') {
const res = await this.analyticsService.getTimeBucketForAllTime(
pid,
period,
)

diff = res.diff
// eslint-disable-next-line prefer-destructuring
newTimeBucket = _includes(res.timeBucket, timeBucket)
? timeBucket
: res.timeBucket[0]
allowedTumebucketForPeriodAll = res.timeBucket
}

this.analyticsService.validateTimebucket(newTimeBucket)
const [filtersQuery, filtersParams, appliedFilters] =
this.analyticsService.getFiltersQuery(filters, DataType.PERFORMANCE)

const safeTimezone = this.analyticsService.getSafeTimezone(timezone)
const { groupFrom, groupTo } = this.analyticsService.getGroupFromTo(
from,
to,
timeBucket,
newTimeBucket,
period,
safeTimezone,
diff,
)
await this.analyticsService.checkProjectAccess(
pid,
Expand All @@ -519,7 +562,7 @@ export class AnalyticsController {
}

const result = await this.analyticsService.groupPerfByTimeBucket(
timeBucket,
newTimeBucket,
groupFrom,
groupTo,
subQuery,
Expand All @@ -531,6 +574,7 @@ export class AnalyticsController {
return {
...result,
appliedFilters,
timeBucket: allowedTumebucketForPeriodAll,
}
}

Expand Down Expand Up @@ -621,6 +665,17 @@ export class AnalyticsController {
this.analyticsService.validatePeriod(period)
}

let diff

if (period === 'all') {
const res = await this.analyticsService.getTimeBucketForAllTime(
pid,
period,
)

diff = res.diff
}

await this.analyticsService.checkProjectAccess(
pid,
uid,
Expand All @@ -634,6 +689,7 @@ export class AnalyticsController {
null,
period,
safeTimezone,
diff,
)

const [filtersQuery, filtersParams, appliedFilters] =
Expand Down Expand Up @@ -1123,7 +1179,25 @@ export class AnalyticsController {
this.analyticsService.validatePeriod(period)
}

this.analyticsService.validateTimebucket(timeBucket)
let newTimeBucket = timeBucket
let diff
let timeBucketForAllTime

if (period === validPeriods[validPeriods.length - 1]) {
const res = await this.analyticsService.getTimeBucketForAllTime(
pid,
period,
)

// eslint-disable-next-line prefer-destructuring
newTimeBucket = _includes(res.timeBucket, timeBucket)
? timeBucket
: res.timeBucket[0]
diff = res.diff
timeBucketForAllTime = res.timeBucket
}

this.analyticsService.validateTimebucket(newTimeBucket)
const [filtersQuery, filtersParams, appliedFilters] =
this.analyticsService.getFiltersQuery(filters, DataType.ANALYTICS)
await this.analyticsService.checkProjectAccess(
Expand All @@ -1136,9 +1210,10 @@ export class AnalyticsController {
const { groupFrom, groupTo } = this.analyticsService.getGroupFromTo(
from,
to,
timeBucket,
newTimeBucket,
period,
safeTimezone,
diff,
)

const paramsData = {
Expand All @@ -1151,7 +1226,7 @@ export class AnalyticsController {
}

const result: any = await this.analyticsService.groupCustomEVByTimeBucket(
timeBucket,
newTimeBucket,
groupFrom,
groupTo,
filtersQuery,
Expand Down Expand Up @@ -1181,6 +1256,7 @@ export class AnalyticsController {
return {
...result,
appliedFilters,
timeBucket: timeBucketForAllTime,
}
}
}
70 changes: 69 additions & 1 deletion apps/production/src/analytics/analytics.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ const GMT_0_TIMEZONES = [
// 'Africa/Casablanca',
]

const validPeriods = [
export const validPeriods = [
'thishour',
'today',
'yesterday',
'1d',
Expand All @@ -101,12 +102,14 @@ const validPeriods = [
'3M',
'12M',
'24M',
'all',
]

const validTimebuckets = [
TimeBucketType.HOUR,
TimeBucketType.DAY,
TimeBucketType.MONTH,
TimeBucketType.YEAR,
]

// mapping of allowed timebuckets per difference between days
Expand All @@ -122,6 +125,8 @@ const timeBucketToDays = [
}, // 4 weeks
{ lt: 366, tb: [TimeBucketType.MONTH] }, // 12 months
{ lt: 732, tb: [TimeBucketType.MONTH] }, // 24 months
{ lt: 1464, tb: [TimeBucketType.MONTH, TimeBucketType.YEAR] }, // 48 months
{ lt: 99999, tb: [TimeBucketType.YEAR] },
]

// Smaller than 64 characters, must start with an English letter and contain only letters (a-z A-Z), numbers (0-9), underscores (_) and dots (.)
Expand All @@ -132,6 +137,7 @@ const timeBucketConversion = {
hour: 'toStartOfHour',
day: 'toStartOfDay',
month: 'toStartOfMonth',
year: 'toStartOfYear',
}

const isValidTimezone = (timezone: string): boolean => {
Expand Down Expand Up @@ -407,6 +413,7 @@ export class AnalyticsService {
timeBucket: TimeBucketType | null,
period: string,
safeTimezone: string,
diff?: number,
): IGetGroupFromTo {
let groupFrom: dayjs.Dayjs
let groupTo: dayjs.Dayjs
Expand Down Expand Up @@ -473,9 +480,15 @@ export class AnalyticsService {
if (period === 'today') {
groupFrom = djsNow.startOf('d')
groupTo = djsNow
} else if (period === 'thishour') {
groupFrom = djsNow.subtract(1, 'hour').startOf('h')
groupTo = djsNow
} else if (period === 'yesterday') {
groupFrom = djsNow.subtract(1, 'day').startOf('d')
groupTo = djsNow.subtract(1, 'day').endOf('d')
} else if (period === 'all' && (diff === 0 || diff === 1)) {
groupFrom = djsNow.subtract(1, 'day').startOf('d')
groupTo = djsNow
} else {
if (period === '1d') {
groupFrom = djsNow.subtract(parseInt(period, 10), _last(period))
Expand Down Expand Up @@ -630,6 +643,53 @@ export class AnalyticsService {
}
}

async getTimeBucketForAllTime(
pid: string,
period: string,
): Promise<{
timeBucket: TimeBucketType[]
diff: number
}> {
if (period !== 'all') {
return null
}

const from: any = await clickhouse
.query(
'SELECT created as from FROM analytics where pid = {pid:FixedString(12)} ORDER BY created ASC LIMIT 1',
{ params: { pid } },
)
.toPromise()
const to: any = await clickhouse
.query(
'SELECT created as to FROM analytics where pid = {pid:FixedString(12)} ORDER BY created DESC LIMIT 1',
{ params: { pid } },
)
.toPromise()

let newTimeBucket = [TimeBucketType.MONTH]
let diff = null

if (from && to) {
diff = dayjs(to[0].to).diff(dayjs(from[0].from), 'days')

const tbMap = _find(timeBucketToDays, ({ lt }) => diff <= lt)

if (_isEmpty(tbMap)) {
throw new PreconditionFailedException(
"The difference between 'from' and 'to' is greater than allowed",
)
}

newTimeBucket = tbMap.tb
}

return {
timeBucket: newTimeBucket,
diff,
}
}

postProcessParsedFilters(parsedFilters: any[]): any[] {
return _reduce(
parsedFilters,
Expand Down Expand Up @@ -1002,6 +1062,10 @@ export class AnalyticsService {
format = 'YYYY-MM'
break

case TimeBucketType.YEAR:
format = 'YYYY'
break

default:
throw new BadRequestException(
`The provided time bucket (${timeBucket}) is incorrect`,
Expand Down Expand Up @@ -1129,6 +1193,10 @@ export class AnalyticsService {
]
}

if (timeBucket === TimeBucketType.YEAR) {
return [`toYear(tz_created) as year`, 'year']
}

return [
`toYear(tz_created) as year,
toMonth(tz_created) as month,
Expand Down
1 change: 1 addition & 0 deletions apps/production/src/analytics/dto/getData.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum TimeBucketType {
HOUR = 'hour',
DAY = 'day',
MONTH = 'month',
YEAR = 'year',
}

// eslint-disable-next-line @typescript-eslint/naming-convention
Expand Down