Compare commits

...

6 commits

Author SHA1 Message Date
Mike Cao
ef3f7274e3 Remember last team.
Some checks are pending
Create docker images (cloud) / Build, push, and deploy (push) Waiting to run
Node.js CI / build (postgresql, 18.18, 10) (push) Waiting to run
2025-11-17 19:12:25 -08:00
Mike Cao
1852acc333 Merge remote-tracking branch 'origin/dev' into dev
Some checks failed
Node.js CI / build (postgresql, 18.18, 10) (push) Has been cancelled
2025-11-14 15:46:59 -08:00
Mike Cao
cb63e49a9b Fixed triggered event lookup. Closes #3742. 2025-11-14 15:42:23 -08:00
Mike Cao
d382ad2975
Merge pull request #3682 from rkoh-rq/patch-1
Some checks are pending
Node.js CI / build (postgresql, 18.18, 10) (push) Waiting to run
fix: quote "event" reserved keyword in journey queries
2025-11-14 11:44:31 -08:00
Mike Cao
b1dc690e2f
Merge branch 'dev' into patch-1 2025-11-14 11:44:20 -08:00
rkoh-rq
3cb7fa34b0
fix: quote "event" reserved keyword in journey queries
Fixes PostgreSQL syntax error by quoting the "event" column alias. This was causing the journey query to fail.

"event" is a reserved keyword in PostgreSQL. Added double quotes to treat it as an identifier rather than a keyword.

Changes:
- Quote "event" in PostgreSQL
- Quote "event" in ClickHouse for consistency
2025-11-04 11:00:33 +08:00
8 changed files with 49 additions and 14 deletions

View file

