2
0

🚸 (results) Improve time filter so that it takes into account user timezone

This commit is contained in:
Baptiste Arnaud
2024-03-15 10:13:46 +01:00
parent 001e696bf6
commit f6d2b15a16
10 changed files with 291 additions and 153 deletions

View File

@ -5,7 +5,10 @@ import { z } from 'zod'
import { canReadTypebots } from '@/helpers/databaseRules'
import { Stats, statsSchema } from '@typebot.io/schemas'
import { defaultTimeFilter, timeFilterValues } from '../constants'
import { parseDateFromTimeFilter } from '../helpers/parseDateFromTimeFilter'
import {
parseFromDateFromTimeFilter,
parseToDateFromTimeFilter,
} from '../helpers/parseDateFromTimeFilter'
export const getStats = authenticatedProcedure
.meta({
@ -21,10 +24,12 @@ export const getStats = authenticatedProcedure
z.object({
typebotId: z.string(),
timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter),
timeZone: z.string().optional(),
})
)
.output(z.object({ stats: statsSchema }))
.query(async ({ input: { typebotId, timeFilter }, ctx: { user } }) => {
.query(
async ({ input: { typebotId, timeFilter, timeZone }, ctx: { user } }) => {
const typebot = await prisma.typebot.findFirst({
where: canReadTypebots(typebotId, user),
select: { publishedTypebot: true, id: true },
@ -35,17 +40,19 @@ export const getStats = authenticatedProcedure
message: 'Published typebot not found',
})
const date = parseDateFromTimeFilter(timeFilter)
const fromDate = parseFromDateFromTimeFilter(timeFilter, timeZone)
const toDate = parseToDateFromTimeFilter(timeFilter, timeZone)
const [totalViews, totalStarts, totalCompleted] = await prisma.$transaction(
[
const [totalViews, totalStarts, totalCompleted] =
await prisma.$transaction([
prisma.result.count({
where: {
typebotId: typebot.id,
isArchived: false,
createdAt: date
createdAt: fromDate
? {
gte: date,
gte: fromDate,
lte: toDate ?? undefined,
}
: undefined,
},
@ -55,9 +62,10 @@ export const getStats = authenticatedProcedure
typebotId: typebot.id,
isArchived: false,
hasStarted: true,
createdAt: date
createdAt: fromDate
? {
gte: date,
gte: fromDate,
lte: toDate ?? undefined,
}
: undefined,
},
@ -67,15 +75,15 @@ export const getStats = authenticatedProcedure
typebotId: typebot.id,
isArchived: false,
isCompleted: true,
createdAt: date
createdAt: fromDate
? {
gte: date,
gte: fromDate,
lte: toDate ?? undefined,
}
: undefined,
},
}),
]
)
])
const stats: Stats = {
totalViews,
@ -86,4 +94,5 @@ export const getStats = authenticatedProcedure
return {
stats,
}
})
}
)

View File

@ -7,7 +7,10 @@ import { totalAnswersSchema } from '@typebot.io/schemas/features/analytics'
import { parseGroups } from '@typebot.io/schemas'
import { isInputBlock } from '@typebot.io/lib'
import { defaultTimeFilter, timeFilterValues } from '../constants'
import { parseDateFromTimeFilter } from '../helpers/parseDateFromTimeFilter'
import {
parseFromDateFromTimeFilter,
parseToDateFromTimeFilter,
} from '../helpers/parseDateFromTimeFilter'
export const getTotalAnswers = authenticatedProcedure
.meta({
@ -23,10 +26,12 @@ export const getTotalAnswers = authenticatedProcedure
z.object({
typebotId: z.string(),
timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter),
timeZone: z.string().optional(),
})
)
.output(z.object({ totalAnswers: z.array(totalAnswersSchema) }))
.query(async ({ input: { typebotId, timeFilter }, ctx: { user } }) => {
.query(
async ({ input: { typebotId, timeFilter, timeZone }, ctx: { user } }) => {
const typebot = await prisma.typebot.findFirst({
where: canReadTypebots(typebotId, user),
select: { publishedTypebot: true },
@ -37,16 +42,18 @@ export const getTotalAnswers = authenticatedProcedure
message: 'Published typebot not found',
})
const date = parseDateFromTimeFilter(timeFilter)
const fromDate = parseFromDateFromTimeFilter(timeFilter, timeZone)
const toDate = parseToDateFromTimeFilter(timeFilter, timeZone)
const totalAnswersPerBlock = await prisma.answer.groupBy({
by: ['blockId'],
where: {
result: {
typebotId: typebot.publishedTypebot.typebotId,
createdAt: date
createdAt: fromDate
? {
gte: date,
gte: fromDate,
lte: toDate ?? undefined,
}
: undefined,
},
@ -67,4 +74,5 @@ export const getTotalAnswers = authenticatedProcedure
total: a._count._all,
})),
}
})
}
)

