better handling dates in clickhouse
This commit is contained in:
@@ -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!);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
apiUrl="http://localhost:3333"
|
||||||
clientId={process.env.NEXT_PUBLIC_OP_CLIENT_ID}
|
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}>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 ?? [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ?? [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ?? [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user