mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-14 21:10:34 +01:00
Merge branch 'dev' into analytics
This commit is contained in:
commit
f16d74e25d
1
.github/workflows/stale-issues.yml
vendored
1
.github/workflows/stale-issues.yml
vendored
@ -22,3 +22,4 @@ jobs:
|
|||||||
operations-per-run: 200
|
operations-per-run: 200
|
||||||
ascending: true
|
ascending: true
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
exempt-issue-labels: bug,enhancement
|
||||||
|
@ -35,7 +35,9 @@ ENV NEXT_TELEMETRY_DISABLED 1
|
|||||||
RUN addgroup --system --gid 1001 nodejs
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
RUN adduser --system --uid 1001 nextjs
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
RUN yarn add npm-run-all dotenv prisma semver
|
RUN set -x \
|
||||||
|
&& apk add --no-cache curl \
|
||||||
|
&& yarn add npm-run-all dotenv prisma semver
|
||||||
|
|
||||||
# You only need to copy next.config.js if you are NOT using the default configuration
|
# You only need to copy next.config.js if you are NOT using the default configuration
|
||||||
COPY --from=builder /app/next.config.js .
|
COPY --from=builder /app/next.config.js .
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
-- AlterTable
|
-- AlterTable
|
||||||
ALTER TABLE `event_data` RENAME COLUMN `event_data_type` TO `data_type`;
|
ALTER TABLE `event_data` CHANGE `event_data_type` `data_type` INTEGER UNSIGNED NOT NULL;
|
||||||
ALTER TABLE `event_data` RENAME COLUMN `event_date_value` TO `date_value`;
|
ALTER TABLE `event_data` CHANGE `event_date_value` `date_value` TIMESTAMP(0) NULL;
|
||||||
ALTER TABLE `event_data` RENAME COLUMN `event_id` TO `event_data_id`;
|
ALTER TABLE `event_data` CHANGE `event_id` `event_data_id` VARCHAR(36) NOT NULL;
|
||||||
ALTER TABLE `event_data` RENAME COLUMN `event_numeric_value` TO `number_value`;
|
ALTER TABLE `event_data` CHANGE `event_numeric_value` `number_value` DECIMAL(19,4) NULL;
|
||||||
ALTER TABLE `event_data` RENAME COLUMN `event_string_value` TO `string_value`;
|
ALTER TABLE `event_data` CHANGE `event_string_value` `string_value` VARCHAR(500) NULL;
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE `session_data` (
|
CREATE TABLE `session_data` (
|
||||||
@ -50,4 +50,4 @@ WHERE data_type = 2;
|
|||||||
|
|
||||||
UPDATE event_data
|
UPDATE event_data
|
||||||
SET string_value = CONCAT(REPLACE(DATE_FORMAT(date_value, '%Y-%m-%d %T'), ' ', 'T'), 'Z')
|
SET string_value = CONCAT(REPLACE(DATE_FORMAT(date_value, '%Y-%m-%d %T'), ' ', 'T'), 'Z')
|
||||||
WHERE data_type = 4;
|
WHERE data_type = 4;
|
||||||
|
@ -13,6 +13,11 @@ services:
|
|||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
restart: always
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl http://localhost:3000/api/heartbeat"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
db:
|
db:
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
environment:
|
environment:
|
||||||
|
@ -63,11 +63,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clickhouse/client": "^0.2.2",
|
"@clickhouse/client": "^0.2.2",
|
||||||
"@fontsource/inter": "^4.5.15",
|
"@fontsource/inter": "^4.5.15",
|
||||||
"@prisma/client": "5.4.2",
|
"@prisma/client": "5.6.0",
|
||||||
"@prisma/extension-read-replicas": "^0.3.0",
|
"@prisma/extension-read-replicas": "^0.3.0",
|
||||||
"@react-spring/web": "^9.7.3",
|
"@react-spring/web": "^9.7.3",
|
||||||
"@tanstack/react-query": "^4.33.0",
|
"@tanstack/react-query": "^4.33.0",
|
||||||
"@umami/prisma-client": "^0.5.0",
|
"@umami/prisma-client": "^0.7.0",
|
||||||
"@umami/redis-client": "^0.18.0",
|
"@umami/redis-client": "^0.18.0",
|
||||||
"chalk": "^4.1.1",
|
"chalk": "^4.1.1",
|
||||||
"chart.js": "^4.2.1",
|
"chart.js": "^4.2.1",
|
||||||
@ -97,9 +97,9 @@
|
|||||||
"next-basics": "^0.37.0",
|
"next-basics": "^0.37.0",
|
||||||
"node-fetch": "^3.2.8",
|
"node-fetch": "^3.2.8",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prisma": "5.4.2",
|
"prisma": "5.6.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-basics": "^0.107.0",
|
"react-basics": "^0.109.0",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-error-boundary": "^4.0.4",
|
"react-error-boundary": "^4.0.4",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { Form, FormRow, Item, Flexbox, Dropdown, Button } from 'react-basics';
|
import { Form, FormRow, Item, Flexbox, Dropdown, Button } from 'react-basics';
|
||||||
import { useMessages, useFilters, useFormat } from 'components/hooks';
|
import { useMessages, useFilters, useFormat, useLocale } from 'components/hooks';
|
||||||
import styles from './FieldFilterForm.module.css';
|
import styles from './FieldFilterForm.module.css';
|
||||||
|
|
||||||
export default function FieldFilterForm({
|
export default function FieldFilterForm({
|
||||||
@ -16,14 +16,30 @@ export default function FieldFilterForm({
|
|||||||
const [value, setValue] = useState();
|
const [value, setValue] = useState();
|
||||||
const { getFilters } = useFilters();
|
const { getFilters } = useFilters();
|
||||||
const { formatValue } = useFormat();
|
const { formatValue } = useFormat();
|
||||||
|
const { locale } = useLocale();
|
||||||
const filters = getFilters(type);
|
const filters = getFilters(type);
|
||||||
|
|
||||||
|
const formattedValues = useMemo(() => {
|
||||||
|
const formatted = {};
|
||||||
|
const format = val => {
|
||||||
|
formatted[val] = formatValue(val, name);
|
||||||
|
return formatted[val];
|
||||||
|
};
|
||||||
|
if (values.length !== 1) {
|
||||||
|
const { compare } = new Intl.Collator(locale, { numeric: true });
|
||||||
|
values.sort((a, b) => compare(formatted[a] ?? format(a), formatted[b] ?? format(b)));
|
||||||
|
} else {
|
||||||
|
format(values[0]);
|
||||||
|
}
|
||||||
|
return formatted;
|
||||||
|
}, [values]);
|
||||||
|
|
||||||
const renderFilterValue = value => {
|
const renderFilterValue = value => {
|
||||||
return filters.find(f => f.value === value)?.label;
|
return filters.find(f => f.value === value)?.label;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderValue = value => {
|
const renderValue = value => {
|
||||||
return formatValue(value, name);
|
return formattedValues[value];
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
@ -59,7 +75,7 @@ export default function FieldFilterForm({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{value => {
|
{value => {
|
||||||
return <Item key={value}>{formatValue(value, name)}</Item>;
|
return <Item key={value}>{formattedValues[value]}</Item>;
|
||||||
}}
|
}}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import { Button, Icon, Text, Modal, Icons, ModalTrigger } from 'react-basics';
|
import { Button, Icon, Text, Modal, Icons, ModalTrigger, useToasts } from 'react-basics';
|
||||||
import UserAddForm from './UserAddForm';
|
import UserAddForm from './UserAddForm';
|
||||||
import useMessages from 'components/hooks/useMessages';
|
import useMessages from 'components/hooks/useMessages';
|
||||||
|
import { setValue } from 'store/cache';
|
||||||
|
|
||||||
export function UserAddButton({ onSave }) {
|
export function UserAddButton({ onSave }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels, messages } = useMessages();
|
||||||
|
const { showToast } = useToasts();
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
onSave();
|
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||||
|
setValue('users', Date.now());
|
||||||
|
onSave?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -34,7 +34,7 @@ export function WebsiteSettings({ websiteId, openExternal = false, analyticsUrl
|
|||||||
|
|
||||||
const handleReset = async value => {
|
const handleReset = async value => {
|
||||||
if (value === 'delete') {
|
if (value === 'delete') {
|
||||||
await router.push('/settings/websites');
|
router.push('/settings/websites');
|
||||||
} else if (value === 'reset') {
|
} else if (value === 'reset') {
|
||||||
showSuccess();
|
showSuccess();
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Button, Text, Icon } from 'react-basics';
|
import { Button, Text, Icon, Icons } from 'react-basics';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { firstBy } from 'thenby';
|
import { firstBy } from 'thenby';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@ -7,7 +7,6 @@ import useDashboard from 'store/dashboard';
|
|||||||
import WebsiteHeader from './WebsiteHeader';
|
import WebsiteHeader from './WebsiteHeader';
|
||||||
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
|
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
|
||||||
import { useMessages, useLocale } from 'components/hooks';
|
import { useMessages, useLocale } from 'components/hooks';
|
||||||
import Icons from 'components/icons';
|
|
||||||
|
|
||||||
export default function WebsiteChartList({ websites, showCharts, limit }) {
|
export default function WebsiteChartList({ websites, showCharts, limit }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
@ -7,7 +7,7 @@ export function ErrorMessage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.error}>
|
<div className={styles.error}>
|
||||||
<Icon className={styles.icon} size="large">
|
<Icon className={styles.icon} size="lg">
|
||||||
<Icons.Alert />
|
<Icons.Alert />
|
||||||
</Icon>
|
</Icon>
|
||||||
<Text>{formatMessage(messages.error)}</Text>
|
<Text>{formatMessage(messages.error)}</Text>
|
||||||
|
@ -10,9 +10,9 @@ import styles from './FilterLink.module.css';
|
|||||||
export interface FilterLinkProps {
|
export interface FilterLinkProps {
|
||||||
id: string;
|
id: string;
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label?: string;
|
||||||
externalUrl: string;
|
externalUrl?: string;
|
||||||
className: string;
|
className?: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { Button, Icon } from 'react-basics';
|
import { Button, Icon, Icons } from 'react-basics';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import MobileMenu from './MobileMenu';
|
import MobileMenu from './MobileMenu';
|
||||||
import Icons from 'components/icons';
|
|
||||||
|
|
||||||
export function HamburgerButton({ menuItems }: { menuItems: any[] }) {
|
export function HamburgerButton({ menuItems }: { menuItems: any[] }) {
|
||||||
const [active, setActive] = useState(false);
|
const [active, setActive] = useState(false);
|
||||||
|
@ -22,7 +22,7 @@ import User from 'assets/user.svg';
|
|||||||
import Users from 'assets/users.svg';
|
import Users from 'assets/users.svg';
|
||||||
import Visitor from 'assets/visitor.svg';
|
import Visitor from 'assets/visitor.svg';
|
||||||
|
|
||||||
const icons: any = {
|
const icons = {
|
||||||
...Icons,
|
...Icons,
|
||||||
AddUser,
|
AddUser,
|
||||||
Bars,
|
Bars,
|
||||||
|
@ -36,9 +36,4 @@
|
|||||||
.header {
|
.header {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
|
||||||
flex-basis: 100%;
|
|
||||||
order: -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,12 @@ import { safeDecodeURI } from 'next-basics';
|
|||||||
import { Button, Icon, Icons, Text } from 'react-basics';
|
import { Button, Icon, Icons, Text } from 'react-basics';
|
||||||
import useNavigation from 'components/hooks/useNavigation';
|
import useNavigation from 'components/hooks/useNavigation';
|
||||||
import useMessages from 'components/hooks/useMessages';
|
import useMessages from 'components/hooks/useMessages';
|
||||||
|
import useFormat from 'components/hooks/useFormat';
|
||||||
import styles from './FilterTags.module.css';
|
import styles from './FilterTags.module.css';
|
||||||
|
|
||||||
export function FilterTags({ params }) {
|
export function FilterTags({ params }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { formatValue } = useFormat();
|
||||||
const {
|
const {
|
||||||
router,
|
router,
|
||||||
makeUrl,
|
makeUrl,
|
||||||
@ -34,7 +36,7 @@ export function FilterTags({ params }) {
|
|||||||
return (
|
return (
|
||||||
<div key={key} className={styles.tag} onClick={() => handleCloseFilter(key)}>
|
<div key={key} className={styles.tag} onClick={() => handleCloseFilter(key)}>
|
||||||
<Text>
|
<Text>
|
||||||
<b>{`${key}`}</b> = {`${safeDecodeURI(params[key])}`}
|
<b>{formatMessage(labels[key])}</b> = {formatValue(safeDecodeURI(params[key]), key)}
|
||||||
</Text>
|
</Text>
|
||||||
<Icon>
|
<Icon>
|
||||||
<Icons.Close />
|
<Icons.Close />
|
||||||
|
@ -24,3 +24,7 @@
|
|||||||
.tag:hover {
|
.tag:hover {
|
||||||
background: var(--blue200);
|
background: var(--blue200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag b {
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
@ -194,7 +194,7 @@ export function incrementDateRange(value, increment) {
|
|||||||
|
|
||||||
const { num, unit } = selectedUnit;
|
const { num, unit } = selectedUnit;
|
||||||
|
|
||||||
const sub = num * increment;
|
const sub = Math.abs(num) * increment;
|
||||||
|
|
||||||
switch (unit) {
|
switch (unit) {
|
||||||
case 'hour':
|
case 'hour':
|
||||||
|
@ -107,11 +107,16 @@ export async function getLocation(ip, req) {
|
|||||||
const result = lookup.get(ip);
|
const result = lookup.get(ip);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
|
const country = result.country?.iso_code ?? result?.registered_country?.iso_code;
|
||||||
|
const subdivision1 = result.subdivisions?.[0]?.iso_code;
|
||||||
|
const subdivision2 = result.subdivisions?.[1]?.names?.en;
|
||||||
|
const city = result.city?.names?.en;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
country: result.country?.iso_code ?? result?.registered_country?.iso_code,
|
country,
|
||||||
subdivision1: result.subdivisions?.[0]?.iso_code,
|
subdivision1: getRegionCode(country, subdivision1),
|
||||||
subdivision2: result.subdivisions?.[1]?.names?.en,
|
subdivision2,
|
||||||
city: result.city?.names?.en,
|
city,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,9 @@ export const useAuth = createMiddleware(async (req, res, next) => {
|
|||||||
} else if (redis.enabled && authKey) {
|
} else if (redis.enabled && authKey) {
|
||||||
const key = await redis.client.get(authKey);
|
const key = await redis.client.get(authKey);
|
||||||
|
|
||||||
user = await getUserById(key?.userId);
|
if (key?.userId) {
|
||||||
|
user = await getUserById(key.userId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
@ -23,15 +23,15 @@ const POSTGRESQL_DATE_FORMATS = {
|
|||||||
year: 'YYYY-01-01',
|
year: 'YYYY-01-01',
|
||||||
};
|
};
|
||||||
|
|
||||||
function getAddMinutesQuery(field: string, minutes: number): string {
|
function getAddIntervalQuery(field: string, interval: string): string {
|
||||||
const db = getDatabaseType(process.env.DATABASE_URL);
|
const db = getDatabaseType(process.env.DATABASE_URL);
|
||||||
|
|
||||||
if (db === POSTGRESQL) {
|
if (db === POSTGRESQL) {
|
||||||
return `${field} + interval '${minutes} minute'`;
|
return `${field} + interval '${interval}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (db === MYSQL) {
|
if (db === MYSQL) {
|
||||||
return `DATE_ADD(${field}, interval ${minutes} minute)`;
|
return `DATE_ADD(${field}, interval ${interval})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,15 +80,15 @@ function getDateQuery(field: string, unit: string, timezone?: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTimestampIntervalQuery(field: string): string {
|
function getTimestampDiffQuery(field1: string, field2: string): string {
|
||||||
const db = getDatabaseType();
|
const db = getDatabaseType();
|
||||||
|
|
||||||
if (db === POSTGRESQL) {
|
if (db === POSTGRESQL) {
|
||||||
return `floor(extract(epoch from max(${field}) - min(${field})))`;
|
return `floor(extract(epoch from (${field2} - ${field1})))`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (db === MYSQL) {
|
if (db === MYSQL) {
|
||||||
return `floor(unix_timestamp(max(${field})) - unix_timestamp(min(${field})))`;
|
return `timestampdiff(second, ${field1}, ${field2})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,11 +216,11 @@ function getSearchMode(): { mode?: Prisma.QueryMode } {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
...prisma,
|
...prisma,
|
||||||
getAddMinutesQuery,
|
getAddIntervalQuery,
|
||||||
getDayDiffQuery,
|
getDayDiffQuery,
|
||||||
getCastColumnQuery,
|
getCastColumnQuery,
|
||||||
getDateQuery,
|
getDateQuery,
|
||||||
getTimestampIntervalQuery,
|
getTimestampDiffQuery,
|
||||||
getFilterQuery,
|
getFilterQuery,
|
||||||
parseFilters,
|
parseFilters,
|
||||||
getPageFilters,
|
getPageFilters,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment-timezone';
|
||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
import { UNIT_TYPES } from './constants';
|
import { UNIT_TYPES } from './constants';
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
|||||||
async function clickhouseQuery(
|
async function clickhouseQuery(
|
||||||
websiteId: string,
|
websiteId: string,
|
||||||
filters: QueryFilters,
|
filters: QueryFilters,
|
||||||
): Promise<{ events: number; fields: number; records: number }> {
|
): Promise<{ events: number; fields: number; records: number }[]> {
|
||||||
const { rawQuery, parseFilters } = clickhouse;
|
const { rawQuery, parseFilters } = clickhouse;
|
||||||
const { filterQuery, params } = await parseFilters(websiteId, filters);
|
const { filterQuery, params } = await parseFilters(websiteId, filters);
|
||||||
|
|
||||||
|
@ -2,15 +2,15 @@ import { md5 } from 'next-basics';
|
|||||||
import { getSessions, getEvents } from 'queries/index';
|
import { getSessions, getEvents } from 'queries/index';
|
||||||
import { EVENT_TYPE } from 'lib/constants';
|
import { EVENT_TYPE } from 'lib/constants';
|
||||||
|
|
||||||
export async function getRealtimeData(websiteId, time) {
|
export async function getRealtimeData(websiteId: string, startDate: Date) {
|
||||||
const [pageviews, sessions, events] = await Promise.all([
|
const [pageviews, sessions, events] = await Promise.all([
|
||||||
getEvents(websiteId, time, EVENT_TYPE.pageView),
|
getEvents(websiteId, startDate, EVENT_TYPE.pageView),
|
||||||
getSessions(websiteId, time),
|
getSessions(websiteId, startDate),
|
||||||
getEvents(websiteId, time, EVENT_TYPE.customEvent),
|
getEvents(websiteId, startDate, EVENT_TYPE.customEvent),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const decorate = (id, data) => {
|
const decorate = (id: string, data: any[]) => {
|
||||||
return data.map(props => ({
|
return data.map((props: { [key: string]: any }) => ({
|
||||||
...props,
|
...props,
|
||||||
__id: md5(id, ...Object.values(props)),
|
__id: md5(id, ...Object.values(props)),
|
||||||
__type: id,
|
__type: id,
|
||||||
|
@ -12,7 +12,8 @@ export async function getWebsiteStats(...args: [websiteId: string, filters: Quer
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||||
const { getDateQuery, getTimestampIntervalQuery, parseFilters, rawQuery } = prisma;
|
const { getDateQuery, getAddIntervalQuery, getTimestampDiffQuery, parseFilters, rawQuery } =
|
||||||
|
prisma;
|
||||||
const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
|
const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
|
||||||
...filters,
|
...filters,
|
||||||
eventType: EVENT_TYPE.pageView,
|
eventType: EVENT_TYPE.pageView,
|
||||||
@ -24,13 +25,16 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
|||||||
sum(t.c) as "pageviews",
|
sum(t.c) as "pageviews",
|
||||||
count(distinct t.session_id) as "uniques",
|
count(distinct t.session_id) as "uniques",
|
||||||
sum(case when t.c = 1 then 1 else 0 end) as "bounces",
|
sum(case when t.c = 1 then 1 else 0 end) as "bounces",
|
||||||
sum(t.time) as "totaltime"
|
sum(case when t.max_time < ${getAddIntervalQuery('t.min_time', '1 hour')}
|
||||||
|
then ${getTimestampDiffQuery('t.min_time', 't.max_time')}
|
||||||
|
else 0 end) as "totaltime"
|
||||||
from (
|
from (
|
||||||
select
|
select
|
||||||
website_event.session_id,
|
website_event.session_id,
|
||||||
${getDateQuery('website_event.created_at', 'hour')},
|
${getDateQuery('website_event.created_at', 'day')},
|
||||||
count(*) as c,
|
count(*) as "c",
|
||||||
${getTimestampIntervalQuery('website_event.created_at')} as "time"
|
min(website_event.created_at) as "min_time",
|
||||||
|
max(website_event.created_at) as "max_time"
|
||||||
from website_event
|
from website_event
|
||||||
join website
|
join website
|
||||||
on website_event.website_id = website.website_id
|
on website_event.website_id = website.website_id
|
||||||
|
@ -35,7 +35,7 @@ async function relationalQuery(
|
|||||||
}[]
|
}[]
|
||||||
> {
|
> {
|
||||||
const { windowMinutes, startDate, endDate, urls } = criteria;
|
const { windowMinutes, startDate, endDate, urls } = criteria;
|
||||||
const { rawQuery, getAddMinutesQuery } = prisma;
|
const { rawQuery, getAddIntervalQuery } = prisma;
|
||||||
const { levelQuery, sumQuery } = getFunnelQuery(urls, windowMinutes);
|
const { levelQuery, sumQuery } = getFunnelQuery(urls, windowMinutes);
|
||||||
|
|
||||||
function getFunnelQuery(
|
function getFunnelQuery(
|
||||||
@ -58,9 +58,9 @@ async function relationalQuery(
|
|||||||
join website_event we
|
join website_event we
|
||||||
on l.session_id = we.session_id
|
on l.session_id = we.session_id
|
||||||
where we.website_id = {{websiteId::uuid}}
|
where we.website_id = {{websiteId::uuid}}
|
||||||
and we.created_at between l.created_at and ${getAddMinutesQuery(
|
and we.created_at between l.created_at and ${getAddIntervalQuery(
|
||||||
`l.created_at `,
|
`l.created_at `,
|
||||||
windowMinutes,
|
`${windowMinutes} minute`,
|
||||||
)}
|
)}
|
||||||
and we.referrer_path = {{${i - 1}}}
|
and we.referrer_path = {{${i - 1}}}
|
||||||
and we.url_path = {{${i}}}
|
and we.url_path = {{${i}}}
|
||||||
|
@ -83,10 +83,17 @@ async function clickhouseQuery(
|
|||||||
limit 500
|
limit 500
|
||||||
`,
|
`,
|
||||||
params,
|
params,
|
||||||
);
|
).then(a => {
|
||||||
|
return Object.values(a).map(a => {
|
||||||
|
return {
|
||||||
|
x: a.x,
|
||||||
|
y: Number(a.y),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFields(fields) {
|
function parseFields(fields: any[]) {
|
||||||
const query = fields.reduce(
|
const query = fields.reduce(
|
||||||
(arr, field) => {
|
(arr, field) => {
|
||||||
const { name } = field;
|
const { name } = field;
|
||||||
@ -99,7 +106,7 @@ function parseFields(fields) {
|
|||||||
return query.join(',\n');
|
return query.join(',\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseGroupBy(fields) {
|
function parseGroupBy(fields: { name: any }[]) {
|
||||||
if (!fields.length) {
|
if (!fields.length) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
59
yarn.lock
59
yarn.lock
@ -1941,22 +1941,22 @@
|
|||||||
"@parcel/watcher-win32-ia32" "2.3.0"
|
"@parcel/watcher-win32-ia32" "2.3.0"
|
||||||
"@parcel/watcher-win32-x64" "2.3.0"
|
"@parcel/watcher-win32-x64" "2.3.0"
|
||||||
|
|
||||||
"@prisma/client@5.4.2":
|
"@prisma/client@5.6.0":
|
||||||
version "5.4.2"
|
version "5.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.4.2.tgz#786f9c1d8f06d955933004ac638d14da4bf14025"
|
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.6.0.tgz#1c15932250d5658fe0127e62faf4ecd96a877259"
|
||||||
integrity sha512-2xsPaz4EaMKj1WS9iW6MlPhmbqtBsXAOeVttSePp8vTFTtvzh2hZbDgswwBdSCgPzmmwF+tLB259QzggvCmJqA==
|
integrity sha512-mUDefQFa1wWqk4+JhKPYq8BdVoFk9NFMBXUI8jAkBfQTtgx8WPx02U2HB/XbAz3GSUJpeJOKJQtNvaAIDs6sug==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@prisma/engines-version" "5.4.1-2.ac9d7041ed77bcc8a8dbd2ab6616b39013829574"
|
"@prisma/engines-version" "5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee"
|
||||||
|
|
||||||
"@prisma/engines-version@5.4.1-2.ac9d7041ed77bcc8a8dbd2ab6616b39013829574":
|
"@prisma/engines-version@5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee":
|
||||||
version "5.4.1-2.ac9d7041ed77bcc8a8dbd2ab6616b39013829574"
|
version "5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee"
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.4.1-2.ac9d7041ed77bcc8a8dbd2ab6616b39013829574.tgz#ff14f2926890edee47e8f1d08df7b4f392ee34bf"
|
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee.tgz#57b003ab5e1ea1523b5cdd7f06b24ebcf5c7fd8c"
|
||||||
integrity sha512-wvupDL4AA1vf4TQNANg7kR7y98ITqPsk6aacfBxZKtrJKRIsWjURHkZCGcQliHdqCiW/hGreO6d6ZuSv9MhdAA==
|
integrity sha512-UoFgbV1awGL/3wXuUK3GDaX2SolqczeeJ5b4FVec9tzeGbSWJboPSbT0psSrmgYAKiKnkOPFSLlH6+b+IyOwAw==
|
||||||
|
|
||||||
"@prisma/engines@5.4.2":
|
"@prisma/engines@5.6.0":
|
||||||
version "5.4.2"
|
version "5.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.4.2.tgz#ba2b7faeb227c76e423e88f962afe6a031319f3f"
|
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.6.0.tgz#82c445aa10633bbc0388aa2d6e411a0bd94c9439"
|
||||||
integrity sha512-fqeucJ3LH0e1eyFdT0zRx+oETLancu5+n4lhiYECyEz6H2RDskPJHJYHkVc0LhkU4Uv7fuEnppKU3nVKNzMh8g==
|
integrity sha512-Mt2q+GNJpU2vFn6kif24oRSBQv1KOkYaterQsi0k2/lA+dLvhRX6Lm26gon6PYHwUM8/h8KRgXIUMU0PCLB6bw==
|
||||||
|
|
||||||
"@prisma/extension-read-replicas@^0.3.0":
|
"@prisma/extension-read-replicas@^0.3.0":
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
@ -2601,10 +2601,10 @@
|
|||||||
"@typescript-eslint/types" "6.8.0"
|
"@typescript-eslint/types" "6.8.0"
|
||||||
eslint-visitor-keys "^3.4.1"
|
eslint-visitor-keys "^3.4.1"
|
||||||
|
|
||||||
"@umami/prisma-client@^0.5.0":
|
"@umami/prisma-client@^0.7.0":
|
||||||
version "0.5.0"
|
version "0.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/@umami/prisma-client/-/prisma-client-0.5.0.tgz#e2287debbf21f9344c989b9e7192491df88513bf"
|
resolved "https://registry.yarnpkg.com/@umami/prisma-client/-/prisma-client-0.7.0.tgz#f9de0dfc861c9ba6379c0789e012d4effa65f1ef"
|
||||||
integrity sha512-BkStMrvxYZQPwEIyy30JJPucTTsmQqb4jD8+ciSHxcBc7039cW0XyX3TL/u9ebZmANzIuNO0XiBArwjWulGIjg==
|
integrity sha512-70Azr4aAYMU6c+Lx69bjumNnKWgOFoq2PuYmp+T2kfCDhMyMLXTLDVD5ArDrwJMl1gWsgvpnumxCirYy+6KhGg==
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk "^4.1.2"
|
chalk "^4.1.2"
|
||||||
debug "^4.3.4"
|
debug "^4.3.4"
|
||||||
@ -3209,11 +3209,16 @@ caniuse-api@^3.0.0:
|
|||||||
lodash.memoize "^4.1.2"
|
lodash.memoize "^4.1.2"
|
||||||
lodash.uniq "^4.5.0"
|
lodash.uniq "^4.5.0"
|
||||||
|
|
||||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001541:
|
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001541:
|
||||||
version "1.0.30001551"
|
version "1.0.30001551"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001551.tgz#1f2cfa8820bd97c971a57349d7fd8f6e08664a3e"
|
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001551.tgz#1f2cfa8820bd97c971a57349d7fd8f6e08664a3e"
|
||||||
integrity sha512-vtBAez47BoGMMzlbYhfXrMV1kvRF2WP/lqiMuDu1Sb4EE4LKEgjopFDSRtZfdVnslNRpOqV/woE+Xgrwj6VQlg==
|
integrity sha512-vtBAez47BoGMMzlbYhfXrMV1kvRF2WP/lqiMuDu1Sb4EE4LKEgjopFDSRtZfdVnslNRpOqV/woE+Xgrwj6VQlg==
|
||||||
|
|
||||||
|
caniuse-lite@^1.0.30001406:
|
||||||
|
version "1.0.30001558"
|
||||||
|
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001558.tgz#d2c6e21fdbfe83817f70feab902421a19b7983ee"
|
||||||
|
integrity sha512-/Et7DwLqpjS47JPEcz6VnxU9PwcIdVi0ciLXRWBQdj1XFye68pSQYpV0QtPTfUKWuOaEig+/Vez2l74eDc1tPQ==
|
||||||
|
|
||||||
chalk@5.3.0:
|
chalk@5.3.0:
|
||||||
version "5.3.0"
|
version "5.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385"
|
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385"
|
||||||
@ -7372,12 +7377,12 @@ pretty-bytes@^5.6.0:
|
|||||||
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
|
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
|
||||||
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
|
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
|
||||||
|
|
||||||
prisma@5.4.2:
|
prisma@5.6.0:
|
||||||
version "5.4.2"
|
version "5.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.4.2.tgz#7eac9276439ec7073ec697c6c0dfa259d96e955e"
|
resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.6.0.tgz#ae2c27fdfb4d53be7f7dafb50d6b8b7f55c93aa5"
|
||||||
integrity sha512-GDMZwZy7mysB2oXU+angQqJ90iaPFdD0rHaZNkn+dio5NRkGLmMqmXs31//tg/qXT3iB0cTQwnGGQNuirhSTZg==
|
integrity sha512-EEaccku4ZGshdr2cthYHhf7iyvCcXqwJDvnoQRAJg5ge2Tzpv0e2BaMCp+CbbDUwoVTzwgOap9Zp+d4jFa2O9A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@prisma/engines" "5.4.2"
|
"@prisma/engines" "5.6.0"
|
||||||
|
|
||||||
promise.series@^0.2.0:
|
promise.series@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
@ -7471,10 +7476,10 @@ rc@^1.2.7:
|
|||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
strip-json-comments "~2.0.1"
|
strip-json-comments "~2.0.1"
|
||||||
|
|
||||||
react-basics@^0.107.0:
|
react-basics@^0.109.0:
|
||||||
version "0.107.0"
|
version "0.109.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.107.0.tgz#e5615792cbb3e4707ba5c8f438b29d6a88cf38b3"
|
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.109.0.tgz#9c1f41ebf6abbcf67f7dd11a16a7fb5e3aedd38d"
|
||||||
integrity sha512-jYnP1z2LTotxXWYwxOBvF26vXxSUBJB0x62YPKkEr1vmJGeg8iOLr8JGF8KE3R6E+NTqzRt6Bmdtt93mjaog4A==
|
integrity sha512-n955CwqIeQ/sTMxxvbtYpWtBWS07Rg39zrNKeUYN/JoCIq0YbbZiZDALAhh1Out+qsIe62NoOFA7JtHzk1EkHQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@react-spring/web" "^9.7.3"
|
"@react-spring/web" "^9.7.3"
|
||||||
classnames "^2.3.1"
|
classnames "^2.3.1"
|
||||||
|
Loading…
Reference in New Issue
Block a user