better handling dates in clickhouse

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-08-28 10:53:23 +02:00
parent df05e2dab3
commit 2be2ff3e12
14 changed files with 97 additions and 71 deletions

View File

@@ -29,7 +29,7 @@ const SkipOnboarding = () => {
text: 'Are you sure you want to skip onboarding? Since you do not have any projects, you will be logged out.', text: 'Are you sure you want to skip onboarding? Since you do not have any projects, you will be logged out.',
onConfirm() { onConfirm() {
auth.signOut(); auth.signOut();
router.replace(process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL); router.replace(process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL!);
}, },
}); });
} }

View File

@@ -62,14 +62,14 @@ function AllProviders({ children }: { children: React.ReactNode }) {
defaultTheme="light" defaultTheme="light"
disableTransitionOnChange disableTransitionOnChange
> >
{process.env.NEXT_PUBLIC_OP_CLIENT_ID && (
<OpenPanelComponent <OpenPanelComponent
clientId={process.env.NEXT_PUBLIC_OP_CLIENT_ID} apiUrl="http://localhost:3333"
clientId={'75479ffd-645b-43d2-a33a-2111a6f5ee00'}
trackScreenViews trackScreenViews
trackOutgoingLinks trackOutgoingLinks
trackAttributes trackAttributes
/> />
)}
<ReduxProvider store={storeRef.current}> <ReduxProvider store={storeRef.current}>
<api.Provider client={trpcClient} queryClient={queryClient}> <api.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>

View File

@@ -20,6 +20,8 @@ import type {
IChartEventFilterValue, IChartEventFilterValue,
} from '@openpanel/validation'; } from '@openpanel/validation';
import { useOverviewOptions } from '../useOverviewOptions';
export interface OverviewFiltersDrawerContentProps { export interface OverviewFiltersDrawerContentProps {
projectId: string; projectId: string;
nuqsOptions?: NuqsOptions; nuqsOptions?: NuqsOptions;
@@ -33,10 +35,11 @@ export function OverviewFiltersDrawerContent({
enableEventsFilter, enableEventsFilter,
mode, mode,
}: OverviewFiltersDrawerContentProps) { }: OverviewFiltersDrawerContentProps) {
const { interval, range } = useOverviewOptions();
const [filters, setFilter] = useEventQueryFilters(nuqsOptions); const [filters, setFilter] = useEventQueryFilters(nuqsOptions);
const [event, setEvent] = useEventQueryNamesFilter(nuqsOptions); const [event, setEvent] = useEventQueryNamesFilter(nuqsOptions);
const eventNames = useEventNames(projectId); const eventNames = useEventNames({ projectId, interval, range });
const eventProperties = useEventProperties(projectId); const eventProperties = useEventProperties({ projectId, interval, range });
const profileProperties = useProfileProperties(projectId); const profileProperties = useProfileProperties(projectId);
const properties = mode === 'events' ? eventProperties : profileProperties; const properties = mode === 'events' ? eventProperties : profileProperties;
@@ -113,11 +116,14 @@ export function FilterOptionEvent({
operator: IChartEventFilterOperator operator: IChartEventFilterOperator
) => void; ) => void;
}) { }) {
const values = useEventValues( const { interval, range } = useOverviewOptions();
const values = useEventValues({
projectId, projectId,
filter.name === 'path' ? 'screen_view' : 'session_start', event: filter.name === 'path' ? 'screen_view' : 'session_start',
filter.name property: filter.name,
); interval,
range,
});
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@@ -1,6 +1,6 @@
import { Combobox } from '@/components/ui/combobox'; import { Combobox } from '@/components/ui/combobox';
import { useAppParams } from '@/hooks/useAppParams'; import { useAppParams } from '@/hooks/useAppParams';
import { useDispatch } from '@/redux'; import { useDispatch, useSelector } from '@/redux';
import { api } from '@/trpc/client'; import { api } from '@/trpc/client';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { DatabaseIcon } from 'lucide-react'; import { DatabaseIcon } from 'lucide-react';
@@ -18,11 +18,14 @@ export function EventPropertiesCombobox({
}: EventPropertiesComboboxProps) { }: EventPropertiesComboboxProps) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { projectId } = useAppParams(); const { projectId } = useAppParams();
const range = useSelector((state) => state.report.range);
const interval = useSelector((state) => state.report.interval);
const query = api.chart.properties.useQuery( const query = api.chart.properties.useQuery(
{ {
event: event.name, event: event.name,
projectId, projectId,
range,
interval,
}, },
{ {
enabled: !!event.name, enabled: !!event.name,

View File

@@ -16,9 +16,14 @@ import type { ReportEventMoreProps } from './ReportEventMore';
export function ReportBreakdowns() { export function ReportBreakdowns() {
const { projectId } = useAppParams(); const { projectId } = useAppParams();
const selectedBreakdowns = useSelector((state) => state.report.breakdowns); const selectedBreakdowns = useSelector((state) => state.report.breakdowns);
const interval = useSelector((state) => state.report.interval);
const range = useSelector((state) => state.report.range);
const dispatch = useDispatch(); const dispatch = useDispatch();
const propertiesQuery = api.chart.properties.useQuery({ const propertiesQuery = api.chart.properties.useQuery({
projectId, projectId,
range,
interval,
}); });
const propertiesCombobox = (propertiesQuery.data ?? []).map((item) => ({ const propertiesCombobox = (propertiesQuery.data ?? []).map((item) => ({
value: item, value: item,

View File

@@ -29,13 +29,18 @@ import type { ReportEventMoreProps } from './ReportEventMore';
export function ReportEvents() { export function ReportEvents() {
const previous = useSelector((state) => state.report.previous); const previous = useSelector((state) => state.report.previous);
const selectedEvents = useSelector((state) => state.report.events); const selectedEvents = useSelector((state) => state.report.events);
const input = useSelector((state) => state.report); const startDate = useSelector((state) => state.report.startDate);
const endDate = useSelector((state) => state.report.endDate);
const range = useSelector((state) => state.report.range);
const interval = useSelector((state) => state.report.interval);
const dispatch = useDispatch(); const dispatch = useDispatch();
const { projectId } = useAppParams(); const { projectId } = useAppParams();
const eventNames = useEventNames(projectId, { const eventNames = useEventNames({
startDate: input.startDate, projectId,
endDate: input.endDate, startDate,
range: input.range, endDate,
range,
interval,
}); });
const dispatchChangeEvent = useDebounceFn((event: IChartEvent) => { const dispatchChangeEvent = useDebounceFn((event: IChartEvent) => {

View File

@@ -26,7 +26,9 @@ interface FilterProps {
export function FilterItem({ filter, event }: FilterProps) { export function FilterItem({ filter, event }: FilterProps) {
const { projectId } = useAppParams(); const { projectId } = useAppParams();
const { range, startDate, endDate } = useSelector((state) => state.report); const { range, startDate, endDate, interval } = useSelector(
(state) => state.report
);
const getLabel = useMappings(); const getLabel = useMappings();
const dispatch = useDispatch(); const dispatch = useDispatch();
const potentialValues = api.chart.values.useQuery({ const potentialValues = api.chart.values.useQuery({
@@ -34,6 +36,7 @@ export function FilterItem({ filter, event }: FilterProps) {
property: filter.name, property: filter.name,
projectId, projectId,
range, range,
interval,
startDate, startDate,
endDate, endDate,
}); });

View File

@@ -15,7 +15,10 @@ interface FiltersComboboxProps {
export function FiltersCombobox({ event }: FiltersComboboxProps) { export function FiltersCombobox({ event }: FiltersComboboxProps) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { range, startDate, endDate } = useSelector((state) => state.report); const interval = useSelector((state) => state.report.interval);
const range = useSelector((state) => state.report.range);
const startDate = useSelector((state) => state.report.startDate);
const endDate = useSelector((state) => state.report.endDate);
const { projectId } = useAppParams(); const { projectId } = useAppParams();
const query = api.chart.properties.useQuery( const query = api.chart.properties.useQuery(
@@ -23,6 +26,7 @@ export function FiltersCombobox({ event }: FiltersComboboxProps) {
event: event.name, event: event.name,
projectId, projectId,
range, range,
interval,
startDate, startDate,
endDate, endDate,
}, },

View File

@@ -1,10 +1,8 @@
import { api } from '@/trpc/client'; import { api } from '@/trpc/client';
export function useEventNames(projectId: string, options?: any) { export function useEventNames(
const query = api.chart.events.useQuery({ params: Parameters<typeof api.chart.events.useQuery>[0]
projectId: projectId, ) {
...(options ? options : {}), const query = api.chart.events.useQuery(params);
});
return query.data ?? []; return query.data ?? [];
} }

View File

@@ -1,10 +1,9 @@
import { api } from '@/trpc/client'; import { api } from '@/trpc/client';
export function useEventProperties(projectId: string, event?: string) { export function useEventProperties(
const query = api.chart.properties.useQuery({ params: Parameters<typeof api.chart.properties.useQuery>[0]
projectId: projectId, ) {
event, const query = api.chart.properties.useQuery(params);
});
return query.data ?? []; return query.data ?? [];
} }

View File

@@ -1,15 +1,8 @@
import { api } from '@/trpc/client'; import { api } from '@/trpc/client';
export function useEventValues( export function useEventValues(
projectId: string, params: Parameters<typeof api.chart.values.useQuery>[0]
event: string,
property: string
) { ) {
const query = api.chart.values.useQuery({ const query = api.chart.values.useQuery(params);
projectId: projectId,
event,
property,
});
return query.data?.values ?? []; return query.data?.values ?? [];
} }

View File

@@ -1,5 +1,8 @@
import type { ResponseJSON } from '@clickhouse/client'; import type { ResponseJSON } from '@clickhouse/client';
import { createClient } from '@clickhouse/client'; import { createClient } from '@clickhouse/client';
import { escape } from 'sqlstring';
import type { IInterval } from '@openpanel/validation';
export const TABLE_NAMES = { export const TABLE_NAMES = {
events: 'events_v2', events: 'events_v2',
@@ -126,6 +129,22 @@ export function formatClickhouseDate(
return date.toISOString().replace('T', ' ').replace(/Z+$/, ''); return date.toISOString().replace('T', ' ').replace(/Z+$/, '');
} }
export function toDate(str: string, interval?: IInterval) {
if (!interval || interval === 'minute' || interval === 'hour') {
if (str.match(/\d{4}-\d{2}-\d{2}/)) {
return escape(str);
}
return str;
}
if (str.match(/\d{4}-\d{2}-\d{2}/)) {
return `toDate(${escape(str)})`;
}
return `toDate(${str})`;
}
export function convertClickhouseDateToJs(date: string) { export function convertClickhouseDateToJs(date: string) {
return new Date(date.replace(' ', 'T') + 'Z'); return new Date(date.replace(' ', 'T') + 'Z');
} }

View File

@@ -6,7 +6,11 @@ import type {
IGetChartDataInput, IGetChartDataInput,
} from '@openpanel/validation'; } from '@openpanel/validation';
import { formatClickhouseDate, TABLE_NAMES } from '../clickhouse-client'; import {
formatClickhouseDate,
TABLE_NAMES,
toDate,
} from '../clickhouse-client';
import { createSqlBuilder } from '../sql-builder'; import { createSqlBuilder } from '../sql-builder';
function getPropertyKey(property: string) { function getPropertyKey(property: string) {
@@ -67,21 +71,11 @@ export function getChartSql({
sb.groupBy.date = 'date'; sb.groupBy.date = 'date';
if (startDate) { if (startDate) {
sb.where.startDate = `created_at >= '${formatClickhouseDate(startDate)}'`; sb.where.startDate = `${toDate('created_at', interval)} >= ${toDate(formatClickhouseDate(startDate), interval)}`;
// if (interval === 'minute' || interval === 'hour') {
// sb.where.startDate = `created_at >= '${formatClickhouseDate(startDate)}'`;
// } else {
// sb.where.startDate = `toDate(created_at) >= '${formatClickhouseDate(startDate, true)}'`;
// }
} }
if (endDate) { if (endDate) {
sb.where.endDate = `created_at <= '${formatClickhouseDate(endDate)}'`; sb.where.endDate = `${toDate('created_at', interval)} <= ${toDate(formatClickhouseDate(endDate), interval)}`;
// if (interval === 'minute' || interval === 'hour') {
// sb.where.endDate = `created_at <= '${formatClickhouseDate(endDate)}'`;
// } else {
// sb.where.endDate = `toDate(created_at) <= '${formatClickhouseDate(endDate, true)}'`;
// }
} }
if (breakdowns.length > 0 && limit) { if (breakdowns.length > 0 && limit) {

View File

@@ -1,22 +1,16 @@
import { subMonths } from 'date-fns';
import { flatten, map, pipe, prop, sort, uniq } from 'ramda'; import { flatten, map, pipe, prop, sort, uniq } from 'ramda';
import { escape } from 'sqlstring'; import { escape } from 'sqlstring';
import { z } from 'zod'; import { z } from 'zod';
import { average, max, min, round, slug, sum } from '@openpanel/common';
import { import {
chQuery, chQuery,
createSqlBuilder, createSqlBuilder,
db, db,
formatClickhouseDate, formatClickhouseDate,
TABLE_NAMES, TABLE_NAMES,
toDate,
} from '@openpanel/db'; } from '@openpanel/db';
import { zChartInput, zRange } from '@openpanel/validation'; import { zChartInput, zRange, zTimeInterval } from '@openpanel/validation';
import type {
FinalChart,
IChartInput,
PreviousValue,
} from '@openpanel/validation';
import { getProjectAccessCached } from '../access'; import { getProjectAccessCached } from '../access';
import { TRPCAccessError } from '../errors'; import { TRPCAccessError } from '../errors';
@@ -34,7 +28,8 @@ export const chartRouter = createTRPCRouter({
.input( .input(
z.object({ z.object({
projectId: z.string(), projectId: z.string(),
range: zRange.default('30d'), range: zRange,
interval: zTimeInterval,
startDate: z.string().nullish(), startDate: z.string().nullish(),
endDate: z.string().nullish(), endDate: z.string().nullish(),
}) })
@@ -42,7 +37,7 @@ export const chartRouter = createTRPCRouter({
.query(async ({ input: { projectId, ...input } }) => { .query(async ({ input: { projectId, ...input } }) => {
const { startDate, endDate } = getChartStartEndDate(input); const { startDate, endDate } = getChartStartEndDate(input);
const events = await chQuery<{ name: string }>( const events = await chQuery<{ name: string }>(
`SELECT DISTINCT name FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} AND created_at BETWEEN toDate('${formatClickhouseDate(startDate)}') AND toDate('${formatClickhouseDate(endDate)}');` `SELECT DISTINCT name FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} AND ${toDate('created_at', input.interval)} BETWEEN ${toDate(formatClickhouseDate(startDate), input.interval)} AND ${toDate(formatClickhouseDate(endDate), input.interval)};`
); );
return [ return [
@@ -58,7 +53,8 @@ export const chartRouter = createTRPCRouter({
z.object({ z.object({
event: z.string().optional(), event: z.string().optional(),
projectId: z.string(), projectId: z.string(),
range: zRange.default('30d'), range: zRange,
interval: zTimeInterval,
startDate: z.string().nullish(), startDate: z.string().nullish(),
endDate: z.string().nullish(), endDate: z.string().nullish(),
}) })
@@ -69,7 +65,7 @@ export const chartRouter = createTRPCRouter({
`SELECT distinct mapKeys(properties) as keys from ${TABLE_NAMES.events} where ${ `SELECT distinct mapKeys(properties) as keys from ${TABLE_NAMES.events} where ${
event && event !== '*' ? `name = ${escape(event)} AND ` : '' event && event !== '*' ? `name = ${escape(event)} AND ` : ''
} project_id = ${escape(projectId)} AND } project_id = ${escape(projectId)} AND
created_at BETWEEN toDate('${formatClickhouseDate(startDate)}') AND toDate('${formatClickhouseDate(endDate)}');` ${toDate('created_at', input.interval)} BETWEEN ${toDate(formatClickhouseDate(startDate), input.interval)} AND ${toDate(formatClickhouseDate(endDate), input.interval)};`
); );
const properties = events const properties = events
@@ -111,7 +107,8 @@ export const chartRouter = createTRPCRouter({
event: z.string(), event: z.string(),
property: z.string(), property: z.string(),
projectId: z.string(), projectId: z.string(),
range: zRange.default('30d'), range: zRange,
interval: zTimeInterval,
startDate: z.string().nullish(), startDate: z.string().nullish(),
endDate: z.string().nullish(), endDate: z.string().nullish(),
}) })
@@ -137,7 +134,7 @@ export const chartRouter = createTRPCRouter({
sb.select.values = `distinct ${property} as values`; sb.select.values = `distinct ${property} as values`;
} }
sb.where.date = `created_at BETWEEN toDate('${formatClickhouseDate(startDate)}') AND toDate('${formatClickhouseDate(endDate)}')`; sb.where.date = `${toDate('created_at', input.interval)} BETWEEN ${toDate(formatClickhouseDate(startDate), input.interval)} AND ${toDate(formatClickhouseDate(endDate), input.interval)};`;
const events = await chQuery<{ values: string[] }>(getSql()); const events = await chQuery<{ values: string[] }>(getSql());