pull/2022/merge
Emma Segal-Grossman 2023-05-07 18:56:48 +00:00 committed by GitHub
commit f361dc5f53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 142 additions and 33 deletions

View File

@ -6,6 +6,7 @@ import classNames from 'classnames';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { percentFilter } from 'lib/filters'; import { percentFilter } from 'lib/filters';
import useDateRange from 'hooks/useDateRange'; import useDateRange from 'hooks/useDateRange';
import useTimezone from 'hooks/useTimezone';
import usePageQuery from 'hooks/usePageQuery'; import usePageQuery from 'hooks/usePageQuery';
import ErrorMessage from 'components/common/ErrorMessage'; import ErrorMessage from 'components/common/ErrorMessage';
import DataTable from './DataTable'; import DataTable from './DataTable';
@ -26,7 +27,8 @@ export function MetricsTable({
delay = null, delay = null,
...props ...props
}) { }) {
const [{ startDate, endDate, modified }] = useDateRange(websiteId); const [{ startDate, endDate, modified, unit }] = useDateRange(websiteId);
const [timezone] = useTimezone();
const { const {
resolveUrl, resolveUrl,
router, router,
@ -53,6 +55,9 @@ export function MetricsTable({
country, country,
region, region,
city, city,
unit,
timezone,
includeEventData: type === 'event',
}), }),
{ onSuccess: onDataLoad, retryDelay: delay || DEFAULT_ANIMATION_DURATION }, { onSuccess: onDataLoad, retryDelay: delay || DEFAULT_ANIMATION_DURATION },
); );
@ -69,7 +74,7 @@ export function MetricsTable({
return items.sort(firstBy('y', -1).thenBy('x')); return items.sort(firstBy('y', -1).thenBy('x'));
} }
return []; return [];
}, [data, error, dataFilter, filterOptions]); }, [data, dataFilter, filterOptions, limit]);
const { dir } = useLocale(); const { dir } = useLocale();
return ( return (

View File

@ -104,6 +104,7 @@ CREATE TABLE umami.event_data
event_string_value Nullable(String), event_string_value Nullable(String),
event_numeric_value Nullable(Decimal64(4)), --922337203685477.5625 event_numeric_value Nullable(Decimal64(4)), --922337203685477.5625
event_date_value Nullable(DateTime('UTC')), event_date_value Nullable(DateTime('UTC')),
event_bool_value Nullable(Boolean),
event_data_type UInt32, event_data_type UInt32,
created_at DateTime('UTC') created_at DateTime('UTC')
) )

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE `event_data` ADD COLUMN `event_bool_value` BOOLEAN NULL;

View File