View File

@ -5,7 +5,10 @@ import { z } from 'zod'
import { canReadTypebots } from '@/helpers/databaseRules'
import { totalVisitedEdgesSchema } from '@typebot.io/schemas'
import { defaultTimeFilter, timeFilterValues } from '../constants'
import { parseDateFromTimeFilter } from '../helpers/parseDateFromTimeFilter'
import {
parseFromDateFromTimeFilter,
parseToDateFromTimeFilter,
} from '../helpers/parseDateFromTimeFilter'
export const getTotalVisitedEdges = authenticatedProcedure
.meta({
@ -21,6 +24,7 @@ export const getTotalVisitedEdges = authenticatedProcedure
z.object({
typebotId: z.string(),
timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter),
timeZone: z.string().optional(),
})
)
.output(
@ -28,7 +32,8 @@ export const getTotalVisitedEdges = authenticatedProcedure
totalVisitedEdges: z.array(totalVisitedEdgesSchema),
})
)
.query(async ({ input: { typebotId, timeFilter }, ctx: { user } }) => {
.query(
async ({ input: { typebotId, timeFilter, timeZone }, ctx: { user } }) => {
const typebot = await prisma.typebot.findFirst({
where: canReadTypebots(typebotId, user),
select: { id: true },
@ -39,16 +44,18 @@ export const getTotalVisitedEdges = authenticatedProcedure
message: 'Published typebot not found',
})
const date = parseDateFromTimeFilter(timeFilter)
const fromDate = parseFromDateFromTimeFilter(timeFilter, timeZone)
const toDate = parseToDateFromTimeFilter(timeFilter, timeZone)
const edges = await prisma.visitedEdge.groupBy({
by: ['edgeId'],
where: {
result: {
typebotId: typebot.id,
createdAt: date
createdAt: fromDate
? {
gte: date,
gte: fromDate,
lte: toDate ?? undefined,
}
: undefined,
},
@ -62,4 +69,5 @@ export const getTotalVisitedEdges = authenticatedProcedure
total: e._count.resultId,
})),
}
})
}
)

View File