@ -5,11 +5,22 @@ import { UpdateNotice } from './UpdateNotice';
import { SideNav } from '@/app/(main)/SideNav'; import { SideNav } from '@/app/(main)/SideNav';
import { useLoginQuery, useConfig, useNavigation } from '@/components/hooks'; import { useLoginQuery, useConfig, useNavigation } from '@/components/hooks';
import { MobileNav } from '@/app/(main)/MobileNav'; import { MobileNav } from '@/app/(main)/MobileNav';
import { useEffect } from 'react';
import { removeItem, setItem } from '@/lib/storage';
import { LAST_TEAM_CONFIG } from '@/lib/constants';
export function App({ children }) { export function App({ children }) {
const { user, isLoading, error } = useLoginQuery(); const { user, isLoading, error } = useLoginQuery();
const config = useConfig(); const config = useConfig();
const { pathname } = useNavigation(); const { pathname, teamId } = useNavigation();
useEffect(() => {
if (teamId) {
setItem(LAST_TEAM_CONFIG, teamId);
} else {
removeItem(LAST_TEAM_CONFIG);
}
}, [teamId]);
if (isLoading || !config) { if (isLoading || !config) {
return <Loading placement="absolute" />; return <Loading placement="absolute" />;

View file

@ -56,7 +56,7 @@ export function FunnelEditForm({
const defaultValues = { const defaultValues = {
name: data?.name || '', name: data?.name || '',
window: data?.parameters?.window || 60, window: data?.parameters?.window || 60,
steps: data?.parameters?.steps || [{ type: 'path', value: '/' }], steps: data?.parameters?.steps || [{ type: 'path', value: '' }],
}; };
return ( return (
@ -82,12 +82,10 @@ export function FunnelEditForm({
validate: value => value.length > 1 || 'At least two steps are required', validate: value => value.length > 1 || 'At least two steps are required',
}} }}
> >
{({ fields, append, remove, getValues }) => { {({ fields, append, remove }) => {
return ( return (
<Grid gap> <Grid gap>
{fields.map(({ id }: { id: string }, index: number) => { {fields.map(({ id }: { id: string }, index: number) => {
const type = getValues(`steps.${index}.type`);
return ( return (
<Grid key={id} columns="260px 1fr auto" gap> <Grid key={id} columns="260px 1fr auto" gap>
<Column> <Column>
@ -103,7 +101,8 @@ export function FunnelEditForm({
name={`steps.${index}.value`} name={`steps.${index}.value`}
rules={{ required: formatMessage(labels.required) }} rules={{ required: formatMessage(labels.required) }}
> >
{({ field }) => { {({ field, context }) => {
const type = context.watch(`steps.${index}.type`);
return <LookupField websiteId={websiteId} type={type} {...field} />; return <LookupField websiteId={websiteId} type={type} {...field} />;
}} }}
</FormField> </FormField>
@ -118,7 +117,7 @@ export function FunnelEditForm({
})} })}
<Row> <Row>
<Button <Button
onPress={() => append({ type: 'path', value: '/' })} onPress={() => append({ type: 'path', value: '' })}
isDisabled={fields.length >= FUNNEL_STEPS_MAX} isDisabled={fields.length >= FUNNEL_STEPS_MAX}
> >
<Icon> <Icon>

View file

@ -17,9 +17,8 @@ export function GoalAddButton({ websiteId }: { websiteId: string }) {
<Modal> <Modal>
<Dialog <Dialog
aria-label="add goal" aria-label="add goal"
variant="modal"
title={formatMessage(labels.goal)} title={formatMessage(labels.goal)}
style={{ minWidth: 800, minHeight: 300 }} style={{ minWidth: 400, minHeight: 300 }}
> >
{({ close }) => <GoalEditForm websiteId={websiteId} onClose={close} />} {({ close }) => <GoalEditForm websiteId={websiteId} onClose={close} />}
</Dialog> </Dialog>

View file

@ -1,5 +1,21 @@
'use client';
import { useEffect } from 'react';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { getItem, removeItem } from '@/lib/storage';
import { LAST_TEAM_CONFIG } from '@/lib/constants';
export default function RootPage() { export default function RootPage() {
redirect('/websites'); useEffect(() => {
const lastTeam = getItem(LAST_TEAM_CONFIG);
if (lastTeam) {
redirect(`/teams/${lastTeam}/websites`);
} else {
removeItem(LAST_TEAM_CONFIG);
redirect(`/websites`);
}
}, []);
return null;
} }

View file

@ -1,13 +1,18 @@
import { Text } from '@umami/react-zen'; import { Text } from '@umami/react-zen';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { useLocale, useTimezone } from '@/components/hooks'; import { useLocale, useTimezone } from '@/components/hooks';
import { isInvalidDate } from '@/lib/date';
export function DateDistance({ date }: { date: Date }) { export function DateDistance({ date }: { date: Date }) {
const { formatTimezoneDate } = useTimezone(); const { formatTimezoneDate } = useTimezone();
const { dateLocale } = useLocale(); const { dateLocale } = useLocale();
if (!isInvalidDate(date)) {
return null;
}
return ( return (
<Text title={formatTimezoneDate(date.toISOString(), 'PPPpp')}> <Text title={formatTimezoneDate(date?.toISOString(), 'PPPpp')}>
{formatDistanceToNow(date, { addSuffix: true, locale: dateLocale })} {formatDistanceToNow(date, { addSuffix: true, locale: dateLocale })}
</Text> </Text>
); );

View file

@ -5,6 +5,7 @@ export const TIMEZONE_CONFIG = 'umami.timezone';
export const DATE_RANGE_CONFIG = 'umami.date-range'; export const DATE_RANGE_CONFIG = 'umami.date-range';
export const THEME_CONFIG = 'umami.theme'; export const THEME_CONFIG = 'umami.theme';
export const DASHBOARD_CONFIG = 'umami.dashboard'; export const DASHBOARD_CONFIG = 'umami.dashboard';
export const LAST_TEAM_CONFIG = 'umami.last-team';
export const VERSION_CHECK = 'umami.version-check'; export const VERSION_CHECK = 'umami.version-check';
export const SHARE_TOKEN_HEADER = 'x-umami-share-token'; export const SHARE_TOKEN_HEADER = 'x-umami-share-token';
export const HOMEPAGE_URL = 'https://umami.is'; export const HOMEPAGE_URL = 'https://umami.is';

View file

@ -369,3 +369,7 @@ export function getDateRangeValue(startDate: Date, endDate: Date) {
export function getMonthDateRangeValue(date: Date) { export function getMonthDateRangeValue(date: Date) {
return getDateRangeValue(startOfMonth(date), endOfMonth(date)); return getDateRangeValue(startOfMonth(date), endOfMonth(date));
} }
export function isInvalidDate(date: any) {
return date instanceof Date && isNaN(date.getTime());
}

View file

@ -73,7 +73,7 @@ async function relationalQuery(
for (let i = 1; i <= steps; i++) { for (let i = 1; i <= steps; i++) {
const endQuery = i < steps ? ',' : ''; const endQuery = i < steps ? ',' : '';
selectQuery += `s.e${i},`; selectQuery += `s.e${i},`;
maxQuery += `\nmax(CASE WHEN event_number = ${i} THEN event ELSE NULL END) AS e${i}${endQuery}`; maxQuery += `\nmax(CASE WHEN event_number = ${i} THEN "event" ELSE NULL END) AS e${i}${endQuery}`;
groupByQuery += `s.e${i}${endQuery} `; groupByQuery += `s.e${i}${endQuery} `;
} }
@ -185,7 +185,7 @@ async function clickhouseQuery(
for (let i = 1; i <= steps; i++) { for (let i = 1; i <= steps; i++) {
const endQuery = i < steps ? ',' : ''; const endQuery = i < steps ? ',' : '';
selectQuery += `s.e${i},`; selectQuery += `s.e${i},`;
maxQuery += `\nmax(CASE WHEN event_number = ${i} THEN event ELSE NULL END) AS e${i}${endQuery}`; maxQuery += `\nmax(CASE WHEN event_number = ${i} THEN "event" ELSE NULL END) AS e${i}${endQuery}`;
groupByQuery += `s.e${i}${endQuery} `; groupByQuery += `s.e${i}${endQuery} `;
} }
@ -230,7 +230,7 @@ async function clickhouseQuery(
WITH events AS ( WITH events AS (
select distinct select distinct
visit_id, visit_id,
coalesce(nullIf(event_name, ''), url_path) event, coalesce(nullIf(event_name, ''), url_path) "event",
row_number() OVER (PARTITION BY visit_id ORDER BY created_at) AS event_number row_number() OVER (PARTITION BY visit_id ORDER BY created_at) AS event_number
from website_event from website_event
${cohortQuery} ${cohortQuery}