@ -14,7 +14,7 @@ model User {
password String @db.VarChar(60) password String @db.VarChar(60)
role String @map("role") @db.VarChar(50) role String @map("role") @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamp(0) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0)
deletedAt DateTime? @map("deleted_at") @db.Timestamp(0) deletedAt DateTime? @map("deleted_at") @db.Timestamp(0)
website Website[] website Website[]
@ -53,7 +53,7 @@ model Website {
resetAt DateTime? @map("reset_at") @db.Timestamp(0) resetAt DateTime? @map("reset_at") @db.Timestamp(0)
userId String? @map("user_id") @db.VarChar(36) userId String? @map("user_id") @db.VarChar(36)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamp(0) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0)
deletedAt DateTime? @map("deleted_at") @db.Timestamp(0) deletedAt DateTime? @map("deleted_at") @db.Timestamp(0)
user User? @relation(fields: [userId], references: [id]) user User? @relation(fields: [userId], references: [id])
@ -99,6 +99,7 @@ model EventData {
eventStringValue String? @map("event_string_value") @db.VarChar(500) eventStringValue String? @map("event_string_value") @db.VarChar(500)
eventNumericValue Decimal? @map("event_numeric_value") @db.Decimal(19, 4) eventNumericValue Decimal? @map("event_numeric_value") @db.Decimal(19, 4)
eventDateValue DateTime? @map("event_date_value") @db.Timestamp(0) eventDateValue DateTime? @map("event_date_value") @db.Timestamp(0)
eventBoolValue Boolean? @map("event_bool_value")
eventDataType Int @map("event_data_type") @db.UnsignedInt eventDataType Int @map("event_data_type") @db.UnsignedInt
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
@ -117,7 +118,7 @@ model Team {
name String @db.VarChar(50) name String @db.VarChar(50)
accessCode String? @unique @map("access_code") @db.VarChar(50) accessCode String? @unique @map("access_code") @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamp(0) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0)
teamUser TeamUser[] teamUser TeamUser[]
teamWebsite TeamWebsite[] teamWebsite TeamWebsite[]
@ -132,7 +133,7 @@ model TeamUser {
userId String @map("user_id") @db.VarChar(36) userId String @map("user_id") @db.VarChar(36)
role String @map("role") @db.VarChar(50) role String @map("role") @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamp(0) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0)
team Team @relation(fields: [teamId], references: [id]) team Team @relation(fields: [teamId], references: [id])
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "event_data" ADD COLUMN "event_bool_value" BOOLEAN;

View File

@ -14,7 +14,7 @@ model User {
password String @db.VarChar(60) password String @db.VarChar(60)
role String @map("role") @db.VarChar(50) role String @map("role") @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamptz(6) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
website Website[] website Website[]
@ -53,7 +53,7 @@ model Website {
resetAt DateTime? @map("reset_at") @db.Timestamptz(6) resetAt DateTime? @map("reset_at") @db.Timestamptz(6)
userId String? @map("user_id") @db.Uuid userId String? @map("user_id") @db.Uuid
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamptz(6) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
user User? @relation(fields: [userId], references: [id]) user User? @relation(fields: [userId], references: [id])
@ -99,6 +99,7 @@ model EventData {
eventStringValue String? @map("event_string_value") @db.VarChar(500) eventStringValue String? @map("event_string_value") @db.VarChar(500)
eventNumericValue Decimal? @map("event_numeric_value") @db.Decimal(19, 4) eventNumericValue Decimal? @map("event_numeric_value") @db.Decimal(19, 4)
eventDateValue DateTime? @map("event_date_value") @db.Timestamptz(6) eventDateValue DateTime? @map("event_date_value") @db.Timestamptz(6)
eventBoolValue Boolean? @map("event_bool_value") @db.Boolean
eventDataType Int @map("event_data_type") @db.Integer eventDataType Int @map("event_data_type") @db.Integer
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
@ -116,7 +117,7 @@ model Team {
name String @db.VarChar(50) name String @db.VarChar(50)
accessCode String? @unique @map("access_code") @db.VarChar(50) accessCode String? @unique @map("access_code") @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamptz(6) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
teamUser TeamUser[] teamUser TeamUser[]
teamWebsite TeamWebsite[] teamWebsite TeamWebsite[]
@ -131,7 +132,7 @@ model TeamUser {
userId String @map("user_id") @db.Uuid userId String @map("user_id") @db.Uuid
role String @map("role") @db.VarChar(50) role String @map("role") @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamptz(6) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
team Team @relation(fields: [teamId], references: [id]) team Team @relation(fields: [teamId], references: [id])
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])

View File

@ -69,6 +69,7 @@ export interface WebsiteActive {
export interface WebsiteMetric { export interface WebsiteMetric {
x: string; x: string;
y: number; y: number;
d?: unknown;
} }
export interface WebsiteMetricFilter { export interface WebsiteMetricFilter {
@ -90,6 +91,7 @@ export interface WebsiteEventMetric {
x: string; x: string;
t: string; t: string;
y: number; y: number;
d?: unknown;
} }
export interface WebsiteEventDataMetric { export interface WebsiteEventDataMetric {

View File

@ -1,10 +1,12 @@
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { methodNotAllowed, ok, unauthorized, badRequest } from 'next-basics';
import { WebsiteMetric, NextApiRequestQueryBody } from 'lib/types'; import { WebsiteMetric, NextApiRequestQueryBody } from 'lib/types';
import { canViewWebsite } from 'lib/auth'; import { canViewWebsite } from 'lib/auth';
import { useAuth, useCors } from 'lib/middleware'; import { useAuth, useCors } from 'lib/middleware';
import { SESSION_COLUMNS, EVENT_COLUMNS, FILTER_COLUMNS } from 'lib/constants'; import { SESSION_COLUMNS, EVENT_COLUMNS, FILTER_COLUMNS } from 'lib/constants';
import { getPageviewMetrics, getSessionMetrics } from 'queries'; import { getEventMetrics, getPageviewMetrics, getSessionMetrics } from 'queries';
import moment from 'moment-timezone';
const unitTypes = ['year', 'month', 'hour', 'day'];
export interface WebsiteMetricsRequestQuery { export interface WebsiteMetricsRequestQuery {
id: string; id: string;
@ -47,6 +49,9 @@ export default async (
country, country,
region, region,
city, city,
includeEventData,
timezone,
unit,
} = req.query; } = req.query;
if (req.method === 'GET') { if (req.method === 'GET') {
@ -54,6 +59,15 @@ export default async (
return unauthorized(res); return unauthorized(res);
} }
if (
typeof timezone !== 'string' ||
typeof unit !== 'string' ||
!moment.tz.zone(String(timezone)) ||
!unitTypes.includes(String(unit))
) {
return badRequest(res);
}
const startDate = new Date(+startAt); const startDate = new Date(+startAt);
const endDate = new Date(+endAt); const endDate = new Date(+endAt);
@ -114,6 +128,17 @@ export default async (
filters[type] = undefined; filters[type] = undefined;
if (type === 'event' && includeEventData) {
const data = await getEventMetrics(websiteId, {
startDate,
endDate,
timezone,
unit,
filters: { url: filters.url, eventName: undefined },
});
return ok(res, data);
}
const data = await getPageviewMetrics(websiteId, { const data = await getPageviewMetrics(websiteId, {
startDate, startDate,
endDate, endDate,

View File

@ -52,18 +52,38 @@ async function relationalQuery(
const filterQuery = getFilterQuery(filters, params); const filterQuery = getFilterQuery(filters, params);
return rawQuery( return rawQuery(
`select `with event_data as (
event_name x, select d.website_event_id,
${getDateQuery('created_at', unit, timezone)} t, jsonb_object_agg(
d.event_key,
case
d.event_data_type
when 1 then to_jsonb(d.event_string_value) -- string
when 2 then to_jsonb(d.event_numeric_value) -- number
when 3 then to_jsonb(d.event_bool_value) -- boolean
when 4 then to_jsonb(d.event_date_value) -- date
when 5 then d.event_string_value::jsonb -- array
end
) filter (
where d.event_key is not null
) as event_data
from event_data d
group by d.website_event_id
)
select
w.event_name x,
d.event_data d,
${getDateQuery('w.created_at', unit, timezone)} t,
count(*) y count(*) y
from website_event from website_event w
left join event_data d on w.event_id = d.website_event_id
where website_id = $1${toUuid()} where website_id = $1${toUuid()}
and created_at >= $2 and created_at >= $2
and created_at between $3 and $4 and created_at between $3 and $4
and event_type = ${EVENT_TYPE.customEvent} and event_type = ${EVENT_TYPE.customEvent}
${filterQuery} ${filterQuery}
group by 1, 2 group by 1, 2, 3
order by 2`, order by 3`,
params, params,
); );
} }
@ -93,17 +113,37 @@ async function clickhouseQuery(
const params = { websiteId }; const params = { websiteId };
return rawQuery( return rawQuery(
`select `with event_data as (
event_name x, select d.website_event_id,
${getDateQuery('created_at', unit, timezone)} t, jsonb_object_agg(
d.event_key,
case
d.event_data_type
when 1 then to_jsonb(d.event_string_value) -- string
when 2 then to_jsonb(d.event_numeric_value) -- number
when 3 then to_jsonb(d.event_bool_value) -- boolean
when 4 then to_jsonb(d.event_date_value) -- date
when 5 then d.event_string_value::jsonb -- array
end
) filter (
where d.event_key is not null
) as event_data
from event_data d
group by d.website_event_id
)
select
w.event_name x,
d.event_data d,
${getDateQuery('w.created_at', unit, timezone)} t,
count(*) y count(*) y
from website_event from website_event w
left join event_data d on w.event_id = d.website_event_id
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
and event_type = ${EVENT_TYPE.customEvent} and w.event_type = ${EVENT_TYPE.customEvent}
and created_at >= ${getDateFormat(resetDate)} and w.created_at >= ${getDateFormat(resetDate)}
and ${getBetweenDates('created_at', startDate, endDate)} and ${getBetweenDates('w.created_at', startDate, endDate)}
${getFilterQuery(filters, params)} ${getFilterQuery(filters, params)}
group by x, t group by x, d, t
order by t`, order by t`,
params, params,
); );

View File

@ -38,13 +38,12 @@ async function relationalQuery(data: {
websiteId, websiteId,
eventKey: a.key, eventKey: a.key,
eventStringValue: eventStringValue:
a.eventDataType === EVENT_DATA_TYPE.string || a.eventDataType === EVENT_DATA_TYPE.string || a.eventDataType === EVENT_DATA_TYPE.array
a.eventDataType === EVENT_DATA_TYPE.boolean ||
a.eventDataType === EVENT_DATA_TYPE.array
? a.value ? a.value
: null, : null,
eventNumericValue: a.eventDataType === EVENT_DATA_TYPE.number ? a.value : null, eventNumericValue: a.eventDataType === EVENT_DATA_TYPE.number ? a.value : null,
eventDateValue: a.eventDataType === EVENT_DATA_TYPE.date ? new Date(a.value) : null, eventDateValue: a.eventDataType === EVENT_DATA_TYPE.date ? new Date(a.value) : null,
eventBoolValue: a.eventDataType === EVENT_DATA_TYPE.boolean ? a.value : null,
eventDataType: a.eventDataType, eventDataType: a.eventDataType,
})); }));
@ -76,13 +75,12 @@ async function clickhouseQuery(data: {
event_name: eventName, event_name: eventName,
event_key: a.key, event_key: a.key,
event_string_value: event_string_value:
a.eventDataType === EVENT_DATA_TYPE.string || a.eventDataType === EVENT_DATA_TYPE.string || a.eventDataType === EVENT_DATA_TYPE.array
a.eventDataType === EVENT_DATA_TYPE.boolean ||
a.eventDataType === EVENT_DATA_TYPE.array
? a.value ? a.value
: null, : null,
event_numeric_value: a.eventDataType === EVENT_DATA_TYPE.number ? a.value : null, event_numeric_value: a.eventDataType === EVENT_DATA_TYPE.number ? a.value : null,
event_date_value: a.eventDataType === EVENT_DATA_TYPE.date ? getDateFormat(a.value) : null, event_date_value: a.eventDataType === EVENT_DATA_TYPE.date ? getDateFormat(a.value) : null,
event_bool_value: a.eventDataType === EVENT_DATA_TYPE.boolean ? a.value : null,
event_data_type: a.eventDataType, event_data_type: a.eventDataType,
created_at: createdAt, created_at: createdAt,
})); }));

View File

@ -102,7 +102,7 @@ async function clickhouseQuery(
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
and event_type = {eventType:UInt32} and event_type = {eventType:UInt32}
and created_at >= ${getDateFormat(resetDate)} and created_at >= ${getDateFormat(resetDate)}
and ${getBetweenDates('created_at', startDate, endDate)} and ${getBetweenDates('created_at', startDate, endDate)}
${excludeDomain} ${excludeDomain}
${filterQuery} ${filterQuery}
group by x group by x

View File

@ -0,0 +1,2 @@
select *
from event_data -- WHERE event_data.website_event_id = '91bac2f4-4039-4b2a-a2ab-8c86f71ef95d'

30
umami-pr-1.session.sql Normal file
View File

@ -0,0 +1,30 @@
WITH event_data_query AS (
SELECT d.website_event_id,
jsonb_object_agg(
d.event_key,
CASE
d.event_data_type
when 1 then to_jsonb(d.event_string_value) -- string
when 2 then to_jsonb(d.event_numeric_value) -- number
when 3 then to_jsonb(d.event_bool_value) -- boolean
when 4 then to_jsonb(d.event_date_value) -- date
when 5 then d.event_string_value::jsonb -- array
end
) filter (
where d.event_key is not null
) as event_data
FROM event_data d
GROUP BY d.website_event_id
)
select e.event_name x,
to_char(
date_trunc('hour', e.created_at),
'YYYY-MM-DD HH24:00:00'
) c,
edq.event_data::jsonb->'data.target' t,
edq.event_data::jsonb->'data.release' r
from website_event e
LEFT JOIN event_data_query edq ON e.event_id = edq.website_event_id
where e.event_name = 'Outbound click'
or e.event_name = 'NewSong PRO click'
order by c desc