@ -17,6 +17,8 @@ import { isDefined } from '@typebot.io/lib'
import { EventsCoordinatesProvider } from '@/features/graph/providers/EventsCoordinateProvider'
import { timeFilterValues } from '../constants'
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
type Props = {
timeFilter: (typeof timeFilterValues)[number]
onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void
@ -35,6 +37,7 @@ export const AnalyticsGraphContainer = ({
{
typebotId: typebot?.id as string,
timeFilter,
timeZone,
},
{ enabled: isDefined(publishedTypebot) }
)
@ -43,6 +46,7 @@ export const AnalyticsGraphContainer = ({
{
typebotId: typebot?.id as string,
timeFilter,
timeZone,
},
{ enabled: isDefined(publishedTypebot) }
)

View File

@ -2,6 +2,8 @@ export const timeFilterValues = [
'today',
'last7Days',
'last30Days',
'monthToDate',
'lastMonth',
'yearToDate',
'allTime',
] as const
@ -13,8 +15,10 @@ export const timeFilterLabels: Record<
today: 'Today',
last7Days: 'Last 7 days',
last30Days: 'Last 30 days',
monthToDate: 'Month to date',
lastMonth: 'Last month',
yearToDate: 'Year to date',
allTime: 'All time',
}
export const defaultTimeFilter = 'today' as const
export const defaultTimeFilter = 'last7Days' as const

View File

@ -1,18 +1,72 @@
import { timeFilterValues } from '../constants'
import {
startOfDay,
subDays,
startOfYear,
startOfMonth,
endOfMonth,
subMonths,
} from 'date-fns'
import { utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz'
export const parseFromDateFromTimeFilter = (
timeFilter: (typeof timeFilterValues)[number],
userTimezone: string = 'UTC'
): Date | null => {
const nowInUserTimezone = utcToZonedTime(new Date(), userTimezone)
export const parseDateFromTimeFilter = (
timeFilter: (typeof timeFilterValues)[number]
): Date | undefined => {
switch (timeFilter) {
case 'today':
return new Date(new Date().setHours(0, 0, 0, 0))
case 'last7Days':
return new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
case 'last30Days':
return new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
case 'yearToDate':
return new Date(new Date().getFullYear(), 0, 1)
case 'today': {
return zonedTimeToUtc(startOfDay(nowInUserTimezone), userTimezone)
}
case 'last7Days': {
return zonedTimeToUtc(
subDays(startOfDay(nowInUserTimezone), 6),
userTimezone
)
}
case 'last30Days': {
return zonedTimeToUtc(
subDays(startOfDay(nowInUserTimezone), 29),
userTimezone
)
}
case 'lastMonth': {
return zonedTimeToUtc(
subMonths(startOfMonth(nowInUserTimezone), 1),
userTimezone
)
}
case 'monthToDate': {
return zonedTimeToUtc(startOfMonth(nowInUserTimezone), userTimezone)
}
case 'yearToDate': {
return zonedTimeToUtc(startOfYear(nowInUserTimezone), userTimezone)
}
case 'allTime':
return
return null
}
}
export const parseToDateFromTimeFilter = (
timeFilter: (typeof timeFilterValues)[number],
userTimezone: string = 'UTC'
): Date | null => {
const nowInUserTimezone = utcToZonedTime(new Date(), userTimezone)
switch (timeFilter) {
case 'lastMonth': {
return zonedTimeToUtc(
subMonths(endOfMonth(nowInUserTimezone), 1),
userTimezone
)
}
case 'last30Days':
case 'last7Days':
case 'today':
case 'monthToDate':
case 'yearToDate':
case 'allTime':
return null
}
}

View File

@ -8,7 +8,10 @@ import {
timeFilterValues,
defaultTimeFilter,
} from '@/features/analytics/constants'
import { parseDateFromTimeFilter } from '@/features/analytics/helpers/parseDateFromTimeFilter'
import {
parseFromDateFromTimeFilter,
parseToDateFromTimeFilter,
} from '@/features/analytics/helpers/parseDateFromTimeFilter'
const maxLimit = 100
@ -32,6 +35,7 @@ export const getResults = authenticatedProcedure
limit: z.coerce.number().min(1).max(maxLimit).default(50),
cursor: z.string().optional(),
timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter),
timeZone: z.string().optional(),
})
)
.output(
@ -77,7 +81,11 @@ export const getResults = authenticatedProcedure
if (!typebot || (await isReadTypebotForbidden(typebot, user)))
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
const date = parseDateFromTimeFilter(input.timeFilter)
const fromDate = parseFromDateFromTimeFilter(
input.timeFilter,
input.timeZone
)
const toDate = parseToDateFromTimeFilter(input.timeFilter, input.timeZone)
const results = await prisma.result.findMany({
take: limit + 1,
@ -86,9 +94,10 @@ export const getResults = authenticatedProcedure
typebotId: typebot.id,
hasStarted: true,
isArchived: false,
createdAt: date
createdAt: fromDate
? {
gte: date,
gte: fromDate,
lte: toDate ?? undefined,
}
: undefined,
},

View File

@ -24,6 +24,8 @@ import {
timeFilterValues,
} from '@/features/analytics/constants'
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
export const ResultsPage = () => {
const router = useRouter()
const { workspace } = useWorkspace()
@ -45,6 +47,7 @@ export const ResultsPage = () => {
{
typebotId: publishedTypebot?.typebotId as string,
timeFilter,
timeZone,
},
{
enabled: !!publishedTypebot,

View File

@ -7,10 +7,13 @@ type Params = {
onError?: (error: string) => void
}
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
export const useResultsQuery = ({ timeFilter, typebotId, onError }: Params) => {
const { data, error, fetchNextPage, hasNextPage, refetch } =
trpc.results.getResults.useInfiniteQuery(
{
timeZone,
timeFilter,
typebotId,
},

View File

@ -2703,10 +2703,19 @@
"today",
"last7Days",
"last30Days",
"monthToDate",
"lastMonth",
"yearToDate",
"allTime"
],
"default": "today"
"default": "last7Days"
}
},
{
"in": "query",
"name": "timeZone",
"schema": {
"type": "string"
}
}
],
@ -2830,10 +2839,19 @@
"today",
"last7Days",
"last30Days",
"monthToDate",
"lastMonth",
"yearToDate",
"allTime"
],
"default": "today"
"default": "last7Days"
}
},
{
"in": "query",
"name": "timeZone",
"schema": {
"type": "string"
}
}
],
@ -2954,10 +2972,19 @@
"today",
"last7Days",
"last30Days",
"monthToDate",
"lastMonth",
"yearToDate",
"allTime"
],
"default": "today"
"default": "last7Days"
}
},
{
"in": "query",
"name": "timeZone",
"schema": {
"type": "string"
}
}
],
@ -11070,10 +11097,19 @@
"today",
"last7Days",
"last30Days",
"monthToDate",
"lastMonth",
"yearToDate",
"allTime"
],
"default": "today"
"default": "last7Days"
}
},
{
"in": "query",
"name": "timeZone",
"schema": {
"type": "string"
}
}
],